diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/content | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/content')
407 files changed, 81723 insertions, 0 deletions
diff --git a/toolkit/content/Makefile.in b/toolkit/content/Makefile.in new file mode 100644 index 0000000000..0e0e6316dc --- /dev/null +++ b/toolkit/content/Makefile.in @@ -0,0 +1,9 @@ +# +# 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/. + +DEFINES += \ + -DCXXFLAGS='$(CXXFLAGS)' \ + -DCPPFLAGS='$(CPPFLAGS)' \ + $(NULL) diff --git a/toolkit/content/TopLevelVideoDocument.js b/toolkit/content/TopLevelVideoDocument.js new file mode 100644 index 0000000000..5a2b8a857c --- /dev/null +++ b/toolkit/content/TopLevelVideoDocument.js @@ -0,0 +1,48 @@ +/* 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/. */ + +"use strict"; + +// <video> is used for top-level audio documents as well +let videoElement = document.getElementsByTagName("video")[0]; + +// 1. Handle fullscreen mode; +// 2. Send keystrokes to the video element if the body element is focused, +// to be received by the event listener in videocontrols.xml. +document.addEventListener("keypress", ev => { + if (ev.synthetic) // prevent recursion + return; + + // Maximize the standalone video when pressing F11, + // but ignore audio elements + if (ev.key == "F11" && videoElement.videoWidth != 0 && videoElement.videoHeight != 0) { + // If we're in browser fullscreen mode, it means the user pressed F11 + // while browser chrome or another tab had focus. + // Don't break leaving that mode, so do nothing here. + if (window.fullScreen) { + return; + } + + // If we're not in broser fullscreen mode, prevent entering into that, + // so we don't end up there after pressing Esc. + ev.preventDefault(); + ev.stopPropagation(); + + if (!document.mozFullScreenElement) { + videoElement.mozRequestFullScreen(); + } else { + document.mozCancelFullScreen(); + } + return; + } + + // Check if the video element is focused, so it already receives + // keystrokes, and don't send it another one from here. + if (document.activeElement == videoElement) + return; + + let newEvent = new KeyboardEvent("keypress", ev); + newEvent.synthetic = true; + videoElement.dispatchEvent(newEvent); +}); diff --git a/toolkit/content/XPCNativeWrapper.js b/toolkit/content/XPCNativeWrapper.js new file mode 100644 index 0000000000..7010a28ab2 --- /dev/null +++ b/toolkit/content/XPCNativeWrapper.js @@ -0,0 +1,7 @@ +/* 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/. */ + +/* + * Moved to C++ implementation in XPConnect. See bug 281988. + */ diff --git a/toolkit/content/about.js b/toolkit/content/about.js new file mode 100644 index 0000000000..ae467d07ad --- /dev/null +++ b/toolkit/content/about.js @@ -0,0 +1,32 @@ +/* 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/. */ + +// get release notes and vendor URL from prefs +var formatter = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Components.interfaces.nsIURLFormatter); +var releaseNotesURL = formatter.formatURLPref("app.releaseNotesURL"); +if (releaseNotesURL != "about:blank") { + var relnotes = document.getElementById("releaseNotesURL"); + relnotes.setAttribute("href", releaseNotesURL); + relnotes.parentNode.removeAttribute("hidden"); +} + +var vendorURL = formatter.formatURLPref("app.vendorURL"); +if (vendorURL != "about:blank") { + var vendor = document.getElementById("vendorURL"); + vendor.setAttribute("href", vendorURL); +} + +// insert the version of the XUL application (!= XULRunner platform version) +var versionNum = Components.classes["@mozilla.org/xre/app-info;1"] + .getService(Components.interfaces.nsIXULAppInfo) + .version; +var version = document.getElementById("version"); +version.textContent += " " + versionNum; + +// append user agent +var ua = navigator.userAgent; +if (ua) { + document.getElementById("buildID").textContent += " " + ua; +} diff --git a/toolkit/content/about.xhtml b/toolkit/content/about.xhtml new file mode 100644 index 0000000000..d5245928f2 --- /dev/null +++ b/toolkit/content/about.xhtml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" + "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % aboutDTD SYSTEM "chrome://global/locale/about.dtd" > +%aboutDTD; +<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> +%globalDTD; +]> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>About:</title> + <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"/> +</head> + +<body dir="&locale.dir;"> + <div id="aboutLogoContainer"> + <a id="vendorURL"> + <img src="about:logo" alt="&brandShortName;"/> + <p id="version">&about.version;</p> + </a> + </div> + + <ul id="aboutPageList"> + <li>&about.credits.beforeLink;<a href="about:credits">&about.credits.linkTitle;</a>&about.credits.afterLink;</li> + <li>&about.license.beforeTheLink;<a href="about:license">&about.license.linkTitle;</a>&about.license.afterTheLink;</li> + <li hidden="true">&about.relnotes.beforeTheLink;<a id="releaseNotesURL">&about.relnotes.linkTitle;</a>&about.relnotes.afterTheLink;</li> + <li>&about.buildconfig.beforeTheLink;<a href="about:buildconfig">&about.buildconfig.linkTitle;</a>&about.buildconfig.afterTheLink;</li> + <li id="buildID">&about.buildIdentifier;</li> + <script type="application/javascript" src="chrome://global/content/about.js"/> + </ul> + +</body> +</html> diff --git a/toolkit/content/aboutAbout.js b/toolkit/content/aboutAbout.js new file mode 100644 index 0000000000..13cb6bd6c3 --- /dev/null +++ b/toolkit/content/aboutAbout.js @@ -0,0 +1,47 @@ +/* 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/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var gProtocols = []; +var gContainer; +window.onload = function () { + gContainer = document.getElementById("abouts"); + findAbouts(); +} + +function findAbouts() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + for (var cid in Cc) { + var result = cid.match(/@mozilla.org\/network\/protocol\/about;1\?what\=(.*)$/); + if (result) { + var aboutType = result[1]; + var contract = "@mozilla.org/network/protocol/about;1?what=" + aboutType; + try { + var am = Cc[contract].getService(Ci.nsIAboutModule); + var uri = ios.newURI("about:"+aboutType, null, null); + var flags = am.getURIFlags(uri); + if (!(flags & Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT)) { + gProtocols.push(aboutType); + } + } catch (e) { + // getService might have thrown if the component doesn't actually + // implement nsIAboutModule + } + } + } + gProtocols.sort().forEach(createProtocolListing); +} + +function createProtocolListing(aProtocol) { + var uri = "about:" + aProtocol; + var li = document.createElement("li"); + var link = document.createElement("a"); + var text = document.createTextNode(uri); + + link.href = uri; + link.appendChild(text); + li.appendChild(link); + gContainer.appendChild(li); +} diff --git a/toolkit/content/aboutAbout.xhtml b/toolkit/content/aboutAbout.xhtml new file mode 100644 index 0000000000..5ec038638a --- /dev/null +++ b/toolkit/content/aboutAbout.xhtml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [ +<!ENTITY % aboutAboutDTD SYSTEM "chrome://global/locale/aboutAbout.dtd" > +%aboutAboutDTD; +<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> +%globalDTD; +]> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>&aboutAbout.title;</title> + <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"/> + <script type="application/javascript" src="chrome://global/content/aboutAbout.js"></script> +</head> + +<body dir="&locale.dir;"> + <h1>&aboutAbout.title;</h1> + <p><em>&aboutAbout.note;</em></p> + <ul id="abouts" class="columns"></ul> +</body> +</html> diff --git a/toolkit/content/aboutNetworking.js b/toolkit/content/aboutNetworking.js new file mode 100644 index 0000000000..9400ae9d70 --- /dev/null +++ b/toolkit/content/aboutNetworking.js @@ -0,0 +1,414 @@ +/* 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/. */ + +'use strict'; + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +const FileUtils = Cu.import("resource://gre/modules/FileUtils.jsm").FileUtils +const gEnv = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment); +const gDashboard = Cc['@mozilla.org/network/dashboard;1'] + .getService(Ci.nsIDashboard); +const gDirServ = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryServiceProvider); + +const gRequestNetworkingData = { + "http": gDashboard.requestHttpConnections, + "sockets": gDashboard.requestSockets, + "dns": gDashboard.requestDNSInfo, + "websockets": gDashboard.requestWebsocketConnections +}; +const gDashboardCallbacks = { + "http": displayHttp, + "sockets": displaySockets, + "dns": displayDns, + "websockets": displayWebsockets +}; + +const REFRESH_INTERVAL_MS = 3000; + +function col(element) { + let col = document.createElement('td'); + let content = document.createTextNode(element); + col.appendChild(content); + return col; +} + +function displayHttp(data) { + let cont = document.getElementById('http_content'); + let parent = cont.parentNode; + let new_cont = document.createElement('tbody'); + new_cont.setAttribute('id', 'http_content'); + + for (let i = 0; i < data.connections.length; i++) { + let row = document.createElement('tr'); + row.appendChild(col(data.connections[i].host)); + row.appendChild(col(data.connections[i].port)); + row.appendChild(col(data.connections[i].spdy)); + row.appendChild(col(data.connections[i].ssl)); + row.appendChild(col(data.connections[i].active.length)); + row.appendChild(col(data.connections[i].idle.length)); + new_cont.appendChild(row); + } + + parent.replaceChild(new_cont, cont); +} + +function displaySockets(data) { + let cont = document.getElementById('sockets_content'); + let parent = cont.parentNode; + let new_cont = document.createElement('tbody'); + new_cont.setAttribute('id', 'sockets_content'); + + for (let i = 0; i < data.sockets.length; i++) { + let row = document.createElement('tr'); + row.appendChild(col(data.sockets[i].host)); + row.appendChild(col(data.sockets[i].port)); + row.appendChild(col(data.sockets[i].tcp)); + row.appendChild(col(data.sockets[i].active)); + row.appendChild(col(data.sockets[i].sent)); + row.appendChild(col(data.sockets[i].received)); + new_cont.appendChild(row); + } + + parent.replaceChild(new_cont, cont); +} + +function displayDns(data) { + let cont = document.getElementById('dns_content'); + let parent = cont.parentNode; + let new_cont = document.createElement('tbody'); + new_cont.setAttribute('id', 'dns_content'); + + for (let i = 0; i < data.entries.length; i++) { + let row = document.createElement('tr'); + row.appendChild(col(data.entries[i].hostname)); + row.appendChild(col(data.entries[i].family)); + let column = document.createElement('td'); + + for (let j = 0; j < data.entries[i].hostaddr.length; j++) { + column.appendChild(document.createTextNode(data.entries[i].hostaddr[j])); + column.appendChild(document.createElement('br')); + } + + row.appendChild(column); + row.appendChild(col(data.entries[i].expiration)); + new_cont.appendChild(row); + } + + parent.replaceChild(new_cont, cont); +} + +function displayWebsockets(data) { + let cont = document.getElementById('websockets_content'); + let parent = cont.parentNode; + let new_cont = document.createElement('tbody'); + new_cont.setAttribute('id', 'websockets_content'); + + for (let i = 0; i < data.websockets.length; i++) { + let row = document.createElement('tr'); + row.appendChild(col(data.websockets[i].hostport)); + row.appendChild(col(data.websockets[i].encrypted)); + row.appendChild(col(data.websockets[i].msgsent)); + row.appendChild(col(data.websockets[i].msgreceived)); + row.appendChild(col(data.websockets[i].sentsize)); + row.appendChild(col(data.websockets[i].receivedsize)); + new_cont.appendChild(row); + } + + parent.replaceChild(new_cont, cont); +} + +function requestAllNetworkingData() { + for (let id in gRequestNetworkingData) + requestNetworkingDataForTab(id); +} + +function requestNetworkingDataForTab(id) { + gRequestNetworkingData[id](gDashboardCallbacks[id]); +} + +function init() { + gDashboard.enableLogging = true; + if (Services.prefs.getBoolPref("network.warnOnAboutNetworking")) { + let div = document.getElementById("warning_message"); + div.classList.add("active"); + div.hidden = false; + document.getElementById("confpref").addEventListener("click", confirm); + } + + requestAllNetworkingData(); + + let autoRefresh = document.getElementById("autorefcheck"); + if (autoRefresh.checked) + setAutoRefreshInterval(autoRefresh); + + autoRefresh.addEventListener("click", function() { + let refrButton = document.getElementById("refreshButton"); + if (this.checked) { + setAutoRefreshInterval(this); + refrButton.disabled = "disabled"; + } else { + clearInterval(this.interval); + refrButton.disabled = null; + } + }); + + let refr = document.getElementById("refreshButton"); + refr.addEventListener("click", requestAllNetworkingData); + if (document.getElementById("autorefcheck").checked) + refr.disabled = "disabled"; + + // Event delegation on #categories element + let menu = document.getElementById("categories"); + menu.addEventListener("click", function click(e) { + if (e.target && e.target.parentNode == menu) + show(e.target); + }); + + let dnsLookupButton = document.getElementById("dnsLookupButton"); + dnsLookupButton.addEventListener("click", function() { + doLookup(); + }); + + let setLogButton = document.getElementById("set-log-file-button"); + setLogButton.addEventListener("click", setLogFile); + + let setModulesButton = document.getElementById("set-log-modules-button"); + setModulesButton.addEventListener("click", setLogModules); + + let startLoggingButton = document.getElementById("start-logging-button"); + startLoggingButton.addEventListener("click", startLogging); + + let stopLoggingButton = document.getElementById("stop-logging-button"); + stopLoggingButton.addEventListener("click", stopLogging); + + try { + let file = gDirServ.getFile("TmpD", {}); + file.append("log.txt"); + document.getElementById("log-file").value = file.path; + } catch (e) { + console.error(e); + } + + // Update the value of the log file. + updateLogFile(); + + // Update the active log modules + updateLogModules(); + + // If we can't set the file and the modules at runtime, + // the start and stop buttons wouldn't really do anything. + if (setLogButton.disabled && setModulesButton.disabled) { + startLoggingButton.disabled = true; + stopLoggingButton.disabled = true; + } +} + +function updateLogFile() { + let logPath = ""; + + // Try to get the environment variable for the log file + logPath = gEnv.get("MOZ_LOG_FILE") || gEnv.get("NSPR_LOG_FILE"); + let currentLogFile = document.getElementById("current-log-file"); + let setLogFileButton = document.getElementById("set-log-file-button"); + + // If the log file was set from an env var, we disable the ability to set it + // at runtime. + if (logPath.length > 0) { + currentLogFile.innerText = logPath; + setLogFileButton.disabled = true; + } else { + // There may be a value set by a pref. + currentLogFile.innerText = gDashboard.getLogPath(); + } +} + +function updateLogModules() { + // Try to get the environment variable for the log file + let logModules = gEnv.get("MOZ_LOG") || + gEnv.get("MOZ_LOG_MODULES") || + gEnv.get("NSPR_LOG_MODULES"); + let currentLogModules = document.getElementById("current-log-modules"); + let setLogModulesButton = document.getElementById("set-log-modules-button"); + if (logModules.length > 0) { + currentLogModules.innerText = logModules; + // If the log modules are set by an environment variable at startup, do not + // allow changing them throught a pref. It would be difficult to figure out + // which ones are enabled and which ones are not. The user probably knows + // what he they are doing. + setLogModulesButton.disabled = true; + } else { + let activeLogModules = []; + try { + if (Services.prefs.getBoolPref("logging.config.add_timestamp")) { + activeLogModules.push("timestamp"); + } + } catch (e) {} + try { + if (Services.prefs.getBoolPref("logging.config.sync")) { + activeLogModules.push("sync"); + } + } catch (e) {} + + let children = Services.prefs.getBranch("logging.").getChildList("", {}); + + for (let pref of children) { + if (pref.startsWith("config.")) { + continue; + } + + try { + let value = Services.prefs.getIntPref(`logging.${pref}`); + activeLogModules.push(`${pref}:${value}`); + } catch (e) { + console.error(e); + } + } + + currentLogModules.innerText = activeLogModules.join(","); + } +} + +function setLogFile() { + let setLogButton = document.getElementById("set-log-file-button"); + if (setLogButton.disabled) { + // There's no point trying since it wouldn't work anyway. + return; + } + let logFile = document.getElementById("log-file").value.trim(); + Services.prefs.setCharPref("logging.config.LOG_FILE", logFile); + updateLogFile(); +} + +function clearLogModules() { + // Turn off all the modules. + let children = Services.prefs.getBranch("logging.").getChildList("", {}); + for (let pref of children) { + if (!pref.startsWith("config.")) { + Services.prefs.clearUserPref(`logging.${pref}`); + } + } + Services.prefs.clearUserPref("logging.config.add_timestamp"); + Services.prefs.clearUserPref("logging.config.sync"); + updateLogModules(); +} + +function setLogModules() { + let setLogModulesButton = document.getElementById("set-log-modules-button"); + if (setLogModulesButton.disabled) { + // The modules were set via env var, so we shouldn't try to change them. + return; + } + + let modules = document.getElementById("log-modules").value.trim(); + + // Clear previously set log modules. + clearLogModules(); + + let logModules = modules.split(","); + for (let module of logModules) { + if (module == "timestamp") { + Services.prefs.setBoolPref("logging.config.add_timestamp", true); + } else if (module == "rotate") { + // XXX: rotate is not yet supported. + } else if (module == "append") { + // XXX: append is not yet supported. + } else if (module == "sync") { + Services.prefs.setBoolPref("logging.config.sync", true); + } else { + let [key, value] = module.split(":"); + Services.prefs.setIntPref(`logging.${key}`, parseInt(value, 10)); + } + } + + updateLogModules(); +} + +function startLogging() { + setLogFile(); + setLogModules(); +} + +function stopLogging() { + clearLogModules(); + // clear the log file as well + Services.prefs.clearUserPref("logging.config.LOG_FILE"); + updateLogFile(); +} + +function confirm () { + let div = document.getElementById("warning_message"); + div.classList.remove("active"); + div.hidden = true; + let warnBox = document.getElementById("warncheck"); + Services.prefs.setBoolPref("network.warnOnAboutNetworking", warnBox.checked); +} + +function show(button) { + let current_tab = document.querySelector(".active"); + let content = document.getElementById(button.getAttribute("value")); + if (current_tab == content) + return; + current_tab.classList.remove("active"); + current_tab.hidden = true; + content.classList.add("active"); + content.hidden = false; + + let current_button = document.querySelector("[selected=true]"); + current_button.removeAttribute("selected"); + button.setAttribute("selected", "true"); + + let autoRefresh = document.getElementById("autorefcheck"); + if (autoRefresh.checked) { + clearInterval(autoRefresh.interval); + setAutoRefreshInterval(autoRefresh); + } + + let title = document.getElementById("sectionTitle"); + title.textContent = button.children[0].textContent; +} + +function setAutoRefreshInterval(checkBox) { + let active_tab = document.querySelector(".active"); + checkBox.interval = setInterval(function() { + requestNetworkingDataForTab(active_tab.id); + }, REFRESH_INTERVAL_MS); +} + +window.addEventListener("DOMContentLoaded", function load() { + window.removeEventListener("DOMContentLoaded", load); + init(); +}); + +function doLookup() { + let host = document.getElementById("host").value; + if (host) { + gDashboard.requestDNSLookup(host, displayDNSLookup); + } +} + +function displayDNSLookup(data) { + let cont = document.getElementById("dnslookuptool_content"); + let parent = cont.parentNode; + let new_cont = document.createElement("tbody"); + new_cont.setAttribute("id", "dnslookuptool_content"); + + if (data.answer) { + for (let address of data.address) { + let row = document.createElement("tr"); + row.appendChild(col(address)); + new_cont.appendChild(row); + } + } + else { + new_cont.appendChild(col(data.error)); + } + + parent.replaceChild(new_cont, cont); +} diff --git a/toolkit/content/aboutNetworking.xhtml b/toolkit/content/aboutNetworking.xhtml new file mode 100644 index 0000000000..440ee78385 --- /dev/null +++ b/toolkit/content/aboutNetworking.xhtml @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + + +<!DOCTYPE html [ +<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD; +<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD; +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD; +<!ENTITY % networkingDTD SYSTEM "chrome://global/locale/aboutNetworking.dtd"> %networkingDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&aboutNetworking.title;</title> + <link rel="stylesheet" href="chrome://mozapps/skin/aboutNetworking.css" type="text/css" /> + <script type="application/javascript;version=1.7" src="chrome://global/content/aboutNetworking.js" /> + </head> + <body id="body"> + <div id="warning_message" class="warningBackground" hidden="true"> + <div class="container"> + <h1 class="title">&aboutNetworking.warning;</h1> + <div class="toggle-container-with-text"> + <input id="warncheck" type="checkbox" checked="yes" /> + <label for="warncheck">&aboutNetworking.showNextTime;</label> + </div> + <div> + <button id="confpref" class="primary">&aboutNetworking.ok;</button> + </div> + </div> + </div> + <div id="categories"> + <div class="category" selected="true" value="http"> + <span class="category-name">&aboutNetworking.HTTP;</span> + </div> + <div class="category" value="sockets"> + <span class="category-name">&aboutNetworking.sockets;</span> + </div> + <div class="category" value="dns"> + <span class="category-name">&aboutNetworking.dns;</span> + </div> + <div class="category" value="websockets"> + <span class="category-name">&aboutNetworking.websockets;</span> + </div> + <hr></hr> + <div class="category" value="dnslookuptool"> + <span class="category-name">&aboutNetworking.dnsLookup;</span> + </div> + <div class="category" value="logging"> + <span class="category-name">&aboutNetworking.logging;</span> + </div> + </div> + <div class="main-content"> + <div class="header"> + <div id="sectionTitle" class="header-name"> + &aboutNetworking.HTTP; + </div> + <div id="refreshDiv" class="toggle-container-with-text"> + <button id="refreshButton">&aboutNetworking.refresh;</button> + <input id="autorefcheck" type="checkbox" name="Autorefresh" /> + <label for="autorefcheck">&aboutNetworking.autoRefresh;</label> + </div> + </div> + + <div id="http" class="tab active"> + <table> + <thead> + <tr> + <th>&aboutNetworking.hostname;</th> + <th>&aboutNetworking.port;</th> + <th>&aboutNetworking.spdy;</th> + <th>&aboutNetworking.ssl;</th> + <th>&aboutNetworking.active;</th> + <th>&aboutNetworking.idle;</th> + </tr> + </thead> + <tbody id="http_content" /> + </table> + </div> + + <div id="sockets" class="tab" hidden="true"> + <table> + <thead> + <tr> + <th>&aboutNetworking.host;</th> + <th>&aboutNetworking.port;</th> + <th>&aboutNetworking.tcp;</th> + <th>&aboutNetworking.active;</th> + <th>&aboutNetworking.sent;</th> + <th>&aboutNetworking.received;</th> + </tr> + </thead> + <tbody id="sockets_content" /> + </table> + </div> + + <div id="dns" class="tab" hidden="true"> + <table> + <thead> + <tr> + <th>&aboutNetworking.hostname;</th> + <th>&aboutNetworking.family;</th> + <th>&aboutNetworking.addresses;</th> + <th>&aboutNetworking.expires;</th> + </tr> + </thead> + <tbody id="dns_content" /> + </table> + </div> + + <div id="websockets" class="tab" hidden="true"> + <table> + <thead> + <tr> + <th>&aboutNetworking.hostname;</th> + <th>&aboutNetworking.ssl;</th> + <th>&aboutNetworking.messagesSent;</th> + <th>&aboutNetworking.messagesReceived;</th> + <th>&aboutNetworking.bytesSent;</th> + <th>&aboutNetworking.bytesReceived;</th> + </tr> + </thead> + <tbody id="websockets_content" /> + </table> + </div> + + <div id="dnslookuptool" class="tab" hidden="true"> + &aboutNetworking.dnsDomain;: <input type="text" name="host" id="host"></input> + <button id="dnsLookupButton">&aboutNetworking.dnsLookupButton;</button> + <hr/> + <table> + <thead> + <tr> + <th>&aboutNetworking.dnsLookupTableColumn;</th> + </tr> + </thead> + <tbody id="dnslookuptool_content" /> + </table> + </div> + + <div id="logging" class="tab" hidden="true"> + <div> + &aboutNetworking.logTutorial; + </div> + <br/> + <div> + <button id="start-logging-button"> &aboutNetworking.startLogging; </button> + <button id="stop-logging-button"> &aboutNetworking.stopLogging; </button> + </div> + <br/> + <br/> + <div> + &aboutNetworking.currentLogFile; <div id="current-log-file"></div><br/> + <input type="text" name="log-file" id="log-file"></input> + <button id="set-log-file-button"> &aboutNetworking.setLogFile; </button> + </div> + <div> + &aboutNetworking.currentLogModules; <div id="current-log-modules"></div><br/> + <input type="text" name="log-modules" id="log-modules" value="timestamp,sync,nsHttp:5,nsSocketTransport:5,nsStreamPump:5,nsHostResolver:5"></input> + <button id="set-log-modules-button"> &aboutNetworking.setLogModules; </button> + </div> + </div> + + </div> + </body> +</html> + diff --git a/toolkit/content/aboutProfiles.js b/toolkit/content/aboutProfiles.js new file mode 100644 index 0000000000..cddf888191 --- /dev/null +++ b/toolkit/content/aboutProfiles.js @@ -0,0 +1,339 @@ +/* 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/. */ + +'use strict'; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/AppConstants.jsm'); + +XPCOMUtils.defineLazyServiceGetter( + this, + 'ProfileService', + '@mozilla.org/toolkit/profile-service;1', + 'nsIToolkitProfileService' +); + +const bundle = Services.strings.createBundle( + 'chrome://global/locale/aboutProfiles.properties'); + +// nsIToolkitProfileService.selectProfile can be used only during the selection +// of the profile in the ProfileManager. If we are showing about:profiles in a +// tab, the selectedProfile returns the default profile. +// In this function we use the ProfD to find the current profile. +function findCurrentProfile() { + let cpd; + try { + cpd = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + } catch (e) {} + + if (cpd) { + let itr = ProfileService.profiles; + while (itr.hasMoreElements()) { + let profile = itr.getNext().QueryInterface(Ci.nsIToolkitProfile); + if (profile.rootDir.path == cpd.path) { + return profile; + } + } + } + + // selectedProfile can trow if nothing is selected or if the selected profile + // has been deleted. + try { + return ProfileService.selectedProfile; + } catch (e) { + return null; + } +} + +function refreshUI() { + let parent = document.getElementById('profiles'); + while (parent.firstChild) { + parent.removeChild(parent.firstChild); + } + + let defaultProfile; + try { + defaultProfile = ProfileService.defaultProfile; + } catch (e) {} + + let currentProfile = findCurrentProfile() || defaultProfile; + + let iter = ProfileService.profiles; + while (iter.hasMoreElements()) { + let profile = iter.getNext().QueryInterface(Ci.nsIToolkitProfile); + display({ profile: profile, + isDefault: profile == defaultProfile, + isCurrentProfile: profile == currentProfile }); + } + + let createButton = document.getElementById('create-button'); + createButton.onclick = createProfileWizard; + + let restartSafeModeButton = document.getElementById('restart-in-safe-mode-button'); + restartSafeModeButton.onclick = function() { restart(true); } + + let restartNormalModeButton = document.getElementById('restart-button'); + restartNormalModeButton.onclick = function() { restart(false); } +} + +function openDirectory(dir) { + let nsLocalFile = Components.Constructor("@mozilla.org/file/local;1", + "nsILocalFile", "initWithPath"); + new nsLocalFile(dir).reveal(); +} + +function display(profileData) { + let parent = document.getElementById('profiles'); + + let div = document.createElement('div'); + parent.appendChild(div); + + let nameStr = bundle.formatStringFromName('name', [profileData.profile.name], 1); + + let name = document.createElement('h2'); + name.appendChild(document.createTextNode(nameStr)); + + div.appendChild(name); + + if (profileData.isCurrentProfile) { + let currentProfile = document.createElement('h3'); + let currentProfileStr = bundle.GetStringFromName('currentProfile'); + currentProfile.appendChild(document.createTextNode(currentProfileStr)); + div.appendChild(currentProfile); + } + + let table = document.createElement('table'); + div.appendChild(table); + + let tbody = document.createElement('tbody'); + table.appendChild(tbody); + + function createItem(title, value, dir = false) { + let tr = document.createElement('tr'); + tbody.appendChild(tr); + + let th = document.createElement('th'); + th.setAttribute('class', 'column'); + th.appendChild(document.createTextNode(title)); + tr.appendChild(th); + + let td = document.createElement('td'); + td.appendChild(document.createTextNode(value)); + tr.appendChild(td); + + if (dir) { + td.appendChild(document.createTextNode(' ')); + let button = document.createElement('button'); + let string = 'openDir'; + if (AppConstants.platform == "win") { + string = 'winOpenDir2'; + } else if (AppConstants.platform == "macosx") { + string = 'macOpenDir'; + } + let buttonText = document.createTextNode(bundle.GetStringFromName(string)); + button.appendChild(buttonText); + td.appendChild(button); + + button.addEventListener('click', function(e) { + openDirectory(value); + }); + } + } + + createItem(bundle.GetStringFromName('isDefault'), + profileData.isDefault ? bundle.GetStringFromName('yes') : bundle.GetStringFromName('no')); + + createItem(bundle.GetStringFromName('rootDir'), profileData.profile.rootDir.path, true); + + if (profileData.profile.localDir.path != profileData.profile.rootDir.path) { + createItem(bundle.GetStringFromName('localDir'), profileData.profile.localDir.path, true); + } + + let renameButton = document.createElement('button'); + renameButton.appendChild(document.createTextNode(bundle.GetStringFromName('rename'))); + renameButton.onclick = function() { + renameProfile(profileData.profile); + }; + div.appendChild(renameButton); + + if (!profileData.isCurrentProfile) { + let removeButton = document.createElement('button'); + removeButton.appendChild(document.createTextNode(bundle.GetStringFromName('remove'))); + removeButton.onclick = function() { + removeProfile(profileData.profile); + }; + + div.appendChild(removeButton); + } + + if (!profileData.isDefault) { + let defaultButton = document.createElement('button'); + defaultButton.appendChild(document.createTextNode(bundle.GetStringFromName('setAsDefault'))); + defaultButton.onclick = function() { + defaultProfile(profileData.profile); + }; + div.appendChild(defaultButton); + } + + if (!profileData.isCurrentProfile) { + let runButton = document.createElement('button'); + runButton.appendChild(document.createTextNode(bundle.GetStringFromName('launchProfile'))); + runButton.onclick = function() { + openProfile(profileData.profile); + }; + div.appendChild(runButton); + } + + let sep = document.createElement('hr'); + div.appendChild(sep); +} + +function CreateProfile(profile) { + ProfileService.selectedProfile = profile; + ProfileService.flush(); + refreshUI(); +} + +function createProfileWizard() { + // This should be rewritten in HTML eventually. + window.openDialog('chrome://mozapps/content/profile/createProfileWizard.xul', + '', 'centerscreen,chrome,modal,titlebar', + ProfileService); +} + +function renameProfile(profile) { + let title = bundle.GetStringFromName('renameProfileTitle'); + let msg = bundle.formatStringFromName('renameProfile', [profile.name], 1); + let newName = { value: profile.name }; + + if (Services.prompt.prompt(window, title, msg, newName, null, + { value: 0 })) { + newName = newName.value; + + if (newName == profile.name) { + return; + } + + try { + profile.name = newName; + } catch (e) { + let title = bundle.GetStringFromName('invalidProfileNameTitle'); + let msg = bundle.formatStringFromName('invalidProfileName', [newName], 1); + Services.prompt.alert(window, title, msg); + return; + } + + ProfileService.flush(); + refreshUI(); + } +} + +function removeProfile(profile) { + let deleteFiles = false; + + if (profile.rootDir.exists()) { + let title = bundle.GetStringFromName('deleteProfileTitle'); + let msg = bundle.formatStringFromName('deleteProfileConfirm', + [profile.rootDir.path], 1); + + let buttonPressed = Services.prompt.confirmEx(window, title, msg, + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) + + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_2), + bundle.GetStringFromName('dontDeleteFiles'), + null, + bundle.GetStringFromName('deleteFiles'), + null, {value:0}); + if (buttonPressed == 1) { + return; + } + + if (buttonPressed == 2) { + deleteFiles = true; + } + } + + // If we are deleting the selected or the default profile we must choose a + // different one. + let isSelected = false; + try { + isSelected = ProfileService.selectedProfile == profile; + } catch (e) {} + + let isDefault = false; + try { + isDefault = ProfileService.defaultProfile == profile; + } catch (e) {} + + if (isSelected || isDefault) { + let itr = ProfileService.profiles; + while (itr.hasMoreElements()) { + let p = itr.getNext().QueryInterface(Ci.nsIToolkitProfile); + if (profile == p) { + continue; + } + + if (isSelected) { + ProfileService.selectedProfile = p; + } + + if (isDefault) { + ProfileService.defaultProfile = p; + } + + break; + } + } + + profile.remove(deleteFiles); + ProfileService.flush(); + refreshUI(); +} + +function defaultProfile(profile) { + ProfileService.defaultProfile = profile; + ProfileService.selectedProfile = profile; + ProfileService.flush(); + refreshUI(); +} + +function openProfile(profile) { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + if (cancelQuit.data) { + return; + } + + Services.startup.createInstanceWithProfile(profile); +} + +function restart(safeMode) { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + if (cancelQuit.data) { + return; + } + + let flags = Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestartNotSameProfile; + + if (safeMode) { + Services.startup.restartInSafeMode(flags); + } else { + Services.startup.quit(flags); + } +} + +window.addEventListener('DOMContentLoaded', function load() { + window.removeEventListener('DOMContentLoaded', load); + refreshUI(); +}); diff --git a/toolkit/content/aboutProfiles.xhtml b/toolkit/content/aboutProfiles.xhtml new file mode 100644 index 0000000000..ae29649326 --- /dev/null +++ b/toolkit/content/aboutProfiles.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + + +<!DOCTYPE html [ +<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD; +<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD; +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD; +<!ENTITY % profilesDTD SYSTEM "chrome://global/locale/aboutProfiles.dtd"> %profilesDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&aboutProfiles.title;</title> + <link rel="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/> + <link rel="stylesheet" href="chrome://mozapps/skin/aboutProfiles.css" type="text/css" /> + <script type="application/javascript;version=1.7" src="chrome://global/content/aboutProfiles.js" /> + </head> + <body id="body"> + <div id="action-box"> + <h3>&aboutProfiles.restart.title;</h3> + <button id="restart-in-safe-mode-button">&aboutProfiles.restart.inSafeMode;</button> + <button id="restart-button">&aboutProfiles.restart.normal;</button> + </div> + + <h1>&aboutProfiles.title;</h1> + <div class="page-subtitle">&aboutProfiles.subtitle;</div> + + <div> + <button id="create-button">&aboutProfiles.create;</button> + </div> + + <div id="profiles" class="tab"></div> + </body> +</html> diff --git a/toolkit/content/aboutRights-unbranded.xhtml b/toolkit/content/aboutRights-unbranded.xhtml new file mode 100644 index 0000000000..dfa12532d1 --- /dev/null +++ b/toolkit/content/aboutRights-unbranded.xhtml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + %brandDTD; + <!ENTITY % aboutRightsDTD SYSTEM "chrome://global/locale/aboutRights.dtd"> + %aboutRightsDTD; +]> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml"> + +<head> + <title>&rights.pagetitle;</title> + <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"/> +</head> + +<body id="your-rights" dir="&rights.locale-direction;" class="aboutPageWideContainer"> + +<h1>&rights.intro-header;</h1> + +<p>&rights.intro;</p> + +<ul> + <li>&rights.intro-point1a;<a href="http://www.mozilla.org/MPL/">&rights.intro-point1b;</a>&rights.intro-point1c;</li> +<!-- Point 2 discusses Mozilla trademarks, and isn't needed when the build is unbranded. + - Point 3 discusses privacy policy, unbranded builds get a placeholder (for the vendor to replace) + - Point 4 discusses web service terms, unbranded builds gets a placeholder (for the vendor to replace) --> + <li>&rights.intro-point3-unbranded;</li> + <li>&rights.intro-point4a-unbranded;<a href="about:rights#webservices" onclick="showServices();">&rights.intro-point4b-unbranded;</a>&rights.intro-point4c-unbranded;</li> +</ul> + +<div id="webservices-container"> + <a name="webservices"/> + <h3>&rights2.webservices-header;</h3> + + <p>&rights.webservices-unbranded;</p> + + <ol> +<!-- Terms only apply to official builds, unbranded builds get a placeholder. --> + <li>&rights.webservices-term1-unbranded;</li> + </ol> +</div> + +<script type="application/javascript"><![CDATA[ + var servicesDiv = document.getElementById("webservices-container"); + servicesDiv.style.display = "none"; + + function showServices() { + servicesDiv.style.display = ""; + } +]]></script> + +</body> +</html> diff --git a/toolkit/content/aboutRights.xhtml b/toolkit/content/aboutRights.xhtml new file mode 100644 index 0000000000..c0c73d7617 --- /dev/null +++ b/toolkit/content/aboutRights.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + %brandDTD; + <!ENTITY % securityPrefsDTD SYSTEM "chrome://browser/locale/preferences/security.dtd"> + %securityPrefsDTD; + <!ENTITY % aboutRightsDTD SYSTEM "chrome://global/locale/aboutRights.dtd"> + %aboutRightsDTD; +]> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml"> + +<head> + <title>&rights.pagetitle;</title> + <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"/> +</head> + +<body id="your-rights" dir="&rights.locale-direction;" class="aboutPageWideContainer"> + +<h1>&rights.intro-header;</h1> + +<p>&rights.intro;</p> + +<ul> + <li>&rights.intro-point1a;<a href="http://www.mozilla.org/MPL/">&rights.intro-point1b;</a>&rights.intro-point1c;</li> +<!-- Point 2 discusses Mozilla trademarks, and isn't needed when the build is unbranded. + - Point 3 discusses privacy policy, unbranded builds get a placeholder (for the vendor to replace) + - Point 4 discusses web service terms, unbranded builds gets a placeholder (for the vendor to replace) --> + <li>&rights.intro-point2-a;<a href="http://www.mozilla.org/foundation/trademarks/policy.html">&rights.intro-point2-b;</a>&rights.intro-point2-c;</li> + <li>&rights.intro-point2.5;</li> + <li>&rights2.intro-point3a;<a href="https://www.mozilla.org/legal/privacy/firefox.html">&rights2.intro-point3b;</a>&rights.intro-point3c;</li> + <li>&rights2.intro-point4a;<a href="about:rights#webservices" onclick="showServices();">&rights.intro-point4b;</a>&rights.intro-point4c;</li> + <li>&rights.intro-point5;</li> +</ul> + +<div id="webservices-container"> + <a name="webservices"/> + <h3>&rights2.webservices-header;</h3> + + <p>&rights2.webservices-a;<a href="about:rights#disabling-webservices" onclick="showDisablingServices();">&rights2.webservices-b;</a>&rights3.webservices-c;</p> + + <div id="disabling-webservices-container" style="margin-left:40px;"> + <a name="disabling-webservices"/> + <p><strong>&rights.safebrowsing-a;</strong>&rights.safebrowsing-b;</p> + <ul> + <li>&rights.safebrowsing-term1;</li> + <li>&rights.safebrowsing-term2;</li> + <li>&rights2.safebrowsing-term3;</li> + <li>&rights.safebrowsing-term4;</li> + </ul> + + <p><strong>&rights.locationawarebrowsing-a;</strong>&rights.locationawarebrowsing-b;</p> + <ul> + <li>&rights.locationawarebrowsing-term1a;<code>&rights.locationawarebrowsing-term1b;</code></li> + <li>&rights.locationawarebrowsing-term2;</li> + <li>&rights.locationawarebrowsing-term3;</li> + <li>&rights.locationawarebrowsing-term4;</li> + </ul> + </div> + + <ol> +<!-- Terms only apply to official builds, unbranded builds get a placeholder. --> + <li>&rights2.webservices-term1;</li> + <li>&rights.webservices-term2;</li> + <li>&rights2.webservices-term3;</li> + <li><strong>&rights.webservices-term4;</strong></li> + <li><strong>&rights.webservices-term5;</strong></li> + <li>&rights.webservices-term6;</li> + <li>&rights.webservices-term7;</li> + </ol> +</div> + +<script type="application/javascript"><![CDATA[ + var servicesDiv = document.getElementById("webservices-container"); + servicesDiv.style.display = "none"; + + function showServices() { + servicesDiv.style.display = ""; + } + + var disablingServicesDiv = document.getElementById("disabling-webservices-container"); + disablingServicesDiv.style.display = "none"; + + function showDisablingServices() { + disablingServicesDiv.style.display = ""; + } +]]></script> + +</body> +</html> diff --git a/toolkit/content/aboutServiceWorkers.js b/toolkit/content/aboutServiceWorkers.js new file mode 100644 index 0000000000..1f0b67c175 --- /dev/null +++ b/toolkit/content/aboutServiceWorkers.js @@ -0,0 +1,184 @@ +/* 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/. */ + +'use strict'; + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); + +const bundle = Services.strings.createBundle( + "chrome://global/locale/aboutServiceWorkers.properties"); + +const brandBundle = Services.strings.createBundle( + "chrome://branding/locale/brand.properties"); + +var gSWM; +var gSWCount = 0; + +function init() { + let enabled = Services.prefs.getBoolPref("dom.serviceWorkers.enabled"); + if (!enabled) { + let div = document.getElementById("warning_not_enabled"); + div.classList.add("active"); + return; + } + + gSWM = Cc["@mozilla.org/serviceworkers/manager;1"] + .getService(Ci.nsIServiceWorkerManager); + if (!gSWM) { + dump("AboutServiceWorkers: Failed to get the ServiceWorkerManager service!\n"); + return; + } + + let data = gSWM.getAllRegistrations(); + if (!data) { + dump("AboutServiceWorkers: Failed to retrieve the registrations.\n"); + return; + } + + let length = data.length; + if (!length) { + let div = document.getElementById("warning_no_serviceworkers"); + div.classList.add("active"); + return; + } + + let ps = undefined; + try { + ps = Cc["@mozilla.org/push/Service;1"] + .getService(Ci.nsIPushService); + } catch (e) { + dump("Could not acquire PushService\n"); + } + + for (let i = 0; i < length; ++i) { + let info = data.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo); + if (!info) { + dump("AboutServiceWorkers: Invalid nsIServiceWorkerRegistrationInfo interface.\n"); + continue; + } + + display(info, ps); + } +} + +function display(info, pushService) { + let parent = document.getElementById("serviceworkers"); + + let div = document.createElement('div'); + parent.appendChild(div); + + let title = document.createElement('h2'); + let titleStr = bundle.formatStringFromName('title', [info.principal.origin], 1); + title.appendChild(document.createTextNode(titleStr)); + div.appendChild(title); + + if (info.principal.appId) { + let b2gtitle = document.createElement('h3'); + let trueFalse = bundle.GetStringFromName(info.principal.isInIsolatedMozBrowserElement ? 'true' : 'false'); + + let b2gtitleStr = + bundle.formatStringFromName('b2gtitle', [ brandBundle.getString("brandShortName"), + info.principal.appId, + trueFalse], 2); + b2gtitle.appendChild(document.createTextNode(b2gtitleStr)); + div.appendChild(b2gtitle); + } + + let list = document.createElement('ul'); + div.appendChild(list); + + function createItem(title, value, makeLink) { + let item = document.createElement('li'); + list.appendChild(item); + + let bold = document.createElement('strong'); + bold.appendChild(document.createTextNode(title + " ")); + item.appendChild(bold); + + let textNode = document.createTextNode(value); + + if (makeLink) { + let link = document.createElement("a"); + link.href = value; + link.target = "_blank"; + link.appendChild(textNode); + item.appendChild(link); + } else { + item.appendChild(textNode); + } + + return textNode; + } + + createItem(bundle.GetStringFromName('scope'), info.scope); + createItem(bundle.GetStringFromName('scriptSpec'), info.scriptSpec, true); + let currentWorkerURL = info.activeWorker ? info.activeWorker.scriptSpec : ""; + createItem(bundle.GetStringFromName('currentWorkerURL'), currentWorkerURL, true); + let activeCacheName = info.activeWorker ? info.activeWorker.cacheName : ""; + createItem(bundle.GetStringFromName('activeCacheName'), activeCacheName); + let waitingCacheName = info.waitingWorker ? info.waitingWorker.cacheName : ""; + createItem(bundle.GetStringFromName('waitingCacheName'), waitingCacheName); + + let pushItem = createItem(bundle.GetStringFromName('pushEndpoint'), bundle.GetStringFromName('waiting')); + if (pushService) { + pushService.getSubscription(info.scope, info.principal, (status, pushRecord) => { + if (Components.isSuccessCode(status)) { + pushItem.data = JSON.stringify(pushRecord); + } else { + dump("about:serviceworkers - retrieving push registration failed\n"); + } + }); + } + + let updateButton = document.createElement("button"); + updateButton.appendChild(document.createTextNode(bundle.GetStringFromName('update'))); + updateButton.onclick = function() { + gSWM.propagateSoftUpdate(info.principal.originAttributes, info.scope); + }; + div.appendChild(updateButton); + + let unregisterButton = document.createElement("button"); + unregisterButton.appendChild(document.createTextNode(bundle.GetStringFromName('unregister'))); + div.appendChild(unregisterButton); + + let loadingMessage = document.createElement('span'); + loadingMessage.appendChild(document.createTextNode(bundle.GetStringFromName('waiting'))); + loadingMessage.classList.add('inactive'); + div.appendChild(loadingMessage); + + unregisterButton.onclick = function() { + let cb = { + unregisterSucceeded: function() { + parent.removeChild(div); + + if (!--gSWCount) { + let div = document.getElementById("warning_no_serviceworkers"); + div.classList.add("active"); + } + }, + + unregisterFailed: function() { + alert(bundle.GetStringFromName('unregisterError')); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIServiceWorkerUnregisterCallback]) + }; + + loadingMessage.classList.remove('inactive'); + gSWM.propagateUnregister(info.principal, cb, info.scope); + }; + + let sep = document.createElement('hr'); + div.appendChild(sep); + + ++gSWCount; +} + +window.addEventListener("DOMContentLoaded", function load() { + window.removeEventListener("DOMContentLoaded", load); + init(); +}); diff --git a/toolkit/content/aboutServiceWorkers.xhtml b/toolkit/content/aboutServiceWorkers.xhtml new file mode 100644 index 0000000000..8b7c86ba18 --- /dev/null +++ b/toolkit/content/aboutServiceWorkers.xhtml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + + +<!DOCTYPE html [ +<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD; +<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD; +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD; +<!ENTITY % serviceworkersDTD SYSTEM "chrome://global/locale/aboutServiceWorkers.dtd"> %serviceworkersDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&aboutServiceWorkers.title;</title> + <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css" /> + <link rel="stylesheet" href="chrome://mozapps/skin/aboutServiceWorkers.css" type="text/css" /> + <script type="application/javascript;version=1.7" src="chrome://global/content/aboutServiceWorkers.js" /> + </head> + <body id="body"> + <div id="warning_not_enabled" class="warningBackground"> + <div class="warningMessage">&aboutServiceWorkers.warning_not_enabled;</div> + </div> + + <div id="warning_no_serviceworkers" class="warningBackground"> + <div class="warningMessage">&aboutServiceWorkers.warning_no_serviceworkers;</div> + </div> + + <div id="serviceworkers" class="tab active"> + <h1>&aboutServiceWorkers.maintitle;</h1> + </div> + </body> +</html> diff --git a/toolkit/content/aboutSupport.js b/toolkit/content/aboutSupport.js new file mode 100644 index 0000000000..95cadfbe7e --- /dev/null +++ b/toolkit/content/aboutSupport.js @@ -0,0 +1,1003 @@ +/* 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/. */ + +"use strict"; + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Troubleshoot.jsm"); +Cu.import("resource://gre/modules/ResetProfile.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils", + "resource://gre/modules/PlacesDBUtils.jsm"); + +window.addEventListener("load", function onload(event) { + try { + window.removeEventListener("load", onload, false); + Troubleshoot.snapshot(function (snapshot) { + for (let prop in snapshotFormatters) + snapshotFormatters[prop](snapshot[prop]); + }); + populateActionBox(); + setupEventListeners(); + } catch (e) { + Cu.reportError("stack of load error for about:support: " + e + ": " + e.stack); + } +}, false); + +// Each property in this object corresponds to a property in Troubleshoot.jsm's +// snapshot data. Each function is passed its property's corresponding data, +// and it's the function's job to update the page with it. +var snapshotFormatters = { + + application: function application(data) { + $("application-box").textContent = data.name; + $("useragent-box").textContent = data.userAgent; + $("os-box").textContent = data.osVersion; + $("supportLink").href = data.supportURL; + let version = AppConstants.MOZ_APP_VERSION_DISPLAY; + if (data.vendor) + version += " (" + data.vendor + ")"; + $("version-box").textContent = version; + $("buildid-box").textContent = data.buildID; + if (data.updateChannel) + $("updatechannel-box").textContent = data.updateChannel; + + let statusText = stringBundle().GetStringFromName("multiProcessStatus.unknown"); + + // Whitelist of known values with string descriptions: + switch (data.autoStartStatus) { + case 0: + case 1: + case 2: + case 4: + case 6: + case 7: + case 8: + statusText = stringBundle().GetStringFromName("multiProcessStatus." + data.autoStartStatus); + break; + + case 10: + statusText = (Services.appinfo.OS == "Darwin" ? "OS X 10.6 - 10.8" : "Windows XP"); + break; + } + + $("multiprocess-box").textContent = stringBundle().formatStringFromName("multiProcessWindows", + [data.numRemoteWindows, data.numTotalWindows, statusText], 3); + + $("safemode-box").textContent = data.safeMode; + }, + + crashes: function crashes(data) { + if (!AppConstants.MOZ_CRASHREPORTER) + return; + + let strings = stringBundle(); + let daysRange = Troubleshoot.kMaxCrashAge / (24 * 60 * 60 * 1000); + $("crashes-title").textContent = + PluralForm.get(daysRange, strings.GetStringFromName("crashesTitle")) + .replace("#1", daysRange); + let reportURL; + try { + reportURL = Services.prefs.getCharPref("breakpad.reportURL"); + // Ignore any non http/https urls + if (!/^https?:/i.test(reportURL)) + reportURL = null; + } + catch (e) { } + if (!reportURL) { + $("crashes-noConfig").style.display = "block"; + $("crashes-noConfig").classList.remove("no-copy"); + return; + } + $("crashes-allReports").style.display = "block"; + $("crashes-allReports").classList.remove("no-copy"); + + if (data.pending > 0) { + $("crashes-allReportsWithPending").textContent = + PluralForm.get(data.pending, strings.GetStringFromName("pendingReports")) + .replace("#1", data.pending); + } + + let dateNow = new Date(); + $.append($("crashes-tbody"), data.submitted.map(function (crash) { + let date = new Date(crash.date); + let timePassed = dateNow - date; + let formattedDate; + if (timePassed >= 24 * 60 * 60 * 1000) + { + let daysPassed = Math.round(timePassed / (24 * 60 * 60 * 1000)); + let daysPassedString = strings.GetStringFromName("crashesTimeDays"); + formattedDate = PluralForm.get(daysPassed, daysPassedString) + .replace("#1", daysPassed); + } + else if (timePassed >= 60 * 60 * 1000) + { + let hoursPassed = Math.round(timePassed / (60 * 60 * 1000)); + let hoursPassedString = strings.GetStringFromName("crashesTimeHours"); + formattedDate = PluralForm.get(hoursPassed, hoursPassedString) + .replace("#1", hoursPassed); + } + else + { + let minutesPassed = Math.max(Math.round(timePassed / (60 * 1000)), 1); + let minutesPassedString = strings.GetStringFromName("crashesTimeMinutes"); + formattedDate = PluralForm.get(minutesPassed, minutesPassedString) + .replace("#1", minutesPassed); + } + return $.new("tr", [ + $.new("td", [ + $.new("a", crash.id, null, {href : reportURL + crash.id}) + ]), + $.new("td", formattedDate) + ]); + })); + }, + + extensions: function extensions(data) { + $.append($("extensions-tbody"), data.map(function (extension) { + return $.new("tr", [ + $.new("td", extension.name), + $.new("td", extension.version), + $.new("td", extension.isActive), + $.new("td", extension.id), + ]); + })); + }, + + experiments: function experiments(data) { + $.append($("experiments-tbody"), data.map(function (experiment) { + return $.new("tr", [ + $.new("td", experiment.name), + $.new("td", experiment.id), + $.new("td", experiment.description), + $.new("td", experiment.active), + $.new("td", experiment.endDate), + $.new("td", [ + $.new("a", experiment.detailURL, null, {href : experiment.detailURL, }) + ]), + $.new("td", experiment.branch), + ]); + })); + }, + + modifiedPreferences: function modifiedPreferences(data) { + $.append($("prefs-tbody"), sortedArrayFromObject(data).map( + function ([name, value]) { + return $.new("tr", [ + $.new("td", name, "pref-name"), + // Very long preference values can cause users problems when they + // copy and paste them into some text editors. Long values generally + // aren't useful anyway, so truncate them to a reasonable length. + $.new("td", String(value).substr(0, 120), "pref-value"), + ]); + } + )); + }, + + lockedPreferences: function lockedPreferences(data) { + $.append($("locked-prefs-tbody"), sortedArrayFromObject(data).map( + function ([name, value]) { + return $.new("tr", [ + $.new("td", name, "pref-name"), + $.new("td", String(value).substr(0, 120), "pref-value"), + ]); + } + )); + }, + + graphics: function graphics(data) { + let strings = stringBundle(); + + function localizedMsg(msgArray) { + let nameOrMsg = msgArray.shift(); + if (msgArray.length) { + // formatStringFromName logs an NS_ASSERTION failure otherwise that says + // "use GetStringFromName". Lame. + try { + return strings.formatStringFromName(nameOrMsg, msgArray, + msgArray.length); + } + catch (err) { + // Throws if nameOrMsg is not a name in the bundle. This shouldn't + // actually happen though, since msgArray.length > 1 => nameOrMsg is a + // name in the bundle, not a message, and the remaining msgArray + // elements are parameters. + return nameOrMsg; + } + } + try { + return strings.GetStringFromName(nameOrMsg); + } + catch (err) { + // Throws if nameOrMsg is not a name in the bundle. + } + return nameOrMsg; + } + + // Read APZ info out of data.info, stripping it out in the process. + let apzInfo = []; + let formatApzInfo = function (info) { + let out = []; + for (let type of ['Wheel', 'Touch', 'Drag']) { + let key = 'Apz' + type + 'Input'; + + if (!(key in info)) + continue; + + delete info[key]; + + let message = localizedMsg([type.toLowerCase() + 'Enabled']); + out.push(message); + } + + return out; + }; + + // Create a <tr> element with key and value columns. + // + // @key Text in the key column. Localized automatically, unless starts with "#". + // @value Text in the value column. Not localized. + function buildRow(key, value) { + let title; + if (key[0] == "#") { + title = key.substr(1); + } else { + try { + title = strings.GetStringFromName(key); + } catch (e) { + title = key; + } + } + return $.new("tr", [ + $.new("th", title, "column"), + $.new("td", value), + ]); + } + + // @where The name in "graphics-<name>-tbody", of the element to append to. + // @trs Array of row elements. + function addRows(where, trs) { + $.append($("graphics-" + where + "-tbody"), trs); + } + + // Build and append a row. + // + // @where The name in "graphics-<name>-tbody", of the element to append to. + function addRow(where, key, value) { + addRows(where, [buildRow(key, value)]); + } + if (data.clearTypeParameters !== undefined) { + addRow("diagnostics", "clearTypeParameters", data.clearTypeParameters); + } + if ("info" in data) { + apzInfo = formatApzInfo(data.info); + + let trs = sortedArrayFromObject(data.info).map(function ([prop, val]) { + return $.new("tr", [ + $.new("th", prop, "column"), + $.new("td", String(val)), + ]); + }); + addRows("diagnostics", trs); + + delete data.info; + } + + if (AppConstants.NIGHTLY_BUILD) { + let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let gpuProcessPid = windowUtils.gpuProcessPid; + + if (gpuProcessPid != -1) { + let gpuProcessKillButton = $.new("button"); + + gpuProcessKillButton.addEventListener("click", function() { + windowUtils.terminateGPUProcess(); + }); + + gpuProcessKillButton.textContent = strings.GetStringFromName("gpuProcessKillButton"); + addRow("diagnostics", "GPUProcessPid", gpuProcessPid); + addRow("diagnostics", "GPUProcess", [gpuProcessKillButton]); + } + } + + // graphics-failures-tbody tbody + if ("failures" in data) { + // If indices is there, it should be the same length as failures, + // (see Troubleshoot.jsm) but we check anyway: + if ("indices" in data && data.failures.length == data.indices.length) { + let combined = []; + for (let i = 0; i < data.failures.length; i++) { + let assembled = assembleFromGraphicsFailure(i, data); + combined.push(assembled); + } + combined.sort(function(a, b) { + if (a.index < b.index) return -1; + if (a.index > b.index) return 1; + return 0; + }); + $.append($("graphics-failures-tbody"), + combined.map(function(val) { + return $.new("tr", [$.new("th", val.header, "column"), + $.new("td", val.message)]); + })); + delete data.indices; + } else { + $.append($("graphics-failures-tbody"), + [$.new("tr", [$.new("th", "LogFailure", "column"), + $.new("td", data.failures.map(function (val) { + return $.new("p", val); + }))])]); + } + } else { + $("graphics-failures-tbody").style.display = "none"; + } + + // Add a new row to the table, and take the key (or keys) out of data. + // + // @where Table section to add to. + // @key Data key to use. + // @colKey The localization key to use, if different from key. + function addRowFromKey(where, key, colKey) { + if (!(key in data)) + return; + colKey = colKey || key; + + let value; + let messageKey = key + "Message"; + if (messageKey in data) { + value = localizedMsg(data[messageKey]); + delete data[messageKey]; + } else { + value = data[key]; + } + delete data[key]; + + if (value) { + addRow(where, colKey, value); + } + } + + // graphics-features-tbody + + let compositor = data.windowLayerManagerRemote + ? data.windowLayerManagerType + : "BasicLayers (" + strings.GetStringFromName("mainThreadNoOMTC") + ")"; + addRow("features", "compositing", compositor); + delete data.windowLayerManagerRemote; + delete data.windowLayerManagerType; + delete data.numTotalWindows; + delete data.numAcceleratedWindows; + delete data.numAcceleratedWindowsMessage; + + addRow("features", "asyncPanZoom", + apzInfo.length + ? apzInfo.join("; ") + : localizedMsg(["apzNone"])); + addRowFromKey("features", "webglRenderer"); + addRowFromKey("features", "webgl2Renderer"); + addRowFromKey("features", "supportsHardwareH264", "hardwareH264"); + addRowFromKey("features", "currentAudioBackend", "audioBackend"); + addRowFromKey("features", "direct2DEnabled", "#Direct2D"); + + if ("directWriteEnabled" in data) { + let message = data.directWriteEnabled; + if ("directWriteVersion" in data) + message += " (" + data.directWriteVersion + ")"; + addRow("features", "#DirectWrite", message); + delete data.directWriteEnabled; + delete data.directWriteVersion; + } + + // Adapter tbodies. + let adapterKeys = [ + ["adapterDescription", "gpuDescription"], + ["adapterVendorID", "gpuVendorID"], + ["adapterDeviceID", "gpuDeviceID"], + ["driverVersion", "gpuDriverVersion"], + ["driverDate", "gpuDriverDate"], + ["adapterDrivers", "gpuDrivers"], + ["adapterSubsysID", "gpuSubsysID"], + ["adapterRAM", "gpuRAM"], + ]; + + function showGpu(id, suffix) { + function get(prop) { + return data[prop + suffix]; + } + + let trs = []; + for (let [prop, key] of adapterKeys) { + let value = get(prop); + if (value === undefined || value === "") + continue; + trs.push(buildRow(key, value)); + } + + if (trs.length == 0) { + $("graphics-" + id + "-tbody").style.display = "none"; + return; + } + + let active = "yes"; + if ("isGPU2Active" in data && ((suffix == "2") != data.isGPU2Active)) { + active = "no"; + } + addRow(id, "gpuActive", strings.GetStringFromName(active)); + addRows(id, trs); + } + showGpu("gpu-1", ""); + showGpu("gpu-2", "2"); + + // Remove adapter keys. + for (let [prop, key] of adapterKeys) { + delete data[prop]; + delete data[prop + "2"]; + } + delete data.isGPU2Active; + + let featureLog = data.featureLog; + delete data.featureLog; + + let features = []; + for (let feature of featureLog.features) { + // Only add interesting decisions - ones that were not automatic based on + // all.js/gfxPrefs defaults. + if (feature.log.length > 1 || feature.log[0].status != "available") { + features.push(feature); + } + } + + if (features.length) { + for (let feature of features) { + let trs = []; + for (let entry of feature.log) { + if (entry.type == "default" && entry.status == "available") + continue; + + let contents; + if (entry.message.length > 0 && entry.message[0] == "#") { + // This is a failure ID. See nsIGfxInfo.idl. + let m; + if (m = /#BLOCKLIST_FEATURE_FAILURE_BUG_(\d+)/.exec(entry.message)) { + let bugSpan = $.new("span"); + bugSpan.textContent = strings.GetStringFromName("blocklistedBug") + "; "; + + let bugHref = $.new("a"); + bugHref.href = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + m[1]; + bugHref.textContent = strings.formatStringFromName("bugLink", [m[1]], 1); + + contents = [bugSpan, bugHref]; + } else { + contents = strings.formatStringFromName( + "unknownFailure", [entry.message.substr(1)], 1); + } + } else { + contents = entry.status + " by " + entry.type + ": " + entry.message; + } + + trs.push($.new("tr", [ + $.new("td", contents), + ])); + } + addRow("decisions", feature.name, [$.new("table", trs)]); + } + } else { + $("graphics-decisions-tbody").style.display = "none"; + } + + if (featureLog.fallbacks.length) { + for (let fallback of featureLog.fallbacks) { + addRow("workarounds", fallback.name, fallback.message); + } + } else { + $("graphics-workarounds-tbody").style.display = "none"; + } + + let crashGuards = data.crashGuards; + delete data.crashGuards; + + if (crashGuards.length) { + for (let guard of crashGuards) { + let resetButton = $.new("button"); + let onClickReset = (function (guard) { + // Note - need this wrapper until bug 449811 fixes |guard| scoping. + return function () { + Services.prefs.setIntPref(guard.prefName, 0); + resetButton.removeEventListener("click", onClickReset); + resetButton.disabled = true; + }; + })(guard); + + resetButton.textContent = strings.GetStringFromName("resetOnNextRestart"); + resetButton.addEventListener("click", onClickReset); + + addRow("crashguards", guard.type + "CrashGuard", [resetButton]); + } + } else { + $("graphics-crashguards-tbody").style.display = "none"; + } + + // Now that we're done, grab any remaining keys in data and drop them into + // the diagnostics section. + for (let key in data) { + let value = data[key]; + if (Array.isArray(value)) { + value = localizedMsg(value); + } + addRow("diagnostics", key, value); + } + }, + + javaScript: function javaScript(data) { + $("javascript-incremental-gc").textContent = data.incrementalGCEnabled; + }, + + accessibility: function accessibility(data) { + $("a11y-activated").textContent = data.isActive; + $("a11y-force-disabled").textContent = data.forceDisabled || 0; + }, + + libraryVersions: function libraryVersions(data) { + let strings = stringBundle(); + let trs = [ + $.new("tr", [ + $.new("th", ""), + $.new("th", strings.GetStringFromName("minLibVersions")), + $.new("th", strings.GetStringFromName("loadedLibVersions")), + ]) + ]; + sortedArrayFromObject(data).forEach( + function ([name, val]) { + trs.push($.new("tr", [ + $.new("td", name), + $.new("td", val.minVersion), + $.new("td", val.version), + ])); + } + ); + $.append($("libversions-tbody"), trs); + }, + + userJS: function userJS(data) { + if (!data.exists) + return; + let userJSFile = Services.dirsvc.get("PrefD", Ci.nsIFile); + userJSFile.append("user.js"); + $("prefs-user-js-link").href = Services.io.newFileURI(userJSFile).spec; + $("prefs-user-js-section").style.display = ""; + // Clear the no-copy class + $("prefs-user-js-section").className = ""; + }, + + sandbox: function sandbox(data) { + if (!AppConstants.MOZ_SANDBOX) + return; + + let strings = stringBundle(); + let tbody = $("sandbox-tbody"); + for (let key in data) { + // Simplify the display a little in the common case. + if (key === "hasPrivilegedUserNamespaces" && + data[key] === data["hasUserNamespaces"]) { + continue; + } + tbody.appendChild($.new("tr", [ + $.new("th", strings.GetStringFromName(key), "column"), + $.new("td", data[key]) + ])); + } + }, +}; + +var $ = document.getElementById.bind(document); + +$.new = function $_new(tag, textContentOrChildren, className, attributes) { + let elt = document.createElement(tag); + if (className) + elt.className = className; + if (attributes) { + for (let attrName in attributes) + elt.setAttribute(attrName, attributes[attrName]); + } + if (Array.isArray(textContentOrChildren)) + this.append(elt, textContentOrChildren); + else + elt.textContent = String(textContentOrChildren); + return elt; +}; + +$.append = function $_append(parent, children) { + children.forEach(c => parent.appendChild(c)); +}; + +function stringBundle() { + return Services.strings.createBundle( + "chrome://global/locale/aboutSupport.properties"); +} + +function assembleFromGraphicsFailure(i, data) +{ + // Only cover the cases we have today; for example, we do not have + // log failures that assert and we assume the log level is 1/error. + let message = data.failures[i]; + let index = data.indices[i]; + let what = ""; + if (message.search(/\[GFX1-\]: \(LF\)/) == 0) { + // Non-asserting log failure - the message is substring(14) + what = "LogFailure"; + message = message.substring(14); + } else if (message.search(/\[GFX1-\]: /) == 0) { + // Non-asserting - the message is substring(9) + what = "Error"; + message = message.substring(9); + } else if (message.search(/\[GFX1\]: /) == 0) { + // Asserting - the message is substring(8) + what = "Assert"; + message = message.substring(8); + } + let assembled = {"index" : index, + "header" : ("(#" + index + ") " + what), + "message" : message}; + return assembled; +} + +function sortedArrayFromObject(obj) { + let tuples = []; + for (let prop in obj) + tuples.push([prop, obj[prop]]); + tuples.sort(([prop1, v1], [prop2, v2]) => prop1.localeCompare(prop2)); + return tuples; +} + +function copyRawDataToClipboard(button) { + if (button) + button.disabled = true; + try { + Troubleshoot.snapshot(function (snapshot) { + if (button) + button.disabled = false; + let str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + str.data = JSON.stringify(snapshot, undefined, 2); + let transferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + transferable.init(getLoadContext()); + transferable.addDataFlavor("text/unicode"); + transferable.setTransferData("text/unicode", str, str.data.length * 2); + Cc["@mozilla.org/widget/clipboard;1"]. + getService(Ci.nsIClipboard). + setData(transferable, null, Ci.nsIClipboard.kGlobalClipboard); + if (AppConstants.platform == "android") { + // Present a toast notification. + let message = { + type: "Toast:Show", + message: stringBundle().GetStringFromName("rawDataCopied"), + duration: "short" + }; + Services.androidBridge.handleGeckoMessage(message); + } + }); + } + catch (err) { + if (button) + button.disabled = false; + throw err; + } +} + +function getLoadContext() { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); +} + +function copyContentsToClipboard() { + // Get the HTML and text representations for the important part of the page. + let contentsDiv = $("contents"); + let dataHtml = contentsDiv.innerHTML; + let dataText = createTextForElement(contentsDiv); + + // We can't use plain strings, we have to use nsSupportsString. + let supportsStringClass = Cc["@mozilla.org/supports-string;1"]; + let ssHtml = supportsStringClass.createInstance(Ci.nsISupportsString); + let ssText = supportsStringClass.createInstance(Ci.nsISupportsString); + + let transferable = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + transferable.init(getLoadContext()); + + // Add the HTML flavor. + transferable.addDataFlavor("text/html"); + ssHtml.data = dataHtml; + transferable.setTransferData("text/html", ssHtml, dataHtml.length * 2); + + // Add the plain text flavor. + transferable.addDataFlavor("text/unicode"); + ssText.data = dataText; + transferable.setTransferData("text/unicode", ssText, dataText.length * 2); + + // Store the data into the clipboard. + let clipboard = Cc["@mozilla.org/widget/clipboard;1"] + .getService(Ci.nsIClipboard); + clipboard.setData(transferable, null, clipboard.kGlobalClipboard); + + if (AppConstants.platform == "android") { + // Present a toast notification. + let message = { + type: "Toast:Show", + message: stringBundle().GetStringFromName("textCopied"), + duration: "short" + }; + Services.androidBridge.handleGeckoMessage(message); + } +} + +// Return the plain text representation of an element. Do a little bit +// of pretty-printing to make it human-readable. +function createTextForElement(elem) { + let serializer = new Serializer(); + let text = serializer.serialize(elem); + + // Actual CR/LF pairs are needed for some Windows text editors. + if (AppConstants.platform == "win") { + text = text.replace(/\n/g, "\r\n"); + } + + return text; +} + +function Serializer() { +} + +Serializer.prototype = { + + serialize: function (rootElem) { + this._lines = []; + this._startNewLine(); + this._serializeElement(rootElem); + this._startNewLine(); + return this._lines.join("\n").trim() + "\n"; + }, + + // The current line is always the line that writing will start at next. When + // an element is serialized, the current line is updated to be the line at + // which the next element should be written. + get _currentLine() { + return this._lines.length ? this._lines[this._lines.length - 1] : null; + }, + + set _currentLine(val) { + return this._lines[this._lines.length - 1] = val; + }, + + _serializeElement: function (elem) { + if (this._ignoreElement(elem)) + return; + + // table + if (elem.localName == "table") { + this._serializeTable(elem); + return; + } + + // all other elements + + let hasText = false; + for (let child of elem.childNodes) { + if (child.nodeType == Node.TEXT_NODE) { + let text = this._nodeText(child); + this._appendText(text); + hasText = hasText || !!text.trim(); + } + else if (child.nodeType == Node.ELEMENT_NODE) + this._serializeElement(child); + } + + // For headings, draw a "line" underneath them so they stand out. + if (/^h[0-9]+$/.test(elem.localName)) { + let headerText = (this._currentLine || "").trim(); + if (headerText) { + this._startNewLine(); + this._appendText("-".repeat(headerText.length)); + } + } + + // Add a blank line underneath block elements but only if they contain text. + if (hasText) { + let display = window.getComputedStyle(elem).getPropertyValue("display"); + if (display == "block") { + this._startNewLine(); + this._startNewLine(); + } + } + }, + + _startNewLine: function (lines) { + let currLine = this._currentLine; + if (currLine) { + // The current line is not empty. Trim it. + this._currentLine = currLine.trim(); + if (!this._currentLine) + // The current line became empty. Discard it. + this._lines.pop(); + } + this._lines.push(""); + }, + + _appendText: function (text, lines) { + this._currentLine += text; + }, + + _isHiddenSubHeading: function (th) { + return th.parentNode.parentNode.style.display == "none"; + }, + + _serializeTable: function (table) { + // Collect the table's column headings if in fact there are any. First + // check thead. If there's no thead, check the first tr. + let colHeadings = {}; + let tableHeadingElem = table.querySelector("thead"); + if (!tableHeadingElem) + tableHeadingElem = table.querySelector("tr"); + if (tableHeadingElem) { + let tableHeadingCols = tableHeadingElem.querySelectorAll("th,td"); + // If there's a contiguous run of th's in the children starting from the + // rightmost child, then consider them to be column headings. + for (let i = tableHeadingCols.length - 1; i >= 0; i--) { + let col = tableHeadingCols[i]; + if (col.localName != "th" || col.classList.contains("title-column")) + break; + colHeadings[i] = this._nodeText(col).trim(); + } + } + let hasColHeadings = Object.keys(colHeadings).length > 0; + if (!hasColHeadings) + tableHeadingElem = null; + + let trs = table.querySelectorAll("table > tr, tbody > tr"); + let startRow = + tableHeadingElem && tableHeadingElem.localName == "tr" ? 1 : 0; + + if (startRow >= trs.length) + // The table's empty. + return; + + if (hasColHeadings && !this._ignoreElement(tableHeadingElem)) { + // Use column headings. Print each tr as a multi-line chunk like: + // Heading 1: Column 1 value + // Heading 2: Column 2 value + for (let i = startRow; i < trs.length; i++) { + if (this._ignoreElement(trs[i])) + continue; + let children = trs[i].querySelectorAll("td"); + for (let j = 0; j < children.length; j++) { + let text = ""; + if (colHeadings[j]) + text += colHeadings[j] + ": "; + text += this._nodeText(children[j]).trim(); + this._appendText(text); + this._startNewLine(); + } + this._startNewLine(); + } + return; + } + + // Don't use column headings. Assume the table has only two columns and + // print each tr in a single line like: + // Column 1 value: Column 2 value + for (let i = startRow; i < trs.length; i++) { + if (this._ignoreElement(trs[i])) + continue; + let children = trs[i].querySelectorAll("th,td"); + let rowHeading = this._nodeText(children[0]).trim(); + if (children[0].classList.contains("title-column")) { + if (!this._isHiddenSubHeading(children[0])) + this._appendText(rowHeading); + } else if (children.length == 1) { + // This is a single-cell row. + this._appendText(rowHeading); + } else { + let childTables = trs[i].querySelectorAll("table"); + if (childTables.length) { + // If we have child tables, don't use nodeText - its trs are already + // queued up from querySelectorAll earlier. + this._appendText(rowHeading + ": "); + } else { + this._appendText(rowHeading + ": " + this._nodeText(children[1]).trim()); + } + } + this._startNewLine(); + } + this._startNewLine(); + }, + + _ignoreElement: function (elem) { + return elem.classList.contains("no-copy"); + }, + + _nodeText: function (node) { + return node.textContent.replace(/\s+/g, " "); + }, +}; + +function openProfileDirectory() { + // Get the profile directory. + let currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile); + let profileDir = currProfD.path; + + // Show the profile directory. + let nsLocalFile = Components.Constructor("@mozilla.org/file/local;1", + "nsILocalFile", "initWithPath"); + new nsLocalFile(profileDir).reveal(); +} + +/** + * Profile reset is only supported for the default profile if the appropriate migrator exists. + */ +function populateActionBox() { + if (ResetProfile.resetSupported()) { + $("reset-box").style.display = "block"; + $("action-box").style.display = "block"; + } + if (!Services.appinfo.inSafeMode) { + $("safe-mode-box").style.display = "block"; + $("action-box").style.display = "block"; + } +} + +// Prompt user to restart the browser in safe mode +function safeModeRestart() { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + if (!cancelQuit.data) { + Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit); + } +} +/** + * Set up event listeners for buttons. + */ +function setupEventListeners() { + $("show-update-history-button").addEventListener("click", function (event) { + var prompter = Cc["@mozilla.org/updates/update-prompt;1"].createInstance(Ci.nsIUpdatePrompt); + prompter.showUpdateHistory(window); + }); + $("reset-box-button").addEventListener("click", function (event) { + ResetProfile.openConfirmationDialog(window); + }); + $("copy-raw-data-to-clipboard").addEventListener("click", function (event) { + copyRawDataToClipboard(this); + }); + $("copy-to-clipboard").addEventListener("click", function (event) { + copyContentsToClipboard(); + }); + $("profile-dir-button").addEventListener("click", function (event) { + openProfileDirectory(); + }); + $("restart-in-safe-mode-button").addEventListener("click", function (event) { + if (Services.obs.enumerateObservers("restart-in-safe-mode").hasMoreElements()) { + Services.obs.notifyObservers(null, "restart-in-safe-mode", ""); + } + else { + safeModeRestart(); + } + }); + $("verify-place-integrity-button").addEventListener("click", function (event) { + PlacesDBUtils.checkAndFixDatabase(function(aLog) { + let msg = aLog.join("\n"); + $("verify-place-result").style.display = "block"; + $("verify-place-result").classList.remove("no-copy"); + $("verify-place-result").textContent = msg; + }); + }); +} diff --git a/toolkit/content/aboutSupport.xhtml b/toolkit/content/aboutSupport.xhtml new file mode 100644 index 0000000000..f5939a6eb2 --- /dev/null +++ b/toolkit/content/aboutSupport.xhtml @@ -0,0 +1,555 @@ +<?xml version="1.0" encoding="UTF-8"?> + +# 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/. + +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD; + <!ENTITY % aboutSupportDTD SYSTEM "chrome://global/locale/aboutSupport.dtd"> %aboutSupportDTD; + <!ENTITY % resetProfileDTD SYSTEM "chrome://global/locale/resetProfile.dtd"> %resetProfileDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&aboutSupport.pageTitle;</title> + + <link rel="icon" type="image/png" id="favicon" + href="chrome://branding/content/icon32.png"/> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" + type="text/css"/> + <link rel="stylesheet" href="chrome://global/skin/aboutSupport.css" + type="text/css"/> + + <script type="application/javascript;version=1.7" + src="chrome://global/content/aboutSupport.js"/> + <script type="application/javascript;version=1.7" + src="chrome://global/content/resetProfile.js"/> + </head> + + <body dir="&locale.dir;"> + + <div id="action-box"> + <div id="reset-box"> + <h3>&refreshProfile.title;</h3> + <button id="reset-box-button"> + &refreshProfile.button.label; + </button> + </div> + <div id="safe-mode-box"> + <h3>&aboutSupport.safeModeTitle;</h3> + <button id="restart-in-safe-mode-button"> + &aboutSupport.restartInSafeMode.label; + </button> + </div> + + </div> + <h1> + &aboutSupport.pageTitle; + </h1> + + <div class="page-subtitle"> + &aboutSupport.pageSubtitle; + </div> + + <div> + <button id="copy-raw-data-to-clipboard"> + &aboutSupport.copyRawDataToClipboard.label; + </button> + <button id="copy-to-clipboard"> + &aboutSupport.copyTextToClipboard.label; + </button> + </div> + + <div id="contents"> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section"> + &aboutSupport.appBasicsTitle; + </h2> + + <table> + <tbody> + <tr> + <th class="column"> + &aboutSupport.appBasicsName; + </th> + + <td id="application-box"> + </td> + </tr> + + <tr> + <th class="column"> + &aboutSupport.appBasicsVersion; + </th> + + <td id="version-box"> + </td> + </tr> + + <tr> + <th class="column"> + &aboutSupport.appBasicsBuildID; + </th> + <td id="buildid-box"></td> + </tr> + +#ifndef ANDROID + <tr class="no-copy"> + <th class="column"> + &aboutSupport.appBasicsUpdateHistory; + </th> + + <td> + <button id="show-update-history-button"> + &aboutSupport.appBasicsShowUpdateHistory; + </button> + </td> + </tr> +#endif + +#ifdef MOZ_UPDATER + <tr> + <th class="column"> + &aboutSupport.appBasicsUpdateChannel; + </th> + <td id="updatechannel-box"></td> + </tr> +#endif + + <tr> + <th class="column"> + &aboutSupport.appBasicsUserAgent; + </th> + + <td id="useragent-box"> + </td> + </tr> + + <tr> + <th class="column"> + &aboutSupport.appBasicsOS; + </th> + + <td id="os-box"> + </td> + </tr> + + <tr id="profile-row" class="no-copy"> + <th class="column"> +#ifdef XP_WIN + &aboutSupport.appBasicsProfileDirWinMac; +#else +#ifdef XP_MACOSX + &aboutSupport.appBasicsProfileDirWinMac; +#else + &aboutSupport.appBasicsProfileDir; +#endif +#endif + </th> + + <td> + <button id="profile-dir-button"> +#ifdef XP_WIN + &aboutSupport.showWin2.label; +#else +#ifdef XP_MACOSX + &aboutSupport.showMac.label; +#else + &aboutSupport.showDir.label; +#endif +#endif + </button> + </td> + </tr> + + <tr class="no-copy"> + <th class="column"> + &aboutSupport.appBasicsEnabledPlugins; + </th> + + <td> + <a href="about:plugins">about:plugins</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column"> + &aboutSupport.appBasicsBuildConfig; + </th> + + <td> + <a href="about:buildconfig">about:buildconfig</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column"> + &aboutSupport.appBasicsMemoryUse; + </th> + + <td> + <a href="about:memory">about:memory</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column"> + &aboutSupport.appBasicsPerformance; + </th> + + <td> + <a href="about:performance">about:performance</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column"> + &aboutSupport.appBasicsServiceWorkers; + </th> + + <td> + <a href="about:serviceworkers">about:serviceworkers</a> + </td> + </tr> + + <tr> + <th class="column"> + &aboutSupport.appBasicsMultiProcessSupport; + </th> + + <td id="multiprocess-box"> + </td> + </tr> + + <tr> + <th class="column"> + &aboutSupport.appBasicsSafeMode; + </th> + + <td id="safemode-box"> + </td> + </tr> + +#ifndef ANDROID + <tr class="no-copy"> + <th class="column"> + &aboutSupport.appBasicsProfiles; + </th> + + <td> + <a href="about:profiles">about:profiles</a> + </td> + </tr> +#endif + + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> +#ifdef MOZ_CRASHREPORTER + + <h2 class="major-section" id="crashes-title"> + &aboutSupport.crashes.title; + </h2> + + <table id="crashes-table"> + <thead> + <tr> + <th> + &aboutSupport.crashes.id; + </th> + <th> + &aboutSupport.crashes.sendDate; + </th> + </tr> + </thead> + <tbody id="crashes-tbody"> + </tbody> + </table> + <p id="crashes-allReports" class="hidden no-copy"> + <a href="about:crashes" id="crashes-allReportsWithPending" class="block">&aboutSupport.crashes.allReports;</a> + </p> + <p id="crashes-noConfig" class="hidden no-copy">&aboutSupport.crashes.noConfig;</p> + +#endif + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section"> + &aboutSupport.extensionsTitle; + </h2> + + <table> + <thead> + <tr> + <th> + &aboutSupport.extensionName; + </th> + <th> + &aboutSupport.extensionVersion; + </th> + <th> + &aboutSupport.extensionEnabled; + </th> + <th> + &aboutSupport.extensionId; + </th> + </tr> + </thead> + <tbody id="extensions-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section"> + &aboutSupport.graphicsTitle; + </h2> + + <table> + <tbody id="graphics-features-tbody"> + <tr> + <th colspan="2" class="title-column"> + &aboutSupport.graphicsFeaturesTitle; + </th> + </tr> + </tbody> + + <tbody id="graphics-tbody"> + </tbody> + + <tbody id="graphics-gpu-1-tbody"> + <tr> + <th colspan="2" class="title-column"> + &aboutSupport.graphicsGPU1Title; + </th> + </tr> + </tbody> + + <tbody id="graphics-gpu-2-tbody"> + <tr> + <th colspan="2" class="title-column"> + &aboutSupport.graphicsGPU2Title; + </th> + </tr> + </tbody> + + <tbody id="graphics-diagnostics-tbody"> + <tr> + <th colspan="2" class="title-column"> + &aboutSupport.graphicsDiagnosticsTitle; + </th> + </tr> + </tbody> + + <tbody id="graphics-decisions-tbody"> + <tr> + <th colspan="2" class="title-column"> + &aboutSupport.graphicsDecisionLogTitle; + </th> + </tr> + </tbody> + + <tbody id="graphics-crashguards-tbody"> + <tr> + <th colspan="2" class="title-column"> + &aboutSupport.graphicsCrashGuardsTitle; + </th> + </tr> + </tbody> + + <tbody id="graphics-workarounds-tbody"> + <tr> + <th colspan="2" class="title-column"> + &aboutSupport.graphicsWorkaroundsTitle; + </th> + </tr> + </tbody> + + <tbody id="graphics-failures-tbody"> + <tr> + <th colspan="2" class="title-column"> + &aboutSupport.graphicsFailureLogTitle; + </th> + </tr> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section"> + &aboutSupport.modifiedKeyPrefsTitle; + </h2> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name"> + &aboutSupport.modifiedPrefsName; + </th> + + <th class="value"> + &aboutSupport.modifiedPrefsValue; + </th> + </thead> + + <tbody id="prefs-tbody"> + </tbody> + </table> + + <section id="prefs-user-js-section" class="hidden no-copy"> + <h3>&aboutSupport.userJSTitle;</h3> + <p>&aboutSupport.userJSDescription;</p> + </section> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section"> + &aboutSupport.lockedKeyPrefsTitle; + </h2> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name"> + &aboutSupport.lockedPrefsName; + </th> + + <th class="value"> + &aboutSupport.lockedPrefsValue; + </th> + </thead> + + <tbody id="locked-prefs-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section"> + &aboutSupport.placeDatabaseTitle; + </h2> + + <table> + <tr class="no-copy"> + <th class="column"> + &aboutSupport.placeDatabaseIntegrity; + </th> + + <td> + <button id="verify-place-integrity-button"> + &aboutSupport.placeDatabaseVerifyIntegrity; + </button> + <pre id="verify-place-result" class="hidden no-copy"></pre> + </td> + </tr> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + <h2 class="major-section"> + &aboutSupport.jsTitle; + </h2> + + <table> + <tbody> + <tr> + <th class="column"> + &aboutSupport.jsIncrementalGC; + </th> + + <td id="javascript-incremental-gc"> + </td> + </tr> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + <h2 class="major-section"> + &aboutSupport.a11yTitle; + </h2> + + <table> + <tbody> + <tr> + <th class="column"> + &aboutSupport.a11yActivated; + </th> + + <td id="a11y-activated"> + </td> + </tr> + <tr> + <th class="column"> + &aboutSupport.a11yForceDisabled; + </th> + + <td id="a11y-force-disabled"> + </td> + </tr> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + <h2 class="major-section"> + &aboutSupport.libraryVersionsTitle; + </h2> + + <table> + <tbody id="libversions-tbody"> + </tbody> + </table> + + + <h2 class="major-section"> + &aboutSupport.experimentsTitle; + </h2> + + <table> + <thead> + <tr> + <th> + &aboutSupport.experimentName; + </th> + <th> + &aboutSupport.experimentId; + </th> + <th> + &aboutSupport.experimentDescription; + </th> + <th> + &aboutSupport.experimentActive; + </th> + <th> + &aboutSupport.experimentEndDate; + </th> + <th> + &aboutSupport.experimentHomepage; + </th> + <th> + &aboutSupport.experimentBranch; + </th> + </tr> + </thead> + <tbody id="experiments-tbody"> + </tbody> + </table> + <!-- - - - - - - - - - - - - - - - - - - - - --> + +#if defined(MOZ_SANDBOX) + <h2 class="major-section" id="sandbox"> + &aboutSupport.sandboxTitle; + </h2> + + <table> + <tbody id="sandbox-tbody"> + </tbody> + </table> +#endif + + </div> + + </body> + +</html> diff --git a/toolkit/content/aboutTelemetry.css b/toolkit/content/aboutTelemetry.css new file mode 100644 index 0000000000..6acf82c201 --- /dev/null +++ b/toolkit/content/aboutTelemetry.css @@ -0,0 +1,271 @@ +/* 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/. */ + +.hidden { + display: none; +} + +html { + background-color: -moz-Dialog; + color: -moz-DialogText; + font: message-box; +} + +body { + padding: 0px; + margin: 0px; +} + +h2 { + font-size: medium; +} + +#page-description { + border: 1px solid threedshadow; + margin: 0px; + padding: 10px; +} + +#settings { + border: 1px solid lightgrey; + padding: 5px; +} + +.description-enabled, +.description-disabled { + margin: 0px; +} + +.description-enabled > span { + color: green; +} + +.description-disabled > span { + color: red; +} + +#ping-picker { + margin-top: 10px; + border: 1px solid lightgrey; + padding: 5px; +} + +#ping-source-picker { + margin-left: 5px; + margin-bottom: 10px; +} + +.data-section, +.data-subsection { + background-color: -moz-Field; + color: -moz-FieldText; + border-top: 1px solid threedshadow; + border-bottom: 1px solid threedshadow; + margin: 0px; + padding: 10px; +} + +.data-section:not(.has-data), +.data-subsection:not(.has-subdata) { + color: gray; +} + + +.section-name { + font-size: x-large; + display: inline; +} + +.has-data .section-name { + cursor: pointer; +} + + +.toggle-caption { + font-style: italic; + cursor: pointer; +} + +.data-section:not(.has-data) .toggle-caption, +.data-subsection:not(.has-subdata) .toggle-caption { + display: none; +} + + +.empty-caption { + font-style: italic; +} + +.has-data .empty-caption, +.has-subdata .empty-caption { + display: none; /* invisible when has-data */ +} + +.data, +.subdata { + margin: 15px; + display: none; +} + +.has-data.expanded .data, +.has-subdata.expanded .subdata { + display: block; +} + + +.stack-title { + font-size: medium; + font-weight: bold; + text-decoration: underline; +} + +#histograms, #addon-histograms, #thread-hang-stats>div { + overflow: hidden; +} + +.histogram { + float: left; + border: 1px solid gray; + white-space: nowrap; + padding: 10px; + position: relative; /* required for position:absolute of the contained .copy-node */ +} + +body[dir="rtl"] .histogram { + float: right; +} + +.histogram-title { + text-overflow: ellipsis; + width: 100%; + white-space: nowrap; + overflow: hidden; +} + +.keyed-histogram { + white-space: nowrap; + padding: 15px; + position: relative; /* required for position:absolute of the contained .copy-node */ + display: block; + overflow: hidden; +} + +.keyed-histogram-title { + text-overflow: ellipsis; + width: 100%; + margin: 10px; + font-weight: bold; + font-size: 120%; + white-space: nowrap; +} + + +.bar { + width: 2em; + margin: 2px; + text-align: center; + float: left; + font-family: monospace; +} + +body[dir="rtl"] .bar { + float: right; +} + +.bar-inner { + background-color: DeepSkyBlue; + border: 1px solid #0000b0; +} + +th { + font-weight: bold; + white-space: nowrap; + text-align: left; +} + +body[dir="rtl"] th { + text-align: right; +} + +caption { + font-weight: bold; + white-space: nowrap; + text-align: left; + font-size: large; +} + +body[dir="rtl"] caption { + text-align: right; +} + +.copy-node { + visibility: hidden; + position: absolute; + bottom: 1px; + right: 1px; +} + +body[dir="rtl"] .copy-node { + left: 1px; +} + +.histogram:hover .copy-node { + visibility: visible; +} + + +.statebox { + display: none; +} + + +.filter-ui { + padding-inline-start: 10em; + display: none; +} + +.has-data.expanded .filter-ui { + display: inline; +} + +.processes-ui { + display: none; +} + +.has-data.expanded .processes-ui { + display: initial; +} + +.filter-blocked { + display: none; +} + +#raw-ping-data-section { + width: 100%; + height: 100%; + background-color:-moz-Dialog; +} + +#raw-ping-data { + background-color:white; + margin: 0px; +} + +#hide-raw-ping { + float: right; + cursor: pointer; + font-size: 20px; + background-color:#d8d8d8; + padding: 5px 10px; +} + +/* addon subsection style */ +.addon-caption { + font-size: larger; + margin: 5px 0; +} + +.process-picker { + margin: 0 0.5em; +} diff --git a/toolkit/content/aboutTelemetry.js b/toolkit/content/aboutTelemetry.js new file mode 100644 index 0000000000..2cf5e89090 --- /dev/null +++ b/toolkit/content/aboutTelemetry.js @@ -0,0 +1,2175 @@ +/* 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/. */ + +'use strict'; + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/TelemetryTimestamps.jsm"); +Cu.import("resource://gre/modules/TelemetryController.jsm"); +Cu.import("resource://gre/modules/TelemetrySession.jsm"); +Cu.import("resource://gre/modules/TelemetryArchive.jsm"); +Cu.import("resource://gre/modules/TelemetryUtils.jsm"); +Cu.import("resource://gre/modules/TelemetryLog.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); + +const Telemetry = Services.telemetry; +const bundle = Services.strings.createBundle( + "chrome://global/locale/aboutTelemetry.properties"); +const brandBundle = Services.strings.createBundle( + "chrome://branding/locale/brand.properties"); + +// Maximum height of a histogram bar (in em for html, in chars for text) +const MAX_BAR_HEIGHT = 18; +const MAX_BAR_CHARS = 25; +const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner"; +const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; +const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql"; +const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl"; +const DEFAULT_SYMBOL_SERVER_URI = "http://symbolapi.mozilla.org"; +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +// ms idle before applying the filter (allow uninterrupted typing) +const FILTER_IDLE_TIMEOUT = 500; + +const isWindows = (Services.appinfo.OS == "WINNT"); +const EOL = isWindows ? "\r\n" : "\n"; + +// This is the ping object currently displayed in the page. +var gPingData = null; + +// Cached value of document's RTL mode +var documentRTLMode = ""; + +/** + * Helper function for determining whether the document direction is RTL. + * Caches result of check on first invocation. + */ +function isRTL() { + if (!documentRTLMode) + documentRTLMode = window.getComputedStyle(document.body).direction; + return (documentRTLMode == "rtl"); +} + +function isArray(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; +} + +function isFlatArray(obj) { + if (!isArray(obj)) { + return false; + } + return !obj.some(e => typeof(e) == "object"); +} + +/** + * This is a helper function for explodeObject. + */ +function flattenObject(obj, map, path, array) { + for (let k of Object.keys(obj)) { + let newPath = [...path, array ? "[" + k + "]" : k]; + let v = obj[k]; + if (!v || (typeof(v) != "object")) { + map.set(newPath.join("."), v); + } else if (isFlatArray(v)) { + map.set(newPath.join("."), "[" + v.join(", ") + "]"); + } else { + flattenObject(v, map, newPath, isArray(v)); + } + } +} + +/** + * This turns a JSON object into a "flat" stringified form. + * + * For an object like {a: "1", b: {c: "2", d: "3"}} it returns a Map of the + * form Map(["a","1"], ["b.c", "2"], ["b.d", "3"]). + */ +function explodeObject(obj) { + let map = new Map(); + flattenObject(obj, map, []); + return map; +} + +function filterObject(obj, filterOut) { + let ret = {}; + for (let k of Object.keys(obj)) { + if (filterOut.indexOf(k) == -1) { + ret[k] = obj[k]; + } + } + return ret; +} + + +/** + * This turns a JSON object into a "flat" stringified form, separated into top-level sections. + * + * For an object like: + * { + * a: {b: "1"}, + * c: {d: "2", e: {f: "3"}} + * } + * it returns a Map of the form: + * Map([ + * ["a", Map(["b","1"])], + * ["c", Map([["d", "2"], ["e.f", "3"]])] + * ]) + */ +function sectionalizeObject(obj) { + let map = new Map(); + for (let k of Object.keys(obj)) { + map.set(k, explodeObject(obj[k])); + } + return map; +} + +/** + * Obtain the main DOMWindow for the current context. + */ +function getMainWindow() { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); +} + +/** + * Obtain the DOMWindow that can open a preferences pane. + * + * This is essentially "get the browser chrome window" with the added check + * that the supposed browser chrome window is capable of opening a preferences + * pane. + * + * This may return null if we can't find the browser chrome window. + */ +function getMainWindowWithPreferencesPane() { + let mainWindow = getMainWindow(); + if (mainWindow && "openAdvancedPreferences" in mainWindow) { + return mainWindow; + } + return null; +} + +/** + * Remove all child nodes of a document node. + */ +function removeAllChildNodes(node) { + while (node.hasChildNodes()) { + node.removeChild(node.lastChild); + } +} + +/** + * Pad a number to two digits with leading "0". + */ +function padToTwoDigits(n) { + return (n > 9) ? n: "0" + n; +} + +/** + * Return yesterdays date with the same time. + */ +function yesterday(date) { + let d = new Date(date); + d.setDate(d.getDate() - 1); + return d; +} + +/** + * This returns a short date string of the form YYYY/MM/DD. + */ +function shortDateString(date) { + return date.getFullYear() + + "/" + padToTwoDigits(date.getMonth() + 1) + + "/" + padToTwoDigits(date.getDate()); +} + +/** + * This returns a short time string of the form hh:mm:ss. + */ +function shortTimeString(date) { + return padToTwoDigits(date.getHours()) + + ":" + padToTwoDigits(date.getMinutes()) + + ":" + padToTwoDigits(date.getSeconds()); +} + +var Settings = { + SETTINGS: [ + // data upload + { + pref: PREF_FHR_UPLOAD_ENABLED, + defaultPrefValue: false, + descriptionEnabledId: "description-upload-enabled", + descriptionDisabledId: "description-upload-disabled", + }, + // extended "Telemetry" recording + { + pref: PREF_TELEMETRY_ENABLED, + defaultPrefValue: false, + descriptionEnabledId: "description-extended-recording-enabled", + descriptionDisabledId: "description-extended-recording-disabled", + }, + ], + + attachObservers: function() { + for (let s of this.SETTINGS) { + let setting = s; + Preferences.observe(setting.pref, this.render, this); + } + + let elements = document.getElementsByClassName("change-data-choices-link"); + for (let el of elements) { + el.addEventListener("click", function() { + if (AppConstants.platform == "android") { + Cu.import("resource://gre/modules/Messaging.jsm"); + Messaging.sendRequest({ + type: "Settings:Show", + resource: "preferences_privacy", + }); + } else { + // Show the data choices preferences on desktop. + let mainWindow = getMainWindowWithPreferencesPane(); + mainWindow.openAdvancedPreferences("dataChoicesTab"); + } + }, false); + } + }, + + detachObservers: function() { + for (let setting of this.SETTINGS) { + Preferences.ignore(setting.pref, this.render, this); + } + }, + + /** + * Updates the button & text at the top of the page to reflect Telemetry state. + */ + render: function() { + for (let setting of this.SETTINGS) { + let enabledElement = document.getElementById(setting.descriptionEnabledId); + let disabledElement = document.getElementById(setting.descriptionDisabledId); + + if (Preferences.get(setting.pref, setting.defaultPrefValue)) { + enabledElement.classList.remove("hidden"); + disabledElement.classList.add("hidden"); + } else { + enabledElement.classList.add("hidden"); + disabledElement.classList.remove("hidden"); + } + } + } +}; + +var PingPicker = { + viewCurrentPingData: null, + viewStructuredPingData: null, + _archivedPings: null, + + attachObservers: function() { + let elements = document.getElementsByName("choose-ping-source"); + for (let el of elements) { + el.addEventListener("change", () => this.onPingSourceChanged(), false); + } + + let displays = document.getElementsByName("choose-ping-display"); + for (let el of displays) { + el.addEventListener("change", () => this.onPingDisplayChanged(), false); + } + + document.getElementById("show-subsession-data").addEventListener("change", () => { + this._updateCurrentPingData(); + }); + + document.getElementById("choose-ping-week").addEventListener("change", () => { + this._renderPingList(); + this._updateArchivedPingData(); + }, false); + document.getElementById("choose-ping-id").addEventListener("change", () => { + this._updateArchivedPingData() + }, false); + + document.getElementById("newer-ping") + .addEventListener("click", () => this._movePingIndex(-1), false); + document.getElementById("older-ping") + .addEventListener("click", () => this._movePingIndex(1), false); + document.getElementById("choose-payload") + .addEventListener("change", () => displayPingData(gPingData), false); + document.getElementById("histograms-processes") + .addEventListener("change", () => displayPingData(gPingData), false); + document.getElementById("keyed-histograms-processes") + .addEventListener("change", () => displayPingData(gPingData), false); + }, + + onPingSourceChanged: function() { + this.update(); + }, + + onPingDisplayChanged: function() { + this.update(); + }, + + update: Task.async(function*() { + let viewCurrent = document.getElementById("ping-source-current").checked; + let viewStructured = document.getElementById("ping-source-structured").checked; + let currentChanged = viewCurrent !== this.viewCurrentPingData; + let structuredChanged = viewStructured !== this.viewStructuredPingData; + this.viewCurrentPingData = viewCurrent; + this.viewStructuredPingData = viewStructured; + + // If we have no archived pings, disable the ping archive selection. + // This can happen on new profiles or if the ping archive is disabled. + let archivedPingList = yield TelemetryArchive.promiseArchivedPingList(); + let sourceArchived = document.getElementById("ping-source-archive"); + sourceArchived.disabled = (archivedPingList.length == 0); + + if (currentChanged) { + if (this.viewCurrentPingData) { + document.getElementById("current-ping-picker").classList.remove("hidden"); + document.getElementById("archived-ping-picker").classList.add("hidden"); + this._updateCurrentPingData(); + } else { + document.getElementById("current-ping-picker").classList.add("hidden"); + yield this._updateArchivedPingList(archivedPingList); + document.getElementById("archived-ping-picker").classList.remove("hidden"); + } + } + + if (structuredChanged) { + if (this.viewStructuredPingData) { + this._showStructuredPingData(); + } else { + this._showRawPingData(); + } + } + }), + + _updateCurrentPingData: function() { + const subsession = document.getElementById("show-subsession-data").checked; + const ping = TelemetryController.getCurrentPingData(subsession); + if (!ping) { + return; + } + displayPingData(ping, true); + }, + + _updateArchivedPingData: function() { + let id = this._getSelectedPingId(); + return TelemetryArchive.promiseArchivedPingById(id) + .then((ping) => displayPingData(ping, true)); + }, + + _updateArchivedPingList: Task.async(function*(pingList) { + // The archived ping list is sorted in ascending timestamp order, + // but descending is more practical for the operations we do here. + pingList.reverse(); + + this._archivedPings = pingList; + + // Collect the start dates for all the weeks we have pings for. + let weekStart = (date) => { + let weekDay = (date.getDay() + 6) % 7; + let monday = new Date(date); + monday.setDate(date.getDate() - weekDay); + return TelemetryUtils.truncateToDays(monday); + }; + + let weekStartDates = new Set(); + for (let p of pingList) { + weekStartDates.add(weekStart(new Date(p.timestampCreated)).getTime()); + } + + // Build a list of the week date ranges we have ping data for. + let plusOneWeek = (date) => { + let d = date; + d.setDate(d.getDate() + 7); + return d; + }; + + this._weeks = Array.from(weekStartDates.values(), startTime => ({ + startDate: new Date(startTime), + endDate: plusOneWeek(new Date(startTime)), + })); + + // Render the archive data. + this._renderWeeks(); + this._renderPingList(); + + // Update the displayed ping. + yield this._updateArchivedPingData(); + }), + + _renderWeeks: function() { + let weekSelector = document.getElementById("choose-ping-week"); + removeAllChildNodes(weekSelector); + + let index = 0; + for (let week of this._weeks) { + let text = shortDateString(week.startDate) + + " - " + shortDateString(yesterday(week.endDate)); + + let option = document.createElement("option"); + let content = document.createTextNode(text); + option.appendChild(content); + weekSelector.appendChild(option); + } + }, + + _getSelectedWeek: function() { + let weekSelector = document.getElementById("choose-ping-week"); + return this._weeks[weekSelector.selectedIndex]; + }, + + _renderPingList: function(id = null) { + let pingSelector = document.getElementById("choose-ping-id"); + removeAllChildNodes(pingSelector); + + let weekRange = this._getSelectedWeek(); + let pings = this._archivedPings.filter( + (p) => p.timestampCreated >= weekRange.startDate.getTime() && + p.timestampCreated < weekRange.endDate.getTime()); + + for (let p of pings) { + let date = new Date(p.timestampCreated); + let text = shortDateString(date) + + " " + shortTimeString(date) + + " - " + p.type; + + let option = document.createElement("option"); + let content = document.createTextNode(text); + option.appendChild(content); + option.setAttribute("value", p.id); + if (id && p.id == id) { + option.selected = true; + } + pingSelector.appendChild(option); + } + }, + + _getSelectedPingId: function() { + let pingSelector = document.getElementById("choose-ping-id"); + let selected = pingSelector.selectedOptions.item(0); + return selected.getAttribute("value"); + }, + + _movePingIndex: function(offset) { + const id = this._getSelectedPingId(); + const index = this._archivedPings.findIndex((p) => p.id == id); + const newIndex = Math.min(Math.max(index + offset, 0), this._archivedPings.length - 1); + const ping = this._archivedPings[newIndex]; + + const weekIndex = this._weeks.findIndex( + (week) => ping.timestampCreated >= week.startDate.getTime() && + ping.timestampCreated < week.endDate.getTime()); + const options = document.getElementById("choose-ping-week").options; + options.item(weekIndex).selected = true; + + this._renderPingList(ping.id); + this._updateArchivedPingData(); + }, + + _showRawPingData: function() { + document.getElementById("raw-ping-data-section").classList.remove("hidden"); + document.getElementById("structured-ping-data-section").classList.add("hidden"); + }, + + _showStructuredPingData: function() { + document.getElementById("raw-ping-data-section").classList.add("hidden"); + document.getElementById("structured-ping-data-section").classList.remove("hidden"); + }, +}; + +var GeneralData = { + /** + * Renders the general data + */ + render: function(aPing) { + setHasData("general-data-section", true); + let table = document.createElement("table"); + + let caption = document.createElement("caption"); + let captionString = bundle.GetStringFromName("generalDataTitle"); + caption.appendChild(document.createTextNode(captionString + "\n")); + table.appendChild(caption); + + let headings = document.createElement("tr"); + this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingName") + "\t"); + this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingValue") + "\t"); + table.appendChild(headings); + + // The payload & environment parts are handled by other renderers. + let ignoreSections = ["payload", "environment"]; + let data = explodeObject(filterObject(aPing, ignoreSections)); + + for (let [path, value] of data) { + let row = document.createElement("tr"); + this.appendColumn(row, "td", path + "\t"); + this.appendColumn(row, "td", value + "\t"); + table.appendChild(row); + } + + let dataDiv = document.getElementById("general-data"); + removeAllChildNodes(dataDiv); + dataDiv.appendChild(table); + }, + + /** + * Helper function for appending a column to the data table. + * + * @param aRowElement Parent row element + * @param aColType Column's tag name + * @param aColText Column contents + */ + appendColumn: function(aRowElement, aColType, aColText) { + let colElement = document.createElement(aColType); + let colTextElement = document.createTextNode(aColText); + colElement.appendChild(colTextElement); + aRowElement.appendChild(colElement); + }, +}; + +var EnvironmentData = { + /** + * Renders the environment data + */ + render: function(ping) { + let dataDiv = document.getElementById("environment-data"); + removeAllChildNodes(dataDiv); + const hasData = !!ping.environment; + setHasData("environment-data-section", hasData); + if (!hasData) { + return; + } + + let data = sectionalizeObject(ping.environment); + + for (let [section, sectionData] of data) { + if (section == "addons") { + break; + } + + let table = document.createElement("table"); + this.appendHeading(table); + + for (let [path, value] of sectionData) { + let row = document.createElement("tr"); + this.appendColumn(row, "td", path); + this.appendColumn(row, "td", value); + table.appendChild(row); + } + + let hasData = sectionData.size > 0; + this.createSubsection(section, hasData, table, dataDiv); + } + + // We use specialized rendering here to make the addon and plugin listings + // more readable. + this.createAddonSection(dataDiv, ping); + }, + + createSubsection: function(title, hasSubdata, subSectionData, dataDiv) { + let dataSection = document.createElement("section"); + dataSection.classList.add("data-subsection"); + + if (hasSubdata) { + dataSection.classList.add("has-subdata"); + } + + // Create section heading + let sectionName = document.createElement("h2"); + sectionName.setAttribute("class", "section-name"); + sectionName.appendChild(document.createTextNode(title)); + sectionName.addEventListener("click", toggleSection, false); + + // Create caption for toggling the subsection visibility. + let toggleCaption = document.createElement("span"); + toggleCaption.setAttribute("class", "toggle-caption"); + let toggleText = bundle.GetStringFromName("environmentDataSubsectionToggle"); + toggleCaption.appendChild(document.createTextNode(" " + toggleText)); + toggleCaption.addEventListener("click", toggleSection, false); + + // Create caption for empty subsections. + let emptyCaption = document.createElement("span"); + emptyCaption.setAttribute("class", "empty-caption"); + let emptyText = bundle.GetStringFromName("environmentDataSubsectionEmpty"); + emptyCaption.appendChild(document.createTextNode(" " + emptyText)); + + // Create data container + let data = document.createElement("div"); + data.setAttribute("class", "subsection-data subdata"); + data.appendChild(subSectionData); + + // Append elements + dataSection.appendChild(sectionName); + dataSection.appendChild(toggleCaption); + dataSection.appendChild(emptyCaption); + dataSection.appendChild(data); + + dataDiv.appendChild(dataSection); + }, + + renderPersona: function(addonObj, addonSection, sectionTitle) { + let table = document.createElement("table"); + table.setAttribute("id", sectionTitle); + this.appendAddonSubsectionTitle(sectionTitle, table); + this.appendRow(table, "persona", addonObj.persona); + addonSection.appendChild(table); + }, + + renderActivePlugins: function(addonObj, addonSection, sectionTitle) { + let data = explodeObject(addonObj); + let table = document.createElement("table"); + table.setAttribute("id", sectionTitle); + this.appendAddonSubsectionTitle(sectionTitle, table); + + for (let plugin of addonObj) { + let data = explodeObject(plugin); + this.appendHeadingName(table, data.get("name")); + + for (let [key, value] of data) { + this.appendRow(table, key, value); + } + } + + addonSection.appendChild(table); + }, + + renderAddonsObject: function(addonObj, addonSection, sectionTitle) { + let table = document.createElement("table"); + table.setAttribute("id", sectionTitle); + this.appendAddonSubsectionTitle(sectionTitle, table); + + for (let id of Object.keys(addonObj)) { + let addon = addonObj[id]; + this.appendHeadingName(table, addon.name || id); + this.appendAddonID(table, id); + let data = explodeObject(addon); + + for (let [key, value] of data) { + this.appendRow(table, key, value); + } + } + + addonSection.appendChild(table); + }, + + renderKeyValueObject: function(addonObj, addonSection, sectionTitle) { + let data = explodeObject(addonObj); + let table = document.createElement("table"); + table.setAttribute("class", sectionTitle); + this.appendAddonSubsectionTitle(sectionTitle, table); + this.appendHeading(table); + + for (let [key, value] of data) { + this.appendRow(table, key, value); + } + + addonSection.appendChild(table); + }, + + appendAddonID: function(table, addonID) { + this.appendRow(table, "id", addonID); + }, + + appendHeading: function(table) { + let headings = document.createElement("tr"); + this.appendColumn(headings, "th", bundle.GetStringFromName("environmentDataHeadingName")); + this.appendColumn(headings, "th", bundle.GetStringFromName("environmentDataHeadingValue")); + table.appendChild(headings); + }, + + appendHeadingName: function(table, name) { + let headings = document.createElement("tr"); + this.appendColumn(headings, "th", name); + headings.cells[0].colSpan = 2; + table.appendChild(headings); + }, + + appendAddonSubsectionTitle: function(section, table) { + let caption = document.createElement("caption"); + caption.setAttribute("class", "addon-caption"); + caption.appendChild(document.createTextNode(section)); + table.appendChild(caption); + }, + + createAddonSection: function(dataDiv, ping) { + let addonSection = document.createElement("div"); + let addons = ping.environment.addons; + this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons"); + this.renderActivePlugins(addons.activePlugins, addonSection, "activePlugins"); + this.renderKeyValueObject(addons.theme, addonSection, "theme"); + this.renderKeyValueObject(addons.activeExperiment, addonSection, "activeExperiment"); + this.renderAddonsObject(addons.activeGMPlugins, addonSection, "activeGMPlugins"); + this.renderPersona(addons, addonSection, "persona"); + + let hasAddonData = Object.keys(ping.environment.addons).length > 0; + this.createSubsection("addons", hasAddonData, addonSection, dataDiv); + }, + + appendRow: function(table, id, value) { + let row = document.createElement("tr"); + this.appendColumn(row, "td", id); + this.appendColumn(row, "td", value); + table.appendChild(row); + }, + /** + * Helper function for appending a column to the data table. + * + * @param aRowElement Parent row element + * @param aColType Column's tag name + * @param aColText Column contents + */ + appendColumn: function(aRowElement, aColType, aColText) { + let colElement = document.createElement(aColType); + let colTextElement = document.createTextNode(aColText); + colElement.appendChild(colTextElement); + aRowElement.appendChild(colElement); + }, +}; + +var TelLog = { + /** + * Renders the telemetry log + */ + render: function(aPing) { + let entries = aPing.payload.log; + const hasData = entries && entries.length > 0; + setHasData("telemetry-log-section", hasData); + if (!hasData) { + return; + } + + let table = document.createElement("table"); + + let caption = document.createElement("caption"); + let captionString = bundle.GetStringFromName("telemetryLogTitle"); + caption.appendChild(document.createTextNode(captionString + "\n")); + table.appendChild(caption); + + let headings = document.createElement("tr"); + this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingId") + "\t"); + this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingTimestamp") + "\t"); + this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingData") + "\t"); + table.appendChild(headings); + + for (let entry of entries) { + let row = document.createElement("tr"); + for (let elem of entry) { + this.appendColumn(row, "td", elem + "\t"); + } + table.appendChild(row); + } + + let dataDiv = document.getElementById("telemetry-log"); + removeAllChildNodes(dataDiv); + dataDiv.appendChild(table); + }, + + /** + * Helper function for appending a column to the data table. + * + * @param aRowElement Parent row element + * @param aColType Column's tag name + * @param aColText Column contents + */ + appendColumn: function(aRowElement, aColType, aColText) { + let colElement = document.createElement(aColType); + let colTextElement = document.createTextNode(aColText); + colElement.appendChild(colTextElement); + aRowElement.appendChild(colElement); + }, +}; + +var SlowSQL = { + + slowSqlHits: bundle.GetStringFromName("slowSqlHits"), + + slowSqlAverage: bundle.GetStringFromName("slowSqlAverage"), + + slowSqlStatement: bundle.GetStringFromName("slowSqlStatement"), + + mainThreadTitle: bundle.GetStringFromName("slowSqlMain"), + + otherThreadTitle: bundle.GetStringFromName("slowSqlOther"), + + /** + * Render slow SQL statistics + */ + render: function SlowSQL_render(aPing) { + // We can add the debug SQL data to the current ping later. + // However, we need to be careful to never send that debug data + // out due to privacy concerns. + // We want to show the actual ping data for archived pings, + // so skip this there. + let debugSlowSql = PingPicker.viewCurrentPingData && Preferences.get(PREF_DEBUG_SLOW_SQL, false); + let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL; + if (!slowSql) { + setHasData("slow-sql-section", false); + return; + } + + let {mainThread, otherThreads} = + debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL; + + let mainThreadCount = Object.keys(mainThread).length; + let otherThreadCount = Object.keys(otherThreads).length; + if (mainThreadCount == 0 && otherThreadCount == 0) { + setHasData("slow-sql-section", false); + return; + } + + setHasData("slow-sql-section", true); + if (debugSlowSql) { + document.getElementById("sql-warning").classList.remove("hidden"); + } + + let slowSqlDiv = document.getElementById("slow-sql-tables"); + removeAllChildNodes(slowSqlDiv); + + // Main thread + if (mainThreadCount > 0) { + let table = document.createElement("table"); + this.renderTableHeader(table, this.mainThreadTitle); + this.renderTable(table, mainThread); + + slowSqlDiv.appendChild(table); + slowSqlDiv.appendChild(document.createElement("hr")); + } + + // Other threads + if (otherThreadCount > 0) { + let table = document.createElement("table"); + this.renderTableHeader(table, this.otherThreadTitle); + this.renderTable(table, otherThreads); + + slowSqlDiv.appendChild(table); + slowSqlDiv.appendChild(document.createElement("hr")); + } + }, + + /** + * Creates a header row for a Slow SQL table + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param aTable Parent table element + * @param aTitle Table's title + */ + renderTableHeader: function SlowSQL_renderTableHeader(aTable, aTitle) { + let caption = document.createElement("caption"); + caption.appendChild(document.createTextNode(aTitle + "\n")); + aTable.appendChild(caption); + + let headings = document.createElement("tr"); + this.appendColumn(headings, "th", this.slowSqlHits + "\t"); + this.appendColumn(headings, "th", this.slowSqlAverage + "\t"); + this.appendColumn(headings, "th", this.slowSqlStatement + "\n"); + aTable.appendChild(headings); + }, + + /** + * Fills out the table body + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param aTable Parent table element + * @param aSql SQL stats object + */ + renderTable: function SlowSQL_renderTable(aTable, aSql) { + for (let [sql, [hitCount, totalTime]] of Object.entries(aSql)) { + let averageTime = totalTime / hitCount; + + let sqlRow = document.createElement("tr"); + + this.appendColumn(sqlRow, "td", hitCount + "\t"); + this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t"); + this.appendColumn(sqlRow, "td", sql + "\n"); + + aTable.appendChild(sqlRow); + } + }, + + /** + * Helper function for appending a column to a Slow SQL table. + * + * @param aRowElement Parent row element + * @param aColType Column's tag name + * @param aColText Column contents + */ + appendColumn: function SlowSQL_appendColumn(aRowElement, aColType, aColText) { + let colElement = document.createElement(aColType); + let colTextElement = document.createTextNode(aColText); + colElement.appendChild(colTextElement); + aRowElement.appendChild(colElement); + } +}; + +var StackRenderer = { + + stackTitle: bundle.GetStringFromName("stackTitle"), + + memoryMapTitle: bundle.GetStringFromName("memoryMapTitle"), + + /** + * Outputs the memory map associated with this hang report + * + * @param aDiv Output div + */ + renderMemoryMap: function StackRenderer_renderMemoryMap(aDiv, memoryMap) { + aDiv.appendChild(document.createTextNode(this.memoryMapTitle)); + aDiv.appendChild(document.createElement("br")); + + for (let currentModule of memoryMap) { + aDiv.appendChild(document.createTextNode(currentModule.join(" "))); + aDiv.appendChild(document.createElement("br")); + } + + aDiv.appendChild(document.createElement("br")); + }, + + /** + * Outputs the raw PCs from the hang's stack + * + * @param aDiv Output div + * @param aStack Array of PCs from the hang stack + */ + renderStack: function StackRenderer_renderStack(aDiv, aStack) { + aDiv.appendChild(document.createTextNode(this.stackTitle)); + let stackText = " " + aStack.join(" "); + aDiv.appendChild(document.createTextNode(stackText)); + + aDiv.appendChild(document.createElement("br")); + aDiv.appendChild(document.createElement("br")); + }, + renderStacks: function StackRenderer_renderStacks(aPrefix, aStacks, + aMemoryMap, aRenderHeader) { + let div = document.getElementById(aPrefix + '-data'); + removeAllChildNodes(div); + + let fetchE = document.getElementById(aPrefix + '-fetch-symbols'); + if (fetchE) { + fetchE.classList.remove("hidden"); + } + let hideE = document.getElementById(aPrefix + '-hide-symbols'); + if (hideE) { + hideE.classList.add("hidden"); + } + + if (aStacks.length == 0) { + return; + } + + setHasData(aPrefix + '-section', true); + + this.renderMemoryMap(div, aMemoryMap); + + for (let i = 0; i < aStacks.length; ++i) { + let stack = aStacks[i]; + aRenderHeader(i); + this.renderStack(div, stack) + } + }, + + /** + * Renders the title of the stack: e.g. "Late Write #1" or + * "Hang Report #1 (6 seconds)". + * + * @param aFormatArgs formating args to be passed to formatStringFromName. + */ + renderHeader: function StackRenderer_renderHeader(aPrefix, aFormatArgs) { + let div = document.getElementById(aPrefix + "-data"); + + let titleElement = document.createElement("span"); + titleElement.className = "stack-title"; + + let titleText = bundle.formatStringFromName( + aPrefix + "-title", aFormatArgs, aFormatArgs.length); + titleElement.appendChild(document.createTextNode(titleText)); + + div.appendChild(titleElement); + div.appendChild(document.createElement("br")); + } +}; + +var RawPayload = { + /** + * Renders the raw payload + */ + render: function(aPing) { + setHasData("raw-payload-section", true); + let pre = document.getElementById("raw-payload-data-pre"); + pre.textContent = JSON.stringify(aPing.payload, null, 2); + } +}; + +function SymbolicationRequest(aPrefix, aRenderHeader, + aMemoryMap, aStacks, aDurations = null) { + this.prefix = aPrefix; + this.renderHeader = aRenderHeader; + this.memoryMap = aMemoryMap; + this.stacks = aStacks; + this.durations = aDurations; +} +/** + * A callback for onreadystatechange. It replaces the numeric stack with + * the symbolicated one returned by the symbolication server. + */ +SymbolicationRequest.prototype.handleSymbolResponse = +function SymbolicationRequest_handleSymbolResponse() { + if (this.symbolRequest.readyState != 4) + return; + + let fetchElement = document.getElementById(this.prefix + "-fetch-symbols"); + fetchElement.classList.add("hidden"); + let hideElement = document.getElementById(this.prefix + "-hide-symbols"); + hideElement.classList.remove("hidden"); + let div = document.getElementById(this.prefix + "-data"); + removeAllChildNodes(div); + let errorMessage = bundle.GetStringFromName("errorFetchingSymbols"); + + if (this.symbolRequest.status != 200) { + div.appendChild(document.createTextNode(errorMessage)); + return; + } + + let jsonResponse = {}; + try { + jsonResponse = JSON.parse(this.symbolRequest.responseText); + } catch (e) { + div.appendChild(document.createTextNode(errorMessage)); + return; + } + + for (let i = 0; i < jsonResponse.length; ++i) { + let stack = jsonResponse[i]; + this.renderHeader(i, this.durations); + + for (let symbol of stack) { + div.appendChild(document.createTextNode(symbol)); + div.appendChild(document.createElement("br")); + } + div.appendChild(document.createElement("br")); + } +}; +/** + * Send a request to the symbolication server to symbolicate this stack. + */ +SymbolicationRequest.prototype.fetchSymbols = +function SymbolicationRequest_fetchSymbols() { + let symbolServerURI = + Preferences.get(PREF_SYMBOL_SERVER_URI, DEFAULT_SYMBOL_SERVER_URI); + let request = {"memoryMap" : this.memoryMap, "stacks" : this.stacks, + "version" : 3}; + let requestJSON = JSON.stringify(request); + + this.symbolRequest = new XMLHttpRequest(); + this.symbolRequest.open("POST", symbolServerURI, true); + this.symbolRequest.setRequestHeader("Content-type", "application/json"); + this.symbolRequest.setRequestHeader("Content-length", + requestJSON.length); + this.symbolRequest.setRequestHeader("Connection", "close"); + this.symbolRequest.onreadystatechange = this.handleSymbolResponse.bind(this); + this.symbolRequest.send(requestJSON); +} + +var ChromeHangs = { + + symbolRequest: null, + + /** + * Renders raw chrome hang data + */ + render: function ChromeHangs_render(aPing) { + let hangs = aPing.payload.chromeHangs; + setHasData("chrome-hangs-section", !!hangs); + if (!hangs) { + return; + } + + let stacks = hangs.stacks; + let memoryMap = hangs.memoryMap; + let durations = hangs.durations; + + StackRenderer.renderStacks("chrome-hangs", stacks, memoryMap, + (index) => this.renderHangHeader(index, durations)); + }, + + renderHangHeader: function ChromeHangs_renderHangHeader(aIndex, aDurations) { + StackRenderer.renderHeader("chrome-hangs", [aIndex + 1, aDurations[aIndex]]); + } +}; + +var ThreadHangStats = { + + /** + * Renders raw thread hang stats data + */ + render: function(aPayload) { + let div = document.getElementById("thread-hang-stats"); + removeAllChildNodes(div); + + let stats = aPayload.threadHangStats; + setHasData("thread-hang-stats-section", stats && (stats.length > 0)); + if (!stats) { + return; + } + + stats.forEach((thread) => { + div.appendChild(this.renderThread(thread)); + }); + }, + + /** + * Creates and fills data corresponding to a thread + */ + renderThread: function(aThread) { + let div = document.createElement("div"); + + let title = document.createElement("h2"); + title.textContent = aThread.name; + div.appendChild(title); + + // Don't localize the histogram name, because the + // name is also used as the div element's ID + Histogram.render(div, aThread.name + "-Activity", + aThread.activity, {exponential: true}, true); + aThread.hangs.forEach((hang, index) => { + let hangName = aThread.name + "-Hang-" + (index + 1); + let hangDiv = Histogram.render( + div, hangName, hang.histogram, {exponential: true}, true); + let stackDiv = document.createElement("div"); + let stack = hang.nativeStack || hang.stack; + stack.forEach((frame) => { + stackDiv.appendChild(document.createTextNode(frame)); + // Leave an extra <br> at the end of the stack listing + stackDiv.appendChild(document.createElement("br")); + }); + // Insert stack after the histogram title + hangDiv.insertBefore(stackDiv, hangDiv.childNodes[1]); + }); + return div; + }, +}; + +var Histogram = { + + hgramSamplesCaption: bundle.GetStringFromName("histogramSamples"), + + hgramAverageCaption: bundle.GetStringFromName("histogramAverage"), + + hgramSumCaption: bundle.GetStringFromName("histogramSum"), + + hgramCopyCaption: bundle.GetStringFromName("histogramCopy"), + + /** + * Renders a single Telemetry histogram + * + * @param aParent Parent element + * @param aName Histogram name + * @param aHgram Histogram information + * @param aOptions Object with render options + * * exponential: bars follow logarithmic scale + * @param aIsBHR whether or not requires fixing the labels for TimeHistogram + */ + render: function Histogram_render(aParent, aName, aHgram, aOptions, aIsBHR) { + let options = aOptions || {}; + let hgram = this.processHistogram(aHgram, aName, aIsBHR); + + let outerDiv = document.createElement("div"); + outerDiv.className = "histogram"; + outerDiv.id = aName; + + let divTitle = document.createElement("div"); + divTitle.className = "histogram-title"; + divTitle.appendChild(document.createTextNode(aName)); + outerDiv.appendChild(divTitle); + + let stats = hgram.sample_count + " " + this.hgramSamplesCaption + ", " + + this.hgramAverageCaption + " = " + hgram.pretty_average + ", " + + this.hgramSumCaption + " = " + hgram.sum; + + let divStats = document.createElement("div"); + divStats.appendChild(document.createTextNode(stats)); + outerDiv.appendChild(divStats); + + if (isRTL()) { + hgram.buckets.reverse(); + hgram.values.reverse(); + } + + let textData = this.renderValues(outerDiv, hgram, options); + + // The 'Copy' button contains the textual data, copied to clipboard on click + let copyButton = document.createElement("button"); + copyButton.className = "copy-node"; + copyButton.appendChild(document.createTextNode(this.hgramCopyCaption)); + copyButton.histogramText = aName + EOL + stats + EOL + EOL + textData; + copyButton.addEventListener("click", function() { + Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper) + .copyString(this.histogramText); + }); + outerDiv.appendChild(copyButton); + + aParent.appendChild(outerDiv); + return outerDiv; + }, + + processHistogram: function(aHgram, aName, aIsBHR) { + const values = Object.keys(aHgram.values).map(k => aHgram.values[k]); + if (!values.length) { + // If we have no values collected for this histogram, just return + // zero values so we still render it. + return { + values: [], + pretty_average: 0, + max: 0, + sample_count: 0, + sum: 0 + }; + } + + const sample_count = values.reduceRight((a, b) => a + b); + const average = Math.round(aHgram.sum * 10 / sample_count) / 10; + const max_value = Math.max(...values); + + function labelFunc(k) { + // - BHR histograms are TimeHistograms: Exactly power-of-two buckets (from 0) + // (buckets: [0..1], [2..3], [4..7], [8..15], ... note the 0..1 anomaly - same bucket) + // - TimeHistogram's JS representation adds a dummy (empty) "0" bucket, and + // the rest of the buckets have the label as the upper value of the + // bucket (non TimeHistograms have the lower value of the bucket as label). + // So JS TimeHistograms bucket labels are: 0 (dummy), 1, 3, 7, 15, ... + // - see toolkit/components/telemetry/Telemetry.cpp + // (CreateJSTimeHistogram, CreateJSThreadHangStats, CreateJSHangHistogram) + // - see toolkit/components/telemetry/ThreadHangStats.h + // Fix BHR labels to the "standard" format for about:telemetry as follows: + // - The dummy 0 label+bucket will be filtered before arriving here + // - If it's 1 -> manually correct it to 0 (the 0..1 anomaly) + // - For the rest, set the label as the bottom value instead of the upper. + // --> so we'll end with the following (non dummy) labels: 0, 2, 4, 8, 16, ... + if (!aIsBHR) { + return k; + } + return k == 1 ? 0 : (k + 1) / 2; + } + + const labelledValues = Object.keys(aHgram.values) + .filter(label => !aIsBHR || Number(label) != 0) // remove dummy 0 label for BHR + .map(k => [labelFunc(Number(k)), aHgram.values[k]]); + + let result = { + values: labelledValues, + pretty_average: average, + max: max_value, + sample_count: sample_count, + sum: aHgram.sum + }; + + return result; + }, + + /** + * Return a non-negative, logarithmic representation of a non-negative number. + * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3 + * + * @param aNumber Non-negative number + */ + getLogValue: function(aNumber) { + return Math.max(0, Math.log10(aNumber) + 1); + }, + + /** + * Create histogram HTML bars, also returns a textual representation + * Both aMaxValue and aSumValues must be positive. + * Values are assumed to use 0 as baseline. + * + * @param aDiv Outer parent div + * @param aHgram The histogram data + * @param aOptions Object with render options (@see #render) + */ + renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) { + let text = ""; + // If the last label is not the longest string, alignment will break a little + let labelPadTo = 0; + if (aHgram.values.length) { + labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length; + } + let maxBarValue = aOptions.exponential ? this.getLogValue(aHgram.max) : aHgram.max; + + for (let [label, value] of aHgram.values) { + let barValue = aOptions.exponential ? this.getLogValue(value) : value; + + // Create a text representation: <right-aligned-label> |<bar-of-#><value> <percentage> + text += EOL + + " ".repeat(Math.max(0, labelPadTo - String(label).length)) + label // Right-aligned label + + " |" + "#".repeat(Math.round(MAX_BAR_CHARS * barValue / maxBarValue)) // Bar + + " " + value // Value + + " " + Math.round(100 * value / aHgram.sample_count) + "%"; // Percentage + + // Construct the HTML labels + bars + let belowEm = Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10; + let aboveEm = MAX_BAR_HEIGHT - belowEm; + + let barDiv = document.createElement("div"); + barDiv.className = "bar"; + barDiv.style.paddingTop = aboveEm + "em"; + + // Add value label or an nbsp if no value + barDiv.appendChild(document.createTextNode(value ? value : '\u00A0')); + + // Create the blue bar + let bar = document.createElement("div"); + bar.className = "bar-inner"; + bar.style.height = belowEm + "em"; + barDiv.appendChild(bar); + + // Add bucket label + barDiv.appendChild(document.createTextNode(label)); + + aDiv.appendChild(barDiv); + } + + return text.substr(EOL.length); // Trim the EOL before the first line + }, + + /** + * Helper function for filtering histogram elements by their id + * Adds the "filter-blocked" class to histogram nodes whose IDs don't match the filter. + * + * @param aContainerNode Container node containing the histogram class nodes to filter + * @param aFilterText either text or /RegEx/. If text, case-insensitive and AND words + */ + filterHistograms: function _filterHistograms(aContainerNode, aFilterText) { + let filter = aFilterText.toString(); + + // Pass if: all non-empty array items match (case-sensitive) + function isPassText(subject, filter) { + for (let item of filter) { + if (item.length && subject.indexOf(item) < 0) { + return false; // mismatch and not a spurious space + } + } + return true; + } + + function isPassRegex(subject, filter) { + return filter.test(subject); + } + + // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx) + let isPassFunc; // filter function, set once, then applied to all elements + filter = filter.trim(); + if (filter[0] != "/") { // Plain text: case insensitive, AND if multi-string + isPassFunc = isPassText; + filter = filter.toLowerCase().split(" "); + } else { + isPassFunc = isPassRegex; + var r = filter.match(/^\/(.*)\/(i?)$/); + try { + filter = RegExp(r[1], r[2]); + } + catch (e) { // Incomplete or bad RegExp - always no match + isPassFunc = function() { + return false; + }; + } + } + + let needLower = (isPassFunc === isPassText); + + let histograms = aContainerNode.getElementsByClassName("histogram"); + for (let hist of histograms) { + hist.classList[isPassFunc((needLower ? hist.id.toLowerCase() : hist.id), filter) ? "remove" : "add"]("filter-blocked"); + } + }, + + /** + * Event handler for change at histograms filter input + * + * When invoked, 'this' is expected to be the filter HTML node. + */ + histogramFilterChanged: function _histogramFilterChanged() { + if (this.idleTimeout) { + clearTimeout(this.idleTimeout); + } + + this.idleTimeout = setTimeout( () => { + Histogram.filterHistograms(document.getElementById(this.getAttribute("target_id")), this.value); + }, FILTER_IDLE_TIMEOUT); + } +}; + +/* + * Helper function to render JS objects with white space between top level elements + * so that they look better in the browser + * @param aObject JavaScript object or array to render + * @return String + */ +function RenderObject(aObject) { + let output = ""; + if (Array.isArray(aObject)) { + if (aObject.length == 0) { + return "[]"; + } + output = "[" + JSON.stringify(aObject[0]); + for (let i = 1; i < aObject.length; i++) { + output += ", " + JSON.stringify(aObject[i]); + } + return output + "]"; + } + let keys = Object.keys(aObject); + if (keys.length == 0) { + return "{}"; + } + output = "{\"" + keys[0] + "\":\u00A0" + JSON.stringify(aObject[keys[0]]); + for (let i = 1; i < keys.length; i++) { + output += ", \"" + keys[i] + "\":\u00A0" + JSON.stringify(aObject[keys[i]]); + } + return output + "}"; +} + +var KeyValueTable = { + /** + * Returns a 2-column table with keys and values + * @param aMeasurements Each key in this JS object is rendered as a row in + * the table with its corresponding value + * @param aKeysLabel Column header for the keys column + * @param aValuesLabel Column header for the values column + */ + render: function KeyValueTable_render(aMeasurements, aKeysLabel, aValuesLabel) { + let table = document.createElement("table"); + this.renderHeader(table, aKeysLabel, aValuesLabel); + this.renderBody(table, aMeasurements); + return table; + }, + + /** + * Create the table header + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param aTable Table element + * @param aKeysLabel Column header for the keys column + * @param aValuesLabel Column header for the values column + */ + renderHeader: function KeyValueTable_renderHeader(aTable, aKeysLabel, aValuesLabel) { + let headerRow = document.createElement("tr"); + aTable.appendChild(headerRow); + + let keysColumn = document.createElement("th"); + keysColumn.appendChild(document.createTextNode(aKeysLabel + "\t")); + let valuesColumn = document.createElement("th"); + valuesColumn.appendChild(document.createTextNode(aValuesLabel + "\n")); + + headerRow.appendChild(keysColumn); + headerRow.appendChild(valuesColumn); + }, + + /** + * Create the table body + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param aTable Table element + * @param aMeasurements Key/value map + */ + renderBody: function KeyValueTable_renderBody(aTable, aMeasurements) { + for (let [key, value] of Object.entries(aMeasurements)) { + // use .valueOf() to unbox Number, String, etc. objects + if (value && + (typeof value == "object") && + (typeof value.valueOf() == "object")) { + value = RenderObject(value); + } + + let newRow = document.createElement("tr"); + aTable.appendChild(newRow); + + let keyField = document.createElement("td"); + keyField.appendChild(document.createTextNode(key + "\t")); + newRow.appendChild(keyField); + + let valueField = document.createElement("td"); + valueField.appendChild(document.createTextNode(value + "\n")); + newRow.appendChild(valueField); + } + } +}; + +var GenericTable = { + /** + * Returns a n-column table. + * @param rows An array of arrays, each containing data to render + * for one row. + * @param headings The column header strings. + */ + render: function(rows, headings) { + let table = document.createElement("table"); + this.renderHeader(table, headings); + this.renderBody(table, rows); + return table; + }, + + /** + * Create the table header. + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param table Table element + * @param headings Array of column header strings. + */ + renderHeader: function(table, headings) { + let headerRow = document.createElement("tr"); + table.appendChild(headerRow); + + for (let i = 0; i < headings.length; ++i) { + let suffix = (i == (headings.length - 1)) ? "\n" : "\t"; + let column = document.createElement("th"); + column.appendChild(document.createTextNode(headings[i] + suffix)); + headerRow.appendChild(column); + } + }, + + /** + * Create the table body + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param table Table element + * @param rows An array of arrays, each containing data to render + * for one row. + */ + renderBody: function(table, rows) { + for (let row of rows) { + row = row.map(value => { + // use .valueOf() to unbox Number, String, etc. objects + if (value && + (typeof value == "object") && + (typeof value.valueOf() == "object")) { + return RenderObject(value); + } + return value; + }); + + let newRow = document.createElement("tr"); + table.appendChild(newRow); + + for (let i = 0; i < row.length; ++i) { + let suffix = (i == (row.length - 1)) ? "\n" : "\t"; + let field = document.createElement("td"); + field.appendChild(document.createTextNode(row[i] + suffix)); + newRow.appendChild(field); + } + } + } +}; + +var KeyedHistogram = { + render: function(parent, id, keyedHistogram) { + let outerDiv = document.createElement("div"); + outerDiv.className = "keyed-histogram"; + outerDiv.id = id; + + let divTitle = document.createElement("div"); + divTitle.className = "keyed-histogram-title"; + divTitle.appendChild(document.createTextNode(id)); + outerDiv.appendChild(divTitle); + + for (let [name, hgram] of Object.entries(keyedHistogram)) { + Histogram.render(outerDiv, name, hgram); + } + + parent.appendChild(outerDiv); + return outerDiv; + }, +}; + +var AddonDetails = { + tableIDTitle: bundle.GetStringFromName("addonTableID"), + tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"), + + /** + * Render the addon details section as a series of headers followed by key/value tables + * @param aPing A ping object to render the data from. + */ + render: function AddonDetails_render(aPing) { + let addonSection = document.getElementById("addon-details"); + removeAllChildNodes(addonSection); + let addonDetails = aPing.payload.addonDetails; + const hasData = addonDetails && Object.keys(addonDetails).length > 0; + setHasData("addon-details-section", hasData); + if (!hasData) { + return; + } + + for (let provider in addonDetails) { + let providerSection = document.createElement("h2"); + let titleText = bundle.formatStringFromName("addonProvider", [provider], 1); + providerSection.appendChild(document.createTextNode(titleText)); + addonSection.appendChild(providerSection); + addonSection.appendChild( + KeyValueTable.render(addonDetails[provider], + this.tableIDTitle, this.tableDetailsTitle)); + } + } +}; + +var Scalars = { + /** + * Render the scalar data - if present - from the payload in a simple key-value table. + * @param aPayload A payload object to render the data from. + */ + render: function(aPayload) { + let scalarsSection = document.getElementById("scalars"); + removeAllChildNodes(scalarsSection); + + if (!aPayload.processes || !aPayload.processes.parent) { + return; + } + + let scalars = aPayload.processes.parent.scalars; + const hasData = scalars && Object.keys(scalars).length > 0; + setHasData("scalars-section", hasData); + if (!hasData) { + return; + } + + const headingName = bundle.GetStringFromName("namesHeader"); + const headingValue = bundle.GetStringFromName("valuesHeader"); + const table = KeyValueTable.render(scalars, headingName, headingValue); + scalarsSection.appendChild(table); + } +}; + +var KeyedScalars = { + /** + * Render the keyed scalar data - if present - from the payload in a simple key-value table. + * @param aPayload A payload object to render the data from. + */ + render: function(aPayload) { + let scalarsSection = document.getElementById("keyed-scalars"); + removeAllChildNodes(scalarsSection); + + if (!aPayload.processes || !aPayload.processes.parent) { + return; + } + + let keyedScalars = aPayload.processes.parent.keyedScalars; + const hasData = keyedScalars && Object.keys(keyedScalars).length > 0; + setHasData("keyed-scalars-section", hasData); + if (!hasData) { + return; + } + + const headingName = bundle.GetStringFromName("namesHeader"); + const headingValue = bundle.GetStringFromName("valuesHeader"); + for (let scalar in keyedScalars) { + // Add the name of the scalar. + let scalarNameSection = document.createElement("h2"); + scalarNameSection.appendChild(document.createTextNode(scalar)); + scalarsSection.appendChild(scalarNameSection); + // Populate the section with the key-value pairs from the scalar. + const table = KeyValueTable.render(keyedScalars[scalar], headingName, headingValue); + scalarsSection.appendChild(table); + } + } +}; + +var Events = { + /** + * Render the event data - if present - from the payload in a simple table. + * @param aPayload A payload object to render the data from. + */ + render: function(aPayload) { + let eventsSection = document.getElementById("events"); + removeAllChildNodes(eventsSection); + + if (!aPayload.processes || !aPayload.processes.parent) { + return; + } + + const events = aPayload.processes.parent.events; + const hasData = events && Object.keys(events).length > 0; + setHasData("events-section", hasData); + if (!hasData) { + return; + } + + const headings = [ + "timestamp", + "category", + "method", + "object", + "value", + "extra", + ]; + + const table = GenericTable.render(events, headings); + eventsSection.appendChild(table); + } +}; + +/** + * Helper function for showing either the toggle element or "No data collected" message for a section + * + * @param aSectionID ID of the section element that needs to be changed + * @param aHasData true (default) indicates that toggle should be displayed + */ +function setHasData(aSectionID, aHasData) { + let sectionElement = document.getElementById(aSectionID); + sectionElement.classList[aHasData ? "add" : "remove"]("has-data"); +} + +/** + * Helper function that expands and collapses sections + + * changes caption on the toggle text + */ +function toggleSection(aEvent) { + let parentElement = aEvent.target.parentElement; + if (!parentElement.classList.contains("has-data") && + !parentElement.classList.contains("has-subdata")) { + return; // nothing to toggle + } + + parentElement.classList.toggle("expanded"); + + // Store section opened/closed state in a hidden checkbox (which is then used on reload) + let statebox = parentElement.getElementsByClassName("statebox")[0]; + if (statebox) { + statebox.checked = parentElement.classList.contains("expanded"); + } +} + +/** + * Sets the text of the page header based on a config pref + bundle strings + */ +function setupPageHeader() +{ + let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla"); + let brandName = brandBundle.GetStringFromName("brandFullName"); + let subtitleText = bundle.formatStringFromName( + "pageSubtitle", [serverOwner, brandName], 2); + + let subtitleElement = document.getElementById("page-subtitle"); + subtitleElement.appendChild(document.createTextNode(subtitleText)); +} + +/** + * Initializes load/unload, pref change and mouse-click listeners + */ +function setupListeners() { + Settings.attachObservers(); + PingPicker.attachObservers(); + + // Clean up observers when page is closed + window.addEventListener("unload", + function unloadHandler(aEvent) { + window.removeEventListener("unload", unloadHandler); + Settings.detachObservers(); + }, false); + + document.getElementById("chrome-hangs-fetch-symbols").addEventListener("click", + function () { + if (!gPingData) { + return; + } + + let hangs = gPingData.payload.chromeHangs; + let req = new SymbolicationRequest("chrome-hangs", + ChromeHangs.renderHangHeader, + hangs.memoryMap, + hangs.stacks, + hangs.durations); + req.fetchSymbols(); + }, false); + + document.getElementById("chrome-hangs-hide-symbols").addEventListener("click", + function () { + if (!gPingData) { + return; + } + + ChromeHangs.render(gPingData); + }, false); + + document.getElementById("late-writes-fetch-symbols").addEventListener("click", + function () { + if (!gPingData) { + return; + } + + let lateWrites = gPingData.payload.lateWrites; + let req = new SymbolicationRequest("late-writes", + LateWritesSingleton.renderHeader, + lateWrites.memoryMap, + lateWrites.stacks); + req.fetchSymbols(); + }, false); + + document.getElementById("late-writes-hide-symbols").addEventListener("click", + function () { + if (!gPingData) { + return; + } + + LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites); + }, false); + + // Clicking on the section name will toggle its state + let sectionHeaders = document.getElementsByClassName("section-name"); + for (let sectionHeader of sectionHeaders) { + sectionHeader.addEventListener("click", toggleSection, false); + } + + // Clicking on the "toggle" text will also toggle section's state + let toggleLinks = document.getElementsByClassName("toggle-caption"); + for (let toggleLink of toggleLinks) { + toggleLink.addEventListener("click", toggleSection, false); + } +} + +function onLoad() { + window.removeEventListener("load", onLoad); + + // Set the text in the page header + setupPageHeader(); + + // Set up event listeners + setupListeners(); + + // Render settings. + Settings.render(); + + // Restore sections states + let stateboxes = document.getElementsByClassName("statebox"); + for (let box of stateboxes) { + if (box.checked) { // Was open. Will still display as empty if not has-data + box.parentElement.classList.add("expanded"); + } + } + + // Update ping data when async Telemetry init is finished. + Telemetry.asyncFetchTelemetryData(() => PingPicker.update()); +} + +var LateWritesSingleton = { + renderHeader: function LateWritesSingleton_renderHeader(aIndex) { + StackRenderer.renderHeader("late-writes", [aIndex + 1]); + }, + + renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) { + setHasData("late-writes-section", !!lateWrites); + if (!lateWrites) { + return; + } + + let stacks = lateWrites.stacks; + let memoryMap = lateWrites.memoryMap; + StackRenderer.renderStacks('late-writes', stacks, memoryMap, + LateWritesSingleton.renderHeader); + } +}; + +/** + * Helper function for sorting the startup milestones in the Simple Measurements + * section into temporal order. + * + * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data + * @return Sorted measurements + */ +function sortStartupMilestones(aSimpleMeasurements) { + const telemetryTimestamps = TelemetryTimestamps.get(); + let startupEvents = Services.startup.getStartupInfo(); + delete startupEvents['process']; + + function keyIsMilestone(k) { + return (k in startupEvents) || (k in telemetryTimestamps); + } + + let sortedKeys = Object.keys(aSimpleMeasurements); + + // Sort the measurements, with startup milestones at the front + ordered by time + sortedKeys.sort(function keyCompare(keyA, keyB) { + let isKeyAMilestone = keyIsMilestone(keyA); + let isKeyBMilestone = keyIsMilestone(keyB); + + // First order by startup vs non-startup measurement + if (isKeyAMilestone && !isKeyBMilestone) + return -1; + if (!isKeyAMilestone && isKeyBMilestone) + return 1; + // Don't change order of non-startup measurements + if (!isKeyAMilestone && !isKeyBMilestone) + return 0; + + // If both keys are startup measurements, order them by value + return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB]; + }); + + // Insert measurements into a result object in sort-order + let result = {}; + for (let key of sortedKeys) { + result[key] = aSimpleMeasurements[key]; + } + + return result; +} + +function renderProcessList(ping, selectEl) { + removeAllChildNodes(selectEl); + let option = document.createElement("option"); + option.appendChild(document.createTextNode("parent")); + option.setAttribute("value", ""); + option.selected = true; + selectEl.appendChild(option); + + if (!("processes" in ping.payload)) { + selectEl.disabled = true; + return; + } + selectEl.disabled = false; + + for (let process of Object.keys(ping.payload.processes)) { + // TODO: parent hgrams are on root payload, not in payload.processes.parent + // When/If that gets moved, you'll need to remove this: + if (process === "parent") { + continue; + } + option = document.createElement("option"); + option.appendChild(document.createTextNode(process)); + option.setAttribute("value", process); + selectEl.appendChild(option); + } +} + +function renderPayloadList(ping) { + // Rebuild the payload select with options: + // Parent Payload (selected) + // Child Payload 1..ping.payload.childPayloads.length + let listEl = document.getElementById("choose-payload"); + removeAllChildNodes(listEl); + + let option = document.createElement("option"); + let text = bundle.GetStringFromName("parentPayload"); + let content = document.createTextNode(text); + let payloadIndex = 0; + option.appendChild(content); + option.setAttribute("value", payloadIndex++); + option.selected = true; + listEl.appendChild(option); + + if (!ping.payload.childPayloads) { + listEl.disabled = true; + return + } + listEl.disabled = false; + + for (; payloadIndex <= ping.payload.childPayloads.length; ++payloadIndex) { + option = document.createElement("option"); + text = bundle.formatStringFromName("childPayloadN", [payloadIndex], 1); + content = document.createTextNode(text); + option.appendChild(content); + option.setAttribute("value", payloadIndex); + listEl.appendChild(option); + } +} + +function toggleElementHidden(element, isHidden) { + if (isHidden) { + element.classList.add("hidden"); + } else { + element.classList.remove("hidden"); + } +} + +function togglePingSections(isMainPing) { + // We always show the sections that are "common" to all pings. + // The raw payload section is only used for pings other than "main" and "saved-session". + let commonSections = new Set(["general-data-section", "environment-data-section"]); + let otherPingSections = new Set(["raw-payload-section"]); + + let elements = document.getElementById("structured-ping-data-section").children; + for (let section of elements) { + if (commonSections.has(section.id)) { + continue; + } + + let showElement = isMainPing != otherPingSections.has(section.id); + toggleElementHidden(section, !showElement); + } +} + +function displayPingData(ping, updatePayloadList = false) { + gPingData = ping; + + // Render raw ping data. + let pre = document.getElementById("raw-ping-data"); + pre.textContent = JSON.stringify(gPingData, null, 2); + + // Update the structured data rendering. + const keysHeader = bundle.GetStringFromName("keysHeader"); + const valuesHeader = bundle.GetStringFromName("valuesHeader"); + + // Update the payload list and process lists + if (updatePayloadList) { + renderPayloadList(ping); + renderProcessList(ping, document.getElementById("histograms-processes")); + renderProcessList(ping, document.getElementById("keyed-histograms-processes")); + } + + // Show general data. + GeneralData.render(ping); + + // Show environment data. + EnvironmentData.render(ping); + + // We only have special rendering code for the payloads from "main" pings. + // For any other pings we just render the raw JSON payload. + let isMainPing = (ping.type == "main" || ping.type == "saved-session"); + togglePingSections(isMainPing); + + if (!isMainPing) { + RawPayload.render(ping); + return; + } + + // Show telemetry log. + TelLog.render(ping); + + // Show slow SQL stats + SlowSQL.render(ping); + + // Show chrome hang stacks + ChromeHangs.render(ping); + + // Render Addon details. + AddonDetails.render(ping); + + // Select payload to render + let payloadSelect = document.getElementById("choose-payload"); + let payloadOption = payloadSelect.selectedOptions.item(0); + let payloadIndex = payloadOption.getAttribute("value"); + + let payload = ping.payload; + if (payloadIndex > 0) { + payload = ping.payload.childPayloads[payloadIndex - 1]; + } + + // Show thread hang stats + ThreadHangStats.render(payload); + + // Show simple measurements + let simpleMeasurements = sortStartupMilestones(payload.simpleMeasurements); + let hasData = Object.keys(simpleMeasurements).length > 0; + setHasData("simple-measurements-section", hasData); + let simpleSection = document.getElementById("simple-measurements"); + removeAllChildNodes(simpleSection); + + if (hasData) { + simpleSection.appendChild(KeyValueTable.render(simpleMeasurements, + keysHeader, valuesHeader)); + } + + LateWritesSingleton.renderLateWrites(payload.lateWrites); + + // Show basic session info gathered + hasData = Object.keys(ping.payload.info).length > 0; + setHasData("session-info-section", hasData); + let infoSection = document.getElementById("session-info"); + removeAllChildNodes(infoSection); + + if (hasData) { + infoSection.appendChild(KeyValueTable.render(ping.payload.info, + keysHeader, valuesHeader)); + } + + // Show scalar data. + Scalars.render(payload); + KeyedScalars.render(payload); + + // Show histogram data + let hgramDiv = document.getElementById("histograms"); + removeAllChildNodes(hgramDiv); + + let histograms = payload.histograms; + + let hgramsSelect = document.getElementById("histograms-processes"); + let hgramsOption = hgramsSelect.selectedOptions.item(0); + let hgramsProcess = hgramsOption.getAttribute("value"); + if (hgramsProcess && + "processes" in ping.payload && + hgramsProcess in ping.payload.processes) { + histograms = ping.payload.processes[hgramsProcess].histograms; + } + + hasData = Object.keys(histograms).length > 0; + setHasData("histograms-section", hasData || hgramsSelect.options.length); + + if (hasData) { + for (let [name, hgram] of Object.entries(histograms)) { + Histogram.render(hgramDiv, name, hgram, {unpacked: true}); + } + + let filterBox = document.getElementById("histograms-filter"); + filterBox.addEventListener("input", Histogram.histogramFilterChanged, false); + if (filterBox.value.trim() != "") { // on load, no need to filter if empty + Histogram.filterHistograms(hgramDiv, filterBox.value); + } + + setHasData("histograms-section", true); + } + + // Show keyed histogram data + let keyedDiv = document.getElementById("keyed-histograms"); + removeAllChildNodes(keyedDiv); + + let keyedHistograms = payload.keyedHistograms; + + let keyedHgramsSelect = document.getElementById("keyed-histograms-processes"); + let keyedHgramsOption = keyedHgramsSelect.selectedOptions.item(0); + let keyedHgramsProcess = keyedHgramsOption.getAttribute("value"); + if (keyedHgramsProcess && + "processes" in ping.payload && + keyedHgramsProcess in ping.payload.processes) { + keyedHistograms = ping.payload.processes[keyedHgramsProcess].keyedHistograms; + } + + setHasData("keyed-histograms-section", keyedHgramsSelect.options.length); + if (keyedHistograms) { + let hasData = false; + for (let [id, keyed] of Object.entries(keyedHistograms)) { + if (Object.keys(keyed).length > 0) { + hasData = true; + KeyedHistogram.render(keyedDiv, id, keyed, {unpacked: true}); + } + } + setHasData("keyed-histograms-section", hasData || keyedHgramsSelect.options.length); + } + + // Show event data. + Events.render(payload); + + // Show addon histogram data + let addonDiv = document.getElementById("addon-histograms"); + removeAllChildNodes(addonDiv); + + let addonHistogramsRendered = false; + let addonData = payload.addonHistograms; + if (addonData) { + for (let [addon, histograms] of Object.entries(addonData)) { + for (let [name, hgram] of Object.entries(histograms)) { + addonHistogramsRendered = true; + Histogram.render(addonDiv, addon + ": " + name, hgram, {unpacked: true}); + } + } + } + + setHasData("addon-histograms-section", addonHistogramsRendered); +} + +window.addEventListener("load", onLoad, false); diff --git a/toolkit/content/aboutTelemetry.xhtml b/toolkit/content/aboutTelemetry.xhtml new file mode 100644 index 0000000000..24b78b9933 --- /dev/null +++ b/toolkit/content/aboutTelemetry.xhtml @@ -0,0 +1,290 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- 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/. --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD; + <!ENTITY % aboutTelemetryDTD SYSTEM "chrome://global/locale/aboutTelemetry.dtd"> %aboutTelemetryDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&aboutTelemetry.pageTitle;</title> + + <link rel="stylesheet" href="chrome://global/content/aboutTelemetry.css" + type="text/css"/> + + <script type="application/javascript;version=1.7" + src="chrome://global/content/aboutTelemetry.js"/> + </head> + + <body dir="&locale.dir;"> + + <header id="page-description"> + <h1>&aboutTelemetry.pageTitle;</h1> + + <h2 id="page-subtitle"></h2> + + <table id="settings"> + <tr> + <td> + <p id="description-upload-enabled" class="description-enabled">&aboutTelemetry.uploadEnabled;</p> + <p id="description-upload-disabled" class="description-disabled">&aboutTelemetry.uploadDisabled;</p> + </td> + <td> + <a href="" class="change-data-choices-link">&aboutTelemetry.changeDataChoices;</a> + </td> + </tr> + <tr> + <td> + <p id="description-extended-recording-enabled" class="description-enabled">&aboutTelemetry.extendedRecordingEnabled;</p> + <p id="description-extended-recording-disabled" class="description-disabled">&aboutTelemetry.extendedRecordingDisabled;</p> + </td> + <td> + <a href="" class="change-data-choices-link">&aboutTelemetry.changeDataChoices;</a> + </td> + </tr> + </table> + + <div id="ping-picker"> + <div id="ping-source-picker"> + &aboutTelemetry.pingDataSource;<br/> + <input type="radio" id="ping-source-current" name="choose-ping-source" value="current" checked="checked" /> + &aboutTelemetry.showCurrentPingData;<br /> + <input type="radio" id="ping-source-archive" name="choose-ping-source" value="archive" /> + &aboutTelemetry.showArchivedPingData;<br /> + </div> + <div id="ping-source-picker"> + &aboutTelemetry.pingDataDisplay;<br/> + <input type="radio" id="ping-source-structured" name="choose-ping-display" value="structured" checked="checked" /> + &aboutTelemetry.structured;<br /> + <input type="radio" id="ping-source-raw" name="choose-ping-display" value="raw" /> + &aboutTelemetry.raw;<br /> + </div> + <div id="current-ping-picker"> + <input id="show-subsession-data" type="checkbox" checked="checked" />&aboutTelemetry.showSubsessionData; + </div> + <div id="archived-ping-picker" class="hidden"> + &aboutTelemetry.choosePing;<br /> + <button id="newer-ping" type="button">&aboutTelemetry.showNewerPing;</button> + <button id="older-ping" type="button">&aboutTelemetry.showOlderPing;</button><br /> + <table> + <tr> + <th>&aboutTelemetry.archiveWeekHeader;</th> + <th>&aboutTelemetry.archivePingHeader;</th> + </tr> + <tr> + <td> + <select id="choose-ping-week"> + </select> + </td> + <td> + <select id="choose-ping-id"> + </select> + </td> + </tr> + </table> + </div> + <table> + <tr> + <th>&aboutTelemetry.payloadChoiceHeader;</th> + </tr> + <tr> + <td> + <select id="choose-payload"> + </select> + </td> + </tr> + </table> + </div> + </header> + + <div id="raw-ping-data-section" class="hidden"> + <pre id="raw-ping-data"></pre> + </div> + + <div id="structured-ping-data-section"> + <section id="general-data-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.generalDataSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="general-data" class="data"> + </div> + </section> + + <section id="environment-data-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.environmentDataSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="environment-data" class="data"> + </div> + </section> + + <section id="session-info-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.sessionInfoSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="session-info" class="data"> + </div> + </section> + + <section id="scalars-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.scalarsSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="scalars" class="data"> + </div> + </section> + + <section id="keyed-scalars-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.keyedScalarsSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="keyed-scalars" class="data"> + </div> + </section> + + <section id="histograms-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.histogramsSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <span class="filter-ui"> + &aboutTelemetry.filterText; <input type="text" class="filter" id="histograms-filter" target_id="histograms"/> + </span> + <div class="processes-ui"> + <select id="histograms-processes" class="process-picker"></select> + </div> + <div id="histograms" class="data"> + </div> + </section> + + <section id="keyed-histograms-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.keyedHistogramsSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div class="processes-ui"> + <select id="keyed-histograms-processes" class="process-picker"></select> + </div> + <div id="keyed-histograms" class="data"> + </div> + </section> + + <section id="events-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">Events</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="events" class="data"> + </div> + </section> + + <section id="simple-measurements-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.simpleMeasurementsSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="simple-measurements" class="data"> + </div> + </section> + + <section id="telemetry-log-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.telemetryLogSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="telemetry-log" class="data"> + </div> + </section> + + <section id="slow-sql-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.slowSqlSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="slow-sql-tables" class="data"> + <p id="sql-warning" class="hidden">&aboutTelemetry.fullSqlWarning;</p> + </div> + </section> + + <section id="chrome-hangs-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.chromeHangsSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="chrome-hangs" class="data"> + <a id="chrome-hangs-fetch-symbols" href="#">&aboutTelemetry.fetchSymbols;</a> + <a id="chrome-hangs-hide-symbols" class="hidden" href="#">&aboutTelemetry.hideSymbols;</a> + <br/> + <br/> + <div id="chrome-hangs-data"> + </div> + </div> + </section> + + <section id="thread-hang-stats-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.threadHangStatsSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="thread-hang-stats" class="data"> + </div> + </section> + + <section id="late-writes-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.lateWritesSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="late-writes" class="data"> + <a id="late-writes-fetch-symbols" href="#">&aboutTelemetry.fetchSymbols;</a> + <a id="late-writes-hide-symbols" class="hidden" href="#">&aboutTelemetry.hideSymbols;</a> + <br/> + <br/> + <div id="late-writes-data"> + </div> + </div> + </section> + + <section id="addon-details-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.addonDetailsSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="addon-details" class="data"> + </div> + </section> + + <section id="addon-histograms-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.addonHistogramsSection;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="addon-histograms" class="data"> + </div> + </section> + + <section id="raw-payload-section" class="data-section"> + <input type="checkbox" class="statebox"/> + <h1 class="section-name">&aboutTelemetry.rawPayload;</h1> + <span class="toggle-caption">&aboutTelemetry.toggle;</span> + <span class="empty-caption">&aboutTelemetry.emptySection;</span> + <div id="raw-payload-data" class="data"> + <pre id="raw-payload-data-pre"></pre> + </div> + </section> + </div> + + </body> + +</html> diff --git a/toolkit/content/aboutwebrtc/aboutWebrtc.css b/toolkit/content/aboutwebrtc/aboutWebrtc.css new file mode 100644 index 0000000000..b9021dde6f --- /dev/null +++ b/toolkit/content/aboutwebrtc/aboutWebrtc.css @@ -0,0 +1,97 @@ +/* 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/. */ + +html { + background-color: #edeceb; + font: message-box; +} + +.controls { + font-size: 1.1em; + display: inline-block; + margin: 0 0.5em; +} + +.control { + margin: 0.5em 0; +} + +.control > button { + margin: 0 0.25em; +} + +.message > p { + margin: 0.5em 0.5em; +} + +.log p { + font-family: monospace; + padding-left: 2em; + text-indent: -2em; + margin-top: 2px; + margin-bottom: 2px; +} + +#content > div { + padding: 1em 2em; + margin: 1em 0; + border: 1px solid #afaca9; + border-radius: 10px; + background: none repeat scroll 0% 0% #fff; +} + +.section-heading * +{ + display: inline-block; +} + +.section-heading > button { + margin-left: 1em; + margin-right: 1em; +} + +.peer-connection > h3 +{ + background-color: #ddd; +} + +.peer-connection table { + width: 100%; + text-align: center; + border: none; +} + +.peer-connection table th, +.peer-connection table td { + padding: 0.4em; +} + +.peer-connection table tr:nth-child(even) { + background-color: #ddd; +} + +.info-label { + font-weight: bold; +} + +.section-ctrl { + margin: 1em 1.5em; +} + +div.fold-trigger { + color: blue; + cursor: pointer; +} + +@media screen { + .fold-closed { + display: none !important; + } +} + +@media print { + .no-print { + display: none !important; + } +} diff --git a/toolkit/content/aboutwebrtc/aboutWebrtc.html b/toolkit/content/aboutwebrtc/aboutWebrtc.html new file mode 100644 index 0000000000..42cda348ef --- /dev/null +++ b/toolkit/content/aboutwebrtc/aboutWebrtc.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> + +<!-- 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/. --> + +<html> +<head> + <meta charset="utf-8" /> + <title>about:webrtc</title> + <link rel="stylesheet" type="text/css" media="all" + href="chrome://global/content/aboutwebrtc/aboutWebrtc.css"/> + <script type="text/javascript;version=1.8" + src="chrome://global/content/aboutwebrtc/aboutWebrtc.js" + defer="defer"></script> +</head> +<body id="body" onload="onLoad()"> + <div id="controls" class="no-print"></div> + <div id="content"></div> +</body> +</html> diff --git a/toolkit/content/aboutwebrtc/aboutWebrtc.js b/toolkit/content/aboutwebrtc/aboutWebrtc.js new file mode 100644 index 0000000000..5c366eeb5a --- /dev/null +++ b/toolkit/content/aboutwebrtc/aboutWebrtc.js @@ -0,0 +1,841 @@ +/* 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/. */ +"use strict"; + +/* global WebrtcGlobalInformation, document */ + +var Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "FilePicker", + "@mozilla.org/filepicker;1", "nsIFilePicker"); +XPCOMUtils.defineLazyGetter(this, "strings", () => { + return Services.strings.createBundle("chrome://global/locale/aboutWebrtc.properties"); +}); + +const getString = strings.GetStringFromName; +const formatString = strings.formatStringFromName; + +const LOGFILE_NAME_DEFAULT = "aboutWebrtc.html"; +const WEBRTC_TRACE_ALL = 65535; + +function getStats() { + return new Promise(resolve => + WebrtcGlobalInformation.getAllStats(stats => resolve(stats))); +} + +function getLog() { + return new Promise(resolve => + WebrtcGlobalInformation.getLogging("", log => resolve(log))); +} + +// Begin initial data queries as page loads. Store returned Promises for +// later use. +var reportsRetrieved = getStats(); +var logRetrieved = getLog(); + +function onLoad() { + document.title = getString("document_title"); + let controls = document.querySelector("#controls"); + if (controls) { + let set = ControlSet.render(); + ControlSet.add(new SavePage()); + ControlSet.add(new DebugMode()); + ControlSet.add(new AecLogging()); + controls.appendChild(set); + } + + let contentElem = document.querySelector("#content"); + if (!contentElem) { + return; + } + + let contentInit = function(data) { + AboutWebRTC.init(onClearStats, onClearLog); + AboutWebRTC.render(contentElem, data); + }; + + Promise.all([reportsRetrieved, logRetrieved]) + .then(([stats, log]) => contentInit({reports: stats.reports, log: log})) + .catch(error => contentInit({error: error})); +} + +function onClearLog() { + WebrtcGlobalInformation.clearLogging(); + getLog() + .then(log => AboutWebRTC.refresh({log: log})) + .catch(error => AboutWebRTC.refresh({logError: error})); +} + +function onClearStats() { + WebrtcGlobalInformation.clearAllStats(); + getStats() + .then(stats => AboutWebRTC.refresh({reports: stats.reports})) + .catch(error => AboutWebRTC.refresh({reportError: error})); +} + +var ControlSet = { + render: function() { + let controls = document.createElement("div"); + let control = document.createElement("div"); + let message = document.createElement("div"); + + controls.className = "controls"; + control.className = "control"; + message.className = "message"; + controls.appendChild(control); + controls.appendChild(message); + + this.controlSection = control; + this.messageSection = message; + return controls; + }, + + add: function(controlObj) { + let [controlElem, messageElem] = controlObj.render(); + this.controlSection.appendChild(controlElem); + this.messageSection.appendChild(messageElem); + } +}; + +function Control() { + this._label = null; + this._message = null; + this._messageHeader = null; +} + +Control.prototype = { + render: function () { + let controlElem = document.createElement("button"); + let messageElem = document.createElement("p"); + + this.ctrl = controlElem; + controlElem.onclick = this.onClick.bind(this); + this.msg = messageElem; + this.update(); + + return [controlElem, messageElem]; + }, + + set label(val) { + return this._labelVal = val || "\xA0"; + }, + + get label() { + return this._labelVal; + }, + + set message(val) { + return this._messageVal = val; + }, + + get message() { + return this._messageVal; + }, + + update: function() { + this.ctrl.textContent = this._label; + + if (this._message) { + this.msg.innerHTML = + `<span class="info-label">${this._messageHeader}:</span> ${this._message}`; + } else { + this.msg.innerHTML = null; + } + }, + + onClick: function(event) { + return true; + } +}; + +function SavePage() { + Control.call(this); + this._messageHeader = getString("save_page_label"); + this._label = getString("save_page_label"); +} + +SavePage.prototype = Object.create(Control.prototype); +SavePage.prototype.constructor = SavePage; + +SavePage.prototype.onClick = function() { + let content = document.querySelector("#content"); + + if (!content) + return; + + FoldEffect.expandAll(); + FilePicker.init(window, getString("save_page_dialog_title"), FilePicker.modeSave); + FilePicker.defaultString = LOGFILE_NAME_DEFAULT; + let rv = FilePicker.show(); + + if (rv == FilePicker.returnOK || rv == FilePicker.returnReplace) { + let fout = FileUtils.openAtomicFileOutputStream( + FilePicker.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE); + + let nodes = content.querySelectorAll(".no-print"); + let noPrintList = []; + for (let node of nodes) { + noPrintList.push(node); + node.style.setProperty("display", "none"); + } + + fout.write(content.outerHTML, content.outerHTML.length); + FileUtils.closeAtomicFileOutputStream(fout); + + for (let node of noPrintList) { + node.style.removeProperty("display"); + } + + this._message = formatString("save_page_msg", [FilePicker.file.path], 1); + this.update(); + } +}; + +function DebugMode() { + Control.call(this); + this._messageHeader = getString("debug_mode_msg_label"); + + if (WebrtcGlobalInformation.debugLevel > 0) { + this.onState(); + } else { + this._label = getString("debug_mode_off_state_label"); + this._message = null; + } +} + +DebugMode.prototype = Object.create(Control.prototype); +DebugMode.prototype.constructor = DebugMode; + +DebugMode.prototype.onState = function() { + this._label = getString("debug_mode_on_state_label"); + try { + let file = Services.prefs.getCharPref("media.webrtc.debug.log_file"); + this._message = formatString("debug_mode_on_state_msg", [file], 1); + } catch (e) { + this._message = null; + } +}; + +DebugMode.prototype.offState = function() { + this._label = getString("debug_mode_off_state_label"); + try { + let file = Services.prefs.getCharPref("media.webrtc.debug.log_file"); + this._message = formatString("debug_mode_off_state_msg", [file], 1); + } catch (e) { + this._message = null; + } +}; + +DebugMode.prototype.onClick = function() { + if (WebrtcGlobalInformation.debugLevel > 0) { + WebrtcGlobalInformation.debugLevel = 0; + this.offState(); + } else { + WebrtcGlobalInformation.debugLevel = WEBRTC_TRACE_ALL; + this.onState(); + } + + this.update(); +}; + +function AecLogging() { + Control.call(this); + this._messageHeader = getString("aec_logging_msg_label"); + + if (WebrtcGlobalInformation.aecDebug) { + this.onState(); + } else { + this._label = getString("aec_logging_off_state_label"); + this._message = null; + } +} + +AecLogging.prototype = Object.create(Control.prototype); +AecLogging.prototype.constructor = AecLogging; + +AecLogging.prototype.offState = function () { + this._label = getString("aec_logging_off_state_label"); + try { + let file = Services.prefs.getCharPref("media.webrtc.debug.aec_log_dir"); + this._message = formatString("aec_logging_off_state_msg", [file], 1); + } catch (e) { + this._message = null; + } +}; + +AecLogging.prototype.onState = function () { + this._label = getString("aec_logging_on_state_label"); + try { + let file = Services.prefs.getCharPref("media.webrtc.debug.aec_log_dir"); + this._message = getString("aec_logging_on_state_msg"); + } catch (e) { + this._message = null; + } +}; + +AecLogging.prototype.onClick = function () { + if (WebrtcGlobalInformation.aecDebug) { + WebrtcGlobalInformation.aecDebug = false; + this.offState(); + } else { + WebrtcGlobalInformation.aecDebug = true; + this.onState(); + } + this.update(); +}; + +var AboutWebRTC = { + _reports: [], + _log: [], + + init: function(onClearStats, onClearLog) { + this._onClearStats = onClearStats; + this._onClearLog = onClearLog; + }, + + render: function(parent, data) { + this._content = parent; + this._setData(data); + + if (data.error) { + let msg = document.createElement("h3"); + msg.textContent = getString("cannot_retrieve_log"); + parent.appendChild(msg); + msg = document.createElement("p"); + msg.innerHTML = `${data.error.name}: ${data.error.message}`; + parent.appendChild(msg); + return; + } + + this._peerConnections = this.renderPeerConnections(); + this._connectionLog = this.renderConnectionLog(); + this._content.appendChild(this._peerConnections); + this._content.appendChild(this._connectionLog); + }, + + _setData: function(data) { + if (data.reports) { + this._reports = data.reports; + } + + if (data.log) { + this._log = data.log; + } + }, + + refresh: function(data) { + this._setData(data); + let pc = this._peerConnections; + this._peerConnections = this.renderPeerConnections(); + let log = this._connectionLog; + this._connectionLog = this.renderConnectionLog(); + this._content.replaceChild(this._peerConnections, pc); + this._content.replaceChild(this._connectionLog, log); + }, + + renderPeerConnections: function() { + let connections = document.createElement("div"); + connections.className = "stats"; + + let heading = document.createElement("span"); + heading.className = "section-heading"; + let elem = document.createElement("h3"); + elem.textContent = getString("stats_heading"); + heading.appendChild(elem); + + elem = document.createElement("button"); + elem.textContent = "Clear History"; + elem.className = "no-print"; + elem.onclick = this._onClearStats; + heading.appendChild(elem); + connections.appendChild(heading); + + if (!this._reports || !this._reports.length) { + return connections; + } + + let reports = [...this._reports]; + reports.sort((a, b) => b.timestamp - a.timestamp); + for (let report of reports) { + let peerConnection = new PeerConnection(report); + connections.appendChild(peerConnection.render()); + } + + return connections; + }, + + renderConnectionLog: function() { + let content = document.createElement("div"); + content.className = "log"; + + let heading = document.createElement("span"); + heading.className = "section-heading"; + let elem = document.createElement("h3"); + elem.textContent = getString("log_heading"); + heading.appendChild(elem); + elem = document.createElement("button"); + elem.textContent = "Clear Log"; + elem.className = "no-print"; + elem.onclick = this._onClearLog; + heading.appendChild(elem); + content.appendChild(heading); + + if (!this._log || !this._log.length) { + return content; + } + + let div = document.createElement("div"); + let sectionCtrl = document.createElement("div"); + sectionCtrl.className = "section-ctrl no-print"; + let foldEffect = new FoldEffect(div, { + showMsg: getString("log_show_msg"), + hideMsg: getString("log_hide_msg") + }); + sectionCtrl.appendChild(foldEffect.render()); + content.appendChild(sectionCtrl); + + for (let line of this._log) { + elem = document.createElement("p"); + elem.textContent = line; + div.appendChild(elem); + } + + content.appendChild(div); + return content; + } +}; + +function PeerConnection(report) { + this._report = report; +} + +PeerConnection.prototype = { + render: function() { + let pc = document.createElement("div"); + pc.className = "peer-connection"; + pc.appendChild(this.renderHeading()); + + let div = document.createElement("div"); + let sectionCtrl = document.createElement("div"); + sectionCtrl.className = "section-ctrl no-print"; + let foldEffect = new FoldEffect(div); + sectionCtrl.appendChild(foldEffect.render()); + pc.appendChild(sectionCtrl); + + div.appendChild(this.renderDesc()); + div.appendChild(new ICEStats(this._report).render()); + div.appendChild(new SDPStats(this._report).render()); + div.appendChild(new RTPStats(this._report).render()); + + pc.appendChild(div); + return pc; + }, + + renderHeading: function () { + let pcInfo = this.getPCInfo(this._report); + let heading = document.createElement("h3"); + let now = new Date(this._report.timestamp).toTimeString(); + heading.textContent = + `[ ${pcInfo.id} ] ${pcInfo.url} ${pcInfo.closed ? `(${getString("connection_closed")})` : ""} ${now}`; + return heading; + }, + + renderDesc: function() { + let info = document.createElement("div"); + let label = document.createElement("span"); + let body = document.createElement("span"); + + label.className = "info-label"; + label.textContent = `${getString("peer_connection_id_label")}: `; + info.appendChild(label); + + body.className = "info-body"; + body.textContent = this._report.pcid; + info.appendChild(body); + + return info; + }, + + getPCInfo: function(report) { + return { + id: report.pcid.match(/id=(\S+)/)[1], + url: report.pcid.match(/url=([^)]+)/)[1], + closed: report.closed + }; + } +}; + +function SDPStats(report) { + this._report = report; +} + +SDPStats.prototype = { + render: function() { + let div = document.createElement("div"); + let elem = document.createElement("h4"); + + elem.textContent = getString("sdp_heading"); + div.appendChild(elem); + + elem = document.createElement("h5"); + elem.textContent = getString("local_sdp_heading"); + div.appendChild(elem); + + elem = document.createElement("pre"); + elem.textContent = this._report.localSdp; + div.appendChild(elem); + + elem = document.createElement("h5"); + elem.textContent = getString("remote_sdp_heading"); + div.appendChild(elem); + + elem = document.createElement("pre"); + elem.textContent = this._report.remoteSdp; + div.appendChild(elem); + + return div; + } +}; + +function RTPStats(report) { + this._report = report; + this._stats = []; +} + +RTPStats.prototype = { + render: function() { + let div = document.createElement("div"); + let heading = document.createElement("h4"); + + heading.textContent = getString("rtp_stats_heading"); + div.appendChild(heading); + + this.generateRTPStats(); + + for (let statSet of this._stats) { + div.appendChild(this.renderRTPStatSet(statSet)); + } + + return div; + }, + + generateRTPStats: function() { + let remoteRtpStats = {}; + let rtpStats = [].concat((this._report.inboundRTPStreamStats || []), + (this._report.outboundRTPStreamStats || [])); + + // Generate an id-to-streamStat index for each streamStat that is marked + // as a remote. This will be used next to link the remote to its local side. + for (let stats of rtpStats) { + if (stats.isRemote) { + remoteRtpStats[stats.id] = stats; + } + } + + // If a streamStat has a remoteId attribute, create a remoteRtpStats + // attribute that references the remote streamStat entry directly. + // That is, the index generated above is merged into the returned list. + for (let stats of rtpStats) { + if (stats.remoteId) { + stats.remoteRtpStats = remoteRtpStats[stats.remoteId]; + } + } + + this._stats = rtpStats; + }, + + renderAvStats: function(stats) { + let statsString = ""; + + if (stats.mozAvSyncDelay) { + statsString += `${getString("av_sync_label")}: ${stats.mozAvSyncDelay} ms `; + } + if (stats.mozJitterBufferDelay) { + statsString += `${getString("jitter_buffer_delay_label")}: ${stats.mozJitterBufferDelay} ms`; + } + + let line = document.createElement("p"); + line.textContent = statsString; + return line; + }, + + renderCoderStats: function(stats) { + let statsString = ""; + let label; + + if (stats.bitrateMean) { + statsString += ` ${getString("avg_bitrate_label")}: ${(stats.bitrateMean / 1000000).toFixed(2)} Mbps`; + if (stats.bitrateStdDev) { + statsString += ` (${(stats.bitrateStdDev / 1000000).toFixed(2)} SD)`; + } + } + + if (stats.framerateMean) { + statsString += ` ${getString("avg_framerate_label")}: ${(stats.framerateMean).toFixed(2)} fps`; + if (stats.framerateStdDev) { + statsString += ` (${stats.framerateStdDev.toFixed(2)} SD)`; + } + } + + if (stats.droppedFrames) { + statsString += ` ${getString("dropped_frames_label")}: ${stats.droppedFrames}`; + } + if (stats.discardedPackets) { + statsString += ` ${getString("discarded_packets_label")}: ${stats.discardedPackets}`; + } + + if (statsString) { + label = (stats.packetsReceived ? ` ${getString("decoder_label")}:` : ` ${getString("encoder_label")}:`); + statsString = label + statsString; + } + + let line = document.createElement("p"); + line.textContent = statsString; + return line; + }, + + renderTransportStats: function(stats, typeLabel) { + let time = new Date(stats.timestamp).toTimeString(); + let statsString = `${typeLabel}: ${time} ${stats.type} SSRC: ${stats.ssrc}`; + + if (stats.packetsReceived) { + statsString += ` ${getString("received_label")}: ${stats.packetsReceived} ${getString("packets")}`; + + if (stats.bytesReceived) { + statsString += ` (${(stats.bytesReceived / 1024).toFixed(2)} Kb)`; + } + + statsString += ` ${getString("lost_label")}: ${stats.packetsLost} ${getString("jitter_label")}: ${stats.jitter}`; + + if (stats.mozRtt) { + statsString += ` RTT: ${stats.mozRtt} ms`; + } + } else if (stats.packetsSent) { + statsString += ` ${getString("sent_label")}: ${stats.packetsSent} ${getString("packets")}`; + if (stats.bytesSent) { + statsString += ` (${(stats.bytesSent / 1024).toFixed(2)} Kb)`; + } + } + + let line = document.createElement("p"); + line.textContent = statsString; + return line; + }, + + renderRTPStatSet: function(stats) { + let div = document.createElement("div"); + let heading = document.createElement("h5"); + + heading.textContent = stats.id; + div.appendChild(heading); + + if (stats.MozAvSyncDelay || stats.mozJitterBufferDelay) { + div.appendChild(this.renderAvStats(stats)); + } + + div.appendChild(this.renderCoderStats(stats)); + div.appendChild(this.renderTransportStats(stats, getString("typeLocal"))); + + if (stats.remoteId && stats.remoteRtpStats) { + div.appendChild(this.renderTransportStats(stats.remoteRtpStats, getString("typeRemote"))); + } + + return div; + }, +}; + +function ICEStats(report) { + this._report = report; +} + +ICEStats.prototype = { + render: function() { + let tbody = []; + for (let stat of this.generateICEStats()) { + tbody.push([ + stat.localcandidate || "", + stat.remotecandidate || "", + stat.state || "", + stat.priority || "", + stat.nominated || "", + stat.selected || "" + ]); + } + + let statsTable = new SimpleTable( + [getString("local_candidate"), getString("remote_candidate"), getString("ice_state"), + getString("priority"), getString("nominated"), getString("selected")], + tbody); + + let div = document.createElement("div"); + let heading = document.createElement("h4"); + + heading.textContent = getString("ice_stats_heading"); + div.appendChild(heading); + div.appendChild(statsTable.render()); + + return div; + }, + + generateICEStats: function() { + // Create an index based on candidate ID for each element in the + // iceCandidateStats array. + let candidates = new Map(); + + for (let candidate of this._report.iceCandidateStats) { + candidates.set(candidate.id, candidate); + } + + // A component may have a remote or local candidate address or both. + // Combine those with both; these will be the peer candidates. + let matched = {}; + let stats = []; + let stat; + + for (let pair of this._report.iceCandidatePairStats) { + let local = candidates.get(pair.localCandidateId); + let remote = candidates.get(pair.remoteCandidateId); + + if (local) { + stat = { + localcandidate: this.candidateToString(local), + state: pair.state, + priority: pair.priority, + nominated: pair.nominated, + selected: pair.selected + }; + matched[local.id] = true; + + if (remote) { + stat.remotecandidate = this.candidateToString(remote); + matched[remote.id] = true; + } + stats.push(stat); + } + } + + for (let c of candidates.values()) { + if (matched[c.id]) + continue; + + stat = {}; + stat[c.type] = this.candidateToString(c); + stats.push(stat); + } + + return stats.sort((a, b) => (b.priority || 0) - (a.priority || 0)); + }, + + candidateToString: function(c) { + if (!c) { + return "*"; + } + + var type = c.candidateType; + + if (c.type == "localcandidate" && c.candidateType == "relayed") { + type = `${c.candidateType}-${c.mozLocalTransport}`; + } + + return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`; + } +}; + +function SimpleTable(heading, data) { + this._heading = heading || []; + this._data = data; +} + +SimpleTable.prototype = { + renderRow: function(list) { + let row = document.createElement("tr"); + + for (let elem of list) { + let cell = document.createElement("td"); + cell.textContent = elem; + row.appendChild(cell); + } + + return row; + }, + + render: function() { + let table = document.createElement("table"); + + if (this._heading) { + table.appendChild(this.renderRow(this._heading)); + } + + for (let row of this._data) { + table.appendChild(this.renderRow(row)); + } + + return table; + } +}; + +function FoldEffect(targetElem, options = {}) { + if (targetElem) { + this._showMsg = "\u25BC " + (options.showMsg || getString("fold_show_msg")); + this._showHint = options.showHint || getString("fold_show_hint"); + this._hideMsg = "\u25B2 " + (options.hideMsg || getString("fold_hide_msg")); + this._hideHint = options.hideHint || getString("fold_hide_hint"); + this._target = targetElem; + } +} + +FoldEffect.prototype = { + render: function() { + this._target.classList.add("fold-target"); + + let ctrl = document.createElement("div"); + this._trigger = ctrl; + ctrl.className = "fold-trigger"; + ctrl.addEventListener("click", this.onClick.bind(this)); + this.close(); + + FoldEffect._sections.push(this); + return ctrl; + }, + + onClick: function() { + if (this._target.classList.contains("fold-closed")) { + this.open(); + } else { + this.close(); + } + return true; + }, + + open: function() { + this._target.classList.remove("fold-closed"); + this._trigger.setAttribute("title", this._hideHint); + this._trigger.textContent = this._hideMsg; + }, + + close: function() { + this._target.classList.add("fold-closed"); + this._trigger.setAttribute("title", this._showHint); + this._trigger.textContent = this._showMsg; + } +}; + +FoldEffect._sections = []; + +FoldEffect.expandAll = function() { + for (let section of this._sections) { + section.open(); + } +}; + +FoldEffect.collapseAll = function() { + for (let section of this._sections) { + section.close(); + } +}; diff --git a/toolkit/content/autocomplete.css b/toolkit/content/autocomplete.css new file mode 100644 index 0000000000..11b36ddac5 --- /dev/null +++ b/toolkit/content/autocomplete.css @@ -0,0 +1,40 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace html url("http://www.w3.org/1999/xhtml"); + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + .ac-site-icon { + image-rendering: -moz-crisp-edges; + } +} + +richlistitem { + -moz-box-orient: horizontal; + overflow: hidden; +} + +.ac-title-text, +.ac-tags-text, +.ac-url-text, +.ac-action-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ac-tags[empty] { + display: none; +} + +.ac-action[actiontype=searchengine]:not([selected]), +.ac-separator[actiontype=searchengine]:not([selected]) { + display: none; +} + +.ac-separator[type=keyword] { + display: none; +} diff --git a/toolkit/content/browser-child.js b/toolkit/content/browser-child.js new file mode 100644 index 0000000000..c819e3db65 --- /dev/null +++ b/toolkit/content/browser-child.js @@ -0,0 +1,625 @@ +/* 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/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/BrowserUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import("resource://gre/modules/RemoteAddonsChild.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PageThumbUtils", + "resource://gre/modules/PageThumbUtils.jsm"); + +if (AppConstants.MOZ_CRASHREPORTER) { + XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", + "@mozilla.org/xre/app-info;1", + "nsICrashReporter"); +} + +function makeInputStream(aString) { + let stream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsISupportsCString); + stream.data = aString; + return stream; // XPConnect will QI this to nsIInputStream for us. +} + +var WebProgressListener = { + init: function() { + this._filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + this._filter.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_ALL); + + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener(this._filter, Ci.nsIWebProgress.NOTIFY_ALL); + }, + + uninit() { + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.removeProgressListener(this._filter); + + this._filter.removeProgressListener(this); + this._filter = null; + }, + + _requestSpec: function (aRequest, aPropertyName) { + if (!aRequest || !(aRequest instanceof Ci.nsIChannel)) + return null; + return aRequest.QueryInterface(Ci.nsIChannel)[aPropertyName].spec; + }, + + _setupJSON: function setupJSON(aWebProgress, aRequest) { + let innerWindowID = null; + if (aWebProgress) { + let domWindowID = null; + try { + let utils = aWebProgress.DOMWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + domWindowID = utils.outerWindowID; + innerWindowID = utils.currentInnerWindowID; + } catch (e) { + // If nsDocShell::Destroy has already been called, then we'll + // get NS_NOINTERFACE when trying to get the DOM window. + // If there is no current inner window, we'll get + // NS_ERROR_NOT_AVAILABLE. + } + + aWebProgress = { + isTopLevel: aWebProgress.isTopLevel, + isLoadingDocument: aWebProgress.isLoadingDocument, + loadType: aWebProgress.loadType, + DOMWindowID: domWindowID + }; + } + + return { + webProgress: aWebProgress || null, + requestURI: this._requestSpec(aRequest, "URI"), + originalRequestURI: this._requestSpec(aRequest, "originalURI"), + documentContentType: content.document && content.document.contentType, + innerWindowID, + }; + }, + + _setupObjects: function setupObjects(aWebProgress, aRequest) { + let domWindow; + try { + domWindow = aWebProgress && aWebProgress.DOMWindow; + } catch (e) { + // If nsDocShell::Destroy has already been called, then we'll + // get NS_NOINTERFACE when trying to get the DOM window. Ignore + // that here. + domWindow = null; + } + + return { + contentWindow: content, + // DOMWindow is not necessarily the content-window with subframes. + DOMWindow: domWindow, + webProgress: aWebProgress, + request: aRequest, + }; + }, + + _send(name, data, objects) { + if (RemoteAddonsChild.useSyncWebProgress) { + sendRpcMessage(name, data, objects); + } else { + sendAsyncMessage(name, data, objects); + } + }, + + onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + let json = this._setupJSON(aWebProgress, aRequest); + let objects = this._setupObjects(aWebProgress, aRequest); + + json.stateFlags = aStateFlags; + json.status = aStatus; + + // It's possible that this state change was triggered by + // loading an internal error page, for which the parent + // will want to know some details, so we'll update it with + // the documentURI. + if (aWebProgress && aWebProgress.isTopLevel) { + json.documentURI = content.document.documentURIObject.spec; + json.charset = content.document.characterSet; + json.mayEnableCharacterEncodingMenu = docShell.mayEnableCharacterEncodingMenu; + json.inLoadURI = WebNavigation.inLoadURI; + } + + this._send("Content:StateChange", json, objects); + }, + + onProgressChange: function onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) { + let json = this._setupJSON(aWebProgress, aRequest); + let objects = this._setupObjects(aWebProgress, aRequest); + + json.curSelf = aCurSelf; + json.maxSelf = aMaxSelf; + json.curTotal = aCurTotal; + json.maxTotal = aMaxTotal; + + this._send("Content:ProgressChange", json, objects); + }, + + onProgressChange64: function onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) { + this.onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal); + }, + + onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) { + let json = this._setupJSON(aWebProgress, aRequest); + let objects = this._setupObjects(aWebProgress, aRequest); + + json.location = aLocationURI ? aLocationURI.spec : ""; + json.flags = aFlags; + + // These properties can change even for a sub-frame navigation. + let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); + json.canGoBack = webNav.canGoBack; + json.canGoForward = webNav.canGoForward; + + if (aWebProgress && aWebProgress.isTopLevel) { + json.documentURI = content.document.documentURIObject.spec; + json.title = content.document.title; + json.charset = content.document.characterSet; + json.mayEnableCharacterEncodingMenu = docShell.mayEnableCharacterEncodingMenu; + json.principal = content.document.nodePrincipal; + json.synthetic = content.document.mozSyntheticDocument; + json.inLoadURI = WebNavigation.inLoadURI; + + if (AppConstants.MOZ_CRASHREPORTER && CrashReporter.enabled) { + let uri = aLocationURI.clone(); + try { + // If the current URI contains a username/password, remove it. + uri.userPass = ""; + } catch (ex) { /* Ignore failures on about: URIs. */ } + CrashReporter.annotateCrashReport("URL", uri.spec); + } + } + + this._send("Content:LocationChange", json, objects); + }, + + onStatusChange: function onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { + let json = this._setupJSON(aWebProgress, aRequest); + let objects = this._setupObjects(aWebProgress, aRequest); + + json.status = aStatus; + json.message = aMessage; + + this._send("Content:StatusChange", json, objects); + }, + + onSecurityChange: function onSecurityChange(aWebProgress, aRequest, aState) { + let json = this._setupJSON(aWebProgress, aRequest); + let objects = this._setupObjects(aWebProgress, aRequest); + + json.state = aState; + json.status = SecurityUI.getSSLStatusAsString(); + + this._send("Content:SecurityChange", json, objects); + }, + + onRefreshAttempted: function onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) { + return true; + }, + + sendLoadCallResult() { + sendAsyncMessage("Content:LoadURIResult"); + }, + + QueryInterface: function QueryInterface(aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsIWebProgressListener2) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) { + return this; + } + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +}; + +WebProgressListener.init(); +addEventListener("unload", () => { + WebProgressListener.uninit(); +}); + +var WebNavigation = { + init: function() { + addMessageListener("WebNavigation:GoBack", this); + addMessageListener("WebNavigation:GoForward", this); + addMessageListener("WebNavigation:GotoIndex", this); + addMessageListener("WebNavigation:LoadURI", this); + addMessageListener("WebNavigation:SetOriginAttributes", this); + addMessageListener("WebNavigation:Reload", this); + addMessageListener("WebNavigation:Stop", this); + }, + + get webNavigation() { + return docShell.QueryInterface(Ci.nsIWebNavigation); + }, + + _inLoadURI: false, + + get inLoadURI() { + return this._inLoadURI; + }, + + receiveMessage: function(message) { + switch (message.name) { + case "WebNavigation:GoBack": + this.goBack(); + break; + case "WebNavigation:GoForward": + this.goForward(); + break; + case "WebNavigation:GotoIndex": + this.gotoIndex(message.data.index); + break; + case "WebNavigation:LoadURI": + this.loadURI(message.data.uri, message.data.flags, + message.data.referrer, message.data.referrerPolicy, + message.data.postData, message.data.headers, + message.data.baseURI); + break; + case "WebNavigation:SetOriginAttributes": + this.setOriginAttributes(message.data.originAttributes); + break; + case "WebNavigation:Reload": + this.reload(message.data.flags); + break; + case "WebNavigation:Stop": + this.stop(message.data.flags); + break; + } + }, + + _wrapURIChangeCall(fn) { + this._inLoadURI = true; + try { + fn(); + } finally { + this._inLoadURI = false; + WebProgressListener.sendLoadCallResult(); + } + }, + + goBack: function() { + if (this.webNavigation.canGoBack) { + this._wrapURIChangeCall(() => this.webNavigation.goBack()); + } + }, + + goForward: function() { + if (this.webNavigation.canGoForward) { + this._wrapURIChangeCall(() => this.webNavigation.goForward()); + } + }, + + gotoIndex: function(index) { + this._wrapURIChangeCall(() => this.webNavigation.gotoIndex(index)); + }, + + loadURI: function(uri, flags, referrer, referrerPolicy, postData, headers, baseURI) { + if (AppConstants.MOZ_CRASHREPORTER && CrashReporter.enabled) { + let annotation = uri; + try { + let url = Services.io.newURI(uri, null, null); + // If the current URI contains a username/password, remove it. + url.userPass = ""; + annotation = url.spec; + } catch (ex) { /* Ignore failures to parse and failures + on about: URIs. */ } + CrashReporter.annotateCrashReport("URL", annotation); + } + if (referrer) + referrer = Services.io.newURI(referrer, null, null); + if (postData) + postData = makeInputStream(postData); + if (headers) + headers = makeInputStream(headers); + if (baseURI) + baseURI = Services.io.newURI(baseURI, null, null); + this._wrapURIChangeCall(() => { + return this.webNavigation.loadURIWithOptions(uri, flags, referrer, referrerPolicy, + postData, headers, baseURI); + }); + }, + + setOriginAttributes: function(originAttributes) { + if (originAttributes) { + this.webNavigation.setOriginAttributesBeforeLoading(originAttributes); + } + }, + + reload: function(flags) { + this.webNavigation.reload(flags); + }, + + stop: function(flags) { + this.webNavigation.stop(flags); + } +}; + +WebNavigation.init(); + +var SecurityUI = { + getSSLStatusAsString: function() { + let status = docShell.securityUI.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus; + + if (status) { + let helper = Cc["@mozilla.org/network/serialization-helper;1"] + .getService(Ci.nsISerializationHelper); + + status.QueryInterface(Ci.nsISerializable); + return helper.serializeToString(status); + } + + return null; + } +}; + +var ControllerCommands = { + init: function () { + addMessageListener("ControllerCommands:Do", this); + addMessageListener("ControllerCommands:DoWithParams", this); + }, + + receiveMessage: function(message) { + switch (message.name) { + case "ControllerCommands:Do": + if (docShell.isCommandEnabled(message.data)) + docShell.doCommand(message.data); + break; + + case "ControllerCommands:DoWithParams": + var data = message.data; + if (docShell.isCommandEnabled(data.cmd)) { + var params = Cc["@mozilla.org/embedcomp/command-params;1"]. + createInstance(Ci.nsICommandParams); + for (var name in data.params) { + var value = data.params[name]; + if (value.type == "long") { + params.setLongValue(name, parseInt(value.value)); + } else { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + } + docShell.doCommandWithParams(data.cmd, params); + } + break; + } + } +} + +ControllerCommands.init() + +addEventListener("DOMTitleChanged", function (aEvent) { + let document = content.document; + switch (aEvent.type) { + case "DOMTitleChanged": + if (!aEvent.isTrusted || aEvent.target.defaultView != content) + return; + + sendAsyncMessage("DOMTitleChanged", { title: document.title }); + break; + } +}, false); + +addEventListener("DOMWindowClose", function (aEvent) { + if (!aEvent.isTrusted) + return; + sendAsyncMessage("DOMWindowClose"); +}, false); + +addEventListener("ImageContentLoaded", function (aEvent) { + if (content.document instanceof Ci.nsIImageDocument) { + let req = content.document.imageRequest; + if (!req.image) + return; + sendAsyncMessage("ImageDocumentLoaded", { width: req.image.width, + height: req.image.height }); + } +}, false); + +const ZoomManager = { + get fullZoom() { + return this._cache.fullZoom; + }, + + get textZoom() { + return this._cache.textZoom; + }, + + set fullZoom(value) { + this._cache.fullZoom = value; + this._markupViewer.fullZoom = value; + }, + + set textZoom(value) { + this._cache.textZoom = value; + this._markupViewer.textZoom = value; + }, + + refreshFullZoom: function() { + return this._refreshZoomValue('fullZoom'); + }, + + refreshTextZoom: function() { + return this._refreshZoomValue('textZoom'); + }, + + /** + * Retrieves specified zoom property value from markupViewer and refreshes + * cache if needed. + * @param valueName Either 'fullZoom' or 'textZoom'. + * @returns Returns true if cached value was actually refreshed. + * @private + */ + _refreshZoomValue: function(valueName) { + let actualZoomValue = this._markupViewer[valueName]; + // Round to remove any floating-point error. + actualZoomValue = Number(actualZoomValue.toFixed(2)); + if (actualZoomValue != this._cache[valueName]) { + this._cache[valueName] = actualZoomValue; + return true; + } + return false; + }, + + get _markupViewer() { + return docShell.contentViewer; + }, + + _cache: { + fullZoom: NaN, + textZoom: NaN + } +}; + +addMessageListener("FullZoom", function (aMessage) { + ZoomManager.fullZoom = aMessage.data.value; +}); + +addMessageListener("TextZoom", function (aMessage) { + ZoomManager.textZoom = aMessage.data.value; +}); + +addEventListener("FullZoomChange", function () { + if (ZoomManager.refreshFullZoom()) { + sendAsyncMessage("FullZoomChange", { value: ZoomManager.fullZoom }); + } +}, false); + +addEventListener("TextZoomChange", function (aEvent) { + if (ZoomManager.refreshTextZoom()) { + sendAsyncMessage("TextZoomChange", { value: ZoomManager.textZoom }); + } +}, false); + +addEventListener("ZoomChangeUsingMouseWheel", function () { + sendAsyncMessage("ZoomChangeUsingMouseWheel", {}); +}, false); + +addMessageListener("UpdateCharacterSet", function (aMessage) { + docShell.charset = aMessage.data.value; + docShell.gatherCharsetMenuTelemetry(); +}); + +/** + * Remote thumbnail request handler for PageThumbs thumbnails. + */ +addMessageListener("Browser:Thumbnail:Request", function (aMessage) { + let snapshot; + let args = aMessage.data.additionalArgs; + let fullScale = args ? args.fullScale : false; + if (fullScale) { + snapshot = PageThumbUtils.createSnapshotThumbnail(content, null, args); + } else { + let snapshotWidth = aMessage.data.canvasWidth; + let snapshotHeight = aMessage.data.canvasHeight; + snapshot = + PageThumbUtils.createCanvas(content, snapshotWidth, snapshotHeight); + PageThumbUtils.createSnapshotThumbnail(content, snapshot, args); + } + + snapshot.toBlob(function (aBlob) { + sendAsyncMessage("Browser:Thumbnail:Response", { + thumbnail: aBlob, + id: aMessage.data.id + }); + }); +}); + +/** + * Remote isSafeForCapture request handler for PageThumbs. + */ +addMessageListener("Browser:Thumbnail:CheckState", function (aMessage) { + let result = PageThumbUtils.shouldStoreContentThumbnail(content, docShell); + sendAsyncMessage("Browser:Thumbnail:CheckState:Response", { + result: result + }); +}); + +/** + * Remote GetOriginalURL request handler for PageThumbs. + */ +addMessageListener("Browser:Thumbnail:GetOriginalURL", function (aMessage) { + let channel = docShell.currentDocumentChannel; + let channelError = PageThumbUtils.isChannelErrorResponse(channel); + let originalURL; + try { + originalURL = channel.originalURI.spec; + } catch (ex) {} + sendAsyncMessage("Browser:Thumbnail:GetOriginalURL:Response", { + channelError: channelError, + originalURL: originalURL, + }); +}); + +/** + * Remote createAboutBlankContentViewer request handler. + */ +addMessageListener("Browser:CreateAboutBlank", function(aMessage) { + if (!content.document || content.document.documentURI != "about:blank") { + throw new Error("Can't create a content viewer unless on about:blank"); + } + let principal = aMessage.data; + principal = BrowserUtils.principalWithMatchingOA(principal, content.document.nodePrincipal); + docShell.createAboutBlankContentViewer(principal); +}); + +// The AddonsChild needs to be rooted so that it stays alive as long as +// the tab. +var AddonsChild = RemoteAddonsChild.init(this); +if (AddonsChild) { + addEventListener("unload", () => { + RemoteAddonsChild.uninit(AddonsChild); + }); +} + +addMessageListener("NetworkPrioritizer:AdjustPriority", (msg) => { + let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); + let loadGroup = webNav.QueryInterface(Ci.nsIDocumentLoader) + .loadGroup.QueryInterface(Ci.nsISupportsPriority); + loadGroup.adjustPriority(msg.data.adjustment); +}); + +addMessageListener("NetworkPrioritizer:SetPriority", (msg) => { + let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); + let loadGroup = webNav.QueryInterface(Ci.nsIDocumentLoader) + .loadGroup.QueryInterface(Ci.nsISupportsPriority); + loadGroup.priority = msg.data.priority; +}); + +addMessageListener("InPermitUnload", msg => { + let inPermitUnload = docShell.contentViewer && docShell.contentViewer.inPermitUnload; + sendAsyncMessage("InPermitUnload", {id: msg.data.id, inPermitUnload}); +}); + +addMessageListener("PermitUnload", msg => { + sendAsyncMessage("PermitUnload", {id: msg.data.id, kind: "start"}); + + let permitUnload = true; + if (docShell && docShell.contentViewer) { + permitUnload = docShell.contentViewer.permitUnload(); + } + + sendAsyncMessage("PermitUnload", {id: msg.data.id, kind: "end", permitUnload}); +}); + +// We may not get any responses to Browser:Init if the browser element +// is torn down too quickly. +var outerWindowID = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; +sendAsyncMessage("Browser:Init", {outerWindowID: outerWindowID}); diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js new file mode 100644 index 0000000000..4ae798fbdc --- /dev/null +++ b/toolkit/content/browser-content.js @@ -0,0 +1,1762 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", + "resource://gre/modules/ReaderMode.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); + +var global = this; + + +// Lazily load the finder code +addMessageListener("Finder:Initialize", function () { + let {RemoteFinderListener} = Cu.import("resource://gre/modules/RemoteFinder.jsm", {}); + new RemoteFinderListener(global); +}); + +var ClickEventHandler = { + init: function init() { + this._scrollable = null; + this._scrolldir = ""; + this._startX = null; + this._startY = null; + this._screenX = null; + this._screenY = null; + this._lastFrame = null; + this.autoscrollLoop = this.autoscrollLoop.bind(this); + + Services.els.addSystemEventListener(global, "mousedown", this, true); + + addMessageListener("Autoscroll:Stop", this); + }, + + isAutoscrollBlocker: function(node) { + let mmPaste = Services.prefs.getBoolPref("middlemouse.paste"); + let mmScrollbarPosition = Services.prefs.getBoolPref("middlemouse.scrollbarPosition"); + + while (node) { + if ((node instanceof content.HTMLAnchorElement || node instanceof content.HTMLAreaElement) && + node.hasAttribute("href")) { + return true; + } + + if (mmPaste && (node instanceof content.HTMLInputElement || + node instanceof content.HTMLTextAreaElement)) { + return true; + } + + if (node instanceof content.XULElement && mmScrollbarPosition + && (node.localName == "scrollbar" || node.localName == "scrollcorner")) { + return true; + } + + node = node.parentNode; + } + return false; + }, + + findNearestScrollableElement: function(aNode) { + // this is a list of overflow property values that allow scrolling + const scrollingAllowed = ['scroll', 'auto']; + + // go upward in the DOM and find any parent element that has a overflow + // area and can therefore be scrolled + for (this._scrollable = aNode; this._scrollable; + this._scrollable = this._scrollable.parentNode) { + // do not use overflow based autoscroll for <html> and <body> + // Elements or non-html elements such as svg or Document nodes + // also make sure to skip select elements that are not multiline + if (!(this._scrollable instanceof content.HTMLElement) || + ((this._scrollable instanceof content.HTMLSelectElement) && !this._scrollable.multiple)) { + continue; + } + + var overflowx = this._scrollable.ownerDocument.defaultView + .getComputedStyle(this._scrollable, '') + .getPropertyValue('overflow-x'); + var overflowy = this._scrollable.ownerDocument.defaultView + .getComputedStyle(this._scrollable, '') + .getPropertyValue('overflow-y'); + // we already discarded non-multiline selects so allow vertical + // scroll for multiline ones directly without checking for a + // overflow property + var scrollVert = this._scrollable.scrollTopMax && + (this._scrollable instanceof content.HTMLSelectElement || + scrollingAllowed.indexOf(overflowy) >= 0); + + // do not allow horizontal scrolling for select elements, it leads + // to visual artifacts and is not the expected behavior anyway + if (!(this._scrollable instanceof content.HTMLSelectElement) && + this._scrollable.scrollLeftMin != this._scrollable.scrollLeftMax && + scrollingAllowed.indexOf(overflowx) >= 0) { + this._scrolldir = scrollVert ? "NSEW" : "EW"; + break; + } else if (scrollVert) { + this._scrolldir = "NS"; + break; + } + } + + if (!this._scrollable) { + this._scrollable = aNode.ownerDocument.defaultView; + if (this._scrollable.scrollMaxX != this._scrollable.scrollMinX) { + this._scrolldir = this._scrollable.scrollMaxY != + this._scrollable.scrollMinY ? "NSEW" : "EW"; + } else if (this._scrollable.scrollMaxY != this._scrollable.scrollMinY) { + this._scrolldir = "NS"; + } else if (this._scrollable.frameElement) { + this.findNearestScrollableElement(this._scrollable.frameElement); + } else { + this._scrollable = null; // abort scrolling + } + } + }, + + startScroll: function(event) { + + this.findNearestScrollableElement(event.originalTarget); + + if (!this._scrollable) + return; + + let [enabled] = sendSyncMessage("Autoscroll:Start", + {scrolldir: this._scrolldir, + screenX: event.screenX, + screenY: event.screenY}); + if (!enabled) { + this._scrollable = null; + return; + } + + Services.els.addSystemEventListener(global, "mousemove", this, true); + addEventListener("pagehide", this, true); + + this._ignoreMouseEvents = true; + this._startX = event.screenX; + this._startY = event.screenY; + this._screenX = event.screenX; + this._screenY = event.screenY; + this._scrollErrorX = 0; + this._scrollErrorY = 0; + this._lastFrame = content.performance.now(); + + content.requestAnimationFrame(this.autoscrollLoop); + }, + + stopScroll: function() { + if (this._scrollable) { + this._scrollable.mozScrollSnap(); + this._scrollable = null; + + Services.els.removeSystemEventListener(global, "mousemove", this, true); + removeEventListener("pagehide", this, true); + } + }, + + accelerate: function(curr, start) { + const speed = 12; + var val = (curr - start) / speed; + + if (val > 1) + return val * Math.sqrt(val) - 1; + if (val < -1) + return val * Math.sqrt(-val) + 1; + return 0; + }, + + roundToZero: function(num) { + if (num > 0) + return Math.floor(num); + return Math.ceil(num); + }, + + autoscrollLoop: function(timestamp) { + if (!this._scrollable) { + // Scrolling has been canceled + return; + } + + // avoid long jumps when the browser hangs for more than + // |maxTimeDelta| ms + const maxTimeDelta = 100; + var timeDelta = Math.min(maxTimeDelta, timestamp - this._lastFrame); + // we used to scroll |accelerate()| pixels every 20ms (50fps) + var timeCompensation = timeDelta / 20; + this._lastFrame = timestamp; + + var actualScrollX = 0; + var actualScrollY = 0; + // don't bother scrolling vertically when the scrolldir is only horizontal + // and the other way around + if (this._scrolldir != 'EW') { + var y = this.accelerate(this._screenY, this._startY) * timeCompensation; + var desiredScrollY = this._scrollErrorY + y; + actualScrollY = this.roundToZero(desiredScrollY); + this._scrollErrorY = (desiredScrollY - actualScrollY); + } + if (this._scrolldir != 'NS') { + var x = this.accelerate(this._screenX, this._startX) * timeCompensation; + var desiredScrollX = this._scrollErrorX + x; + actualScrollX = this.roundToZero(desiredScrollX); + this._scrollErrorX = (desiredScrollX - actualScrollX); + } + + const kAutoscroll = 15; // defined in mozilla/layers/ScrollInputMethods.h + Services.telemetry.getHistogramById("SCROLL_INPUT_METHODS").add(kAutoscroll); + + this._scrollable.scrollBy({ + left: actualScrollX, + top: actualScrollY, + behavior: "instant" + }); + content.requestAnimationFrame(this.autoscrollLoop); + }, + + handleEvent: function(event) { + if (event.type == "mousemove") { + this._screenX = event.screenX; + this._screenY = event.screenY; + } else if (event.type == "mousedown") { + if (event.isTrusted & + !event.defaultPrevented && + event.button == 1 && + !this._scrollable && + !this.isAutoscrollBlocker(event.originalTarget)) { + this.startScroll(event); + } + } else if (event.type == "pagehide") { + if (this._scrollable) { + var doc = + this._scrollable.ownerDocument || this._scrollable.document; + if (doc == event.target) { + sendAsyncMessage("Autoscroll:Cancel"); + } + } + } + }, + + receiveMessage: function(msg) { + switch (msg.name) { + case "Autoscroll:Stop": { + this.stopScroll(); + break; + } + } + }, +}; +ClickEventHandler.init(); + +var PopupBlocking = { + popupData: null, + popupDataInternal: null, + + init: function() { + addEventListener("DOMPopupBlocked", this, true); + addEventListener("pageshow", this, true); + addEventListener("pagehide", this, true); + + addMessageListener("PopupBlocking:UnblockPopup", this); + addMessageListener("PopupBlocking:GetBlockedPopupList", this); + }, + + receiveMessage: function(msg) { + switch (msg.name) { + case "PopupBlocking:UnblockPopup": { + let i = msg.data.index; + if (this.popupData && this.popupData[i]) { + let data = this.popupData[i]; + let internals = this.popupDataInternal[i]; + let dwi = internals.requestingWindow; + + // If we have a requesting window and the requesting document is + // still the current document, open the popup. + if (dwi && dwi.document == internals.requestingDocument) { + dwi.open(data.popupWindowURIspec, data.popupWindowName, data.popupWindowFeatures); + } + } + break; + } + + case "PopupBlocking:GetBlockedPopupList": { + let popupData = []; + let length = this.popupData ? this.popupData.length : 0; + + // Limit 15 popup URLs to be reported through the UI + length = Math.min(length, 15); + + for (let i = 0; i < length; i++) { + let popupWindowURIspec = this.popupData[i].popupWindowURIspec; + + if (popupWindowURIspec == global.content.location.href) { + popupWindowURIspec = "<self>"; + } else { + // Limit 500 chars to be sent because the URI will be cropped + // by the UI anyway, and data: URIs can be significantly larger. + popupWindowURIspec = popupWindowURIspec.substring(0, 500) + } + + popupData.push({popupWindowURIspec}); + } + + sendAsyncMessage("PopupBlocking:ReplyGetBlockedPopupList", {popupData}); + break; + } + } + }, + + handleEvent: function(ev) { + switch (ev.type) { + case "DOMPopupBlocked": + return this.onPopupBlocked(ev); + case "pageshow": + return this.onPageShow(ev); + case "pagehide": + return this.onPageHide(ev); + } + return undefined; + }, + + onPopupBlocked: function(ev) { + if (!this.popupData) { + this.popupData = new Array(); + this.popupDataInternal = new Array(); + } + + let obj = { + popupWindowURIspec: ev.popupWindowURI ? ev.popupWindowURI.spec : "about:blank", + popupWindowFeatures: ev.popupWindowFeatures, + popupWindowName: ev.popupWindowName + }; + + let internals = { + requestingWindow: ev.requestingWindow, + requestingDocument: ev.requestingWindow.document, + }; + + this.popupData.push(obj); + this.popupDataInternal.push(internals); + this.updateBlockedPopups(true); + }, + + onPageShow: function(ev) { + if (this.popupData) { + let i = 0; + while (i < this.popupData.length) { + // Filter out irrelevant reports. + if (this.popupDataInternal[i].requestingWindow && + (this.popupDataInternal[i].requestingWindow.document == + this.popupDataInternal[i].requestingDocument)) { + i++; + } else { + this.popupData.splice(i, 1); + this.popupDataInternal.splice(i, 1); + } + } + if (this.popupData.length == 0) { + this.popupData = null; + this.popupDataInternal = null; + } + this.updateBlockedPopups(false); + } + }, + + onPageHide: function(ev) { + if (this.popupData) { + this.popupData = null; + this.popupDataInternal = null; + this.updateBlockedPopups(false); + } + }, + + updateBlockedPopups: function(freshPopup) { + sendAsyncMessage("PopupBlocking:UpdateBlockedPopups", + { + count: this.popupData ? this.popupData.length : 0, + freshPopup + }); + }, +}; +PopupBlocking.init(); + +XPCOMUtils.defineLazyGetter(this, "console", () => { + // Set up console.* for frame scripts. + let Console = Components.utils.import("resource://gre/modules/Console.jsm", {}); + return new Console.ConsoleAPI(); +}); + +var Printing = { + // Bug 1088061: nsPrintEngine's DoCommonPrint currently expects the + // progress listener passed to it to QI to an nsIPrintingPromptService + // in order to know that a printing progress dialog has been shown. That's + // really all the interface is used for, hence the fact that I don't actually + // implement the interface here. Bug 1088061 has been filed to remove + // this hackery. + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsIPrintingPromptService]), + + MESSAGES: [ + "Printing:Preview:Enter", + "Printing:Preview:Exit", + "Printing:Preview:Navigate", + "Printing:Preview:ParseDocument", + "Printing:Preview:UpdatePageCount", + "Printing:Print", + ], + + init() { + this.MESSAGES.forEach(msgName => addMessageListener(msgName, this)); + addEventListener("PrintingError", this, true); + }, + + get shouldSavePrintSettings() { + return Services.prefs.getBoolPref("print.use_global_printsettings", false) && + Services.prefs.getBoolPref("print.save_print_settings", false); + }, + + handleEvent(event) { + if (event.type == "PrintingError") { + let win = event.target.defaultView; + let wbp = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebBrowserPrint); + let nsresult = event.detail; + sendAsyncMessage("Printing:Error", { + isPrinting: wbp.doingPrint, + nsresult: nsresult, + }); + } + }, + + receiveMessage(message) { + let objects = message.objects; + let data = message.data; + switch (message.name) { + case "Printing:Preview:Enter": { + this.enterPrintPreview(Services.wm.getOuterWindowWithId(data.windowID), data.simplifiedMode); + break; + } + + case "Printing:Preview:Exit": { + this.exitPrintPreview(); + break; + } + + case "Printing:Preview:Navigate": { + this.navigate(data.navType, data.pageNum); + break; + } + + case "Printing:Preview:ParseDocument": { + this.parseDocument(data.URL, Services.wm.getOuterWindowWithId(data.windowID)); + break; + } + + case "Printing:Preview:UpdatePageCount": { + this.updatePageCount(); + break; + } + + case "Printing:Print": { + this.print(Services.wm.getOuterWindowWithId(data.windowID), data.simplifiedMode); + break; + } + } + }, + + getPrintSettings() { + try { + let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"] + .getService(Ci.nsIPrintSettingsService); + + let printSettings = PSSVC.globalPrintSettings; + if (!printSettings.printerName) { + printSettings.printerName = PSSVC.defaultPrinterName; + } + // First get any defaults from the printer + PSSVC.initPrintSettingsFromPrinter(printSettings.printerName, + printSettings); + // now augment them with any values from last time + PSSVC.initPrintSettingsFromPrefs(printSettings, true, + printSettings.kInitSaveAll); + + return printSettings; + } catch (e) { + Components.utils.reportError(e); + } + + return null; + }, + + parseDocument(URL, contentWindow) { + // By using ReaderMode primitives, we parse given document and place the + // resulting JS object into the DOM of current browser. + let articlePromise = ReaderMode.parseDocument(contentWindow.document).catch(Cu.reportError); + articlePromise.then(function (article) { + // We make use of a web progress listener in order to know when the content we inject + // into the DOM has finished rendering. If our layout engine is still painting, we + // will wait for MozAfterPaint event to be fired. + let webProgressListener = { + onStateChange: function (webProgress, req, flags, status) { + if (flags & Ci.nsIWebProgressListener.STATE_STOP) { + webProgress.removeProgressListener(webProgressListener); + let domUtils = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + // Here we tell the parent that we have parsed the document successfully + // using ReaderMode primitives and we are able to enter on preview mode. + if (domUtils.isMozAfterPaintPending) { + addEventListener("MozAfterPaint", function onPaint() { + removeEventListener("MozAfterPaint", onPaint); + sendAsyncMessage("Printing:Preview:ReaderModeReady"); + }); + } else { + sendAsyncMessage("Printing:Preview:ReaderModeReady"); + } + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference, + Ci.nsIObserver, + ]), + }; + + // Here we QI the docShell into a nsIWebProgress passing our web progress listener in. + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener(webProgressListener, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST); + + content.document.head.innerHTML = ""; + + // Set title of document + content.document.title = article.title; + + // Set base URI of document. Print preview code will read this value to + // populate the URL field in print settings so that it doesn't show + // "about:blank" as its URI. + let headBaseElement = content.document.createElement("base"); + headBaseElement.setAttribute("href", URL); + content.document.head.appendChild(headBaseElement); + + // Create link element referencing aboutReader.css and append it to head + let headStyleElement = content.document.createElement("link"); + headStyleElement.setAttribute("rel", "stylesheet"); + headStyleElement.setAttribute("href", "chrome://global/skin/aboutReader.css"); + headStyleElement.setAttribute("type", "text/css"); + content.document.head.appendChild(headStyleElement); + + content.document.body.innerHTML = ""; + + // Create container div (main element) and append it to body + let containerElement = content.document.createElement("div"); + containerElement.setAttribute("id", "container"); + content.document.body.appendChild(containerElement); + + // Create header div and append it to container + let headerElement = content.document.createElement("div"); + headerElement.setAttribute("id", "reader-header"); + headerElement.setAttribute("class", "header"); + containerElement.appendChild(headerElement); + + // Create style element for header div and import simplifyMode.css + let controlHeaderStyle = content.document.createElement("style"); + controlHeaderStyle.setAttribute("scoped", ""); + controlHeaderStyle.textContent = "@import url(\"chrome://global/content/simplifyMode.css\");"; + headerElement.appendChild(controlHeaderStyle); + + // Jam the article's title and byline into header div + let titleElement = content.document.createElement("h1"); + titleElement.setAttribute("id", "reader-title"); + titleElement.textContent = article.title; + headerElement.appendChild(titleElement); + + let bylineElement = content.document.createElement("div"); + bylineElement.setAttribute("id", "reader-credits"); + bylineElement.setAttribute("class", "credits"); + bylineElement.textContent = article.byline; + headerElement.appendChild(bylineElement); + + // Display header element + headerElement.style.display = "block"; + + // Create content div and append it to container + let contentElement = content.document.createElement("div"); + contentElement.setAttribute("class", "content"); + containerElement.appendChild(contentElement); + + // Create style element for content div and import aboutReaderContent.css + let controlContentStyle = content.document.createElement("style"); + controlContentStyle.setAttribute("scoped", ""); + controlContentStyle.textContent = "@import url(\"chrome://global/skin/aboutReaderContent.css\");"; + contentElement.appendChild(controlContentStyle); + + // Jam the article's content into content div + let readerContent = content.document.createElement("div"); + readerContent.setAttribute("id", "moz-reader-content"); + contentElement.appendChild(readerContent); + + let articleUri = Services.io.newURI(article.url, null, null); + let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils); + let contentFragment = parserUtils.parseFragment(article.content, + Ci.nsIParserUtils.SanitizerDropForms | Ci.nsIParserUtils.SanitizerAllowStyle, + false, articleUri, readerContent); + + readerContent.appendChild(contentFragment); + + // Display reader content element + readerContent.style.display = "block"; + }); + }, + + enterPrintPreview(contentWindow, simplifiedMode) { + // We'll call this whenever we've finished reflowing the document, or if + // we errored out while attempting to print preview (in which case, we'll + // notify the parent that we've failed). + let notifyEntered = (error) => { + removeEventListener("printPreviewUpdate", onPrintPreviewReady); + sendAsyncMessage("Printing:Preview:Entered", { + failed: !!error, + }); + }; + + let onPrintPreviewReady = () => { + notifyEntered(); + }; + + // We have to wait for the print engine to finish reflowing all of the + // documents and subdocuments before we can tell the parent to flip to + // the print preview UI - otherwise, the print preview UI might ask for + // information (like the number of pages in the document) before we have + // our PresShells set up. + addEventListener("printPreviewUpdate", onPrintPreviewReady); + + try { + let printSettings = this.getPrintSettings(); + + // If we happen to be on simplified mode, we need to set docURL in order + // to generate header/footer content correctly, since simplified tab has + // "about:blank" as its URI. + if (printSettings && simplifiedMode) + printSettings.docURL = contentWindow.document.baseURI; + + docShell.printPreview.printPreview(printSettings, contentWindow, this); + } catch (error) { + // This might fail if we, for example, attempt to print a XUL document. + // In that case, we inform the parent to bail out of print preview. + Components.utils.reportError(error); + notifyEntered(error); + } + }, + + exitPrintPreview() { + docShell.printPreview.exitPrintPreview(); + }, + + print(contentWindow, simplifiedMode) { + let printSettings = this.getPrintSettings(); + let rv = Cr.NS_OK; + + // If we happen to be on simplified mode, we need to set docURL in order + // to generate header/footer content correctly, since simplified tab has + // "about:blank" as its URI. + if (printSettings && simplifiedMode) { + printSettings.docURL = contentWindow.document.baseURI; + } + + try { + let print = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebBrowserPrint); + + if (print.doingPrintPreview) { + this.logKeyedTelemetry("PRINT_DIALOG_OPENED_COUNT", "FROM_PREVIEW"); + } else { + this.logKeyedTelemetry("PRINT_DIALOG_OPENED_COUNT", "FROM_PAGE"); + } + + print.print(printSettings, null); + + if (print.doingPrintPreview) { + if (simplifiedMode) { + this.logKeyedTelemetry("PRINT_COUNT", "SIMPLIFIED"); + } else { + this.logKeyedTelemetry("PRINT_COUNT", "WITH_PREVIEW"); + } + } else { + this.logKeyedTelemetry("PRINT_COUNT", "WITHOUT_PREVIEW"); + } + } catch (e) { + // Pressing cancel is expressed as an NS_ERROR_ABORT return value, + // causing an exception to be thrown which we catch here. + if (e.result != Cr.NS_ERROR_ABORT) { + Cu.reportError(`In Printing:Print:Done handler, got unexpected rv + ${e.result}.`); + sendAsyncMessage("Printing:Error", { + isPrinting: true, + nsresult: e.result, + }); + } + } + + if (this.shouldSavePrintSettings) { + let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"] + .getService(Ci.nsIPrintSettingsService); + + PSSVC.savePrintSettingsToPrefs(printSettings, true, + printSettings.kInitSaveAll); + PSSVC.savePrintSettingsToPrefs(printSettings, false, + printSettings.kInitSavePrinterName); + } + }, + + logKeyedTelemetry(id, key) { + let histogram = Services.telemetry.getKeyedHistogramById(id); + histogram.add(key); + }, + + updatePageCount() { + let numPages = docShell.printPreview.printPreviewNumPages; + sendAsyncMessage("Printing:Preview:UpdatePageCount", { + numPages: numPages, + }); + }, + + navigate(navType, pageNum) { + docShell.printPreview.printPreviewNavigate(navType, pageNum); + }, + + /* nsIWebProgressListener for print preview */ + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + sendAsyncMessage("Printing:Preview:StateChange", { + stateFlags: aStateFlags, + status: aStatus, + }); + }, + + onProgressChange(aWebProgress, aRequest, aCurSelfProgress, + aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress) { + sendAsyncMessage("Printing:Preview:ProgressChange", { + curSelfProgress: aCurSelfProgress, + maxSelfProgress: aMaxSelfProgress, + curTotalProgress: aCurTotalProgress, + maxTotalProgress: aMaxTotalProgress, + }); + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {}, + onSecurityChange(aWebProgress, aRequest, aState) {}, +} +Printing.init(); + +function SwitchDocumentDirection(aWindow) { + // document.dir can also be "auto", in which case it won't change + if (aWindow.document.dir == "ltr" || aWindow.document.dir == "") { + aWindow.document.dir = "rtl"; + } else if (aWindow.document.dir == "rtl") { + aWindow.document.dir = "ltr"; + } + for (let run = 0; run < aWindow.frames.length; run++) { + SwitchDocumentDirection(aWindow.frames[run]); + } +} + +addMessageListener("SwitchDocumentDirection", () => { + SwitchDocumentDirection(content.window); +}); + +var FindBar = { + /* Please keep in sync with toolkit/content/widgets/findbar.xml */ + FIND_NORMAL: 0, + FIND_TYPEAHEAD: 1, + FIND_LINKS: 2, + + _findMode: 0, + + init() { + addMessageListener("Findbar:UpdateState", this); + Services.els.addSystemEventListener(global, "keypress", this, false); + Services.els.addSystemEventListener(global, "mouseup", this, false); + }, + + receiveMessage(msg) { + switch (msg.name) { + case "Findbar:UpdateState": + this._findMode = msg.data.findMode; + break; + } + }, + + handleEvent(event) { + switch (event.type) { + case "keypress": + this._onKeypress(event); + break; + case "mouseup": + this._onMouseup(event); + break; + } + }, + + /** + * Returns whether FAYT can be used for the given event in + * the current content state. + */ + _canAndShouldFastFind() { + let should = false; + let can = BrowserUtils.canFastFind(content); + if (can) { + // XXXgijs: why all these shenanigans? Why not use the event's target? + let focusedWindow = {}; + let elt = Services.focus.getFocusedElementForWindow(content, true, focusedWindow); + let win = focusedWindow.value; + should = BrowserUtils.shouldFastFind(elt, win); + } + return { can, should } + }, + + _onKeypress(event) { + // Useless keys: + if (event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented) { + return undefined; + } + + // Check the focused element etc. + let fastFind = this._canAndShouldFastFind(); + + // Can we even use find in this page at all? + if (!fastFind.can) { + return undefined; + } + + let fakeEvent = {}; + for (let k in event) { + if (typeof event[k] != "object" && typeof event[k] != "function" && + !(k in content.KeyboardEvent)) { + fakeEvent[k] = event[k]; + } + } + // sendSyncMessage returns an array of the responses from all listeners + let rv = sendSyncMessage("Findbar:Keypress", { + fakeEvent: fakeEvent, + shouldFastFind: fastFind.should + }); + if (rv.indexOf(false) !== -1) { + event.preventDefault(); + return false; + } + return undefined; + }, + + _onMouseup(event) { + if (this._findMode != this.FIND_NORMAL) + sendAsyncMessage("Findbar:Mouseup"); + }, +}; +FindBar.init(); + +let WebChannelMessageToChromeListener = { + // Preference containing the list (space separated) of origins that are + // allowed to send non-string values through a WebChannel, mainly for + // backwards compatability. See bug 1238128 for more information. + URL_WHITELIST_PREF: "webchannel.allowObject.urlWhitelist", + + // Cached list of whitelisted principals, we avoid constructing this if the + // value in `_lastWhitelistValue` hasn't changed since we constructed it last. + _cachedWhitelist: [], + _lastWhitelistValue: "", + + init() { + addEventListener("WebChannelMessageToChrome", e => { + this._onMessageToChrome(e); + }, true, true); + }, + + _getWhitelistedPrincipals() { + let whitelist = Services.prefs.getCharPref(this.URL_WHITELIST_PREF); + if (whitelist != this._lastWhitelistValue) { + let urls = whitelist.split(/\s+/); + this._cachedWhitelist = urls.map(origin => + Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin)); + } + return this._cachedWhitelist; + }, + + _onMessageToChrome(e) { + // If target is window then we want the document principal, otherwise fallback to target itself. + let principal = e.target.nodePrincipal ? e.target.nodePrincipal : e.target.document.nodePrincipal; + + if (e.detail) { + if (typeof e.detail != 'string') { + // Check if the principal is one of the ones that's allowed to send + // non-string values for e.detail. + let objectsAllowed = this._getWhitelistedPrincipals().some(whitelisted => + principal.originNoSuffix == whitelisted.originNoSuffix); + if (!objectsAllowed) { + Cu.reportError("WebChannelMessageToChrome sent with an object from a non-whitelisted principal"); + return; + } + } + sendAsyncMessage("WebChannelMessageToChrome", e.detail, { eventTarget: e.target }, principal); + } else { + Cu.reportError("WebChannel message failed. No message detail."); + } + } +}; + +WebChannelMessageToChromeListener.init(); + +// This should be kept in sync with /browser/base/content.js. +// Add message listener for "WebChannelMessageToContent" messages from chrome scripts. +addMessageListener("WebChannelMessageToContent", function (e) { + if (e.data) { + // e.objects.eventTarget will be defined if sending a response to + // a WebChannelMessageToChrome event. An unsolicited send + // may not have an eventTarget defined, in this case send to the + // main content window. + let eventTarget = e.objects.eventTarget || content; + + // Use nodePrincipal if available, otherwise fallback to document principal. + let targetPrincipal = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget.document.nodePrincipal : eventTarget.nodePrincipal; + + if (e.principal.subsumes(targetPrincipal)) { + // If eventTarget is a window, use it as the targetWindow, otherwise + // find the window that owns the eventTarget. + let targetWindow = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget : eventTarget.ownerDocument.defaultView; + + eventTarget.dispatchEvent(new targetWindow.CustomEvent("WebChannelMessageToContent", { + detail: Cu.cloneInto({ + id: e.data.id, + message: e.data.message, + }, targetWindow), + })); + } else { + Cu.reportError("WebChannel message failed. Principal mismatch."); + } + } else { + Cu.reportError("WebChannel message failed. No message data."); + } +}); + +var AudioPlaybackListener = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + init() { + Services.obs.addObserver(this, "audio-playback", false); + Services.obs.addObserver(this, "AudioFocusChanged", false); + Services.obs.addObserver(this, "MediaControl", false); + + addMessageListener("AudioPlayback", this); + addEventListener("unload", () => { + AudioPlaybackListener.uninit(); + }); + }, + + uninit() { + Services.obs.removeObserver(this, "audio-playback"); + Services.obs.removeObserver(this, "AudioFocusChanged"); + Services.obs.removeObserver(this, "MediaControl"); + + removeMessageListener("AudioPlayback", this); + }, + + handleMediaControlMessage(msg) { + let utils = global.content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let suspendTypes = Ci.nsISuspendedTypes; + switch (msg) { + case "mute": + utils.audioMuted = true; + break; + case "unmute": + utils.audioMuted = false; + break; + case "lostAudioFocus": + utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE_DISPOSABLE; + break; + case "lostAudioFocusTransiently": + utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE; + break; + case "gainAudioFocus": + utils.mediaSuspend = suspendTypes.NONE_SUSPENDED; + break; + case "mediaControlPaused": + utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE_DISPOSABLE; + break; + case "mediaControlStopped": + utils.mediaSuspend = suspendTypes.SUSPENDED_STOP_DISPOSABLE; + break; + case "blockInactivePageMedia": + utils.mediaSuspend = suspendTypes.SUSPENDED_BLOCK; + break; + case "resumeMedia": + utils.mediaSuspend = suspendTypes.NONE_SUSPENDED; + break; + default: + dump("Error : wrong media control msg!\n"); + break; + } + }, + + observe(subject, topic, data) { + if (topic === "audio-playback") { + if (subject && subject.top == global.content) { + let name = "AudioPlayback:"; + if (data === "block") { + name += "Block"; + } else { + name += (data === "active") ? "Start" : "Stop"; + } + sendAsyncMessage(name); + } + } else if (topic == "AudioFocusChanged" || topic == "MediaControl") { + this.handleMediaControlMessage(data); + } + }, + + receiveMessage(msg) { + if (msg.name == "AudioPlayback") { + this.handleMediaControlMessage(msg.data.type); + } + }, +}; +AudioPlaybackListener.init(); + +addMessageListener("Browser:PurgeSessionHistory", function BrowserPurgeHistory() { + let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; + if (!sessionHistory) { + return; + } + + // place the entry at current index at the end of the history list, so it won't get removed + if (sessionHistory.index < sessionHistory.count - 1) { + let indexEntry = sessionHistory.getEntryAtIndex(sessionHistory.index, false); + sessionHistory.QueryInterface(Components.interfaces.nsISHistoryInternal); + indexEntry.QueryInterface(Components.interfaces.nsISHEntry); + sessionHistory.addEntry(indexEntry, true); + } + + let purge = sessionHistory.count; + if (global.content.location.href != "about:blank") { + --purge; // Don't remove the page the user's staring at from shistory + } + + if (purge > 0) { + sessionHistory.PurgeHistory(purge); + } +}); + +var ViewSelectionSource = { + init: function () { + addMessageListener("ViewSource:GetSelection", this); + }, + + receiveMessage: function(message) { + if (message.name == "ViewSource:GetSelection") { + let selectionDetails; + try { + selectionDetails = message.objects.target ? this.getMathMLSelection(message.objects.target) + : this.getSelection(); + } finally { + sendAsyncMessage("ViewSource:GetSelectionDone", selectionDetails); + } + } + }, + + /** + * A helper to get a path like FIXptr, but with an array instead of the + * "tumbler" notation. + * See FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm + */ + getPath: function(ancestor, node) { + var n = node; + var p = n.parentNode; + if (n == ancestor || !p) + return null; + var path = new Array(); + if (!path) + return null; + do { + for (var i = 0; i < p.childNodes.length; i++) { + if (p.childNodes.item(i) == n) { + path.push(i); + break; + } + } + n = p; + p = n.parentNode; + } while (n != ancestor && p); + return path; + }, + + getSelection: function () { + // These are markers used to delimit the selection during processing. They + // are removed from the final rendering. + // We use noncharacter Unicode codepoints to minimize the risk of clashing + // with anything that might legitimately be present in the document. + // U+FDD0..FDEF <noncharacters> + const MARK_SELECTION_START = "\uFDD0"; + const MARK_SELECTION_END = "\uFDEF"; + + var focusedWindow = Services.focus.focusedWindow || content; + var selection = focusedWindow.getSelection(); + + var range = selection.getRangeAt(0); + var ancestorContainer = range.commonAncestorContainer; + var doc = ancestorContainer.ownerDocument; + + var startContainer = range.startContainer; + var endContainer = range.endContainer; + var startOffset = range.startOffset; + var endOffset = range.endOffset; + + // let the ancestor be an element + var Node = doc.defaultView.Node; + if (ancestorContainer.nodeType == Node.TEXT_NODE || + ancestorContainer.nodeType == Node.CDATA_SECTION_NODE) + ancestorContainer = ancestorContainer.parentNode; + + // for selectAll, let's use the entire document, including <html>...</html> + // @see nsDocumentViewer::SelectAll() for how selectAll is implemented + try { + if (ancestorContainer == doc.body) + ancestorContainer = doc.documentElement; + } catch (e) { } + + // each path is a "child sequence" (a.k.a. "tumbler") that + // descends from the ancestor down to the boundary point + var startPath = this.getPath(ancestorContainer, startContainer); + var endPath = this.getPath(ancestorContainer, endContainer); + + // clone the fragment of interest and reset everything to be relative to it + // note: it is with the clone that we operate/munge from now on. Also note + // that we clone into a data document to prevent images in the fragment from + // loading and the like. The use of importNode here, as opposed to adoptNode, + // is _very_ important. + // XXXbz wish there were a less hacky way to create an untrusted document here + var isHTML = (doc.createElement("div").tagName == "DIV"); + var dataDoc = isHTML ? + ancestorContainer.ownerDocument.implementation.createHTMLDocument("") : + ancestorContainer.ownerDocument.implementation.createDocument("", "", null); + ancestorContainer = dataDoc.importNode(ancestorContainer, true); + startContainer = ancestorContainer; + endContainer = ancestorContainer; + + // Only bother with the selection if it can be remapped. Don't mess with + // leaf elements (such as <isindex>) that secretly use anynomous content + // for their display appearance. + var canDrawSelection = ancestorContainer.hasChildNodes(); + var tmpNode; + if (canDrawSelection) { + var i; + for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) { + startContainer = startContainer.childNodes.item(startPath[i]); + } + for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) { + endContainer = endContainer.childNodes.item(endPath[i]); + } + + // add special markers to record the extent of the selection + // note: |startOffset| and |endOffset| are interpreted either as + // offsets in the text data or as child indices (see the Range spec) + // (here, munging the end point first to keep the start point safe...) + if (endContainer.nodeType == Node.TEXT_NODE || + endContainer.nodeType == Node.CDATA_SECTION_NODE) { + // do some extra tweaks to try to avoid the view-source output to look like + // ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection). + // To get a neat output, the idea here is to remap the end point from: + // 1. ...<tag>]... to ...]<tag>... + // 2. ...]</tag>... to ...</tag>]... + if ((endOffset > 0 && endOffset < endContainer.data.length) || + !endContainer.parentNode || !endContainer.parentNode.parentNode) + endContainer.insertData(endOffset, MARK_SELECTION_END); + else { + tmpNode = dataDoc.createTextNode(MARK_SELECTION_END); + endContainer = endContainer.parentNode; + if (endOffset === 0) + endContainer.parentNode.insertBefore(tmpNode, endContainer); + else + endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling); + } + } + else { + tmpNode = dataDoc.createTextNode(MARK_SELECTION_END); + endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset)); + } + + if (startContainer.nodeType == Node.TEXT_NODE || + startContainer.nodeType == Node.CDATA_SECTION_NODE) { + // do some extra tweaks to try to avoid the view-source output to look like + // ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection). + // To get a neat output, the idea here is to remap the start point from: + // 1. ...<tag>[... to ...[<tag>... + // 2. ...[</tag>... to ...</tag>[... + if ((startOffset > 0 && startOffset < startContainer.data.length) || + !startContainer.parentNode || !startContainer.parentNode.parentNode || + startContainer != startContainer.parentNode.lastChild) + startContainer.insertData(startOffset, MARK_SELECTION_START); + else { + tmpNode = dataDoc.createTextNode(MARK_SELECTION_START); + startContainer = startContainer.parentNode; + if (startOffset === 0) + startContainer.parentNode.insertBefore(tmpNode, startContainer); + else + startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling); + } + } + else { + tmpNode = dataDoc.createTextNode(MARK_SELECTION_START); + startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset)); + } + } + + // now extract and display the syntax highlighted source + tmpNode = dataDoc.createElementNS("http://www.w3.org/1999/xhtml", "div"); + tmpNode.appendChild(ancestorContainer); + + return { uri: (isHTML ? "view-source:data:text/html;charset=utf-8," : + "view-source:data:application/xml;charset=utf-8,") + + encodeURIComponent(tmpNode.innerHTML), + drawSelection: canDrawSelection, + baseURI: doc.baseURI }; + }, + + /** + * Reformat the source of a MathML node to highlight the node that was targetted. + * + * @param node + * Some element within the fragment of interest. + */ + getMathMLSelection: function(node) { + var Node = node.ownerDocument.defaultView.Node; + this._lineCount = 0; + this._startTargetLine = 0; + this._endTargetLine = 0; + this._targetNode = node; + if (this._targetNode && this._targetNode.nodeType == Node.TEXT_NODE) + this._targetNode = this._targetNode.parentNode; + + // walk up the tree to the top-level element (e.g., <math>, <svg>) + var topTag = "math"; + var topNode = this._targetNode; + while (topNode && topNode.localName != topTag) { + topNode = topNode.parentNode; + } + if (!topNode) + return undefined; + + // serialize + const VIEW_SOURCE_CSS = "resource://gre-resources/viewsource.css"; + const BUNDLE_URL = "chrome://global/locale/viewSource.properties"; + + let bundle = Services.strings.createBundle(BUNDLE_URL); + var title = bundle.GetStringFromName("viewMathMLSourceTitle"); + var wrapClass = this.wrapLongLines ? ' class="wrap"' : ''; + var source = + '<!DOCTYPE html>' + + '<html>' + + '<head><title>' + title + '</title>' + + '<link rel="stylesheet" type="text/css" href="' + VIEW_SOURCE_CSS + '">' + + '<style type="text/css">' + + '#target { border: dashed 1px; background-color: lightyellow; }' + + '</style>' + + '</head>' + + '<body id="viewsource"' + wrapClass + + ' onload="document.title=\''+title+'\'; document.getElementById(\'target\').scrollIntoView(true)">' + + '<pre>' + + this.getOuterMarkup(topNode, 0) + + '</pre></body></html>' + ; // end + + return { uri: "data:text/html;charset=utf-8," + encodeURIComponent(source), + drawSelection: false, baseURI: node.ownerDocument.baseURI }; + }, + + get wrapLongLines() { + return Services.prefs.getBoolPref("view_source.wrap_long_lines"); + }, + + getInnerMarkup: function(node, indent) { + var str = ''; + for (var i = 0; i < node.childNodes.length; i++) { + str += this.getOuterMarkup(node.childNodes.item(i), indent); + } + return str; + }, + + getOuterMarkup: function(node, indent) { + var Node = node.ownerDocument.defaultView.Node; + var newline = ""; + var padding = ""; + var str = ""; + if (node == this._targetNode) { + this._startTargetLine = this._lineCount; + str += '</pre><pre id="target">'; + } + + switch (node.nodeType) { + case Node.ELEMENT_NODE: // Element + // to avoid the wide gap problem, '\n' is not emitted on the first + // line and the lines before & after the <pre id="target">...</pre> + if (this._lineCount > 0 && + this._lineCount != this._startTargetLine && + this._lineCount != this._endTargetLine) { + newline = "\n"; + } + this._lineCount++; + for (var k = 0; k < indent; k++) { + padding += " "; + } + str += newline + padding + + '<<span class="start-tag">' + node.nodeName + '</span>'; + for (var i = 0; i < node.attributes.length; i++) { + var attr = node.attributes.item(i); + if (attr.nodeName.match(/^[-_]moz/)) { + continue; + } + str += ' <span class="attribute-name">' + + attr.nodeName + + '</span>=<span class="attribute-value">"' + + this.unicodeToEntity(attr.nodeValue) + + '"</span>'; + } + if (!node.hasChildNodes()) { + str += "/>"; + } + else { + str += ">"; + var oldLine = this._lineCount; + str += this.getInnerMarkup(node, indent + 2); + if (oldLine == this._lineCount) { + newline = ""; + padding = ""; + } + else { + newline = (this._lineCount == this._endTargetLine) ? "" : "\n"; + this._lineCount++; + } + str += newline + padding + + '</<span class="end-tag">' + node.nodeName + '</span>>'; + } + break; + case Node.TEXT_NODE: // Text + var tmp = node.nodeValue; + tmp = tmp.replace(/(\n|\r|\t)+/g, " "); + tmp = tmp.replace(/^ +/, ""); + tmp = tmp.replace(/ +$/, ""); + if (tmp.length != 0) { + str += '<span class="text">' + this.unicodeToEntity(tmp) + '</span>'; + } + break; + default: + break; + } + + if (node == this._targetNode) { + this._endTargetLine = this._lineCount; + str += '</pre><pre>'; + } + return str; + }, + + unicodeToEntity: function(text) { + const charTable = { + '&': '&<span class="entity">amp;</span>', + '<': '&<span class="entity">lt;</span>', + '>': '&<span class="entity">gt;</span>', + '"': '&<span class="entity">quot;</span>' + }; + + function charTableLookup(letter) { + return charTable[letter]; + } + + function convertEntity(letter) { + try { + var unichar = this._entityConverter + .ConvertToEntity(letter, entityVersion); + var entity = unichar.substring(1); // extract '&' + return '&<span class="entity">' + entity + '</span>'; + } catch (ex) { + return letter; + } + } + + if (!this._entityConverter) { + try { + this._entityConverter = Cc["@mozilla.org/intl/entityconverter;1"] + .createInstance(Ci.nsIEntityConverter); + } catch (e) { } + } + + const entityVersion = Ci.nsIEntityConverter.entityW3C; + + var str = text; + + // replace chars in our charTable + str = str.replace(/[<>&"]/g, charTableLookup); + + // replace chars > 0x7f via nsIEntityConverter + str = str.replace(/[^\0-\u007f]/g, convertEntity); + + return str; + } +}; + +ViewSelectionSource.init(); + +addEventListener("MozApplicationManifest", function(e) { + let doc = e.target; + let info = { + uri: doc.documentURI, + characterSet: doc.characterSet, + manifest: doc.documentElement.getAttribute("manifest"), + principal: doc.nodePrincipal, + }; + sendAsyncMessage("MozApplicationManifest", info); +}, false); + +let AutoCompletePopup = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup]), + + _connected: false, + + MESSAGES: [ + "FormAutoComplete:HandleEnter", + "FormAutoComplete:PopupClosed", + "FormAutoComplete:PopupOpened", + "FormAutoComplete:RequestFocus", + ], + + init: function() { + addEventListener("unload", this); + addEventListener("DOMContentLoaded", this); + + for (let messageName of this.MESSAGES) { + addMessageListener(messageName, this); + } + + this._input = null; + this._popupOpen = false; + }, + + destroy: function() { + if (this._connected) { + let controller = Cc["@mozilla.org/satchel/form-fill-controller;1"] + .getService(Ci.nsIFormFillController); + controller.detachFromBrowser(docShell); + this._connected = false; + } + + removeEventListener("unload", this); + removeEventListener("DOMContentLoaded", this); + + for (let messageName of this.MESSAGES) { + removeMessageListener(messageName, this); + } + }, + + handleEvent(event) { + switch (event.type) { + case "DOMContentLoaded": { + removeEventListener("DOMContentLoaded", this); + + // We need to wait for a content viewer to be available + // before we can attach our AutoCompletePopup handler, + // since nsFormFillController assumes one will exist + // when we call attachToBrowser. + + // Hook up the form fill autocomplete controller. + let controller = Cc["@mozilla.org/satchel/form-fill-controller;1"] + .getService(Ci.nsIFormFillController); + controller.attachToBrowser(docShell, + this.QueryInterface(Ci.nsIAutoCompletePopup)); + this._connected = true; + break; + } + + case "unload": { + this.destroy(); + break; + } + } + }, + + receiveMessage(message) { + switch (message.name) { + case "FormAutoComplete:HandleEnter": { + this.selectedIndex = message.data.selectedIndex; + + let controller = Cc["@mozilla.org/autocomplete/controller;1"] + .getService(Ci.nsIAutoCompleteController); + controller.handleEnter(message.data.isPopupSelection); + break; + } + + case "FormAutoComplete:PopupClosed": { + this._popupOpen = false; + break; + } + + case "FormAutoComplete:PopupOpened": { + this._popupOpen = true; + break; + } + + case "FormAutoComplete:RequestFocus": { + if (this._input) { + this._input.focus(); + } + break; + } + } + }, + + get input () { return this._input; }, + get overrideValue () { return null; }, + set selectedIndex (index) { + sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index }); + }, + get selectedIndex () { + // selectedIndex getter must be synchronous because we need the + // correct value when the controller is in controller::HandleEnter. + // We can't easily just let the parent inform us the new value every + // time it changes because not every action that can change the + // selectedIndex is trivial to catch (e.g. moving the mouse over the + // list). + return sendSyncMessage("FormAutoComplete:GetSelectedIndex", {}); + }, + get popupOpen () { + return this._popupOpen; + }, + + openAutocompletePopup: function (input, element) { + if (this._popupOpen || !input) { + return; + } + + let rect = BrowserUtils.getElementBoundingScreenRect(element); + let window = element.ownerDocument.defaultView; + let dir = window.getComputedStyle(element).direction; + let results = this.getResultsFromController(input); + + sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", + { results, rect, dir }); + this._input = input; + }, + + closePopup: function () { + // We set this here instead of just waiting for the + // PopupClosed message to do it so that we don't end + // up in a state where the content thinks that a popup + // is open when it isn't (or soon won't be). + this._popupOpen = false; + sendAsyncMessage("FormAutoComplete:ClosePopup", {}); + }, + + invalidate: function () { + if (this._popupOpen) { + let results = this.getResultsFromController(this._input); + sendAsyncMessage("FormAutoComplete:Invalidate", { results }); + } + }, + + selectBy: function(reverse, page) { + this._index = sendSyncMessage("FormAutoComplete:SelectBy", { + reverse: reverse, + page: page + }); + }, + + getResultsFromController(inputField) { + let results = []; + + if (!inputField) { + return results; + } + + let controller = inputField.controller; + if (!(controller instanceof Ci.nsIAutoCompleteController)) { + return results; + } + + for (let i = 0; i < controller.matchCount; ++i) { + let result = {}; + result.value = controller.getValueAt(i); + result.label = controller.getLabelAt(i); + result.comment = controller.getCommentAt(i); + result.style = controller.getStyleAt(i); + result.image = controller.getImageAt(i); + results.push(result); + } + + return results; + }, +} + +AutoCompletePopup.init(); + +/** + * DateTimePickerListener is the communication channel between the input box + * (content) for date/time input types and its picker (chrome). + */ +let DateTimePickerListener = { + /** + * On init, just listen for the event to open the picker, once the picker is + * opened, we'll listen for update and close events. + */ + init: function() { + addEventListener("MozOpenDateTimePicker", this); + this._inputElement = null; + + addEventListener("unload", () => { + this.uninit(); + }); + }, + + uninit: function() { + removeEventListener("MozOpenDateTimePicker", this); + this._inputElement = null; + }, + + /** + * Cleanup function called when picker is closed. + */ + close: function() { + this.removeListeners(); + this._inputElement.setDateTimePickerState(false); + this._inputElement = null; + }, + + /** + * Called after picker is opened to start listening for input box update + * events. + */ + addListeners: function() { + addEventListener("MozUpdateDateTimePicker", this); + addEventListener("MozCloseDateTimePicker", this); + addEventListener("pagehide", this); + + addMessageListener("FormDateTime:PickerValueChanged", this); + addMessageListener("FormDateTime:PickerClosed", this); + }, + + /** + * Stop listeneing for events when picker is closed. + */ + removeListeners: function() { + removeEventListener("MozUpdateDateTimePicker", this); + removeEventListener("MozCloseDateTimePicker", this); + removeEventListener("pagehide", this); + + removeMessageListener("FormDateTime:PickerValueChanged", this); + removeMessageListener("FormDateTime:PickerClosed", this); + }, + + /** + * Helper function that returns the CSS direction property of the element. + */ + getComputedDirection: function(aElement) { + return aElement.ownerDocument.defaultView.getComputedStyle(aElement) + .getPropertyValue("direction"); + }, + + /** + * Helper function that returns the rect of the element, which is the position + * relative to the left/top of the content area. + */ + getBoundingContentRect: function(aElement) { + return BrowserUtils.getElementBoundingRect(aElement); + }, + + getTimePickerPref: function() { + return Services.prefs.getBoolPref("dom.forms.datetime.timepicker"); + }, + + /** + * nsIMessageListener. + */ + receiveMessage: function(aMessage) { + switch (aMessage.name) { + case "FormDateTime:PickerClosed": { + this.close(); + break; + } + case "FormDateTime:PickerValueChanged": { + this._inputElement.updateDateTimeInputBox(aMessage.data); + break; + } + default: + break; + } + }, + + /** + * nsIDOMEventListener, for chrome events sent by the input element and other + * DOM events. + */ + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "MozOpenDateTimePicker": { + // Time picker is disabled when preffed off + if (!(aEvent.originalTarget instanceof content.HTMLInputElement) || + (aEvent.originalTarget.type == "time" && !this.getTimePickerPref())) { + return; + } + this._inputElement = aEvent.originalTarget; + this._inputElement.setDateTimePickerState(true); + this.addListeners(); + + let value = this._inputElement.getDateTimeInputBoxValue(); + sendAsyncMessage("FormDateTime:OpenPicker", { + rect: this.getBoundingContentRect(this._inputElement), + dir: this.getComputedDirection(this._inputElement), + type: this._inputElement.type, + detail: { + // Pass partial value if it's available, otherwise pass input + // element's value. + value: Object.keys(value).length > 0 ? value + : this._inputElement.value, + step: this._inputElement.step, + min: this._inputElement.min, + max: this._inputElement.max, + }, + }); + break; + } + case "MozUpdateDateTimePicker": { + let value = this._inputElement.getDateTimeInputBoxValue(); + sendAsyncMessage("FormDateTime:UpdatePicker", { value }); + break; + } + case "MozCloseDateTimePicker": { + sendAsyncMessage("FormDateTime:ClosePicker"); + this.close(); + break; + } + case "pagehide": { + if (this._inputElement && + this._inputElement.ownerDocument == aEvent.target) { + sendAsyncMessage("FormDateTime:ClosePicker"); + this.close(); + } + break; + } + default: + break; + } + }, +} + +DateTimePickerListener.init(); diff --git a/toolkit/content/buildconfig.html b/toolkit/content/buildconfig.html new file mode 100644 index 0000000000..d3373f54c1 --- /dev/null +++ b/toolkit/content/buildconfig.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +# 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/. +# +#filter substitution +#include @TOPOBJDIR@/source-repo.h +<html> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width; user-scalable=false;"> + <title>about:buildconfig</title> + <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"> + <style type="text/css"> + th { text-align: start; } + h2 { margin-top: 1.5em; } + th, td { vertical-align: top; } + </style> +</head> +<body class="aboutPageWideContainer"> +<h1>about:buildconfig</h1> +#ifdef MOZ_SOURCE_URL +<h2>Source</h2> +<p>Built from <a href="@MOZ_SOURCE_URL@">@MOZ_SOURCE_URL@</a></p> +#endif +<h2>Build platform</h2> +<table> + <tbody> + <tr> + <th>target</th> + </tr> + <tr> + <td>@target@</td> + </tr> + </tbody> +</table> +<h2>Build tools</h2> +<table> + <tbody> + <tr> + <th>Compiler</th> + <th>Version</th> + <th>Compiler flags</th> + </tr> + <tr> + <td>@CC@</td> + <td>@CC_VERSION@</td> + <td>@CFLAGS@</td> + </tr> + <tr> + <td>@CXX@</td> + <td>@CC_VERSION@</td> +#ifndef BUILD_FASTER + <td>@CXXFLAGS@ @CPPFLAGS@</td> +#endif + </tr> + </tbody> +</table> +<h2>Configure options</h2> +<p>@MOZ_CONFIGURE_OPTIONS@</p> +#ifdef ANDROID +<h2>Package name</h2> +<p>@ANDROID_PACKAGE_NAME@</p> +#endif +</body> +</html> diff --git a/toolkit/content/contentAreaUtils.js b/toolkit/content/contentAreaUtils.js new file mode 100644 index 0000000000..2b7af30dec --- /dev/null +++ b/toolkit/content/contentAreaUtils.js @@ -0,0 +1,1330 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadLastDir", + "resource://gre/modules/DownloadLastDir.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", + "resource://gre/modules/Deprecated.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); + +var ContentAreaUtils = { + + // this is for backwards compatibility. + get ioService() { + return Services.io; + }, + + get stringBundle() { + delete this.stringBundle; + return this.stringBundle = + Services.strings.createBundle("chrome://global/locale/contentAreaCommands.properties"); + } +} + +function urlSecurityCheck(aURL, aPrincipal, aFlags) +{ + return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags); +} + +/** + * Determine whether or not a given focused DOMWindow is in the content area. + **/ +function isContentFrame(aFocusedWindow) +{ + if (!aFocusedWindow) + return false; + + return (aFocusedWindow.top == window.content); +} + +function forbidCPOW(arg, func, argname) +{ + if (arg && (typeof(arg) == "object" || typeof(arg) == "function") && + Components.utils.isCrossProcessWrapper(arg)) { + throw new Error(`no CPOWs allowed for argument ${argname} to ${func}`); + } +} + +// Clientele: (Make sure you don't break any of these) +// - File -> Save Page/Frame As... +// - Context -> Save Page/Frame As... +// - Context -> Save Link As... +// - Alt-Click links in web pages +// - Alt-Click links in the UI +// +// Try saving each of these types: +// - A complete webpage using File->Save Page As, and Context->Save Page As +// - A webpage as HTML only using the above methods +// - A webpage as Text only using the above methods +// - An image with an extension (e.g. .jpg) in its file name, using +// Context->Save Image As... +// - An image without an extension (e.g. a banner ad on cnn.com) using +// the above method. +// - A linked document using Save Link As... +// - A linked document using Alt-click Save Link As... +// +function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, + aSkipPrompt, aReferrer, aSourceDocument, aIsContentWindowPrivate) +{ + forbidCPOW(aURL, "saveURL", "aURL"); + forbidCPOW(aReferrer, "saveURL", "aReferrer"); + // Allow aSourceDocument to be a CPOW. + + internalSave(aURL, null, aFileName, null, null, aShouldBypassCache, + aFilePickerTitleKey, null, aReferrer, aSourceDocument, + aSkipPrompt, null, aIsContentWindowPrivate); +} + +// Just like saveURL, but will get some info off the image before +// calling internalSave +// Clientele: (Make sure you don't break any of these) +// - Context -> Save Image As... +const imgICache = Components.interfaces.imgICache; +const nsISupportsCString = Components.interfaces.nsISupportsCString; + +/** + * Offers to save an image URL to the file system. + * + * @param aURL (string) + * The URL of the image to be saved. + * @param aFileName (string) + * The suggested filename for the saved file. + * @param aFilePickerTitleKey (string, optional) + * Localized string key for an alternate title for the file + * picker. If set to null, this will default to something sensible. + * @param aShouldBypassCache (bool) + * If true, the image will always be retrieved from the server instead + * of the network or image caches. + * @param aSkipPrompt (bool) + * If true, we will attempt to save the file with the suggested + * filename to the default downloads folder without showing the + * file picker. + * @param aReferrer (nsIURI, optional) + * The referrer URI object (not a URL string) to use, or null + * if no referrer should be sent. + * @param aDoc (nsIDocument, deprecated, optional) + * The content document that the save is being initiated from. If this + * is omitted, then aIsContentWindowPrivate must be provided. + * @param aContentType (string, optional) + * The content type of the image. + * @param aContentDisp (string, optional) + * The content disposition of the image. + * @param aIsContentWindowPrivate (bool) + * Whether or not the containing window is in private browsing mode. + * Does not need to be provided is aDoc is passed. + */ +function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, + aSkipPrompt, aReferrer, aDoc, aContentType, aContentDisp, + aIsContentWindowPrivate) +{ + forbidCPOW(aURL, "saveImageURL", "aURL"); + forbidCPOW(aReferrer, "saveImageURL", "aReferrer"); + + if (aDoc && aIsContentWindowPrivate == undefined) { + if (Components.utils.isCrossProcessWrapper(aDoc)) { + Deprecated.warning("saveImageURL should not be passed document CPOWs. " + + "The caller should pass in the content type and " + + "disposition themselves", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1243643"); + } + // This will definitely not work for in-browser code or multi-process compatible + // add-ons due to bug 1233497, which makes unsafe CPOW usage throw by default. + Deprecated.warning("saveImageURL should be passed the private state of " + + "the containing window.", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1243643"); + aIsContentWindowPrivate = + PrivateBrowsingUtils.isContentWindowPrivate(aDoc.defaultView); + } + + // We'd better have the private state by now. + if (aIsContentWindowPrivate == undefined) { + throw new Error("saveImageURL couldn't compute private state of content window"); + } + + if (!aShouldBypassCache && (aDoc && !Components.utils.isCrossProcessWrapper(aDoc)) && + (!aContentType && !aContentDisp)) { + try { + var imageCache = Components.classes["@mozilla.org/image/tools;1"] + .getService(Components.interfaces.imgITools) + .getImgCacheForDocument(aDoc); + var props = + imageCache.findEntryProperties(makeURI(aURL, getCharsetforSave(null)), aDoc); + if (props) { + aContentType = props.get("type", nsISupportsCString); + aContentDisp = props.get("content-disposition", nsISupportsCString); + } + } catch (e) { + // Failure to get type and content-disposition off the image is non-fatal + } + } + + internalSave(aURL, null, aFileName, aContentDisp, aContentType, + aShouldBypassCache, aFilePickerTitleKey, null, aReferrer, + null, aSkipPrompt, null, aIsContentWindowPrivate); +} + +// This is like saveDocument, but takes any browser/frame-like element +// (nsIFrameLoaderOwner) and saves the current document inside it, +// whether in-process or out-of-process. +function saveBrowser(aBrowser, aSkipPrompt, aOuterWindowID=0) +{ + if (!aBrowser) { + throw "Must have a browser when calling saveBrowser"; + } + let persistable = aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .QueryInterface(Ci.nsIWebBrowserPersistable); + let stack = Components.stack.caller; + persistable.startPersistence(aOuterWindowID, { + onDocumentReady: function (document) { + saveDocument(document, aSkipPrompt); + }, + onError: function (status) { + throw new Components.Exception("saveBrowser failed asynchronously in startPersistence", + status, stack); + } + }); +} + +// Saves a document; aDocument can be an nsIWebBrowserPersistDocument +// (see saveBrowser, above) or an nsIDOMDocument. +// +// aDocument can also be a CPOW for a remote nsIDOMDocument, in which +// case "save as" modes that serialize the document's DOM are +// unavailable. This is a temporary measure for the "Save Frame As" +// command (bug 1141337) and pre-e10s add-ons. +function saveDocument(aDocument, aSkipPrompt) +{ + const Ci = Components.interfaces; + + if (!aDocument) + throw "Must have a document when calling saveDocument"; + + let contentDisposition = null; + let cacheKeyInt = null; + + if (aDocument instanceof Ci.nsIWebBrowserPersistDocument) { + // nsIWebBrowserPersistDocument exposes these directly. + contentDisposition = aDocument.contentDisposition; + cacheKeyInt = aDocument.cacheKey; + } else if (aDocument instanceof Ci.nsIDOMDocument) { + // Otherwise it's an actual nsDocument (and possibly a CPOW). + // We want to use cached data because the document is currently visible. + let ifreq = + aDocument.defaultView + .QueryInterface(Ci.nsIInterfaceRequestor); + + try { + contentDisposition = + ifreq.getInterface(Ci.nsIDOMWindowUtils) + .getDocumentMetadata("content-disposition"); + } catch (ex) { + // Failure to get a content-disposition is ok + } + + try { + let shEntry = + ifreq.getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIWebPageDescriptor) + .currentDescriptor + .QueryInterface(Ci.nsISHEntry); + + let cacheKey = shEntry.cacheKey + .QueryInterface(Ci.nsISupportsPRUint32) + .data; + // cacheKey might be a CPOW, which can't be passed to native + // code, but the data attribute is just a number. + cacheKeyInt = cacheKey.data; + } catch (ex) { + // We might not find it in the cache. Oh, well. + } + } + + // Convert the cacheKey back into an XPCOM object. + let cacheKey = null; + if (cacheKeyInt) { + cacheKey = Cc["@mozilla.org/supports-PRUint32;1"] + .createInstance(Ci.nsISupportsPRUint32); + cacheKey.data = cacheKeyInt; + } + + internalSave(aDocument.documentURI, aDocument, null, contentDisposition, + aDocument.contentType, false, null, null, + aDocument.referrer ? makeURI(aDocument.referrer) : null, + aDocument, aSkipPrompt, cacheKey); +} + +function DownloadListener(win, transfer) { + function makeClosure(name) { + return function() { + transfer[name].apply(transfer, arguments); + } + } + + this.window = win; + + // Now... we need to forward all calls to our transfer + for (var i in transfer) { + if (i != "QueryInterface") + this[i] = makeClosure(i); + } +} + +DownloadListener.prototype = { + QueryInterface: function dl_qi(aIID) + { + if (aIID.equals(Components.interfaces.nsIInterfaceRequestor) || + aIID.equals(Components.interfaces.nsIWebProgressListener) || + aIID.equals(Components.interfaces.nsIWebProgressListener2) || + aIID.equals(Components.interfaces.nsISupports)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + getInterface: function dl_gi(aIID) + { + if (aIID.equals(Components.interfaces.nsIAuthPrompt) || + aIID.equals(Components.interfaces.nsIAuthPrompt2)) { + var ww = + Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIPromptFactory); + return ww.getPrompt(this.window, aIID); + } + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + +const kSaveAsType_Complete = 0; // Save document with attached objects. +XPCOMUtils.defineConstant(this, "kSaveAsType_Complete", 0); +// const kSaveAsType_URL = 1; // Save document or URL by itself. +const kSaveAsType_Text = 2; // Save document, converting to plain text. +XPCOMUtils.defineConstant(this, "kSaveAsType_Text", kSaveAsType_Text); + +/** + * internalSave: Used when saving a document or URL. + * + * If aChosenData is null, this method: + * - Determines a local target filename to use + * - Prompts the user to confirm the destination filename and save mode + * (aContentType affects this) + * - [Note] This process involves the parameters aURL, aReferrer (to determine + * how aURL was encoded), aDocument, aDefaultFileName, aFilePickerTitleKey, + * and aSkipPrompt. + * + * If aChosenData is non-null, this method: + * - Uses the provided source URI and save file name + * - Saves the document as complete DOM if possible (aDocument present and + * right aContentType) + * - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and + * aSkipPrompt are ignored. + * + * In any case, this method: + * - Creates a 'Persist' object (which will perform the saving in the + * background) and then starts it. + * - [Note] This part of the process only involves the parameters aDocument, + * aShouldBypassCache and aReferrer. The source, the save name and the save + * mode are the ones determined previously. + * + * @param aURL + * The String representation of the URL of the document being saved + * @param aDocument + * The document to be saved + * @param aDefaultFileName + * The caller-provided suggested filename if we don't + * find a better one + * @param aContentDisposition + * The caller-provided content-disposition header to use. + * @param aContentType + * The caller-provided content-type to use + * @param aShouldBypassCache + * If true, the document will always be refetched from the server + * @param aFilePickerTitleKey + * Alternate title for the file picker + * @param aChosenData + * If non-null this contains an instance of object AutoChosen (see below) + * which holds pre-determined data so that the user does not need to be + * prompted for a target filename. + * @param aReferrer + * the referrer URI object (not URL string) to use, or null + * if no referrer should be sent. + * @param aInitiatingDocument [optional] + * The document from which the save was initiated. + * If this is omitted then aIsContentWindowPrivate has to be provided. + * @param aSkipPrompt [optional] + * If set to true, we will attempt to save the file to the + * default downloads folder without prompting. + * @param aCacheKey [optional] + * If set will be passed to saveURI. See nsIWebBrowserPersist for + * allowed values. + * @param aIsContentWindowPrivate [optional] + * This parameter is provided when the aInitiatingDocument is not a + * real document object. Stores whether aInitiatingDocument.defaultView + * was private or not. + */ +function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition, + aContentType, aShouldBypassCache, aFilePickerTitleKey, + aChosenData, aReferrer, aInitiatingDocument, aSkipPrompt, + aCacheKey, aIsContentWindowPrivate) +{ + forbidCPOW(aURL, "internalSave", "aURL"); + forbidCPOW(aReferrer, "internalSave", "aReferrer"); + forbidCPOW(aCacheKey, "internalSave", "aCacheKey"); + // Allow aInitiatingDocument to be a CPOW. + + if (aSkipPrompt == undefined) + aSkipPrompt = false; + + if (aCacheKey == undefined) + aCacheKey = null; + + // Note: aDocument == null when this code is used by save-link-as... + var saveMode = GetSaveModeForContentType(aContentType, aDocument); + + var file, sourceURI, saveAsType; + // Find the URI object for aURL and the FileName/Extension to use when saving. + // FileName/Extension will be ignored if aChosenData supplied. + if (aChosenData) { + file = aChosenData.file; + sourceURI = aChosenData.uri; + saveAsType = kSaveAsType_Complete; + + continueSave(); + } else { + var charset = null; + if (aDocument) + charset = aDocument.characterSet; + else if (aReferrer) + charset = aReferrer.originCharset; + var fileInfo = new FileInfo(aDefaultFileName); + initFileInfo(fileInfo, aURL, charset, aDocument, + aContentType, aContentDisposition); + sourceURI = fileInfo.uri; + + var fpParams = { + fpTitleKey: aFilePickerTitleKey, + fileInfo: fileInfo, + contentType: aContentType, + saveMode: saveMode, + saveAsType: kSaveAsType_Complete, + file: file + }; + + // Find a URI to use for determining last-downloaded-to directory + let relatedURI = aReferrer || sourceURI; + + promiseTargetFile(fpParams, aSkipPrompt, relatedURI).then(aDialogAccepted => { + if (!aDialogAccepted) + return; + + saveAsType = fpParams.saveAsType; + file = fpParams.file; + + continueSave(); + }).then(null, Components.utils.reportError); + } + + function continueSave() { + // XXX We depend on the following holding true in appendFiltersForContentType(): + // If we should save as a complete page, the saveAsType is kSaveAsType_Complete. + // If we should save as text, the saveAsType is kSaveAsType_Text. + var useSaveDocument = aDocument && + (((saveMode & SAVEMODE_COMPLETE_DOM) && (saveAsType == kSaveAsType_Complete)) || + ((saveMode & SAVEMODE_COMPLETE_TEXT) && (saveAsType == kSaveAsType_Text))); + // If we're saving a document, and are saving either in complete mode or + // as converted text, pass the document to the web browser persist component. + // If we're just saving the HTML (second option in the list), send only the URI. + let nonCPOWDocument = + aDocument && !Components.utils.isCrossProcessWrapper(aDocument); + + let isPrivate = aIsContentWindowPrivate; + if (isPrivate === undefined) { + isPrivate = aInitiatingDocument instanceof Components.interfaces.nsIDOMDocument + ? PrivateBrowsingUtils.isContentWindowPrivate(aInitiatingDocument.defaultView) + : aInitiatingDocument.isPrivate; + } + + var persistArgs = { + sourceURI : sourceURI, + sourceReferrer : aReferrer, + sourceDocument : useSaveDocument ? aDocument : null, + targetContentType : (saveAsType == kSaveAsType_Text) ? "text/plain" : null, + targetFile : file, + sourceCacheKey : aCacheKey, + sourcePostData : nonCPOWDocument ? getPostData(aDocument) : null, + bypassCache : aShouldBypassCache, + isPrivate : isPrivate, + }; + + // Start the actual save process + internalPersist(persistArgs); + } +} + +/** + * internalPersist: Creates a 'Persist' object (which will perform the saving + * in the background) and then starts it. + * + * @param persistArgs.sourceURI + * The nsIURI of the document being saved + * @param persistArgs.sourceCacheKey [optional] + * If set will be passed to saveURI + * @param persistArgs.sourceDocument [optional] + * The document to be saved, or null if not saving a complete document + * @param persistArgs.sourceReferrer + * Required and used only when persistArgs.sourceDocument is NOT present, + * the nsIURI of the referrer to use, or null if no referrer should be + * sent. + * @param persistArgs.sourcePostData + * Required and used only when persistArgs.sourceDocument is NOT present, + * represents the POST data to be sent along with the HTTP request, and + * must be null if no POST data should be sent. + * @param persistArgs.targetFile + * The nsIFile of the file to create + * @param persistArgs.targetContentType + * Required and used only when persistArgs.sourceDocument is present, + * determines the final content type of the saved file, or null to use + * the same content type as the source document. Currently only + * "text/plain" is meaningful. + * @param persistArgs.bypassCache + * If true, the document will always be refetched from the server + * @param persistArgs.isPrivate + * Indicates whether this is taking place in a private browsing context. + */ +function internalPersist(persistArgs) +{ + var persist = makeWebBrowserPersist(); + + // Calculate persist flags. + const nsIWBP = Components.interfaces.nsIWebBrowserPersist; + const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES | + nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES; + if (persistArgs.bypassCache) + persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE; + else + persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE; + + // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof): + persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; + + // Find the URI associated with the target file + var targetFileURL = makeFileURI(persistArgs.targetFile); + + // Create download and initiate it (below) + var tr = Components.classes["@mozilla.org/transfer;1"].createInstance(Components.interfaces.nsITransfer); + tr.init(persistArgs.sourceURI, + targetFileURL, "", null, null, null, persist, persistArgs.isPrivate); + persist.progressListener = new DownloadListener(window, tr); + + if (persistArgs.sourceDocument) { + // Saving a Document, not a URI: + var filesFolder = null; + if (persistArgs.targetContentType != "text/plain") { + // Create the local directory into which to save associated files. + filesFolder = persistArgs.targetFile.clone(); + + var nameWithoutExtension = getFileBaseName(filesFolder.leafName); + var filesFolderLeafName = + ContentAreaUtils.stringBundle + .formatStringFromName("filesFolder", [nameWithoutExtension], 1); + + filesFolder.leafName = filesFolderLeafName; + } + + var encodingFlags = 0; + if (persistArgs.targetContentType == "text/plain") { + encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED; + encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS; + encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT; + } + else { + encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES; + } + + const kWrapColumn = 80; + persist.saveDocument(persistArgs.sourceDocument, targetFileURL, filesFolder, + persistArgs.targetContentType, encodingFlags, kWrapColumn); + } else { + persist.savePrivacyAwareURI(persistArgs.sourceURI, + persistArgs.sourceCacheKey, + persistArgs.sourceReferrer, + Components.interfaces.nsIHttpChannel.REFERRER_POLICY_NO_REFERRER_WHEN_DOWNGRADE, + persistArgs.sourcePostData, + null, + targetFileURL, + persistArgs.isPrivate); + } +} + +/** + * Structure for holding info about automatically supplied parameters for + * internalSave(...). This allows parameters to be supplied so the user does not + * need to be prompted for file info. + * @param aFileAutoChosen This is an nsIFile object that has been + * pre-determined as the filename for the target to save to + * @param aUriAutoChosen This is the nsIURI object for the target + */ +function AutoChosen(aFileAutoChosen, aUriAutoChosen) { + this.file = aFileAutoChosen; + this.uri = aUriAutoChosen; +} + +/** + * Structure for holding info about a URL and the target filename it should be + * saved to. This structure is populated by initFileInfo(...). + * @param aSuggestedFileName This is used by initFileInfo(...) when it + * cannot 'discover' the filename from the url + * @param aFileName The target filename + * @param aFileBaseName The filename without the file extension + * @param aFileExt The extension of the filename + * @param aUri An nsIURI object for the url that is being saved + */ +function FileInfo(aSuggestedFileName, aFileName, aFileBaseName, aFileExt, aUri) { + this.suggestedFileName = aSuggestedFileName; + this.fileName = aFileName; + this.fileBaseName = aFileBaseName; + this.fileExt = aFileExt; + this.uri = aUri; +} + +/** + * Determine what the 'default' filename string is, its file extension and the + * filename without the extension. This filename is used when prompting the user + * for confirmation in the file picker dialog. + * @param aFI A FileInfo structure into which we'll put the results of this method. + * @param aURL The String representation of the URL of the document being saved + * @param aURLCharset The charset of aURL. + * @param aDocument The document to be saved + * @param aContentType The content type we're saving, if it could be + * determined by the caller. + * @param aContentDisposition The content-disposition header for the object + * we're saving, if it could be determined by the caller. + */ +function initFileInfo(aFI, aURL, aURLCharset, aDocument, + aContentType, aContentDisposition) +{ + try { + // Get an nsIURI object from aURL if possible: + try { + aFI.uri = makeURI(aURL, aURLCharset); + // Assuming nsiUri is valid, calling QueryInterface(...) on it will + // populate extra object fields (eg filename and file extension). + var url = aFI.uri.QueryInterface(Components.interfaces.nsIURL); + aFI.fileExt = url.fileExtension; + } catch (e) { + } + + // Get the default filename: + aFI.fileName = getDefaultFileName((aFI.suggestedFileName || aFI.fileName), + aFI.uri, aDocument, aContentDisposition); + // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied + // if saveURL(...) was the original caller (hence both aContentType and + // aDocument are blank). If they were saving a link to a website then make + // the extension .htm . + if (!aFI.fileExt && !aDocument && !aContentType && (/^http(s?):\/\//i.test(aURL))) { + aFI.fileExt = "htm"; + aFI.fileBaseName = aFI.fileName; + } else { + aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType); + aFI.fileBaseName = getFileBaseName(aFI.fileName); + } + } catch (e) { + } +} + +/** + * Given the Filepicker Parameters (aFpP), show the file picker dialog, + * prompting the user to confirm (or change) the fileName. + * @param aFpP + * A structure (see definition in internalSave(...) method) + * containing all the data used within this method. + * @param aSkipPrompt + * If true, attempt to save the file automatically to the user's default + * download directory, thus skipping the explicit prompt for a file name, + * but only if the associated preference is set. + * If false, don't save the file automatically to the user's + * default download directory, even if the associated preference + * is set, but ask for the target explicitly. + * @param aRelatedURI + * An nsIURI associated with the download. The last used + * directory of the picker is retrieved from/stored in the + * Content Pref Service using this URI. + * @return Promise + * @resolve a boolean. When true, it indicates that the file picker dialog + * is accepted. + */ +function promiseTargetFile(aFpP, /* optional */ aSkipPrompt, /* optional */ aRelatedURI) +{ + return Task.spawn(function*() { + let downloadLastDir = new DownloadLastDir(window); + let prefBranch = Services.prefs.getBranch("browser.download."); + let useDownloadDir = prefBranch.getBoolPref("useDownloadDir"); + + if (!aSkipPrompt) + useDownloadDir = false; + + // Default to the user's default downloads directory configured + // through download prefs. + let dirPath = yield Downloads.getPreferredDownloadsDirectory(); + let dirExists = yield OS.File.exists(dirPath); + let dir = new FileUtils.File(dirPath); + + if (useDownloadDir && dirExists) { + dir.append(getNormalizedLeafName(aFpP.fileInfo.fileName, + aFpP.fileInfo.fileExt)); + aFpP.file = uniqueFile(dir); + return true; + } + + // We must prompt for the file name explicitly. + // If we must prompt because we were asked to... + let deferred = Promise.defer(); + if (useDownloadDir) { + // Keep async behavior in both branches + Services.tm.mainThread.dispatch(function() { + deferred.resolve(null); + }, Components.interfaces.nsIThread.DISPATCH_NORMAL); + } else { + downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB(aFile) { + deferred.resolve(aFile); + }); + } + let file = yield deferred.promise; + if (file && (yield OS.File.exists(file.path))) { + dir = file; + dirExists = true; + } + + if (!dirExists) { + // Default to desktop. + dir = Services.dirsvc.get("Desk", Components.interfaces.nsIFile); + } + + let fp = makeFilePicker(); + let titleKey = aFpP.fpTitleKey || "SaveLinkTitle"; + fp.init(window, ContentAreaUtils.stringBundle.GetStringFromName(titleKey), + Components.interfaces.nsIFilePicker.modeSave); + + fp.displayDirectory = dir; + fp.defaultExtension = aFpP.fileInfo.fileExt; + fp.defaultString = getNormalizedLeafName(aFpP.fileInfo.fileName, + aFpP.fileInfo.fileExt); + appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt, + aFpP.saveMode); + + // The index of the selected filter is only preserved and restored if there's + // more than one filter in addition to "All Files". + if (aFpP.saveMode != SAVEMODE_FILEONLY) { + try { + fp.filterIndex = prefBranch.getIntPref("save_converter_index"); + } + catch (e) { + } + } + + let deferComplete = Promise.defer(); + fp.open(function(aResult) { + deferComplete.resolve(aResult); + }); + let result = yield deferComplete.promise; + if (result == Components.interfaces.nsIFilePicker.returnCancel || !fp.file) { + return false; + } + + if (aFpP.saveMode != SAVEMODE_FILEONLY) + prefBranch.setIntPref("save_converter_index", fp.filterIndex); + + // Do not store the last save directory as a pref inside the private browsing mode + downloadLastDir.setFile(aRelatedURI, fp.file.parent); + + fp.file.leafName = validateFileName(fp.file.leafName); + + aFpP.saveAsType = fp.filterIndex; + aFpP.file = fp.file; + aFpP.fileURL = fp.fileURL; + + return true; + }); +} + +// Since we're automatically downloading, we don't get the file picker's +// logic to check for existing files, so we need to do that here. +// +// Note - this code is identical to that in +// mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in +// If you are updating this code, update that code too! We can't share code +// here since that code is called in a js component. +function uniqueFile(aLocalFile) +{ + var collisionCount = 0; + while (aLocalFile.exists()) { + collisionCount++; + if (collisionCount == 1) { + // Append "(2)" before the last dot in (or at the end of) the filename + // special case .ext.gz etc files so we don't wind up with .tar(2).gz + if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) + aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&"); + else + aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&"); + } + else { + // replace the last (n) in the filename with (n+1) + aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount + 1) + ")"); + } + } + return aLocalFile; +} + +/** + * Download a URL using the new jsdownloads API. + * + * @param aURL + * the url to download + * @param [optional] aFileName + * the destination file name, if omitted will be obtained from the url. + * @param aInitiatingDocument + * The document from which the download was initiated. + */ +function DownloadURL(aURL, aFileName, aInitiatingDocument) { + // For private browsing, try to get document out of the most recent browser + // window, or provide our own if there's no browser window. + let isPrivate = aInitiatingDocument.defaultView + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsILoadContext) + .usePrivateBrowsing; + + let fileInfo = new FileInfo(aFileName); + initFileInfo(fileInfo, aURL, null, null, null, null); + + let filepickerParams = { + fileInfo: fileInfo, + saveMode: SAVEMODE_FILEONLY + }; + + Task.spawn(function* () { + let accepted = yield promiseTargetFile(filepickerParams, true, fileInfo.uri); + if (!accepted) + return; + + let file = filepickerParams.file; + let download = yield Downloads.createDownload({ + source: { url: aURL, isPrivate: isPrivate }, + target: { path: file.path, partFilePath: file.path + ".part" } + }); + download.tryToKeepPartialData = true; + + // Ignore errors because failures are reported through the download list. + download.start().catch(() => {}); + + // Add the download to the list, allowing it to be managed. + let list = yield Downloads.getList(Downloads.ALL); + list.add(download); + }).then(null, Components.utils.reportError); +} + +// We have no DOM, and can only save the URL as is. +const SAVEMODE_FILEONLY = 0x00; +XPCOMUtils.defineConstant(this, "SAVEMODE_FILEONLY", SAVEMODE_FILEONLY); +// We have a DOM and can save as complete. +const SAVEMODE_COMPLETE_DOM = 0x01; +XPCOMUtils.defineConstant(this, "SAVEMODE_COMPLETE_DOM", SAVEMODE_COMPLETE_DOM); +// We have a DOM which we can serialize as text. +const SAVEMODE_COMPLETE_TEXT = 0x02; +XPCOMUtils.defineConstant(this, "SAVEMODE_COMPLETE_TEXT", SAVEMODE_COMPLETE_TEXT); + +// If we are able to save a complete DOM, the 'save as complete' filter +// must be the first filter appended. The 'save page only' counterpart +// must be the second filter appended. And the 'save as complete text' +// filter must be the third filter appended. +function appendFiltersForContentType(aFilePicker, aContentType, aFileExtension, aSaveMode) +{ + // The bundle name for saving only a specific content type. + var bundleName; + // The corresponding filter string for a specific content type. + var filterString; + + // Every case where GetSaveModeForContentType can return non-FILEONLY + // modes must be handled here. + if (aSaveMode != SAVEMODE_FILEONLY) { + switch (aContentType) { + case "text/html": + bundleName = "WebPageHTMLOnlyFilter"; + filterString = "*.htm; *.html"; + break; + + case "application/xhtml+xml": + bundleName = "WebPageXHTMLOnlyFilter"; + filterString = "*.xht; *.xhtml"; + break; + + case "image/svg+xml": + bundleName = "WebPageSVGOnlyFilter"; + filterString = "*.svg; *.svgz"; + break; + + case "text/xml": + case "application/xml": + bundleName = "WebPageXMLOnlyFilter"; + filterString = "*.xml"; + break; + } + } + + if (!bundleName) { + if (aSaveMode != SAVEMODE_FILEONLY) + throw "Invalid save mode for type '" + aContentType + "'"; + + var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension); + if (mimeInfo) { + + var extEnumerator = mimeInfo.getFileExtensions(); + + var extString = ""; + while (extEnumerator.hasMore()) { + var extension = extEnumerator.getNext(); + if (extString) + extString += "; "; // If adding more than one extension, + // separate by semi-colon + extString += "*." + extension; + } + + if (extString) + aFilePicker.appendFilter(mimeInfo.description, extString); + } + } + + if (aSaveMode & SAVEMODE_COMPLETE_DOM) { + aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"), + filterString); + // We should always offer a choice to save document only if + // we allow saving as complete. + aFilePicker.appendFilter(ContentAreaUtils.stringBundle.GetStringFromName(bundleName), + filterString); + } + + if (aSaveMode & SAVEMODE_COMPLETE_TEXT) + aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterText); + + // Always append the all files (*) filter + aFilePicker.appendFilters(Components.interfaces.nsIFilePicker.filterAll); +} + +function getPostData(aDocument) +{ + const Ci = Components.interfaces; + + if (aDocument instanceof Ci.nsIWebBrowserPersistDocument) { + return aDocument.postData; + } + try { + // Find the session history entry corresponding to the given document. In + // the current implementation, nsIWebPageDescriptor.currentDescriptor always + // returns a session history entry. + let sessionHistoryEntry = + aDocument.defaultView + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIWebPageDescriptor) + .currentDescriptor + .QueryInterface(Ci.nsISHEntry); + return sessionHistoryEntry.postData; + } + catch (e) { + } + return null; +} + +function makeWebBrowserPersist() +{ + const persistContractID = "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"; + const persistIID = Components.interfaces.nsIWebBrowserPersist; + return Components.classes[persistContractID].createInstance(persistIID); +} + +function makeURI(aURL, aOriginCharset, aBaseURI) +{ + return BrowserUtils.makeURI(aURL, aOriginCharset, aBaseURI); +} + +function makeFileURI(aFile) +{ + return BrowserUtils.makeFileURI(aFile); +} + +function makeFilePicker() +{ + const fpContractID = "@mozilla.org/filepicker;1"; + const fpIID = Components.interfaces.nsIFilePicker; + return Components.classes[fpContractID].createInstance(fpIID); +} + +function getMIMEService() +{ + const mimeSvcContractID = "@mozilla.org/mime;1"; + const mimeSvcIID = Components.interfaces.nsIMIMEService; + const mimeSvc = Components.classes[mimeSvcContractID].getService(mimeSvcIID); + return mimeSvc; +} + +// Given aFileName, find the fileName without the extension on the end. +function getFileBaseName(aFileName) +{ + // Remove the file extension from aFileName: + return aFileName.replace(/\.[^.]*$/, ""); +} + +function getMIMETypeForURI(aURI) +{ + try { + return getMIMEService().getTypeFromURI(aURI); + } + catch (e) { + } + return null; +} + +function getMIMEInfoForType(aMIMEType, aExtension) +{ + if (aMIMEType || aExtension) { + try { + return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension); + } + catch (e) { + } + } + return null; +} + +function getDefaultFileName(aDefaultFileName, aURI, aDocument, + aContentDisposition) +{ + // 1) look for a filename in the content-disposition header, if any + if (aContentDisposition) { + const mhpContractID = "@mozilla.org/network/mime-hdrparam;1"; + const mhpIID = Components.interfaces.nsIMIMEHeaderParam; + const mhp = Components.classes[mhpContractID].getService(mhpIID); + var dummy = { value: null }; // Need an out param... + var charset = getCharsetforSave(aDocument); + + var fileName = null; + try { + fileName = mhp.getParameter(aContentDisposition, "filename", charset, + true, dummy); + } + catch (e) { + try { + fileName = mhp.getParameter(aContentDisposition, "name", charset, true, + dummy); + } + catch (e) { + } + } + if (fileName) + return fileName; + } + + let docTitle; + if (aDocument) { + // If the document looks like HTML or XML, try to use its original title. + docTitle = validateFileName(aDocument.title).trim(); + if (docTitle) { + let contentType = aDocument.contentType; + if (contentType == "application/xhtml+xml" || + contentType == "application/xml" || + contentType == "image/svg+xml" || + contentType == "text/html" || + contentType == "text/xml") { + // 2) Use the document title + return docTitle; + } + } + } + + try { + var url = aURI.QueryInterface(Components.interfaces.nsIURL); + if (url.fileName != "") { + // 3) Use the actual file name, if present + var textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"] + .getService(Components.interfaces.nsITextToSubURI); + return validateFileName(textToSubURI.unEscapeURIForUI(url.originCharset || "UTF-8", url.fileName)); + } + } catch (e) { + // This is something like a data: and so forth URI... no filename here. + } + + if (docTitle) + // 4) Use the document title + return docTitle; + + if (aDefaultFileName) + // 5) Use the caller-provided name, if any + return validateFileName(aDefaultFileName); + + // 6) If this is a directory, use the last directory name + var path = aURI.path.match(/\/([^\/]+)\/$/); + if (path && path.length > 1) + return validateFileName(path[1]); + + try { + if (aURI.host) + // 7) Use the host. + return aURI.host; + } catch (e) { + // Some files have no information at all, like Javascript generated pages + } + try { + // 8) Use the default file name + return ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName"); + } catch (e) { + // in case localized string cannot be found + } + // 9) If all else fails, use "index" + return "index"; +} + +function validateFileName(aFileName) +{ + var re = /[\/]+/g; + if (navigator.appVersion.indexOf("Windows") != -1) { + re = /[\\\/\|]+/g; + aFileName = aFileName.replace(/[\"]+/g, "'"); + aFileName = aFileName.replace(/[\*\:\?]+/g, " "); + aFileName = aFileName.replace(/[\<]+/g, "("); + aFileName = aFileName.replace(/[\>]+/g, ")"); + } + else if (navigator.appVersion.indexOf("Macintosh") != -1) + re = /[\:\/]+/g; + else if (navigator.appVersion.indexOf("Android") != -1) { + // On mobile devices, the filesystem may be very limited in what + // it considers valid characters. To avoid errors, we sanitize + // conservatively. + const dangerousChars = "*?<>|\":/\\[];,+="; + var processed = ""; + for (var i = 0; i < aFileName.length; i++) + processed += aFileName.charCodeAt(i) >= 32 && + !(dangerousChars.indexOf(aFileName[i]) >= 0) ? aFileName[i] + : "_"; + + // Last character should not be a space + processed = processed.trim(); + + // If a large part of the filename has been sanitized, then we + // will use a default filename instead + if (processed.replace(/_/g, "").length <= processed.length/2) { + // We purposefully do not use a localized default filename, + // which we could have done using + // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName") + // since it may contain invalid characters. + var original = processed; + processed = "download"; + + // Preserve a suffix, if there is one + if (original.indexOf(".") >= 0) { + var suffix = original.split(".").slice(-1)[0]; + if (suffix && suffix.indexOf("_") < 0) + processed += "." + suffix; + } + } + return processed; + } + + return aFileName.replace(re, "_"); +} + +function getNormalizedLeafName(aFile, aDefaultExtension) +{ + if (!aDefaultExtension) + return aFile; + + if (AppConstants.platform == "win") { + // Remove trailing dots and spaces on windows + aFile = aFile.replace(/[\s.]+$/, ""); + } + + // Remove leading dots + aFile = aFile.replace(/^\.+/, ""); + + // Fix up the file name we're saving to to include the default extension + var i = aFile.lastIndexOf("."); + if (aFile.substr(i + 1) != aDefaultExtension) + return aFile + "." + aDefaultExtension; + + return aFile; +} + +function getDefaultExtension(aFilename, aURI, aContentType) +{ + if (aContentType == "text/plain" || aContentType == "application/octet-stream" || aURI.scheme == "ftp") + return ""; // temporary fix for bug 120327 + + // First try the extension from the filename + const stdURLContractID = "@mozilla.org/network/standard-url;1"; + const stdURLIID = Components.interfaces.nsIURL; + var url = Components.classes[stdURLContractID].createInstance(stdURLIID); + url.filePath = aFilename; + + var ext = url.fileExtension; + + // This mirrors some code in nsExternalHelperAppService::DoContent + // Use the filename first and then the URI if that fails + + var mimeInfo = getMIMEInfoForType(aContentType, ext); + + if (ext && mimeInfo && mimeInfo.extensionExists(ext)) + return ext; + + // Well, that failed. Now try the extension from the URI + var urlext; + try { + url = aURI.QueryInterface(Components.interfaces.nsIURL); + urlext = url.fileExtension; + } catch (e) { + } + + if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) { + return urlext; + } + try { + if (mimeInfo) + return mimeInfo.primaryExtension; + } + catch (e) { + } + // Fall back on the extensions in the filename and URI for lack + // of anything better. + return ext || urlext; +} + +function GetSaveModeForContentType(aContentType, aDocument) +{ + // We can only save a complete page if we have a loaded document, + // and it's not a CPOW -- nsWebBrowserPersist needs a real document. + if (!aDocument || Components.utils.isCrossProcessWrapper(aDocument)) + return SAVEMODE_FILEONLY; + + // Find the possible save modes using the provided content type + var saveMode = SAVEMODE_FILEONLY; + switch (aContentType) { + case "text/html": + case "application/xhtml+xml": + case "image/svg+xml": + saveMode |= SAVEMODE_COMPLETE_TEXT; + // Fall through + case "text/xml": + case "application/xml": + saveMode |= SAVEMODE_COMPLETE_DOM; + break; + } + + return saveMode; +} + +function getCharsetforSave(aDocument) +{ + if (aDocument) + return aDocument.characterSet; + + if (document.commandDispatcher.focusedWindow) + return document.commandDispatcher.focusedWindow.document.characterSet; + + return window.content.document.characterSet; +} + +/** + * Open a URL from chrome, determining if we can handle it internally or need to + * launch an external application to handle it. + * @param aURL The URL to be opened + * + * WARNING: Please note that openURL() does not perform any content security checks!!! + */ +function openURL(aURL) +{ + var uri = makeURI(aURL); + + var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Components.interfaces.nsIExternalProtocolService); + + if (!protocolSvc.isExposedProtocol(uri.scheme)) { + // If we're not a browser, use the external protocol service to load the URI. + protocolSvc.loadUrl(uri); + } + else { + var recentWindow = Services.wm.getMostRecentWindow("navigator:browser"); + if (recentWindow) { + recentWindow.openUILinkIn(uri.spec, "tab"); + return; + } + + var loadgroup = Components.classes["@mozilla.org/network/load-group;1"] + .createInstance(Components.interfaces.nsILoadGroup); + var appstartup = Services.startup; + + var loadListener = { + onStartRequest: function ll_start(aRequest, aContext) { + appstartup.enterLastWindowClosingSurvivalArea(); + }, + onStopRequest: function ll_stop(aRequest, aContext, aStatusCode) { + appstartup.exitLastWindowClosingSurvivalArea(); + }, + QueryInterface: function ll_QI(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIRequestObserver) || + iid.equals(Components.interfaces.nsISupportsWeakReference)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + } + } + loadgroup.groupObserver = loadListener; + + var uriListener = { + onStartURIOpen: function(uri) { return false; }, + doContent: function(ctype, preferred, request, handler) { return false; }, + isPreferred: function(ctype, desired) { return false; }, + canHandleContent: function(ctype, preferred, desired) { return false; }, + loadCookie: null, + parentContentListener: null, + getInterface: function(iid) { + if (iid.equals(Components.interfaces.nsIURIContentListener)) + return this; + if (iid.equals(Components.interfaces.nsILoadGroup)) + return loadgroup; + throw Components.results.NS_ERROR_NO_INTERFACE; + } + } + + var channel = NetUtil.newChannel({ + uri: uri, + loadUsingSystemPrincipal: true + }); + + var uriLoader = Components.classes["@mozilla.org/uriloader;1"] + .getService(Components.interfaces.nsIURILoader); + uriLoader.openURI(channel, + Components.interfaces.nsIURILoader.IS_CONTENT_PREFERRED, + uriListener); + } +} diff --git a/toolkit/content/customizeToolbar.css b/toolkit/content/customizeToolbar.css new file mode 100644 index 0000000000..ae90a2f28a --- /dev/null +++ b/toolkit/content/customizeToolbar.css @@ -0,0 +1,39 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ +@namespace html url("http://www.w3.org/1999/xhtml"); /* namespace for HTML elements */ + +#palette-box { + overflow: auto; + display: block; + min-height: 3em; +} + +#palette-box > toolbarpaletteitem { + width: 110px; + height: 94px; + overflow: hidden; + display: inline-block; +} + +.toolbarpaletteitem-box { + -moz-box-pack: center; + -moz-box-flex: 1; + width: 110px; + max-width: 110px; +} + +toolbarpaletteitem > label { + text-align: center; +} + +#main-box > box { + overflow: hidden; +} + +/* Hide the toolbarbutton label because we replicate it on the wrapper */ +.toolbarbutton-text { + display: none; +} diff --git a/toolkit/content/customizeToolbar.js b/toolkit/content/customizeToolbar.js new file mode 100644 index 0000000000..b96b60b987 --- /dev/null +++ b/toolkit/content/customizeToolbar.js @@ -0,0 +1,839 @@ +/* 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/. */ + +var gToolboxDocument = null; +var gToolbox = null; +var gCurrentDragOverItem = null; +var gToolboxChanged = false; +var gToolboxSheet = false; +var gPaletteBox = null; + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/AppConstants.jsm"); + +function onLoad() +{ + if ("arguments" in window && window.arguments[0]) { + InitWithToolbox(window.arguments[0]); + repositionDialog(window); + } + else if (window.frameElement && + "toolbox" in window.frameElement) { + gToolboxSheet = true; + InitWithToolbox(window.frameElement.toolbox); + repositionDialog(window.frameElement.panel); + } +} + +function InitWithToolbox(aToolbox) +{ + gToolbox = aToolbox; + dispatchCustomizationEvent("beforecustomization"); + gToolboxDocument = gToolbox.ownerDocument; + gToolbox.customizing = true; + forEachCustomizableToolbar(function (toolbar) { + toolbar.setAttribute("customizing", "true"); + }); + gPaletteBox = document.getElementById("palette-box"); + + var elts = getRootElements(); + for (let i=0; i < elts.length; i++) { + elts[i].addEventListener("dragstart", onToolbarDragStart, true); + elts[i].addEventListener("dragover", onToolbarDragOver, true); + elts[i].addEventListener("dragexit", onToolbarDragExit, true); + elts[i].addEventListener("drop", onToolbarDrop, true); + } + + initDialog(); +} + +function onClose() +{ + if (!gToolboxSheet) + window.close(); + else + finishToolbarCustomization(); +} + +function onUnload() +{ + if (!gToolboxSheet) + finishToolbarCustomization(); +} + +function finishToolbarCustomization() +{ + removeToolboxListeners(); + unwrapToolbarItems(); + persistCurrentSets(); + gToolbox.customizing = false; + forEachCustomizableToolbar(function (toolbar) { + toolbar.removeAttribute("customizing"); + }); + + notifyParentComplete(); +} + +function initDialog() +{ + if (!gToolbox.toolbarset) { + document.getElementById("newtoolbar").hidden = true; + } + + var mode = gToolbox.getAttribute("mode"); + document.getElementById("modelist").value = mode; + var smallIconsCheckbox = document.getElementById("smallicons"); + smallIconsCheckbox.checked = gToolbox.getAttribute("iconsize") == "small"; + if (mode == "text") + smallIconsCheckbox.disabled = true; + + // Build up the palette of other items. + buildPalette(); + + // Wrap all the items on the toolbar in toolbarpaletteitems. + wrapToolbarItems(); +} + +function repositionDialog(aWindow) +{ + // Position the dialog touching the bottom of the toolbox and centered with + // it. + if (!aWindow) + return; + + var width; + if (aWindow != window) + width = aWindow.getBoundingClientRect().width; + else if (document.documentElement.hasAttribute("width")) + width = document.documentElement.getAttribute("width"); + else + width = parseInt(document.documentElement.style.width); + var screenX = gToolbox.boxObject.screenX + + ((gToolbox.boxObject.width - width) / 2); + var screenY = gToolbox.boxObject.screenY + gToolbox.boxObject.height; + + aWindow.moveTo(screenX, screenY); +} + +function removeToolboxListeners() +{ + var elts = getRootElements(); + for (let i=0; i < elts.length; i++) { + elts[i].removeEventListener("dragstart", onToolbarDragStart, true); + elts[i].removeEventListener("dragover", onToolbarDragOver, true); + elts[i].removeEventListener("dragexit", onToolbarDragExit, true); + elts[i].removeEventListener("drop", onToolbarDrop, true); + } +} + +/** + * Invoke a callback on the toolbox to notify it that the dialog is done + * and going away. + */ +function notifyParentComplete() +{ + if ("customizeDone" in gToolbox) + gToolbox.customizeDone(gToolboxChanged); + dispatchCustomizationEvent("aftercustomization"); +} + +function toolboxChanged(aType) +{ + gToolboxChanged = true; + if ("customizeChange" in gToolbox) + gToolbox.customizeChange(aType); + dispatchCustomizationEvent("customizationchange"); +} + +function dispatchCustomizationEvent(aEventName) { + var evt = document.createEvent("Events"); + evt.initEvent(aEventName, true, true); + gToolbox.dispatchEvent(evt); +} + +/** + * Persist the current set of buttons in all customizable toolbars to + * localstore. + */ +function persistCurrentSets() +{ + if (!gToolboxChanged || gToolboxDocument.defaultView.closed) + return; + + var customCount = 0; + forEachCustomizableToolbar(function (toolbar) { + // Calculate currentset and store it in the attribute. + var currentSet = toolbar.currentSet; + toolbar.setAttribute("currentset", currentSet); + + var customIndex = toolbar.hasAttribute("customindex"); + if (customIndex) { + if (!toolbar.hasChildNodes()) { + // Remove custom toolbars whose contents have been removed. + gToolbox.removeChild(toolbar); + } else if (gToolbox.toolbarset) { + // Persist custom toolbar info on the <toolbarset/> + gToolbox.toolbarset.setAttribute("toolbar"+(++customCount), + toolbar.toolbarName + ":" + currentSet); + gToolboxDocument.persist(gToolbox.toolbarset.id, "toolbar"+customCount); + } + } + + if (!customIndex) { + // Persist the currentset attribute directly on hardcoded toolbars. + gToolboxDocument.persist(toolbar.id, "currentset"); + } + }); + + // Remove toolbarX attributes for removed toolbars. + while (gToolbox.toolbarset && gToolbox.toolbarset.hasAttribute("toolbar"+(++customCount))) { + gToolbox.toolbarset.removeAttribute("toolbar"+customCount); + gToolboxDocument.persist(gToolbox.toolbarset.id, "toolbar"+customCount); + } +} + +/** + * Wraps all items in all customizable toolbars in a toolbox. + */ +function wrapToolbarItems() +{ + forEachCustomizableToolbar(function (toolbar) { + Array.forEach(toolbar.childNodes, function (item) { + if (AppConstants.platform == "macosx") { + if (item.firstChild && item.firstChild.localName == "menubar") + return; + } + if (isToolbarItem(item)) { + let wrapper = wrapToolbarItem(item); + cleanupItemForToolbar(item, wrapper); + } + }); + }); +} + +function getRootElements() +{ + return [gToolbox].concat(gToolbox.externalToolbars); +} + +/** + * Unwraps all items in all customizable toolbars in a toolbox. + */ +function unwrapToolbarItems() +{ + let elts = getRootElements(); + for (let i=0; i < elts.length; i++) { + let paletteItems = elts[i].getElementsByTagName("toolbarpaletteitem"); + let paletteItem; + while ((paletteItem = paletteItems.item(0)) != null) { + let toolbarItem = paletteItem.firstChild; + restoreItemForToolbar(toolbarItem, paletteItem); + paletteItem.parentNode.replaceChild(toolbarItem, paletteItem); + } + } +} + +/** + * Creates a wrapper that can be used to contain a toolbaritem and prevent + * it from receiving UI events. + */ +function createWrapper(aId, aDocument) +{ + var wrapper = aDocument.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "toolbarpaletteitem"); + + wrapper.id = "wrapper-"+aId; + return wrapper; +} + +/** + * Wraps an item that has been cloned from a template and adds + * it to the end of the palette. + */ +function wrapPaletteItem(aPaletteItem) +{ + var wrapper = createWrapper(aPaletteItem.id, document); + + wrapper.appendChild(aPaletteItem); + + // XXX We need to call this AFTER the palette item has been appended + // to the wrapper or else we crash dropping certain buttons on the + // palette due to removal of the command and disabled attributes - JRH + cleanUpItemForPalette(aPaletteItem, wrapper); + + gPaletteBox.appendChild(wrapper); +} + +/** + * Wraps an item that is currently on a toolbar and replaces the item + * with the wrapper. This is not used when dropping items from the palette, + * only when first starting the dialog and wrapping everything on the toolbars. + */ +function wrapToolbarItem(aToolbarItem) +{ + var wrapper = createWrapper(aToolbarItem.id, gToolboxDocument); + + wrapper.flex = aToolbarItem.flex; + + aToolbarItem.parentNode.replaceChild(wrapper, aToolbarItem); + + wrapper.appendChild(aToolbarItem); + + return wrapper; +} + +/** + * Get the list of ids for the current set of items on each toolbar. + */ +function getCurrentItemIds() +{ + var currentItems = {}; + forEachCustomizableToolbar(function (toolbar) { + var child = toolbar.firstChild; + while (child) { + if (isToolbarItem(child)) + currentItems[child.id] = 1; + child = child.nextSibling; + } + }); + return currentItems; +} + +/** + * Builds the palette of draggable items that are not yet in a toolbar. + */ +function buildPalette() +{ + // Empty the palette first. + while (gPaletteBox.lastChild) + gPaletteBox.removeChild(gPaletteBox.lastChild); + + // Add the toolbar separator item. + var templateNode = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "toolbarseparator"); + templateNode.id = "separator"; + wrapPaletteItem(templateNode); + + // Add the toolbar spring item. + templateNode = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "toolbarspring"); + templateNode.id = "spring"; + templateNode.flex = 1; + wrapPaletteItem(templateNode); + + // Add the toolbar spacer item. + templateNode = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "toolbarspacer"); + templateNode.id = "spacer"; + templateNode.flex = 1; + wrapPaletteItem(templateNode); + + var currentItems = getCurrentItemIds(); + templateNode = gToolbox.palette.firstChild; + while (templateNode) { + // Check if the item is already in a toolbar before adding it to the palette. + if (!(templateNode.id in currentItems)) { + var paletteItem = document.importNode(templateNode, true); + wrapPaletteItem(paletteItem); + } + + templateNode = templateNode.nextSibling; + } +} + +/** + * Makes sure that an item that has been cloned from a template + * is stripped of any attributes that may adversely affect its + * appearance in the palette. + */ +function cleanUpItemForPalette(aItem, aWrapper) +{ + aWrapper.setAttribute("place", "palette"); + setWrapperType(aItem, aWrapper); + + if (aItem.hasAttribute("title")) + aWrapper.setAttribute("title", aItem.getAttribute("title")); + else if (aItem.hasAttribute("label")) + aWrapper.setAttribute("title", aItem.getAttribute("label")); + else if (isSpecialItem(aItem)) { + var stringBundle = document.getElementById("stringBundle"); + // Remove the common "toolbar" prefix to generate the string name. + var title = stringBundle.getString(aItem.localName.slice(7) + "Title"); + aWrapper.setAttribute("title", title); + } + aWrapper.setAttribute("tooltiptext", aWrapper.getAttribute("title")); + + // Remove attributes that screw up our appearance. + aItem.removeAttribute("command"); + aItem.removeAttribute("observes"); + aItem.removeAttribute("type"); + aItem.removeAttribute("width"); + + Array.forEach(aWrapper.querySelectorAll("[disabled]"), function(aNode) { + aNode.removeAttribute("disabled"); + }); +} + +/** + * Makes sure that an item that has been cloned from a template + * is stripped of all properties that may adversely affect its + * appearance in the toolbar. Store critical properties on the + * wrapper so they can be put back on the item when we're done. + */ +function cleanupItemForToolbar(aItem, aWrapper) +{ + setWrapperType(aItem, aWrapper); + aWrapper.setAttribute("place", "toolbar"); + + if (aItem.hasAttribute("command")) { + aWrapper.setAttribute("itemcommand", aItem.getAttribute("command")); + aItem.removeAttribute("command"); + } + + if (aItem.checked) { + aWrapper.setAttribute("itemchecked", "true"); + aItem.checked = false; + } + + if (aItem.disabled) { + aWrapper.setAttribute("itemdisabled", "true"); + aItem.disabled = false; + } +} + +/** + * Restore all the properties that we stripped off above. + */ +function restoreItemForToolbar(aItem, aWrapper) +{ + if (aWrapper.hasAttribute("itemdisabled")) + aItem.disabled = true; + + if (aWrapper.hasAttribute("itemchecked")) + aItem.checked = true; + + if (aWrapper.hasAttribute("itemcommand")) { + let commandID = aWrapper.getAttribute("itemcommand"); + aItem.setAttribute("command", commandID); + + // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing + let command = gToolboxDocument.getElementById(commandID); + if (command && command.hasAttribute("disabled")) + aItem.setAttribute("disabled", command.getAttribute("disabled")); + } +} + +function setWrapperType(aItem, aWrapper) +{ + if (aItem.localName == "toolbarseparator") { + aWrapper.setAttribute("type", "separator"); + } else if (aItem.localName == "toolbarspring") { + aWrapper.setAttribute("type", "spring"); + } else if (aItem.localName == "toolbarspacer") { + aWrapper.setAttribute("type", "spacer"); + } else if (aItem.localName == "toolbaritem" && aItem.firstChild) { + aWrapper.setAttribute("type", aItem.firstChild.localName); + } +} + +function setDragActive(aItem, aValue) +{ + var node = aItem; + var direction = window.getComputedStyle(aItem, null).direction; + var value = direction == "ltr"? "left" : "right"; + if (aItem.localName == "toolbar") { + node = aItem.lastChild; + value = direction == "ltr"? "right" : "left"; + } + + if (!node) + return; + + if (aValue) { + if (!node.hasAttribute("dragover")) + node.setAttribute("dragover", value); + } else { + node.removeAttribute("dragover"); + } +} + +function addNewToolbar() +{ + var promptService = Services.prompt; + var stringBundle = document.getElementById("stringBundle"); + var message = stringBundle.getString("enterToolbarName"); + var title = stringBundle.getString("enterToolbarTitle"); + + var name = {}; + + // Quitting from the toolbar dialog while the new toolbar prompt is up + // can cause things to become unresponsive on the Mac. Until dialog modality + // is fixed (395465), disable the "Done" button explicitly. + var doneButton = document.getElementById("donebutton"); + doneButton.disabled = true; + + while (true) { + + if (!promptService.prompt(window, title, message, name, null, {})) { + doneButton.disabled = false; + return; + } + + if (!name.value) { + message = stringBundle.getFormattedString("enterToolbarBlank", [name.value]); + continue; + } + + var dupeFound = false; + + // Check for an existing toolbar with the same display name + for (let i = 0; i < gToolbox.childNodes.length; ++i) { + var toolbar = gToolbox.childNodes[i]; + var toolbarName = toolbar.getAttribute("toolbarname"); + + if (toolbarName == name.value && + toolbar.getAttribute("type") != "menubar" && + toolbar.nodeName == 'toolbar') { + dupeFound = true; + break; + } + } + + if (!dupeFound) + break; + + message = stringBundle.getFormattedString("enterToolbarDup", [name.value]); + } + + gToolbox.appendCustomToolbar(name.value, ""); + + toolboxChanged(); + + doneButton.disabled = false; +} + +/** + * Restore the default set of buttons to fixed toolbars, + * remove all custom toolbars, and rebuild the palette. + */ +function restoreDefaultSet() +{ + // Unwrap the items on the toolbar. + unwrapToolbarItems(); + + // Remove all of the customized toolbars. + var child = gToolbox.lastChild; + while (child) { + if (child.hasAttribute("customindex")) { + var thisChild = child; + child = child.previousSibling; + thisChild.currentSet = "__empty"; + gToolbox.removeChild(thisChild); + } else { + child = child.previousSibling; + } + } + + // Restore the defaultset for fixed toolbars. + forEachCustomizableToolbar(function (toolbar) { + var defaultSet = toolbar.getAttribute("defaultset"); + if (defaultSet) + toolbar.currentSet = defaultSet; + }); + + // Restore the default icon size and mode. + document.getElementById("smallicons").checked = (updateIconSize() == "small"); + document.getElementById("modelist").value = updateToolbarMode(); + + // Now rebuild the palette. + buildPalette(); + + // Now re-wrap the items on the toolbar. + wrapToolbarItems(); + + toolboxChanged("reset"); +} + +function updateIconSize(aSize) { + return updateToolboxProperty("iconsize", aSize, "large"); +} + +function updateToolbarMode(aModeValue) { + var mode = updateToolboxProperty("mode", aModeValue, "icons"); + + var iconSizeCheckbox = document.getElementById("smallicons"); + iconSizeCheckbox.disabled = mode == "text"; + + return mode; +} + +function updateToolboxProperty(aProp, aValue, aToolkitDefault) { + var toolboxDefault = gToolbox.getAttribute("default" + aProp) || + aToolkitDefault; + + gToolbox.setAttribute(aProp, aValue || toolboxDefault); + gToolboxDocument.persist(gToolbox.id, aProp); + + forEachCustomizableToolbar(function (toolbar) { + var toolbarDefault = toolbar.getAttribute("default" + aProp) || + toolboxDefault; + if (toolbar.getAttribute("lock" + aProp) == "true" && + toolbar.getAttribute(aProp) == toolbarDefault) + return; + + toolbar.setAttribute(aProp, aValue || toolbarDefault); + gToolboxDocument.persist(toolbar.id, aProp); + }); + + toolboxChanged(aProp); + + return aValue || toolboxDefault; +} + +function forEachCustomizableToolbar(callback) { + Array.filter(gToolbox.childNodes, isCustomizableToolbar).forEach(callback); + Array.filter(gToolbox.externalToolbars, isCustomizableToolbar).forEach(callback); +} + +function isCustomizableToolbar(aElt) +{ + return aElt.localName == "toolbar" && + aElt.getAttribute("customizable") == "true"; +} + +function isSpecialItem(aElt) +{ + return aElt.localName == "toolbarseparator" || + aElt.localName == "toolbarspring" || + aElt.localName == "toolbarspacer"; +} + +function isToolbarItem(aElt) +{ + return aElt.localName == "toolbarbutton" || + aElt.localName == "toolbaritem" || + aElt.localName == "toolbarseparator" || + aElt.localName == "toolbarspring" || + aElt.localName == "toolbarspacer"; +} + +// Drag and Drop observers + +function onToolbarDragExit(aEvent) +{ + if (isUnwantedDragEvent(aEvent)) { + return; + } + + if (gCurrentDragOverItem) + setDragActive(gCurrentDragOverItem, false); +} + +function onToolbarDragStart(aEvent) +{ + var item = aEvent.target; + while (item && item.localName != "toolbarpaletteitem") { + if (item.localName == "toolbar") + return; + item = item.parentNode; + } + + item.setAttribute("dragactive", "true"); + + var dt = aEvent.dataTransfer; + var documentId = gToolboxDocument.documentElement.id; + dt.setData("text/toolbarwrapper-id/" + documentId, item.firstChild.id); + dt.effectAllowed = "move"; +} + +function onToolbarDragOver(aEvent) +{ + if (isUnwantedDragEvent(aEvent)) { + return; + } + + var documentId = gToolboxDocument.documentElement.id; + if (!aEvent.dataTransfer.types.includes("text/toolbarwrapper-id/" + documentId.toLowerCase())) + return; + + var toolbar = aEvent.target; + var dropTarget = aEvent.target; + while (toolbar && toolbar.localName != "toolbar") { + dropTarget = toolbar; + toolbar = toolbar.parentNode; + } + + // Make sure we are dragging over a customizable toolbar. + if (!toolbar || !isCustomizableToolbar(toolbar)) { + gCurrentDragOverItem = null; + return; + } + + var previousDragItem = gCurrentDragOverItem; + + if (dropTarget.localName == "toolbar") { + gCurrentDragOverItem = dropTarget; + } else { + gCurrentDragOverItem = null; + + var direction = window.getComputedStyle(dropTarget.parentNode, null).direction; + var dropTargetCenter = dropTarget.boxObject.x + (dropTarget.boxObject.width / 2); + var dragAfter; + if (direction == "ltr") + dragAfter = aEvent.clientX > dropTargetCenter; + else + dragAfter = aEvent.clientX < dropTargetCenter; + + if (dragAfter) { + gCurrentDragOverItem = dropTarget.nextSibling; + if (!gCurrentDragOverItem) + gCurrentDragOverItem = toolbar; + } else + gCurrentDragOverItem = dropTarget; + } + + if (previousDragItem && gCurrentDragOverItem != previousDragItem) { + setDragActive(previousDragItem, false); + } + + setDragActive(gCurrentDragOverItem, true); + + aEvent.preventDefault(); + aEvent.stopPropagation(); +} + +function onToolbarDrop(aEvent) +{ + if (isUnwantedDragEvent(aEvent)) { + return; + } + + if (!gCurrentDragOverItem) + return; + + setDragActive(gCurrentDragOverItem, false); + + var documentId = gToolboxDocument.documentElement.id; + var draggedItemId = aEvent.dataTransfer.getData("text/toolbarwrapper-id/" + documentId); + if (gCurrentDragOverItem.id == draggedItemId) + return; + + var toolbar = aEvent.target; + while (toolbar.localName != "toolbar") + toolbar = toolbar.parentNode; + + var draggedPaletteWrapper = document.getElementById("wrapper-"+draggedItemId); + if (!draggedPaletteWrapper) { + // The wrapper has been dragged from the toolbar. + // Get the wrapper from the toolbar document and make sure that + // it isn't being dropped on itself. + let wrapper = gToolboxDocument.getElementById("wrapper-"+draggedItemId); + if (wrapper == gCurrentDragOverItem) + return; + + // Don't allow non-removable kids (e.g., the menubar) to move. + if (wrapper.firstChild.getAttribute("removable") != "true") + return; + + // Remove the item from its place in the toolbar. + wrapper.parentNode.removeChild(wrapper); + + // Determine which toolbar we are dropping on. + var dropToolbar = null; + if (gCurrentDragOverItem.localName == "toolbar") + dropToolbar = gCurrentDragOverItem; + else + dropToolbar = gCurrentDragOverItem.parentNode; + + // Insert the item into the toolbar. + if (gCurrentDragOverItem != dropToolbar) + dropToolbar.insertBefore(wrapper, gCurrentDragOverItem); + else + dropToolbar.appendChild(wrapper); + } else { + // The item has been dragged from the palette + + // Create a new wrapper for the item. We don't know the id yet. + let wrapper = createWrapper("", gToolboxDocument); + + // Ask the toolbar to clone the item's template, place it inside the wrapper, and insert it in the toolbar. + var newItem = toolbar.insertItem(draggedItemId, gCurrentDragOverItem == toolbar ? null : gCurrentDragOverItem, wrapper); + + // Prepare the item and wrapper to look good on the toolbar. + cleanupItemForToolbar(newItem, wrapper); + wrapper.id = "wrapper-"+newItem.id; + wrapper.flex = newItem.flex; + + // Remove the wrapper from the palette. + if (draggedItemId != "separator" && + draggedItemId != "spring" && + draggedItemId != "spacer") + gPaletteBox.removeChild(draggedPaletteWrapper); + } + + gCurrentDragOverItem = null; + + toolboxChanged(); +} + +function onPaletteDragOver(aEvent) +{ + if (isUnwantedDragEvent(aEvent)) { + return; + } + var documentId = gToolboxDocument.documentElement.id; + if (aEvent.dataTransfer.types.includes("text/toolbarwrapper-id/" + documentId.toLowerCase())) + aEvent.preventDefault(); +} + +function onPaletteDrop(aEvent) +{ + if (isUnwantedDragEvent(aEvent)) { + return; + } + var documentId = gToolboxDocument.documentElement.id; + var itemId = aEvent.dataTransfer.getData("text/toolbarwrapper-id/" + documentId); + + var wrapper = gToolboxDocument.getElementById("wrapper-"+itemId); + if (wrapper) { + // Don't allow non-removable kids (e.g., the menubar) to move. + if (wrapper.firstChild.getAttribute("removable") != "true") + return; + + var wrapperType = wrapper.getAttribute("type"); + if (wrapperType != "separator" && + wrapperType != "spacer" && + wrapperType != "spring") { + restoreItemForToolbar(wrapper.firstChild, wrapper); + wrapPaletteItem(document.importNode(wrapper.firstChild, true)); + gToolbox.palette.appendChild(wrapper.firstChild); + } + + // The item was dragged out of the toolbar. + wrapper.parentNode.removeChild(wrapper); + } + + toolboxChanged(); +} + + +function isUnwantedDragEvent(aEvent) { + try { + if (Services.prefs.getBoolPref("toolkit.customization.unsafe_drag_events")) { + return false; + } + } catch (ex) {} + + /* Discard drag events that originated from a separate window to + prevent content->chrome privilege escalations. */ + let mozSourceNode = aEvent.dataTransfer.mozSourceNode; + // mozSourceNode is null in the dragStart event handler or if + // the drag event originated in an external application. + if (!mozSourceNode) { + return true; + } + let sourceWindow = mozSourceNode.ownerDocument.defaultView; + return sourceWindow != window && sourceWindow != gToolboxDocument.defaultView; +} + diff --git a/toolkit/content/customizeToolbar.xul b/toolkit/content/customizeToolbar.xul new file mode 100644 index 0000000000..09c45e8345 --- /dev/null +++ b/toolkit/content/customizeToolbar.xul @@ -0,0 +1,67 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE dialog [ +<!ENTITY % customizeToolbarDTD SYSTEM "chrome://global/locale/customizeToolbar.dtd"> + %customizeToolbarDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://global/content/customizeToolbar.css" type="text/css"?> +<?xml-stylesheet href="chrome://global/skin/customizeToolbar.css" type="text/css"?> + +<window id="CustomizeToolbarWindow" + title="&dialog.title;" + onload="onLoad();" + onunload="onUnload();" + style="&dialog.dimensions;" + persist="width height" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/javascript" src="chrome://global/content/customizeToolbar.js"/> + +<stringbundle id="stringBundle" src="chrome://global/locale/customizeToolbar.properties"/> + +<keyset id="CustomizeToolbarKeyset"> + <key id="cmd_close1" keycode="VK_ESCAPE" oncommand="onClose();"/> + <key id="cmd_close2" keycode="VK_RETURN" oncommand="onClose();"/> +</keyset> + +<vbox id="main-box" flex="1"> + <description id="instructions"> + &instructions.description; + </description> + + <vbox flex="1" id="palette-box" + ondragstart="onToolbarDragStart(event)" + ondragover="onPaletteDragOver(event)" + ondrop="onPaletteDrop(event)"/> + + <box align="center"> + <label value="&show.label;"/> + <menulist id="modelist" value="icons" oncommand="updateToolbarMode(this.value);"> + <menupopup id="modelistpopup"> + <menuitem id="modefull" value="full" label="&iconsAndText.label;"/> + <menuitem id="modeicons" value="icons" label="&icons.label;"/> + <menuitem id="modetext" value="text" label="&text.label;"/> + </menupopup> + </menulist> + + <checkbox id="smallicons" oncommand="updateIconSize(this.checked ? 'small' : 'large');" label="&useSmallIcons.label;"/> + + <button id="newtoolbar" label="&addNewToolbar.label;" oncommand="addNewToolbar();" icon="add"/> + <button id="restoreDefault" label="&restoreDefaultSet.label;" oncommand="restoreDefaultSet();" icon="revert"/> + </box> + + <separator class="groove"/> + + <hbox align="center" pack="end"> + <button id="donebutton" label="&saveChanges.label;" oncommand="onClose();" + default="true" icon="close"/> + </hbox> +</vbox> + +</window> diff --git a/toolkit/content/directionDetector.html b/toolkit/content/directionDetector.html new file mode 100644 index 0000000000..5e91ba7f57 --- /dev/null +++ b/toolkit/content/directionDetector.html @@ -0,0 +1,13 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> + <head> + <link rel="stylesheet" type="text/css" href="chrome://global/locale/intl.css"> + </head> + <body> + <window id="target" style="display: none;"></window> + </body> +</html> diff --git a/toolkit/content/editMenuOverlay.js b/toolkit/content/editMenuOverlay.js new file mode 100644 index 0000000000..a610b641ab --- /dev/null +++ b/toolkit/content/editMenuOverlay.js @@ -0,0 +1,39 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +// update menu items that rely on focus or on the current selection +function goUpdateGlobalEditMenuItems() +{ + // Don't bother updating the edit commands if they aren't visible in any way + // (i.e. the Edit menu isn't open, nor is the context menu open, nor have the + // cut, copy, and paste buttons been added to the toolbars) for performance. + // This only works in applications/on platforms that set the gEditUIVisible + // flag, so we check to see if that flag is defined before using it. + if (typeof gEditUIVisible != "undefined" && !gEditUIVisible) + return; + + goUpdateCommand("cmd_undo"); + goUpdateCommand("cmd_redo"); + goUpdateCommand("cmd_cut"); + goUpdateCommand("cmd_copy"); + goUpdateCommand("cmd_paste"); + goUpdateCommand("cmd_selectAll"); + goUpdateCommand("cmd_delete"); + goUpdateCommand("cmd_switchTextDirection"); +} + +// update menu items that relate to undo/redo +function goUpdateUndoEditMenuItems() +{ + goUpdateCommand("cmd_undo"); + goUpdateCommand("cmd_redo"); +} + +// update menu items that depend on clipboard contents +function goUpdatePasteMenuItems() +{ + goUpdateCommand("cmd_paste"); +} diff --git a/toolkit/content/editMenuOverlay.xul b/toolkit/content/editMenuOverlay.xul new file mode 100644 index 0000000000..9097019900 --- /dev/null +++ b/toolkit/content/editMenuOverlay.xul @@ -0,0 +1,108 @@ +<?xml version="1.0"?> <!-- -*- Mode: HTML -*- --> +<!-- 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/. --> + + +<!DOCTYPE overlay SYSTEM "chrome://global/locale/editMenuOverlay.dtd"> + +<overlay id="editMenuOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://global/content/editMenuOverlay.js"/> + + <commandset id="editMenuCommands"> + <commandset id="editMenuCommandSetAll" commandupdater="true" events="focus,select" + oncommandupdate="goUpdateGlobalEditMenuItems()"/> + <commandset id="editMenuCommandSetUndo" commandupdater="true" events="undo" + oncommandupdate="goUpdateUndoEditMenuItems()"/> + <commandset id="editMenuCommandSetPaste" commandupdater="true" events="clipboard" + oncommandupdate="goUpdatePasteMenuItems()"/> + <command id="cmd_undo" oncommand="goDoCommand('cmd_undo')"/> + <command id="cmd_redo" oncommand="goDoCommand('cmd_redo')"/> + <command id="cmd_cut" oncommand="goDoCommand('cmd_cut')"/> + <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')"/> + <command id="cmd_paste" oncommand="goDoCommand('cmd_paste')"/> + <command id="cmd_delete" oncommand="goDoCommand('cmd_delete')"/> + <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')"/> + <command id="cmd_switchTextDirection" oncommand="goDoCommand('cmd_switchTextDirection');"/> + </commandset> + + <!-- These key nodes are here only for show. The real bindings come from + XBL, in platformHTMLBindings.xml. See bugs 57078 and 71779. --> + + <keyset id="editMenuKeys"> + <key id="key_undo" key="&undoCmd.key;" modifiers="accel" command="cmd_undo"/> +#ifdef XP_UNIX + <key id="key_redo" key="&undoCmd.key;" modifiers="accel,shift" command="cmd_redo"/> +#else + <key id="key_redo" key="&redoCmd.key;" modifiers="accel" command="cmd_redo"/> +#endif + <key id="key_cut" key="&cutCmd.key;" modifiers="accel" command="cmd_cut"/> + <key id="key_copy" key="©Cmd.key;" modifiers="accel" command="cmd_copy"/> + <key id="key_paste" key="&pasteCmd.key;" modifiers="accel" command="cmd_paste"/> + <key id="key_delete" keycode="VK_DELETE" command="cmd_delete"/> + <key id="key_selectAll" key="&selectAllCmd.key;" modifiers="accel" command="cmd_selectAll"/> + <key id="key_find" key="&findCmd.key;" modifiers="accel" command="cmd_find"/> + <key id="key_findAgain" key="&findAgainCmd.key;" modifiers="accel" command="cmd_findAgain"/> + <key id="key_findPrevious" key="&findAgainCmd.key;" modifiers="shift,accel" command="cmd_findPrevious"/> + <key id="key_findAgain2" keycode="&findAgainCmd.key2;" command="cmd_findAgain"/> + <key id="key_findPrevious2" keycode="&findAgainCmd.key2;" modifiers="shift" command="cmd_findPrevious"/> + </keyset> + + <!-- Edit Menu --> + <menu id="menu_edit" label="&editMenu.label;" + accesskey="&editMenu.accesskey;"/> + + <menuitem id="menu_undo" label="&undoCmd.label;" + key="key_undo" accesskey="&undoCmd.accesskey;" + command="cmd_undo"/> + <menuitem id="menu_redo" label="&redoCmd.label;" + key="key_redo" accesskey="&redoCmd.accesskey;" + command="cmd_redo"/> + <menuitem id="menu_cut" label="&cutCmd.label;" + key="key_cut" accesskey="&cutCmd.accesskey;" + command="cmd_cut"/> + <menuitem id="menu_copy" label="©Cmd.label;" + key="key_copy" accesskey="©Cmd.accesskey;" + command="cmd_copy"/> + <menuitem id="menu_paste" label="&pasteCmd.label;" + key="key_paste" accesskey="&pasteCmd.accesskey;" + command="cmd_paste"/> + <menuitem id="menu_delete" label="&deleteCmd.label;" + key="key_delete" accesskey="&deleteCmd.accesskey;" + command="cmd_delete"/> + <menuitem id="menu_selectAll" label="&selectAllCmd.label;" + key="key_selectAll" accesskey="&selectAllCmd.accesskey;" + command="cmd_selectAll"/> + <menuitem id="menu_find" label="&findCmd.label;" + key="key_find" accesskey="&findCmd.accesskey;" + command="cmd_find"/> + <menuitem id="menu_findAgain" label="&findAgainCmd.label;" + key="key_findAgain" accesskey="&findAgainCmd.accesskey;" + command="cmd_findAgain"/> + <menuitem id="menu_findPrevious" label="&findPreviousCmd.label;" + key="key_findPrevious" accesskey="&findPreviousCmd.accesskey;" + command="cmd_findPrevious"/> + + <menuitem id="cMenu_undo" label="&undoCmd.label;" + accesskey="&undoCmd.accesskey;" command="cmd_undo"/> + <menuitem id="cMenu_redo" label="&redoCmd.label;" + accesskey="&redoCmd.accesskey;" command="cmd_redo"/> + <menuitem id="cMenu_cut" label="&cutCmd.label;" + accesskey="&cutCmd.accesskey;" command="cmd_cut"/> + <menuitem id="cMenu_copy" label="©Cmd.label;" + accesskey="©Cmd.accesskey;" command="cmd_copy"/> + <menuitem id="cMenu_paste" label="&pasteCmd.label;" + accesskey="&pasteCmd.accesskey;" command="cmd_paste"/> + <menuitem id="cMenu_delete" label="&deleteCmd.label;" + accesskey="&deleteCmd.accesskey;" command="cmd_delete"/> + <menuitem id="cMenu_selectAll" label="&selectAllCmd.label;" + accesskey="&selectAllCmd.accesskey;" command="cmd_selectAll"/> + <menuitem id="cMenu_find" label="&findCmd.label;" + accesskey="&findCmd.accesskey;" command="cmd_find"/> + <menuitem id="cMenu_findAgain" label="&findAgainCmd.label;" + accesskey="&findAgainCmd.accesskey;" command="cmd_findAgain"/> + <menuitem id="cMenu_findPrevious" label="&findPreviousCmd.label;" + accesskey="&findPreviousCmd.accesskey;" command="cmd_findPrevious"/> +</overlay> diff --git a/toolkit/content/filepicker.properties b/toolkit/content/filepicker.properties new file mode 100644 index 0000000000..5be84e3a0b --- /dev/null +++ b/toolkit/content/filepicker.properties @@ -0,0 +1,12 @@ +# 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/. + +allFilter=* +htmlFilter=*.html; *.htm; *.shtml; *.xhtml +textFilter=*.txt; *.text +imageFilter=*.jpe; *.jpg; *.jpeg; *.gif; *.png; *.bmp; *.ico; *.svg; *.svgz; *.tif; *.tiff; *.ai; *.drw; *.pct; *.psp; *.xcf; *.psd; *.raw +xmlFilter=*.xml +xulFilter=*.xul +audioFilter=*.aac; *.aif; *.flac; *.iff; *.m4a; *.m4b; *.mid; *.midi; *.mp3; *.mpa; *.mpc; *.oga; *.ogg; *.ra; *.ram; *.snd; *.wav; *.wma +videoFilter=*.avi; *.divx; *.flv; *.m4v; *.mkv; *.mov; *.mp4; *.mpeg; *.mpg; *.ogm; *.ogv; *.ogx; *.rm; *.rmvb; *.smil; *.webm; *.wmv; *.xvid diff --git a/toolkit/content/findUtils.js b/toolkit/content/findUtils.js new file mode 100644 index 0000000000..397a98f6cb --- /dev/null +++ b/toolkit/content/findUtils.js @@ -0,0 +1,111 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); + +var gFindBundle; + +function nsFindInstData() {} +nsFindInstData.prototype = +{ + // set the next three attributes on your object to override the defaults + browser : null, + + get rootSearchWindow() { return this._root || this.window.content; }, + set rootSearchWindow(val) { this._root = val; }, + + get currentSearchWindow() { + if (this._current) + return this._current; + + var focusedWindow = this.window.document.commandDispatcher.focusedWindow; + if (!focusedWindow || focusedWindow == this.window) + focusedWindow = this.window.content; + + return focusedWindow; + }, + set currentSearchWindow(val) { this._current = val; }, + + get webBrowserFind() { return this.browser.webBrowserFind; }, + + init : function() { + var findInst = this.webBrowserFind; + // set up the find to search the focussedWindow, bounded by the content window. + var findInFrames = findInst.QueryInterface(Components.interfaces.nsIWebBrowserFindInFrames); + findInFrames.rootSearchFrame = this.rootSearchWindow; + findInFrames.currentSearchFrame = this.currentSearchWindow; + + // always search in frames for now. We could add a checkbox to the dialog for this. + findInst.searchFrames = true; + }, + + window : window, + _root : null, + _current : null +} + +// browser is the <browser> element +// rootSearchWindow is the window to constrain the search to (normally window.content) +// currentSearchWindow is the frame to start searching (can be, and normally, rootSearchWindow) +function findInPage(findInstData) +{ + // is the dialog up already? + if ("findDialog" in window && window.findDialog) + window.findDialog.focus(); + else + { + findInstData.init(); + window.findDialog = window.openDialog("chrome://global/content/finddialog.xul", "_blank", "chrome,resizable=no,dependent=yes", findInstData); + } +} + +function findAgainInPage(findInstData, reverse) +{ + if ("findDialog" in window && window.findDialog) + window.findDialog.focus(); + else + { + // get the find service, which stores global find state, and init the + // nsIWebBrowser find with it. We don't assume that there was a previous + // Find that set this up. + var findService = Components.classes["@mozilla.org/find/find_service;1"] + .getService(Components.interfaces.nsIFindService); + + var searchString = findService.searchString; + if (searchString.length == 0) { + // no previous find text + findInPage(findInstData); + return; + } + + findInstData.init(); + var findInst = findInstData.webBrowserFind; + findInst.searchString = searchString; + findInst.matchCase = findService.matchCase; + findInst.wrapFind = findService.wrapFind; + findInst.entireWord = findService.entireWord; + findInst.findBackwards = findService.findBackwards ^ reverse; + + var found = findInst.findNext(); + if (!found) { + if (!gFindBundle) + gFindBundle = document.getElementById("findBundle"); + + Services.prompt.alert(window, gFindBundle.getString("notFoundTitle"), gFindBundle.getString("notFoundWarning")); + } + + // Reset to normal value, otherwise setting can get changed in find dialog + findInst.findBackwards = findService.findBackwards; + } +} + +function canFindAgainInPage() +{ + var findService = Components.classes["@mozilla.org/find/find_service;1"] + .getService(Components.interfaces.nsIFindService); + return (findService.searchString.length > 0); +} + diff --git a/toolkit/content/finddialog.js b/toolkit/content/finddialog.js new file mode 100644 index 0000000000..fc58753211 --- /dev/null +++ b/toolkit/content/finddialog.js @@ -0,0 +1,151 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/FormHistory.jsm"); + +var dialog; // Quick access to document/form elements. +var gFindInst; // nsIWebBrowserFind that we're going to use +var gFindInstData; // use this to update the find inst data + +function initDialogObject() +{ + // Create dialog object and initialize. + dialog = {}; + dialog.findKey = document.getElementById("dialog.findKey"); + dialog.caseSensitive = document.getElementById("dialog.caseSensitive"); + dialog.wrap = document.getElementById("dialog.wrap"); + dialog.find = document.getElementById("btnFind"); + dialog.up = document.getElementById("radioUp"); + dialog.down = document.getElementById("radioDown"); + dialog.rg = dialog.up.radioGroup; + dialog.bundle = null; + + // Move dialog to center, if it not been shown before + var windowElement = document.getElementById("findDialog"); + if (!windowElement.hasAttribute("screenX") || !windowElement.hasAttribute("screenY")) + { + sizeToContent(); + moveToAlertPosition(); + } +} + +function fillDialog() +{ + // get the find service, which stores global find state + var findService = Components.classes["@mozilla.org/find/find_service;1"] + .getService(Components.interfaces.nsIFindService); + + // Set initial dialog field contents. Use the gFindInst attributes first, + // this is necessary for window.find() + dialog.findKey.value = gFindInst.searchString ? gFindInst.searchString : findService.searchString; + dialog.caseSensitive.checked = gFindInst.matchCase ? gFindInst.matchCase : findService.matchCase; + dialog.wrap.checked = gFindInst.wrapFind ? gFindInst.wrapFind : findService.wrapFind; + var findBackwards = gFindInst.findBackwards ? gFindInst.findBackwards : findService.findBackwards; + if (findBackwards) + dialog.rg.selectedItem = dialog.up; + else + dialog.rg.selectedItem = dialog.down; +} + +function saveFindData() +{ + // get the find service, which stores global find state + var findService = Components.classes["@mozilla.org/find/find_service;1"] + .getService(Components.interfaces.nsIFindService); + + // Set data attributes per user input. + findService.searchString = dialog.findKey.value; + findService.matchCase = dialog.caseSensitive.checked; + findService.wrapFind = dialog.wrap.checked; + findService.findBackwards = dialog.up.selected; +} + +function onLoad() +{ + initDialogObject(); + + // get the find instance + var arg0 = window.arguments[0]; + // If the dialog was opened from window.find(), + // arg0 will be an instance of nsIWebBrowserFind + if (arg0 instanceof Components.interfaces.nsIWebBrowserFind) { + gFindInst = arg0; + } else { + gFindInstData = arg0; + gFindInst = gFindInstData.webBrowserFind; + } + + fillDialog(); + doEnabling(); + + if (dialog.findKey.value) + dialog.findKey.select(); + dialog.findKey.focus(); +} + +function onUnload() +{ + window.opener.findDialog = 0; +} + +function onAccept() +{ + if (gFindInstData && gFindInst != gFindInstData.webBrowserFind) { + gFindInstData.init(); + gFindInst = gFindInstData.webBrowserFind; + } + + // Transfer dialog contents to the find service. + saveFindData(); + updateFormHistory(); + + // set up the find instance + gFindInst.searchString = dialog.findKey.value; + gFindInst.matchCase = dialog.caseSensitive.checked; + gFindInst.wrapFind = dialog.wrap.checked; + gFindInst.findBackwards = dialog.up.selected; + + // Search. + var result = gFindInst.findNext(); + + if (!result) + { + if (!dialog.bundle) + dialog.bundle = document.getElementById("findBundle"); + Services.prompt.alert(window, dialog.bundle.getString("notFoundTitle"), + dialog.bundle.getString("notFoundWarning")); + dialog.findKey.select(); + dialog.findKey.focus(); + } + return false; +} + +function doEnabling() +{ + dialog.find.disabled = !dialog.findKey.value; +} + +function updateFormHistory() +{ + if (window.opener.PrivateBrowsingUtils && + window.opener.PrivateBrowsingUtils.isWindowPrivate(window.opener) || + !dialog.findKey.value) + return; + + if (FormHistory.enabled) { + FormHistory.update({ + op: "bump", + fieldname: "find-dialog", + value: dialog.findKey.value + }, { + handleError: function(aError) { + Components.utils.reportError("Saving find to form history failed: " + + aError.message); + } + }); + } +} diff --git a/toolkit/content/finddialog.xul b/toolkit/content/finddialog.xul new file mode 100644 index 0000000000..c49092e6f6 --- /dev/null +++ b/toolkit/content/finddialog.xul @@ -0,0 +1,58 @@ +<?xml version="1.0"?> <!-- -*- Mode: HTML -*- --> +# 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/. + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://global/locale/finddialog.dtd"> + +<dialog id="findDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + orient="horizontal" + windowtype="findInPage" + onload="onLoad();" + onunload="onUnload();" + ondialogaccept="return onAccept();" + buttons="accept,cancel" + title="&findDialog.title;" + persist="screenX screenY"> + + <script type="application/javascript" src="chrome://global/content/finddialog.js"/> + <stringbundle id="findBundle" src="chrome://global/locale/finddialog.properties"/> + + <hbox> + <vbox> + <hbox align="center"> + <label value="&findField.label;" accesskey="&findField.accesskey;" control="dialog.findKey"/> + <textbox id="dialog.findKey" flex="1" + type="autocomplete" + autocompletesearch="form-history" + autocompletesearchparam="find-dialog" + oninput="doEnabling();"/> + </hbox> + <hbox align="center"> + <vbox> + <checkbox id="dialog.caseSensitive" label="&caseSensitiveCheckbox.label;" accesskey="&caseSensitiveCheckbox.accesskey;"/> + <checkbox id="dialog.wrap" label="&wrapCheckbox.label;" accesskey="&wrapCheckbox.accesskey;" checked="true"/> + </vbox> + <groupbox orient="horizontal"> + <caption label="&direction.label;"/> + <radiogroup orient="horizontal"> + <radio id="radioUp" label="&up.label;" accesskey="&up.accesskey;"/> + <radio id="radioDown" label="&down.label;" accesskey="&down.accesskey;" selected="true"/> + </radiogroup> + </groupbox> + </hbox> + </vbox> + <vbox> + <button id="btnFind" label="&findButton.label;" accesskey="&findButton.accesskey;" + dlgtype="accept" icon="find"/> +#ifdef XP_UNIX + <button label="&closeButton.label;" icon="close" dlgtype="cancel"/> +#else + <button label="&cancelButton.label;" icon="cancel" dlgtype="cancel"/> +#endif + </vbox> + </hbox> +</dialog> diff --git a/toolkit/content/globalOverlay.js b/toolkit/content/globalOverlay.js new file mode 100644 index 0000000000..1df3d65fc1 --- /dev/null +++ b/toolkit/content/globalOverlay.js @@ -0,0 +1,168 @@ +/* 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/. */ + +function closeWindow(aClose, aPromptFunction) +{ + let { AppConstants } = Components.utils.import("resource://gre/modules/AppConstants.jsm"); + + // Closing the last window doesn't quit the application on OS X. + if (AppConstants.platform != "macosx") { + var windowCount = 0; + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var e = wm.getEnumerator(null); + + while (e.hasMoreElements()) { + var w = e.getNext(); + if (w.closed) { + continue; + } + if (++windowCount == 2) + break; + } + + // If we're down to the last window and someone tries to shut down, check to make sure we can! + if (windowCount == 1 && !canQuitApplication("lastwindow")) + return false; + if (windowCount != 1 && typeof(aPromptFunction) == "function" && !aPromptFunction()) + return false; + } else if (typeof(aPromptFunction) == "function" && !aPromptFunction()) { + return false; + } + + if (aClose) { + window.close(); + return window.closed; + } + + return true; +} + +function canQuitApplication(aData) +{ + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + if (!os) return true; + + try { + var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"] + .createInstance(Components.interfaces.nsISupportsPRBool); + os.notifyObservers(cancelQuit, "quit-application-requested", aData || null); + + // Something aborted the quit process. + if (cancelQuit.data) + return false; + } + catch (ex) { } + return true; +} + +function goQuitApplication() +{ + if (!canQuitApplication()) + return false; + + var appStartup = Components.classes['@mozilla.org/toolkit/app-startup;1']. + getService(Components.interfaces.nsIAppStartup); + + appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit); + return true; +} + +// +// Command Updater functions +// +function goUpdateCommand(aCommand) +{ + try { + var controller = top.document.commandDispatcher + .getControllerForCommand(aCommand); + + var enabled = false; + if (controller) + enabled = controller.isCommandEnabled(aCommand); + + goSetCommandEnabled(aCommand, enabled); + } + catch (e) { + Components.utils.reportError("An error occurred updating the " + + aCommand + " command: " + e); + } +} + +function goDoCommand(aCommand) +{ + try { + var controller = top.document.commandDispatcher + .getControllerForCommand(aCommand); + if (controller && controller.isCommandEnabled(aCommand)) + controller.doCommand(aCommand); + } + catch (e) { + Components.utils.reportError("An error occurred executing the " + + aCommand + " command: " + e); + } +} + + +function goSetCommandEnabled(aID, aEnabled) +{ + var node = document.getElementById(aID); + + if (node) { + if (aEnabled) + node.removeAttribute("disabled"); + else + node.setAttribute("disabled", "true"); + } +} + +function goSetMenuValue(aCommand, aLabelAttribute) +{ + var commandNode = top.document.getElementById(aCommand); + if (commandNode) { + var label = commandNode.getAttribute(aLabelAttribute); + if (label) + commandNode.setAttribute("label", label); + } +} + +function goSetAccessKey(aCommand, aValueAttribute) +{ + var commandNode = top.document.getElementById(aCommand); + if (commandNode) { + var value = commandNode.getAttribute(aValueAttribute); + if (value) + commandNode.setAttribute("accesskey", value); + } +} + +// this function is used to inform all the controllers attached to a node that an event has occurred +// (e.g. the tree controllers need to be informed of blur events so that they can change some of the +// menu items back to their default values) +function goOnEvent(aNode, aEvent) +{ + var numControllers = aNode.controllers.getControllerCount(); + var controller; + + for (var controllerIndex = 0; controllerIndex < numControllers; controllerIndex++) { + controller = aNode.controllers.getControllerAt(controllerIndex); + if (controller) + controller.onEvent(aEvent); + } +} + +function setTooltipText(aID, aTooltipText) +{ + var element = document.getElementById(aID); + if (element) + element.setAttribute("tooltiptext", aTooltipText); +} + +this.__defineGetter__("NS_ASSERT", function() { + delete this.NS_ASSERT; + var tmpScope = {}; + Components.utils.import("resource://gre/modules/debug.js", tmpScope); + return this.NS_ASSERT = tmpScope.NS_ASSERT; +}); diff --git a/toolkit/content/gmp-sources/eme-adobe.json b/toolkit/content/gmp-sources/eme-adobe.json new file mode 100644 index 0000000000..3bd808be8e --- /dev/null +++ b/toolkit/content/gmp-sources/eme-adobe.json @@ -0,0 +1,31 @@ +{ + "vendors": { + "gmp-eme-adobe": { + "platforms": { + "WINNT_x86-msvc-x64": { + "alias": "WINNT_x86-msvc" + }, + "WINNT_x86-msvc": { + "fileUrl": "https://cdmdownload.adobe.com/firefox/win/x86/primetime_gmp_win_x86_gmc_40673.zip", + "hashValue": "8aad35fc13814b0f1daacddb0d599eedd685287d5afddc97c2f740c8aea270636ccd75b1d1a57364b84e8eb1b23c9f1c126c057d95f3d8217b331dc4b1d5340f", + "filesize": 3694349 + }, + "WINNT_x86_64-msvc-x64": { + "alias": "WINNT_x86_64-msvc" + }, + "WINNT_x86-msvc-x86": { + "alias": "WINNT_x86-msvc" + }, + "WINNT_x86_64-msvc": { + "fileUrl": "https://cdmdownload.adobe.com/firefox/win/x64/primetime_gmp_win_x64_gmc_40673.zip", + "hashValue": "bd1e1a370c5f9dadc247c9f00dd203fab1a75ff3afed8439a0a0bfcc7e1767d0da68497140cbe48daa70e2535dde5f220dd7b344619cecd830a6b685efb9d5a0", + "filesize": 4853103 + } + }, + "version": "17" + } + }, + "hashFunction": "sha512", + "name": "CDM-17", + "schema_version": 1000 +} diff --git a/toolkit/content/gmp-sources/openh264.json b/toolkit/content/gmp-sources/openh264.json new file mode 100644 index 0000000000..a5ba3a5250 --- /dev/null +++ b/toolkit/content/gmp-sources/openh264.json @@ -0,0 +1,67 @@ +{ + "vendors": { + "gmp-gmpopenh264": { + "platforms": { + "WINNT_x86-msvc-x64": { + "alias": "WINNT_x86-msvc" + }, + "Android_x86-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-android-x86-0410d336bb748149a4f560eb6108090f078254b1.zip", + "hashValue": "8ce4d4318aa6ae9ac1376500d5fceecb3df38727aa920efd9f7829c139face4a069cab683d3902e7cdb89daad2a7e928ffba120812ae343f052a833812dad387", + "filesize": 1640053 + }, + "WINNT_x86-msvc": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-win32-0410d336bb748149a4f560eb6108090f078254b1.zip", + "hashValue": "991e01c3b95fa13fac52e0512e1936f1edae42ecbbbcc55447a36915eb3ca8f836546cc780343751691e0188872e5bc56fe3ad5f23f3243e90b96a637561b89e", + "filesize": 356940 + }, + "WINNT_x86-msvc-x86": { + "alias": "WINNT_x86-msvc" + }, + "Linux_x86_64-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-linux64-0410d336bb748149a4f560eb6108090f078254b1.zip", + "hashValue": "e1086ee6e4fb60a1aa11b5626594b97695533a8e269d776877cebd5cf29088619e2c164e7bd1eba5486f772c943f2efec723f69cc48478ec84a11d7b61ca1865", + "filesize": 515722 + }, + "Darwin_x86-gcc3-u-i386-x86_64": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-macosx32-0410d336bb748149a4f560eb6108090f078254b1.zip", + "hashValue": "64b0e13e6319b7a31ed35a46bea5abcfe6af04ba59a277db07677236cfb685813763731ff6b44b85e03e1489f3b15f8df0128a299a36720531b9f4ba6e1c1f58", + "filesize": 382435 + }, + "Darwin_x86_64-gcc3": { + "alias": "Darwin_x86_64-gcc3-u-i386-x86_64" + }, + "Linux_x86-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-linux32-0410d336bb748149a4f560eb6108090f078254b1.zip", + "hashValue": "19084f0230218c584715861f4723e072b1af02e26995762f368105f670f60ecb4082531bc4e33065a4675dd1296f6872a6cb101547ef2d19ef3e25e2e16d4dc0", + "filesize": 515857 + }, + "Darwin_x86_64-gcc3-u-i386-x86_64": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-macosx64-0410d336bb748149a4f560eb6108090f078254b1.zip", + "hashValue": "3b52343070a2f75e91b7b0d3bb33935352237c7e1d2fdc6a467d039ffbbda6a72087f9e0a369fe95e6c4c789ff3052f0c134af721d7273db9ba66d077d85b327", + "filesize": 390308 + }, + "Android_arm-eabi-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-android-arm-0410d336bb748149a4f560eb6108090f078254b1.zip", + "hashValue": "7a15245c781f32df310ebb88cb8a783512eab934b38ffd889d6420473d40eddbe8a89c17cc60d4e7647c156b04d20030e1ae0081e3f90a0d8f94626ec5f4d817", + "filesize": 1515895 + }, + "Darwin_x86-gcc3": { + "alias": "Darwin_x86-gcc3-u-i386-x86_64" + }, + "WINNT_x86_64-msvc-x64": { + "alias": "WINNT_x86_64-msvc" + }, + "WINNT_x86_64-msvc": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-win64-0410d336bb748149a4f560eb6108090f078254b1.zip", + "hashValue": "5030b47065e817db5c40bca9c62ac27292bbf636e24698f45dc67f03fa6420b97bd2f792c1cb39df65776c1e7597c70122ac7abf36fb2ad0603734e9e8ec4ef3", + "filesize": 404355 + } + }, + "version": "1.6" + } + }, + "hashFunction": "sha512", + "name": "OpenH264-1.6", + "schema_version": 1000 +} diff --git a/toolkit/content/gmp-sources/widevinecdm.json b/toolkit/content/gmp-sources/widevinecdm.json new file mode 100644 index 0000000000..02ef7fee5c --- /dev/null +++ b/toolkit/content/gmp-sources/widevinecdm.json @@ -0,0 +1,49 @@ +{ + "vendors": { + "gmp-widevinecdm": { + "platforms": { + "WINNT_x86-msvc-x64": { + "alias": "WINNT_x86-msvc" + }, + "WINNT_x86-msvc": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-win-ia32.zip", + "hashValue": "d7e10d09c87a157af865f8388ba70ae672bd9e38987bdd94077af52d6b1abaa745b3db92e9f93f607af6420c68210f7cfd518a9d2c99fecf79aed3385cbcbc0b", + "filesize": 2884452 + }, + "WINNT_x86-msvc-x86": { + "alias": "WINNT_x86-msvc" + }, + "Linux_x86_64-gcc3": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-linux-x64.zip", + "hashValue": "1edfb58a44792d2a53694f46fcc698161edafb2a1fe0e5c31b50c1d52408b5e8918d9f33271c62a19a65017694ebeacb4f390fe914688ca7b1952cdb84ed55ec", + "filesize": 2975492 + }, + "Darwin_x86_64-gcc3-u-i386-x86_64": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-mac-x64.zip", + "hashValue": "1916805e84a49e04748204f4e4c48ae52c8312f7c04afedacacd7dfab2de424412bc988a8c3e5bcb0865f8844b569c0eb9589dae51e74d9bdfe46792c9d1631f", + "filesize": 2155607 + }, + "Darwin_x86_64-gcc3": { + "alias": "Darwin_x86_64-gcc3-u-i386-x86_64" + }, + "Linux_x86-gcc3": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-linux-ia32.zip", + "hashValue": "5c4beb72ea693740a013b60bc5491a042bb82fa5ca6845a2f450579e2e1e465263f19e7ab6d08d91deb8219b30f092ab6e6745300d5adda627f270c95e5a66e0", + "filesize": 3084582 + }, + "WINNT_x86_64-msvc-x64": { + "alias": "WINNT_x86_64-msvc" + }, + "WINNT_x86_64-msvc": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/903-win-x64.zip", + "hashValue": "33497f3458846e11fa52413f6477bfe1a7f502da262c3a2ce9fe6d773a4a2d023c54228596eb162444b55c87fb126de01f60fa729d897ef5e6eec73b2dfbdc7a", + "filesize": 2853777 + } + }, + "version": "1.4.8.903" + } + }, + "hashFunction": "sha512", + "name": "Widevine-1.4.8.903", + "schema_version": 1000 +} diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn new file mode 100644 index 0000000000..590356b646 --- /dev/null +++ b/toolkit/content/jar.mn @@ -0,0 +1,120 @@ +toolkit.jar: +% content global %content/global/ contentaccessible=yes +% content global-platform %content/global-platform/ platform +* content/global/license.html + content/global/XPCNativeWrapper.js + content/global/minimal-xul.css +* content/global/xul.css + content/global/textbox.css + content/global/menulist.css + content/global/autocomplete.css + content/global/about.js + content/global/about.xhtml + content/global/aboutAbout.js + content/global/aboutAbout.xhtml +#ifdef MOZILLA_OFFICIAL + content/global/aboutRights.xhtml +#else + content/global/aboutRights.xhtml (aboutRights-unbranded.xhtml) +#endif + content/global/aboutNetworking.js + content/global/aboutNetworking.xhtml +#ifndef ANDROID + content/global/aboutProfiles.js + content/global/aboutProfiles.xhtml +#endif + content/global/aboutServiceWorkers.js + content/global/aboutServiceWorkers.xhtml + content/global/aboutwebrtc/aboutWebrtc.css (aboutwebrtc/aboutWebrtc.css) + content/global/aboutwebrtc/aboutWebrtc.js (aboutwebrtc/aboutWebrtc.js) + content/global/aboutwebrtc/aboutWebrtc.html (aboutwebrtc/aboutWebrtc.html) + content/global/aboutSupport.js +* content/global/aboutSupport.xhtml + content/global/aboutTelemetry.js + content/global/aboutTelemetry.xhtml + content/global/aboutTelemetry.css + content/global/directionDetector.html + content/global/plugins.html + content/global/plugins.css + content/global/browser-child.js + content/global/browser-content.js +* content/global/buildconfig.html + content/global/contentAreaUtils.js +#ifndef MOZ_FENNEC + content/global/customizeToolbar.css + content/global/customizeToolbar.js + content/global/customizeToolbar.xul +#endif +#ifndef MOZ_FENNEC + content/global/editMenuOverlay.js +* content/global/editMenuOverlay.xul + content/global/finddialog.js +* content/global/finddialog.xul + content/global/findUtils.js +#endif + content/global/filepicker.properties + content/global/globalOverlay.js + content/global/mozilla.xhtml + content/global/process-content.js + content/global/resetProfile.css + content/global/resetProfile.js + content/global/resetProfile.xul + content/global/resetProfileProgress.xul + content/global/select-child.js + content/global/TopLevelVideoDocument.js + content/global/timepicker.xhtml + content/global/treeUtils.js + content/global/viewZoomOverlay.js + content/global/bindings/autocomplete.xml (widgets/autocomplete.xml) + content/global/bindings/browser.xml (widgets/browser.xml) + content/global/bindings/button.xml (widgets/button.xml) + content/global/bindings/checkbox.xml (widgets/checkbox.xml) + content/global/bindings/colorpicker.xml (widgets/colorpicker.xml) + content/global/bindings/datetimepicker.xml (widgets/datetimepicker.xml) + content/global/bindings/datetimepopup.xml (widgets/datetimepopup.xml) + content/global/bindings/datetimebox.xml (widgets/datetimebox.xml) + content/global/bindings/datetimebox.css (widgets/datetimebox.css) +* content/global/bindings/dialog.xml (widgets/dialog.xml) + content/global/bindings/editor.xml (widgets/editor.xml) + content/global/bindings/expander.xml (widgets/expander.xml) + content/global/bindings/filefield.xml (widgets/filefield.xml) +* content/global/bindings/findbar.xml (widgets/findbar.xml) + content/global/bindings/general.xml (widgets/general.xml) + content/global/bindings/groupbox.xml (widgets/groupbox.xml) + content/global/bindings/listbox.xml (widgets/listbox.xml) + content/global/bindings/menu.xml (widgets/menu.xml) + content/global/bindings/menulist.xml (widgets/menulist.xml) + content/global/bindings/notification.xml (widgets/notification.xml) + content/global/bindings/numberbox.xml (widgets/numberbox.xml) + content/global/bindings/popup.xml (widgets/popup.xml) +* content/global/bindings/preferences.xml (widgets/preferences.xml) + content/global/bindings/progressmeter.xml (widgets/progressmeter.xml) + content/global/bindings/radio.xml (widgets/radio.xml) + content/global/bindings/remote-browser.xml (widgets/remote-browser.xml) + content/global/bindings/resizer.xml (widgets/resizer.xml) + content/global/bindings/richlistbox.xml (widgets/richlistbox.xml) + content/global/bindings/scale.xml (widgets/scale.xml) + content/global/bindings/scrollbar.xml (widgets/scrollbar.xml) + content/global/bindings/scrollbox.xml (widgets/scrollbox.xml) + content/global/bindings/spinner.js (widgets/spinner.js) + content/global/bindings/splitter.xml (widgets/splitter.xml) + content/global/bindings/spinbuttons.xml (widgets/spinbuttons.xml) + content/global/bindings/stringbundle.xml (widgets/stringbundle.xml) +* content/global/bindings/tabbox.xml (widgets/tabbox.xml) + content/global/bindings/text.xml (widgets/text.xml) +* content/global/bindings/textbox.xml (widgets/textbox.xml) + content/global/bindings/timekeeper.js (widgets/timekeeper.js) + content/global/bindings/timepicker.js (widgets/timepicker.js) + content/global/bindings/toolbar.xml (widgets/toolbar.xml) + content/global/bindings/toolbarbutton.xml (widgets/toolbarbutton.xml) +* content/global/bindings/tree.xml (widgets/tree.xml) + content/global/bindings/videocontrols.xml (widgets/videocontrols.xml) + content/global/bindings/videocontrols.css (widgets/videocontrols.css) +* content/global/bindings/wizard.xml (widgets/wizard.xml) +#ifdef XP_MACOSX + content/global/macWindowMenu.js +#endif + content/global/svg/svgBindings.xml (/layout/svg/resources/content/svgBindings.xml) + content/global/gmp-sources/eme-adobe.json (gmp-sources/eme-adobe.json) + content/global/gmp-sources/openh264.json (gmp-sources/openh264.json) + content/global/gmp-sources/widevinecdm.json (gmp-sources/widevinecdm.json) diff --git a/toolkit/content/license.html b/toolkit/content/license.html new file mode 100644 index 0000000000..2e79824461 --- /dev/null +++ b/toolkit/content/license.html @@ -0,0 +1,5906 @@ +<!DOCTYPE HTML> +<!-- 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/. --> + +<html lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> + <title>about:license</title> + + <style type="text/css"> + .path { + font-family: monospace; + } + + dt { + font-weight: bold; + } + </style> + <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"> + </head> + + <body id="lic-info" class="aboutPageWideContainer"> + <h1><a id="top"></a>about:license</h1> + + <div> + +#ifdef APP_LICENSE_BLOCK +#includesubst @APP_LICENSE_BLOCK@ +#endif + + <p>All of the <b>source code</b> to this product is + available under licenses which are both + <a href="http://www.gnu.org/philosophy/free-sw.html">free</a> and + <a href="http://www.opensource.org/docs/definition.php">open source</a>. + A URL identifying the specific source code used to create this copy can be found + on the <a href="about:buildconfig">build configuration page</a>, and you can read + <a href="https://developer.mozilla.org/en/Mozilla_Source_Code_%28Mercurial%29">instructions + on how to download and build the code for yourself</a>. + </p> + + <p>More specifically, most of the source code is available under the + <a href="about:license#mpl">Mozilla Public License 2.0</a> (MPL). + The MPL has a + <a href="http://www.mozilla.org/MPL/2.0/FAQ.html">FAQ</a> to help + you understand it. The remainder of the software which is not + under the MPL is available under one of a variety of other + free and open source licenses. Those that require reproduction + of the license text in the distribution are given below. + (Note: your copy of this product may not contain code covered by one + or more of the licenses listed here, depending on the exact product + and version you choose.) + </p> + + <ul> + <li><a href="about:license#mpl">Mozilla Public License 2.0</a> + <br><br> + </li> + <li><a href="about:license#lgpl">GNU Lesser General Public License 2.1</a> + <br><br> + </li> + <li><a href="about:license#lgpl-3.0">GNU Lesser General Public License 3.0</a> + <br><br> + </li> + <li><a href="about:license#gpl-3.0">GNU General Public License 3.0</a> + <br><br> + </li> + <li><a href="about:license#ACE">ACE License</a></li> + <li><a href="about:license#acorn">acorn License</a></li> +#ifdef MOZ_INSTALL_TRACKING + <li><a href="about:license#adjust">Adjust SDK License</a></li> +#endif + <li><a href="about:license#adobecmap">Adobe CMap License</a></li> + <li><a href="about:license#android">Android Open Source License</a></li> + <li><a href="about:license#angle">ANGLE License</a></li> + <li><a href="about:license#apache">Apache License 2.0</a></li> + <li><a href="about:license#apple">Apple License</a></li> + <li><a href="about:license#apple-mozilla">Apple/Mozilla NPRuntime License</a></li> + <li><a href="about:license#arm">ARM License</a></li> + <li><a href="about:license#bspatch">bspatch License</a></li> + <li><a href="about:license#cairo">Cairo Component Licenses</a></li> + <li><a href="about:license#chromium">Chromium License</a></li> + <li><a href="about:license#codemirror">CodeMirror License</a></li> + <li><a href="about:license#cubic-bezier">cubic-bezier License</a></li> + <li><a href="about:license#d3">D3 License</a></li> + <li><a href="about:license#dagre-d3">Dagre-D3 License</a></li> + <li><a href="about:license#dtoa">dtoa License</a></li> + <li><a href="about:license#hunspell-nl">Dutch Spellchecking Dictionary License</a></li> +#if defined(XP_WIN) || defined(XP_LINUX) + <li><a href="about:license#emojione">EmojiOne License</a></li> +#endif + <li><a href="about:license#hunspell-ee">Estonian Spellchecking Dictionary License</a></li> + <li><a href="about:license#expat">Expat License</a></li> + <li><a href="about:license#firebug">Firebug License</a></li> + <li><a href="about:license#gfx-font-list">gfxFontList License</a></li> + <li><a href="about:license#google-bsd">Google BSD License</a></li> + <li><a href="about:license#gears">Google Gears License</a></li> + <li><a href="about:license#gears-istumbler">Google Gears/iStumbler License</a></li> + <li><a href="about:license#vp8">Google VP8 License</a></li> + <li><a href="about:license#gyp">gyp License</a></li> + <li><a href="about:license#halloc">halloc License</a></li> + <li><a href="about:license#harfbuzz">HarfBuzz License</a></li> + <li><a href="about:license#icu">ICU License</a></li> + <li><a href="about:license#immutable">Immutable.js License</a></li> + <li><a href="about:license#jpnic">Japan Network Information Center License</a></li> + <li><a href="about:license#jemalloc">jemalloc License</a></li> + <li><a href="about:license#jquery">jQuery License</a></li> + <li><a href="about:license#k_exp">k_exp License</a></li> + <li><a href="about:license#khronos">Khronos group License</a></li> + <li><a href="about:license#kiss_fft">Kiss FFT License</a></li> +#ifdef MOZ_USE_LIBCXX + <li><a href="about:license#libc++">libc++ License</a></li> +#endif + <li><a href="about:license#libcubeb">libcubeb License</a></li> + <li><a href="about:license#libevent">libevent License</a></li> + <li><a href="about:license#libffi">libffi License</a></li> + <li><a href="about:license#libjingle">libjingle License</a></li> + <li><a href="about:license#libnestegg">libnestegg License</a></li> + <li><a href="about:license#libsoundtouch">libsoundtouch License</a></li> + <li><a href="about:license#libyuv">libyuv License</a></li> + <li><a href="about:license#hunspell-lt">Lithuanian Spellchecking Dictionary License</a></li> + <li><a href="about:license#microformatsshiv">MIT license — microformat-shiv</a></li> + <li><a href="about:license#myspell">MySpell License</a></li> + <li><a href="about:license#nicer">nICEr License</a></li> + <li><a href="about:license#node-properties">node-properties License</a></li> + <li><a href="about:license#nrappkit">nrappkit License</a></li> + <li><a href="about:license#openaes">OpenAES License</a></li> + <li><a href="about:license#openvision">OpenVision License</a></li> + <li><a href="about:license#pbkdf2-sha256">pbkdf2_sha256 License</a></li> +#ifdef MOZ_WEBSPEECH_POCKETSPHINX + <li><a href="about:license#pocketsphinx">Pocketsphinx License</a></li> +#endif + <li><a href="about:license#praton">praton License</a></li> + <li><a href="about:license#qcms">qcms License</a></li> + <li><a href="about:license#qrcode-generator">QR Code Generator License</a></li> + <li><a href="about:license#react">React License</a></li> + <li><a href="about:license#react-redux">React-Redux License</a></li> + <li><a href="about:license#react-virtualized">React Virtualized License</a></li> + <li><a href="about:license#xdg">Red Hat xdg_user_dir_lookup License</a></li> + <li><a href="about:license#redux">Redux License</a></li> + <li><a href="about:license#reselect">Reselect License</a></li> + <li><a href="about:license#hunspell-ru">Russian Spellchecking Dictionary License</a></li> + <li><a href="about:license#sctp">SCTP Licenses</a></li> + <li><a href="about:license#skia">Skia License</a></li> + <li><a href="about:license#snappy">Snappy License</a></li> + <li><a href="about:license#sprintf.js">sprintf.js License</a></li> + <li><a href="about:license#sunsoft">SunSoft License</a></li> + <li><a href="about:license#superfasthash">SuperFastHash License</a></li> + <li><a href="about:license#unicode">Unicode License</a></li> + <li><a href="about:license#ucal">University of California License</a></li> + <li><a href="about:license#hunspell-en-US">US English Spellchecking Dictionary Licenses</a></li> + <li><a href="about:license#v8">V8 License</a></li> +#if defined(XP_WIN) || defined(XP_MACOSX) || defined(XP_LINUX) + <li><a href="about:license#valve">Valve BSD License</a></li> +#endif + <li><a href="about:license#vtune">VTune License</a></li> + <li><a href="about:license#webrtc">WebRTC License</a></li> + <li><a href="about:license#x264">x264 License</a></li> + <li><a href="about:license#xiph">Xiph.org Foundation License</a></li> + </ul> + +<br> + + <ul> + <li><a href="about:license#other-notices">Other Required Notices</a> + <li><a href="about:license#optional-notices">Optional Notices</a> +#ifdef XP_WIN + <li><a href="about:license#proprietary-notices">Proprietary Operating System Components</a> +#endif + </ul> + + </div> + + <hr> + + <h1 id="mpl">Mozilla Public License 2.0</h1> + + <h2 id="definitions">1. Definitions</h2> + + <dl> + <dt>1.1. "Contributor"</dt> + + <dd> + <p>means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software.</p> + </dd> + + <dt>1.2. "Contributor Version"</dt> + + <dd> + <p>means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution.</p> + </dd> + + <dt>1.3. "Contribution"</dt> + + <dd> + <p>means Covered Software of a particular Contributor.</p> + </dd> + + <dt>1.4. "Covered Software"</dt> + + <dd> + <p>means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code Form, + and Modifications of such Source Code Form, in each case including + portions thereof.</p> + </dd> + + <dt>1.5. "Incompatible With Secondary Licenses"</dt> + + <dd> + <p>means</p> + + <ol type="a"> + <li> + <p>that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or</p> + </li> + + <li> + <p>that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms + of a Secondary License.</p> + </li> + </ol> + </dd> + + <dt>1.6. "Executable Form"</dt> + + <dd> + <p>means any form of the work other than Source Code Form.</p> + </dd> + + <dt>1.7. "Larger Work"</dt> + + <dd> + <p>means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software.</p> + </dd> + + <dt>1.8. "License"</dt> + + <dd> + <p>means this document.</p> + </dd> + + <dt>1.9. "Licensable"</dt> + + <dd> + <p>means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and all + of the rights conveyed by this License.</p> + </dd> + + <dt>1.10. "Modifications"</dt> + + <dd> + <p>means any of the following:</p> + + <ol type="a"> + <li> + <p>any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; + or</p> + </li> + + <li> + <p>any new file in Source Code Form that contains any Covered + Software.</p> + </li> + </ol> + </dd> + + <dt>1.11. "Patent Claims" of a Contributor</dt> + + <dd> + <p>means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version.</p> + </dd> + + <dt>1.12. "Secondary License"</dt> + + <dd> + <p>means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses.</p> + </dd> + + <dt>1.13. "Source Code Form"</dt> + + <dd> + <p>means the form of the work preferred for making modifications.</p> + </dd> + + <dt>1.14. "You" (or "Your")</dt> + + <dd> + <p>means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, + is controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, to + cause the direction or management of such entity, whether by contract + or otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity.</p> + </dd> + </dl> + + <h2 id="license-grants-and-conditions">2. License Grants and + Conditions</h2> + + <h3 id="grants">2.1. Grants</h3> + + <p>Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license:</p> + + <ol type="a"> + <li> + <p>under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and</p> + </li> + + <li> + <p>under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version.</p> + </li> + </ol> + + <h3 id="effective-date">2.2. Effective Date</h3> + + <p>The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution.</p> + + <h3 id="limitations-on-grant-scope">2.3. Limitations on Grant Scope</h3> + + <p>The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor:</p> + + <ol type="a"> + <li> + <p>for any code that a Contributor has removed from Covered Software; + or</p> + </li> + + <li> + <p>for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or</p> + </li> + + <li> + <p>under Patent Claims infringed by Covered Software in the absence of + its Contributions.</p> + </li> + </ol> + + <p>This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4).</p> + + <h3 id="subsequent-licenses">2.4. Subsequent Licenses</h3> + + <p>No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3).</p> + + <h3 id="representation">2.5. Representation</h3> + + <p>Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License.</p> + + <h3 id="fair-use">2.6. Fair Use</h3> + + <p>This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents.</p> + + <h3 id="conditions">2.7. Conditions</h3> + + <p>Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted + in Section 2.1.</p> + + <h2 id="responsibilities">3. Responsibilities</h2> + + <h3 id="distribution-of-source-form">3.1. Distribution of Source Form</h3> + + <p>All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients' rights in the Source Code Form.</p> + + <h3 id="distribution-of-executable-form">3.2. Distribution of Executable + Form</h3> + + <p>If You distribute Covered Software in Executable Form then:</p> + + <ol type="a"> + <li> + <p>such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code Form + by reasonable means in a timely manner, at a charge no more than the + cost of distribution to the recipient; and</p> + </li> + + <li> + <p>You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License.</p> + </li> + </ol> + + <h3 id="distribution-of-a-larger-work">3.3. Distribution of a Larger + Work</h3> + + <p>You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s).</p> + + <h3 id="notices">3.4. Notices</h3> + + <p>You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies.</p> + + <h3 id="application-of-additional-terms">3.5. Application of Additional + Terms</h3> + + <p>You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction.</p> + + <h2 id="inability-to-comply-due-to-statute-or-regulation">4. Inability to + Comply Due to Statute or Regulation</h2> + + <p>If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it.</p> + + <h2 id="termination">5. Termination</h2> + + <h3>5.1.</h3> + + <p>The rights granted under this License will terminate automatically + if You fail to comply with any of its terms. However, if You become + compliant, then the rights granted under this License from a particular + Contributor are reinstated (a) provisionally, unless and until such + Contributor explicitly and finally terminates Your grants, and (b) on an + ongoing basis, if such Contributor fails to notify You of the + non-compliance by some reasonable means prior to 60 days after You have + come back into compliance. Moreover, Your grants from a particular + Contributor are reinstated on an ongoing basis if such Contributor notifies + You of the non-compliance by some reasonable means, this is the first time + You have received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice.</p> + + <h3>5.2.</h3> + + <p>If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate.</p> + + <h3>5.3.</h3> + + <p>In the event of termination under Sections 5.1 or 5.2 above, all + end user license agreements (excluding distributors and resellers) which + have been validly granted by You or Your distributors under this License + prior to termination shall survive termination.</p> + + <h2 id="disclaimer-of-warranty">6. Disclaimer of Warranty</h2> + + <p><em>Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer.</em></p> + + <h2 id="limitation-of-liability">7. Limitation of Liability</h2> + + <p><em>Under no circumstances and under no legal theory, whether tort + (including negligence), contract, or otherwise, shall any Contributor, or + anyone who distributes Covered Software as permitted above, be liable to + You for any direct, indirect, special, incidental, or consequential damages + of any character including, without limitation, damages for lost profits, + loss of goodwill, work stoppage, computer failure or malfunction, or any + and all other commercial damages or losses, even if such party shall have + been informed of the possibility of such damages. This limitation of + liability shall not apply to liability for death or personal injury + resulting from such party's negligence to the extent applicable law + prohibits such limitation. Some jurisdictions do not allow the exclusion or + limitation of incidental or consequential damages, so this exclusion and + limitation may not apply to You.</em></p> + + <h2 id="litigation">8. Litigation</h2> + + <p>Any litigation relating to this License may be brought only in the + courts of a jurisdiction where the defendant maintains its principal place + of business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims.</p> + + <h2 id="miscellaneous">9. Miscellaneous</h2> + + <p>This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor.</p> + + <h2 id="versions-of-the-license">10. Versions of the License</h2> + + <h3 id="new-versions">10.1. New Versions</h3> + + <p>Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number.</p> + + <h3 id="effect-of-new-versions">10.2. Effect of New Versions</h3> + + <p>You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward.</p> + + <h3 id="modified-versions">10.3. Modified Versions</h3> + + <p>If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any references + to the name of the license steward (except to note that such modified + license differs from this License).</p> + + <h3 id= + "distributing-source-code-form-that-is-incompatible-with-secondary-licenses"> + 10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses</h3> + + <p>If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached.</p> + + <h2 id="exhibit-a---source-code-form-license-notice">Exhibit A - Source + Code Form License Notice</h2> + + <blockquote> + <p>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/.</p> + </blockquote> + + <p>If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE file + in a relevant directory) where a recipient would be likely to look for such + a notice.</p> + + <p>You may add additional accurate notices of copyright ownership.</p> + + <h2 id="exhibit-b---incompatible-with-secondary-licenses-notice">Exhibit B + - "Incompatible With Secondary Licenses" Notice</h2> + + <blockquote> + <p>This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0.</p> + </blockquote> + + + <hr> + + <h1 id="lgpl">GNU Lesser General Public License 2.1</h1> + +<p>This product contains code from the following LGPLed libraries:</p> + +<ul> +<li><a href="http://www.surina.net/soundtouch/">libsoundtouch</a> +<li><a href="http://libav.org/">libav</a> +<li><a href="http://ffmpeg.org/">FFmpeg</a> +</ul> + +<pre> +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] +</pre> + +<h3><a id="SEC2">Preamble</a></h3> + +<p> + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. +</p> +<p> + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. +</p> +<p> + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. +</p> +<p> + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. +</p> +<p> + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. +</p> +<p> + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. +</p> +<p> + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. +</p> +<p> + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. +</p> +<p> + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. +</p> +<p> + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. +</p> +<p> + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. +</p> +<p> + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. +</p> +<p> + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. +</p> +<p> + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. +</p> +<p> + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. +</p> + +<h3><a id="SEC3">TERMS AND CONDITIONS FOR COPYING, +DISTRIBUTION AND MODIFICATION</a></h3> + + +<p> +<strong>0.</strong> +This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". +</p> +<p> + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. +</p> +<p> + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) +</p> +<p> + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. +</p> +<p> + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. +</p> +<p> +<strong>1.</strong> +You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. +</p> +<p> + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. +</p> +<p> +<strong>2.</strong> +You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: +</p> + +<ul> + <li><strong>a)</strong> + The modified work must itself be a software library.</li> + <li><strong>b)</strong> + You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change.</li> + + <li><strong>c)</strong> + You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License.</li> + + <li><strong>d)</strong> + If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + <p> + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.)</p></li> +</ul> + +<p> +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be +reasonably considered independent and separate works in themselves, then +this License, and its terms, do not apply to those sections when you +distribute them as separate works. But when you distribute the same +sections as part of a whole which is a work based on the Library, the +distribution of the whole must be on the terms of this License, whose +permissions for other licensees extend to the entire whole, and thus to +each and every part regardless of who wrote it. +</p> +<p> +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works +based on the Library. +</p> +<p> +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of +this License. +</p> +<p> +<strong>3.</strong> +You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. +</p> +<p> + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. +</p> +<p> + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. +</p> +<p> +<strong>4.</strong> +You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. +</p> +<p> + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. +</p> +<p> +<strong>5.</strong> +A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. +</p> +<p> + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. +</p> +<p> + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. +</p> +<p> + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) +</p> +<p> + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. +</p> +<p> +<strong>6.</strong> +As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. +</p> +<p> + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: +</p> + +<ul> + <li><strong>a)</strong> Accompany the work with the complete + corresponding machine-readable source code for the Library + including whatever changes were used in the work (which must be + distributed under Sections 1 and 2 above); and, if the work is an + executable linked with the Library, with the complete + machine-readable "work that uses the Library", as object code + and/or source code, so that the user can modify the Library and + then relink to produce a modified executable containing the + modified Library. (It is understood that the user who changes the + contents of definitions files in the Library will not necessarily + be able to recompile the application to use the modified + definitions.)</li> + + <li><strong>b)</strong> Use a suitable shared library mechanism + for linking with the Library. A suitable mechanism is one that + (1) uses at run time a copy of the library already present on the + user's computer system, rather than copying library functions into + the executable, and (2) will operate properly with a modified + version of the library, if the user installs one, as long as the + modified version is interface-compatible with the version that the + work was made with.</li> + + <li><strong>c)</strong> Accompany the work with a written offer, + valid for at least three years, to give the same user the + materials specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution.</li> + + <li><strong>d)</strong> If distribution of the work is made by + offering access to copy from a designated place, offer equivalent + access to copy the above specified materials from the same + place.</li> + + <li><strong>e)</strong> Verify that the user has already received + a copy of these materials or that you have already sent this user + a copy.</li> +</ul> + +<p> + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. +</p> +<p> + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. +</p> +<p> +<strong>7.</strong> You may place library facilities that are a work +based on the Library side-by-side in a single library together with +other library facilities not covered by this License, and distribute +such a combined library, provided that the separate distribution of +the work based on the Library and of the other library facilities is +otherwise permitted, and provided that you do these two things: +</p> + +<ul> + <li><strong>a)</strong> Accompany the combined library with a copy + of the same work based on the Library, uncombined with any other + library facilities. This must be distributed under the terms of + the Sections above.</li> + + <li><strong>b)</strong> Give prominent notice with the combined + library of the fact that part of it is a work based on the + Library, and explaining where to find the accompanying uncombined + form of the same work.</li> +</ul> + +<p> +<strong>8.</strong> You may not copy, modify, sublicense, link with, +or distribute the Library except as expressly provided under this +License. Any attempt otherwise to copy, modify, sublicense, link +with, or distribute the Library is void, and will automatically +terminate your rights under this License. However, parties who have +received copies, or rights, from you under this License will not have +their licenses terminated so long as such parties remain in full +compliance. +</p> +<p> +<strong>9.</strong> +You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. +</p> +<p> +<strong>10.</strong> +Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. +</p> +<p> +<strong>11.</strong> +If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. +</p> +<p> +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. +</p> +<p> +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. +</p> +<p> +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. +</p> +<p> +<strong>12.</strong> +If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. +</p> +<p> +<strong>13.</strong> +The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. +</p> +<p> +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. +</p> +<p> +<strong>14.</strong> +If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. +</p> +<p> +<strong>NO WARRANTY</strong> +</p> +<p> +<strong>15.</strong> +BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. +</p> +<p> +<strong>16.</strong> +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. +</p> + + <hr> + + <h1 id="lgpl-3.0">GNU Lesser General Public License 3.0</h1> + +<p>Some versions of this product contains code from the following LGPLed libraries:</p> + +<ul> +<li><a +href="https://addons.mozilla.org/en-US/firefox/addon/görans-hemmasnickrade-ordli/">Swedish dictionary</a> +</ul> + +<pre>Copyright © 2007 Free Software Foundation, Inc. + <<a href="http://fsf.org/">http://fsf.org/</a>> + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed.</pre> + +<p>This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below.</p> + +<h3><a id="section0">0. Additional Definitions</a></h3> + +<p>As used herein, “this License” refers to version 3 of the GNU Lesser +General Public License, and the “GNU GPL” refers to version 3 of the GNU +General Public License.</p> + +<p>“The Library” refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below.</p> + +<p>An “Application” is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library.</p> + +<p>A “Combined Work” is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the “Linked +Version”.</p> + +<p>The “Minimal Corresponding Source” for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version.</p> + +<p>The “Corresponding Application Code” for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work.</p> + +<h3><a id="section1">1. Exception to Section 3 of the GNU GPL.</a></h3> + +<p>You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL.</p> + +<h3><a id="section2">2. Conveying Modified Versions.</a></h3> + +<p>If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version:</p> + +<ul> +<li>a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or</li> + +<li>b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy.</li> +</ul> + +<h3><a id="section3">3. Object Code Incorporating Material from Library Header Files.</a></h3> + +<p>The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following:</p> + +<ul> +<li>a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License.</li> + +<li>b) Accompany the object code with a copy of the GNU GPL and this license + document.</li> +</ul> + +<h3><a id="section4">4. Combined Works.</a></h3> + +<p>You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following:</p> + +<ul> +<li>a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License.</li> + +<li>b) Accompany the Combined Work with a copy of the GNU GPL and this license + document.</li> + +<li>c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document.</li> + +<li>d) Do one of the following: + +<ul> +<li>0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source.</li> + +<li>1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version.</li> +</ul></li> + +<li>e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.)</li> +</ul> + +<h3><a id="section5">5. Combined Libraries.</a></h3> + +<p>You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following:</p> + +<ul> +<li>a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License.</li> + +<li>b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work.</li> +</ul> + +<h3><a id="section6">6. Revised Versions of the GNU Lesser General Public License.</a></h3> + +<p>The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns.</p> + +<p>Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License “or any later version” +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation.</p> + +<p>If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library.</p> + + + <hr> + + + <h1 id="gpl-3.0">GNU General Public License 3.0</h1> + + <p>This license does not apply to any of the code shipped with + Firefox, but may apply to Disconnect.me blocklists downloaded + after installation for use with the tracking protection feature. + Firefox and such blocklists are separate and independent works as + described in Sections 5 and 6 of this license.</p> + +<pre>Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. +<<a href="http://fsf.org/">http://fsf.org/</a>> + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed.</pre> + +<h2><a name="preamble"></a>Preamble</h2> + +<p>The GNU General Public License is a free, copyleft license for +software and other kinds of works.</p> + +<p>The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too.</p> + +<p>When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things.</p> + +<p>To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others.</p> + +<p>For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights.</p> + +<p>Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it.</p> + +<p>For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions.</p> + +<p>Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users.</p> + +<p>Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free.</p> + +<p>The precise terms and conditions for copying, distribution and +modification follow.</p> + +<h2><a name="terms"></a>TERMS AND CONDITIONS</h2> + +<h3><a name="section0"></a>0. Definitions.</h3> + +<p>“This License” refers to version 3 of the GNU General Public License.</p> + +<p>“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks.</p> + +<p>“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations.</p> + +<p>To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work.</p> + +<p>A “covered work” means either the unmodified Program or a work based +on the Program.</p> + +<p>To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well.</p> + +<p>To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying.</p> + +<p>An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion.</p> + +<h3><a name="section1"></a>1. Source Code.</h3> + +<p>The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work.</p> + +<p>A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language.</p> + +<p>The “System Libraries” of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it.</p> + +<p>The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work.</p> + +<p>The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source.</p> + +<p>The Corresponding Source for a work in source code form is that +same work.</p> + +<h3><a name="section2"></a>2. Basic Permissions.</h3> + +<p>All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law.</p> + +<p>You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you.</p> + +<p>Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary.</p> + +<h3><a name="section3"></a>3. Protecting Users' Legal Rights From Anti-Circumvention Law.</h3> + +<p>No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures.</p> + +<p>When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures.</p> + +<h3><a name="section4"></a>4. Conveying Verbatim Copies.</h3> + +<p>You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program.</p> + +<p>You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee.</p> + +<h3><a name="section5"></a>5. Conveying Modified Source Versions.</h3> + +<p>You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions:</p> + +<ul> +<li>a) The work must carry prominent notices stating that you modified + it, and giving a relevant date.</li> + +<li>b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + “keep intact all notices”.</li> + +<li>c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it.</li> + +<li>d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so.</li> +</ul> + +<p>A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate.</p> + +<h3><a name="section6"></a>6. Conveying Non-Source Forms.</h3> + +<p>You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways:</p> + +<ul> +<li>a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange.</li> + +<li>b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge.</li> + +<li>c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b.</li> + +<li>d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements.</li> + +<li>e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d.</li> +</ul> + +<p>A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work.</p> + +<p>A “User Product” is either (1) a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product.</p> + +<p>“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made.</p> + +<p>If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM).</p> + +<p>The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network.</p> + +<p>Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying.</p> + +<h3><a name="section7"></a>7. Additional Terms.</h3> + +<p>“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions.</p> + +<p>When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission.</p> + +<p>Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms:</p> + +<ul> +<li>a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or</li> + +<li>b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or</li> + +<li>c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or</li> + +<li>d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or</li> + +<li>e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or</li> + +<li>f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors.</li> +</ul> + +<p>All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying.</p> + +<p>If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms.</p> + +<p>Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way.</p> + +<h3><a name="section8"></a>8. Termination.</h3> + +<p>You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11).</p> + +<p>However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation.</p> + +<p>Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice.</p> + +<p>Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10.</p> + +<h3><a name="section9"></a>9. Acceptance Not Required for Having Copies.</h3> + +<p>You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so.</p> + +<h3><a name="section10"></a>10. Automatic Licensing of Downstream Recipients.</h3> + +<p>Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License.</p> + +<p>An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts.</p> + +<p>You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it.</p> + +<h3><a name="section11"></a>11. Patents.</h3> + +<p>A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”.</p> + +<p>A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License.</p> + +<p>Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version.</p> + +<p>In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party.</p> + +<p>If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid.</p> + +<p>If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it.</p> + +<p>A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007.</p> + +<p>Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law.</p> + +<h3><a name="section12"></a>12. No Surrender of Others' Freedom.</h3> + +<p>If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program.</p> + +<h3><a name="section13"></a>13. Use with the GNU Affero General Public License.</h3> + +<p>Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such.</p> + +<h3><a name="section14"></a>14. Revised Versions of this License.</h3> + +<p>The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns.</p> + +<p>Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation.</p> + +<p>If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program.</p> + +<p>Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version.</p> + +<h3><a name="section15"></a>15. Disclaimer of Warranty.</h3> + +<p>THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION.</p> + +<h3><a name="section16"></a>16. Limitation of Liability.</h3> + +<p>IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES.</p> + +<h3><a name="section17"></a>17. Interpretation of Sections 15 and 16.</h3> + +<p>If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee.</p> + +<p>END OF TERMS AND CONDITIONS</p> + +<h2><a name="howto"></a>How to Apply These Terms to Your New Programs</h2> + +<p>If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms.</p> + +<p>To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found.</p> + +<pre> <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +</pre> + +<p>Also add information on how to contact you by electronic and paper mail.</p> + +<p>If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode:</p> + +<pre> <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. +</pre> + +<p>The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an “about box”.</p> + +<p>You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<<a href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>>.</p> + +<p>The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<<a href="http://www.gnu.org/philosophy/why-not-lgpl.html">http://www.gnu.org/philosophy/why-not-lgpl.html</a>>.</p> + +<hr> + + <h1><a id="ACE"></a>ACE License</h1> + + <p>This license applies to the file + <span class="path">media/webrtc/trunk/webrtc/system_wrappers/source/condition_variable_event_win.cc</span>.</p> + +<pre> +ACE(TM), TAO(TM), CIAO(TM), DAnCE(TM), and CoSMIC(TM) +(henceforth referred to as "DOC software") are copyrighted by +Douglas C. Schmidt and his research group at Washington University, +University of California, Irvine, and Vanderbilt University, +Copyright (c) 1993-2009, all rights reserved. +Since DOC software is open-source, freely available software, +you are free to use, modify, copy, and distribute--perpetually and +irrevocably--the DOC software source code and object code produced +from the source, as well as copy and distribute modified versions of +this software. You must, however, include this copyright statement +along with any code built using DOC software that you release. No +copyright statement needs to be provided if you just ship binary +executables of your software products. +</pre> + + + <hr> + + <h1><a id="adobecmap"></a>Adobe CMap License</h1> + + <p>This license applies to files in the directory + <span class="path">browser/extensions/pdfjs/content/web/cmaps/</span>.</p> + +<pre> +Copyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the +following conditions are met: + +Redistributions of source code must retain the above +copyright notice, this list of conditions and the following +disclaimer. + +Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials +provided with the distribution. + +Neither the name of Adobe Systems Incorporated nor the names +of its contributors may be used to endorse or promote +products derived from this software without specific prior +written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="android"></a>Android Open Source License</h1> + + <p>This license applies to various files in the Mozilla codebase, + including those in the directory <span class="path">gfx/skia/</span>.</p> +<!-- This is the wrong directory, what was intended? --> + +<pre> + Copyright 2009, The Android Open Source Project + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="angle"></a>ANGLE License</h1> + + <p>This license applies to files in the directory <span class="path">gfx/angle/</span>.</p> + +<pre> +Copyright (C) 2002-2010 The ANGLE Project Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + Neither the name of TransGaming Inc., Google Inc., 3DLabs Inc. + Ltd., nor the names of their contributors may be used to endorse + or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="acorn"></a>acorn License</h1> + + <p>This license applies to all files in + <span class="path">devtools/shared/acorn</span>. + </p> +<pre> +Copyright (C) 2012 by Marijn Haverbeke <marijnh@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Please note that some subdirectories of the CodeMirror distribution +include their own LICENSE files, and are released under different +licences. +</pre> + + + <hr> + +#ifdef MOZ_INSTALL_TRACKING + <h1><a id="adjust"></a>Adjust SDK License</h1> + + <p>This license applies to all files in the directory + <span class="path">mobile/android/thirdparty/com/adjust/sdk</span>.</p> + +<pre> +Copyright (c) 2012-2014 adjust GmbH, +http://www.adjust.com + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + <hr> + +#endif + <h1><a id="apache"></a>Apache License 2.0</h1> + + <p>This license applies to various files in the Mozilla codebase.</p> + +<pre> + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +</pre> + + + + <hr> + + <h1><a id="apple"></a>Apple License</h1> + + <p>This license applies to certain files in the directories <span class="path">dom/media/webaudio/blink</span>, and <span class="path">widget/cocoa</span>.</p> + +<pre> +Copyright (C) 2008, 2009 Apple Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="apple-mozilla"></a>Apple/Mozilla NPRuntime License</h1> + + <p>This license applies to the file + <span class="path">dom/plugins/base/npruntime.h</span>.</p> + +<pre> +Copyright © 2004, Apple Computer, Inc. and The Mozilla Foundation. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. Neither the names of Apple Computer, Inc. ("Apple") or The Mozilla +Foundation ("Mozilla") nor the names of their contributors may be used +to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY APPLE, MOZILLA AND THEIR CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE, MOZILLA OR +THEIR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="arm"></a>ARM License</h1> + + <p>This license applies to files in the directory <span class="path">js/src/jit/arm64/vixl/</span>.</p> + +<pre> +Copyright 2013, ARM Limited +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of ARM Limited nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="bspatch"></a>bspatch License</h1> + + <p>This license applies to the files + <span class="path">toolkit/mozapps/update/updater/bspatch.cpp</span> and + <span class="path">toolkit/mozapps/update/updater/bspatch.h</span>. + </p> + +<pre> +Copyright 2003,2004 Colin Percival +All rights reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted providing that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="cairo"></a>Cairo Component Licenses</h1> + + <p>This license, with different copyright holders, applies to certain files + in the directory <span class="path">gfx/cairo/</span>. The copyright + holders and the applicable ranges of dates are as follows: + + <ul> +<li>2004 Richard D. Worth +<li>2004, 2005 Red Hat, Inc. +<li>2003 USC, Information Sciences Institute +<li>2004 David Reveman +<li>2005 Novell, Inc. +<li>2004 David Reveman, Peter Nilsson +<li>2000 Keith Packard, member of The XFree86 Project, Inc. +<li>2005 Lars Knoll & Zack Rusin, Trolltech +<li>1998, 2000, 2002, 2004 Keith Packard +<li>2004 Nicholas Miell +<li>2005 Trolltech AS +<li>2000 SuSE, Inc. +<li>2003 Carl Worth +<li>1987, 1988, 1989, 1998 The Open Group +<li>1987, 1988, 1989 Digital Equipment Corporation, Maynard, Massachusetts. +<li>1998 Keith Packard +<li>2003 Richard Henderson + </ul> + +<pre> +Copyright © <date> <copyright holder> + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without +fee, provided that the above copyright notice appear in all copies +and that both that copyright notice and this permission notice +appear in supporting documentation, and that the name of +<copyright holder> not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior permission. +<copyright holder> makes no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +<COPYRIGHT HOLDER> DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + + <hr> + + <h1><a id="chromium"></a>Chromium License</h1> + + <p>This license applies to parts of the code in:</p> + <ul> + <li><span class="path">editor/libeditor/EditorEventListener.cpp</span></li> + <li><span class="path">security/sandbox/</span></li> + <li><span class="path">widget/cocoa/GfxInfo.mm</span></li> + </ul> + <p>and also some files in these directories:</p> + <ul> + <li><span class="path">dom/media/webspeech/recognition/</span></li> + <li><span class="path">dom/plugins/</span></li> + <li><span class="path">gfx/ots/</span></li> + <li><span class="path">gfx/ycbcr/</span></li> + <li><span class="path">ipc/chromium/</span></li> + <li><span class="path">media/openmax_dl/</span></li> + <li><span class="path">toolkit/components/downloads/chromium/</span></li> + <li><span class="path">toolkit/components/url-classifier/chromium/</span></li> + <li><span class="path">tools/profiler/</span></li> + </ul> + +<pre> +Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="codemirror"></a>CodeMirror License</h1> + + <p>This license applies to all files in + <span class="path">devtools/client/sourceeditor/codemirror</span> and + to specified files in the <span class="path">devtools/client/sourceeditor/test/</span>: + </p> + <ul> + <li><span class="path">cm_comment_test.js</span></li> + <li><span class="path">cm_driver.js</span></li> + <li><span class="path">cm_mode_javascript_test.js</span></li> + <li><span class="path">cm_mode_test.css</span></li> + <li><span class="path">cm_mode_test.js</span></li> + <li><span class="path">cm_test.js</span></li> + </ul> +<pre> +Copyright (C) 2013 by Marijn Haverbeke <marijnh@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Please note that some subdirectories of the CodeMirror distribution +include their own LICENSE files, and are released under different +licences. +</pre> + + + <hr> + + <h1><a id="cubic-bezier"></a>cubic-bezier License</h1> + + <p>This license applies to the file + <span class="path">devtools/client/shared/widgets/CubicBezierWidget.js + </span>.</p> +<pre> +Copyright (c) 2013 Lea Verou. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="d3"></a>D3 License</h1> + + <p>This license applies to the file + <span class="path">devtools/client/shared/d3.js</span>. + </p> +<pre> +Copyright (c) 2014, Michael Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="dagre-d3"></a>Dagre-D3 License</h1> + + <p>This license applies to the file + <span class="path">devtools/client/webaudioeditor/lib/dagre-d3.js</span>. + </p> +<pre> +Copyright (c) 2013 Chris Pettitt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="dtoa"></a>dtoa License</h1> + + <p>This license applies to the file + <span class="path">nsprpub/pr/src/misc/dtoa.c</span>.</p> + +<pre> +The author of this software is David M. Gay. + +Copyright (c) 1991, 2000, 2001 by Lucent Technologies. + +Permission to use, copy, modify, and distribute this software for any +purpose without fee is hereby granted, provided that this entire notice +is included in all copies of any software which is or includes a copy +or modification of this software and in all copies of the supporting +documentation for such software. + +THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED +WARRANTY. IN PARTICULAR, NEITHER THE AUTHOR NOR LUCENT MAKES ANY +REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY +OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. +</pre> + + + <hr> + + <h1><a id="hunspell-nl"></a>Dutch Spellchecking Dictionary License</h1> + + <p>This license applies to the Dutch Spellchecking Dictionary. (This + code only ships in some localized versions of this product.)</p> + +<pre> +Copyright (c) 2006, 2007 OpenTaal +Copyright (c) 2001, 2002, 2003, 2005 Simon Brouwer e.a. +Copyright (c) 1996 Nederlandstalige Tex Gebruikersgroep + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. +* Neither the name of the OpenTaal, Simon Brouwer e.a., or Nederlandstalige Tex +Gebruikersgroep nor the names of its contributors may be used to endorse or +promote products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + +#if defined(XP_WIN) || defined(XP_LINUX) + <h1><a id="emojione"></a>EmojiOne License</h1> + + <p>This license applies to the emoji art contained within the bundled +emoji font file.</p> + +<pre> +Copyright (c) 2016 Ranks.com Inc. +Copyright (c) 2014 Twitter, Inc and other contributors. + +Creative Commons Attribution 4.0 International (CC BY 4.0) + +See https://creativecommons.org/licenses/by/4.0/legalcode or +for the human readable summary: https://creativecommons.org/licenses/by/4.0/ + +You are free to: + +Share — copy and redistribute the material in any medium or format + +Adapt — remix, transform, and build upon the material for any purpose, even commercially. + +The licensor cannot revoke these freedoms as long as you follow the license terms. + +Under the following terms: + +Attribution — You must give appropriate credit, provide a link to the license, +and indicate if changes were made. You may do so in any reasonable manner, +but not in any way that suggests the licensor endorses you or your use. + +No additional restrictions — You may not apply legal terms or technological +measures that legally restrict others from doing anything the license permits. + +Notices: + +You do not have to comply with the license for elements of the material in +the public domain or where your use is permitted by an applicable exception or +limitation. No warranties are given. The license may not give you all of the +permissions necessary for your intended use. For example, other rights such as +publicity, privacy, or moral rights may limit how you use the material. +</pre> + + + <hr> + +#endif + <h1><a id="hunspell-ee"></a>Estonian Spellchecking Dictionary License</h1> + + <p>This license applies to precursor works to certain files which are + part of the Estonian Spellchecking Dictionary. The + shipped versions are under the GNU Lesser General Public License. (This + code only ships in some localized versions of this product.)</p> + +<pre> +Copyright © Institute of the Estonian Language + +E-mail: litsents@eki.ee +URL: http://www.eki.ee/tarkvara/ + +The present Licence Agreement gives the user of this Software Product +(hereinafter: Product) the right to use the Product for whatever purpose +(incl. distribution, copying, altering, inclusion in other software, and +selling) on the following conditions: + +1. The present Licence Agreement should belong unaltered to each copy ever + made of this Product; +2. Neither the Institute of the Estonian Language (hereinafter: IEL) nor the + author(s) of the Product will take responsibility for any detriment, direct + or indirect, possibly ensuing from the application of the Product; +3. The IEL is ready to share the Product with other users as we wish to + advance research on the Estonian language and to promote the use of + Estonian in rapidly developing infotechnology, yet we refuse to bind + ourselves to any further obligation, which means that the IEL is not + obliged either to warrant the suitability of the Product for a specific + purpose, to improve the software, or to provide a more detailed description + of the underlying algorithms. (Which does not mean, though, that we may not + do it.) + +Notification Request: + +As a courtesy, we would appreciate being informed whenever our linguistic +products are used to create derivative works. If you modify our software or +include it in other products, please inform us by sending e-mail to +litsents@eki.ee or by letter to + +Institute of the Estonian Language +Roosikrantsi 6 +10119 Tallinn +ESTONIA + +Phone & Fax: +372 6411443 +</pre> + + + + <hr> + + <h1><a id="expat"></a>Expat License</h1> + + <p>This license applies to certain files in the directory + <span class="path">parser/expat/</span>.</p> + +<pre> +Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd + and Clark Cooper +Copyright (c) 2001, 2002, 2003 Expat maintainers. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + + + <hr> + + + <h1><a id="firebug"></a>Firebug License</h1> + + <p>This license applies to the code + <span class="path">devtools/shared/webconsole/network-helper.js</span>.</p> + +<pre> +Copyright (c) 2007, Parakey Inc. +All rights reserved. + +Redistribution and use of this software in source and binary forms, with or +without modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of Parakey Inc. nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of Parakey Inc. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + + <hr> + + <h1><a id="gfx-font-list"></a>gfxFontList License</h1> + + <p>This license applies to the files + <span class="path">gfx/thebes/gfxMacPlatformFontList.mm</span> and + <span class="path">gfx/thebes/gfxPlatformFontList.cpp</span>. + </p> + +<pre> +Copyright (C) 2006 Apple Computer, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + + <hr> + + <h1><a id="google-bsd"></a>Google BSD License</h1> + + <p>This license applies to files in the directories + <span class="path">toolkit/crashreporter/google-breakpad/</span> and + <span class="path">toolkit/components/protobuf/</span>.</p> + +<pre> +Copyright (c) 2006, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="vp8"></a>Google VP8 License</h1> + + <p>This license applies to certain files in the directory + <span class="path">media/libvpx</span>.</p> +<pre> +Copyright (c) 2010, Google, Inc. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +- Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Subject to the terms and conditions of the above License, Google +hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this +section) patent license to make, have made, use, offer to sell, sell, +import, and otherwise transfer this implementation of VP8, where such +license applies only to those patent claims, both currently owned by +Google and acquired in the future, licensable by Google that are +necessarily infringed by this implementation of VP8. If You or your +agent or exclusive licensee institute or order or agree to the +institution of patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that this +implementation of VP8 or any code incorporated within this +implementation of VP8 constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any rights +granted to You under this License for this implementation of VP8 +shall terminate as of the date such litigation is filed. +</pre> + + <hr> + + <h1><a id="gears-istumbler"></a>Google Gears/iStumbler License</h1> + + <p>This license applies to the file + <span class="path">netwerk/wifi/osx_wifi.h</span>.</p> + +<pre> +Copyright 2008, Google Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of Google Inc. nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The contents of this file are taken from Apple80211.h from the iStumbler +project (http://www.istumbler.net). This project is released under the BSD +license with the following restrictions. + +Copyright (c) 02006, Alf Watt (alf@istumbler.net). All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of iStumbler nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="gyp"></a>gyp License</h1> + + <p>This license applies to certain files in the directory + <span class="path">media/webrtc/trunk/tools/gyp</span>.</p> +<pre> +Copyright (c) 2009 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="halloc"></a>halloc License</h1> + + <p>This license applies to certain files in the directory + <span class="path">media/libnestegg/src</span>.</p> +<pre> +Copyright (c) 2004-2010 Alex Pankratov. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the project nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="harfbuzz"></a>HarfBuzz License</h1> + + <p>This license, with different copyright holders, applies to the files in + the directory <span class="path">gfx/harfbuzz/</span>. + The copyright holders and the applicable ranges of dates are as follows:</p> + + <ul> + <li>1998-2004 David Turner and Werner Lemberg</li> + <li>2004, 2007, 2008, 2009, 2010 Red Hat, Inc.</li> + <li>2006 Behdad Esfahbod</li> + <li>2007 Chris Wilson</li> + <li>2009 Keith Stribley <devel@thanlwinsoft.org></li> + <li>2010 Mozilla Foundation</li> + </ul> + +<pre> +Copyright (C) <date> <copyright holder> + + This is part of HarfBuzz, an OpenType Layout engine library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +</pre> + + + <hr> + + <h1><a id="icu"></a>ICU License</h1> + + <p>This license applies to some code in the + <span class="path">gfx/thebes</span> directory.</p> + +<pre> +ICU License - ICU 1.8.1 and later + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1995-2012 International Business Machines Corporation and +others + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, provided that the above copyright notice(s) and this +permission notice appear in all copies of the Software and that both the +above copyright notice(s) and this permission notice appear in supporting +documentation. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization of the +copyright holder. +All trademarks and registered trademarks mentioned herein are the property +of their respective owners. +</pre> + <hr> + <h1><a id="immutable"></a>Immutable.js License</h1> + +<pre> +BSD License + +For Immutable JS software + +Copyright (c) 2014-2015, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="jpnic"></a>Japan Network Information Center License</h1> + <p>This license applies to certain files in the + directory <span class="path">netwerk/dns/</span>.</p> +<pre> +Copyright (c) 2001,2002 Japan Network Information Center. +All rights reserved. + +By using this file, you agree to the terms and conditions set forth below. + + LICENSE TERMS AND CONDITIONS + +The following License Terms and Conditions apply, unless a different +license is obtained from Japan Network Information Center ("JPNIC"), +a Japanese association, Kokusai-Kougyou-Kanda Bldg 6F, 2-3-4 Uchi-Kanda, +Chiyoda-ku, Tokyo 101-0047, Japan. + +1. Use, Modification and Redistribution (including distribution of any + modified or derived work) in source and/or binary forms is permitted + under this License Terms and Conditions. + +2. Redistribution of source code must retain the copyright notices as they + appear in each source code file, this License Terms and Conditions. + +3. Redistribution in binary form must reproduce the Copyright Notice, + this License Terms and Conditions, in the documentation and/or other + materials provided with the distribution. For the purposes of binary + distribution the "Copyright Notice" refers to the following language: + "Copyright (c) 2000-2002 Japan Network Information Center. All rights + reserved." + +4. The name of JPNIC may not be used to endorse or promote products + derived from this Software without specific prior written approval of + JPNIC. + +5. Disclaimer/Limitation of Liability: THIS SOFTWARE IS PROVIDED BY JPNIC + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JPNIC BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +</pre> + + <hr> + + <h1><a id="jemalloc"></a>jemalloc License</h1> + + <p>This license applies to files in the directories + <span class="path">memory/mozjemalloc/</span> and + <span class="path">memory/jemalloc/</span>. + </p> + +<pre> +Copyright (C) 2002-2012 Jason Evans <jasone@canonware.com>. +All rights reserved. +Copyright (C) 2007-2012 Mozilla Foundation. All rights reserved. +Copyright (C) 2009-2012 Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice(s), this list of conditions and the following disclaimer as + the first lines of this file unmodified other than the possible + addition of one or more copyright notices. +2. Redistributions in binary form must reproduce the above copyright + notice(s), this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="jquery"></a>jQuery License</h1> + + <p>This license applies to all copies of jQuery in the code.</p> + +<pre> +Copyright (c) 2010 John Resig, http://jquery.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + <hr> + + <h1><a id="k_exp"></a>k_exp License</h1> + + <p>This license applies to the file + <span class="path">modules/fdlibm/src/k_exp.cpp</span>. + </p> + +<pre> +Copyright (c) 2011 David Schultz <das@FreeBSD.ORG> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="khronos"></a>Khronos group License</h1> + + <p>This license applies to the following files:</p> + + <ul> + <li class="path">openmax_dl/dl/api/omxtypes.h</li> + <li class="path">openmax_dl/dl/sp/api/omxSP.h</li> + </ul> + +<pre> +Copyright 2005-2008 The Khronos Group Inc. All Rights Reserved. + +These materials are protected by copyright laws and contain material +proprietary to the Khronos Group, Inc. You may use these materials +for implementing Khronos specifications, without altering or removing +any trademark, copyright or other notice from the specification. + +Khronos Group makes no, and expressly disclaims any, representations +or warranties, express or implied, regarding these materials, including, +without limitation, any implied warranties of merchantability or fitness +for a particular purpose or non-infringement of any intellectual property. +Khronos Group makes no, and expressly disclaims any, warranties, express +or implied, regarding the correctness, accuracy, completeness, timeliness, +and reliability of these materials. + +Under no circumstances will the Khronos Group, or any of its Promoters, +Contributors or Members or their respective partners, officers, directors, +employees, agents or representatives be liable for any damages, whether +direct, indirect, special or consequential damages for lost revenues, +lost profits, or otherwise, arising from or in connection with these +materials. + +Khronos and OpenMAX are trademarks of the Khronos Group Inc. +</pre> + + <hr> + + <h1><a id="kiss_fft"></a>Kiss FFT License</h1> + + <p>This license applies to files in the directory + <span class="path">media/kiss_fft/</span>.</p> + +<pre> +Copyright (c) 2003-2010 Mark Borgerding + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the author nor the names of any contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + +#ifdef MOZ_USE_LIBCXX + <h1><a id="libc++"></a>libc++ License</h1> + + <p class="correctme">This license applies to the copy of libc++ obtained + from the Android NDK.</p> + +<pre> +Copyright (c) 2009-2014 by the contributors listed in the libc++ CREDITS.TXT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + <hr> + +#endif + + <h1><a id="libcubeb"></a>libcubeb License</h1> + + <p class="correctme">This license applies to files in the directory + <span class="path">media/libcubeb</span>. + </p> + +<pre> +Copyright © 2011 Mozilla Foundation + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + <hr> + + <h1><a id="libevent"></a>libevent License</h1> + + <p>This license applies to files in the directory + <span class="path">ipc/chromium/src/third_party/libevent/</span>. + </p> + +<pre> +Copyright 2000-2002 Niels Provos <provos@citi.umich.edu> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="libffi"></a>libffi License</h1> + + <p>This license applies to files in the directory + <span class="path">js/src/ctypes/libffi/</span>. + </p> + +<pre> +libffi - Copyright (c) 1996-2008 Red Hat, Inc and others. +See source files for details. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +``Software''), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="libjingle"></a>libjingle License</h1> + + <p>This license applies to the following files:</p> + <ul> + <li class="path">media/mtransport/sigslot.h</li> + <li class="path">media/mtransport/test/gtest_utils.h</li> + </ul> + +<pre> +Copyright (c) 2004--2005, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="libnestegg"></a>libnestegg License</h1> + + <p>This license applies to certain files in the directory + <span class="path">media/libnestegg</span>. + </p> + +<pre> +Copyright © 2010 Mozilla Foundation + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + <hr> + + <h1><a id="libsoundtouch"></a>libsoundtouch License</h1> + + <p>This license applies to certain files in the directory + <span class="path">media/libsoundtouch/src/</span>. + </p> + +<pre> +The SoundTouch Library Copyright © Olli Parviainen 2001-2012 + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +</pre> + + <hr> + + <h1><a id="libyuv"></a>libyuv License</h1> + + <p>This license applies to files in the directory + <span class="path">media/libyuv</span> except + for the file <span class="path">media/libyuv/source/x86inc.asm</span>. + </p> + +<pre> +Copyright (c) 2011, The LibYuv project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="hunspell-lt"></a>Lithuanian Spellchecking Dictionary License</h1> + + <p>This license applies to the Lithuanian Spellchecking Dictionary. (This + code only ships in some localized versions of this product.)</p> + +<pre> +Copyright (c) 2000-2013, Albertas Agejevas and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holders nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL ALBERTAS AGEJEVAS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="microformatsshiv"></a>MIT license — microformat-shiv</h1> + + <p>This license applies to some files in the directory + <span class="path">toolkit/components/microformats</span>.</p> + +<pre> +MIT license — microformat-shiv + +Copyright (c) 2012-2013 Glenn Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="myspell"></a>MySpell License</h1> + + <p>This license applies to some files in the directory + <span class="path">extensions/spellcheck/hunspell</span>.</p> + +<pre> +Copyright 2002 Kevin B. Hendricks, Stratford, Ontario, Canada +And Contributors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. All modifications to the source code must be clearly marked as + such. Binary redistributions based on modified source code + must be clearly marked as modified versions in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY KEVIN B. HENDRICKS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +KEVIN B. HENDRICKS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="nicer"></a>nICEr License</h1> + + <p>This license applies to certain files in the directory + <span class="path">media/mtransport/third_party/nICEr</span>.</p> + +<pre> + Copyright (C) 2007, Adobe Systems Inc. + Copyright (C) 2007-2008, Network Resonance, Inc. + +Each source file bears an individual copyright notice. + +The following license applies to this distribution as a whole. + + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of Adobe Systems, Network Resonance nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="openaes"></a>OpenAES License</h1> + + <p>This license applies to certain files in the directory + <span class="path">media/gmp-clearkey/0.1/openaes</span>. + </p> + +<pre> +Copyright (c) 2012, Nabil S. Al Ramli, www.nalramli.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="openvision"></a>OpenVision License</h1> + + <p>This license applies to the file + <span class="path">extensions/auth/gssapi.h</span>.</p> + +<pre> +Copyright 1993 by OpenVision Technologies, Inc. + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appears in all copies and +that both that copyright notice and this permission notice appear in +supporting documentation, and that the name of OpenVision not be used +in advertising or publicity pertaining to distribution of the software +without specific, written prior permission. OpenVision makes no +representations about the suitability of this software for any +purpose. It is provided "as is" without express or implied warranty. + +OPENVISION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL OPENVISION BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +</pre> + + <hr> + + <h1><a id="node-properties"></a>node-properties License</h1> + + <p>This license applies to + <span class="path">devtools/shared/node-properties/node-properties.js</span>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2014 Gabriel Llamas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + <hr> + + <h1><a id="nrappkit"></a>nrappkit License</h1> + + <p>This license applies to certain files in the directory + <span class="path">media/mtransport/third_party/nrappkit</span>.</p> + +<pre> +Copyright (C) 2001-2007, Network Resonance, Inc. +All Rights Reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Network Resonance, Inc. nor the name of any + contributors to this software may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +</pre> + + <p>This license applies to certain files in the directory + <span class="path">media/mtransport/third_party/nrappkit</span>.</p> + +<pre> +Copyright (C) 1999-2003 RTFM, Inc. +All Rights Reserved + +This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla +<ekr@rtfm.com> and licensed by RTFM, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + + This product includes software developed by Eric Rescorla for + RTFM, Inc. + +4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be + used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE ERIC RESCORLA AND RTFM ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +oDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + <p>Note that RTFM, Inc. has waived clause (3) above as of June 20, 2012 + for files appearing in this distribution. This waiver applies only to + files included in this distribution. it does not apply to any other + part of ssldump not included in this distribution.</p> + + <p>This license applies to the file <span class="path">media/mtransport/third_party/nrappkit/src/port/generic/include/sys/queue.h</span>.</p> + +<pre> +Copyright (c) 1991, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + + <p>This license applies to the file: + <span class="path">media/mtransport/third_party/nrappkit/src/util/util.c</span>.</p> + +<pre> +Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="praton"></a>praton License</h1> + + <p>This license applies to the file + <span class="path">nsprpub/pr/src/misc/praton.c</span>.</p> + +<pre> +Copyright (c) 1983, 1990, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + + +Portions Copyright (c) 1993 by Digital Equipment Corporation. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies, and that +the name of Digital Equipment Corporation not be used in advertising or +publicity pertaining to distribution of the document or software without +specific, written prior permission. + +THE SOFTWARE IS PROVIDED "AS IS" AND DIGITAL EQUIPMENT CORP. DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DIGITAL EQUIPMENT +CORPORATION BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + +Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC") +Portions Copyright (c) 1996-1999 by Internet Software Consortium. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + + <hr> + + <h1><a id="pbkdf2-sha256"></a>pbkdf2_sha256 License</h1> + + <p>This license applies to the code + <span class="path">mozglue/android/pbkdf2_sha256.c</span> and + <span class="path">mozglue/android/pbkdf2_sha256.h</span>. + </p> + +<pre> +Copyright 2005,2007,2009 Colin Percival +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> +#ifdef MOZ_WEBSPEECH_POCKETSPHINX + + <hr> + + <h1><a id="pocketsphinx"></a>Pocketsphinx License</h1> + + <p>This license applies to files in the directories + <span class="path">media/pocketsphinx/</span> and + <span class="path">media/sphinxbase/</span>. + </p> + +<pre> +Copyright (c) 1999-2014 Carnegie Mellon University. All rights +reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +This work was supported in part by funding from the Defense Advanced +Research Projects Agency and the National Science Foundation of the +United States of America, and the CMU Sphinx Speech Consortium. + +THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ``AS IS'' AND +ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY +NOR ITS EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> +#endif + + <hr> + + <h1><a id="qcms"></a>qcms License</h1> + + <p>This license applies to certain files in the directory + <span class="path">gfx/qcms/</span>.</p> +<pre> +Copyright (C) 2009 Mozilla Corporation +Copyright (C) 1998-2007 Marti Maria + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject +to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="qrcode-generator"></a>QR Code Generator License</h1> + + <p>This license applies to certain files in the directory + <span class="path">devtools/shared/qrcode/encoder/</span>.</p> +<pre> +Copyright (c) 2009 Kazuhiko Arase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="react"></a>React License</h1> + + <p>This license applies to various files in the Mozilla codebase.</p> + +<pre> +Copyright (c) 2013-2015, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="react-redux"></a>React-Redux License</h1> + + <p>This license applies to the file + <span class="path">devtools/client/shared/vendor/react-redux.js</span>.</p> +<pre> +Copyright (c) 2015 Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + <hr> + + <h1><a id="react-virtualized"></a>React Virtualized License</h1> + + <p>This license applies to the file + <span class="path">devtools/client/shared/vendor/react-virtualized.js</span>.</p> +<pre> +Copyright (c) 2015 Brian Vaughn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + <hr> + + <h1><a id="xdg"></a>Red Hat xdg_user_dir_lookup License</h1> + + <p>This license applies to the + <span class="path">xdg_user_dir_lookup</span> function in + <span class="path">xpcom/io/SpecialSystemDirectory.cpp</span>.</p> + +<pre> +Copyright (c) 2007 Red Hat, Inc. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + <hr> + + <h1><a id="redux"></a>Redux License</h1> + + <p>This license applies to the file + <span class="path">devtools/client/shared/vendor/redux.js</span>.</p> +<pre> +Copyright (c) 2015 Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + +<hr> + +<h1><a id="reselect"></a>Reselect License</h1> + +<p>This license applies to the file +<span class="path">devtools/client/shared/vendor/reselect.js</span>.</p> +<pre> +The MIT License (MIT) + +Copyright (c) 2015-2016 Reselect Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + <hr> + + <h1><a id="hunspell-ru"></a>Russian Spellchecking Dictionary License</h1> + + <p>This license applies to the Russian Spellchecking Dictionary. (This + code only ships in some localized versions of this product.)</p> + +<pre> +* Copyright (c) 1997-2008, Alexander I. Lebedev + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Modified versions must be clearly marked as such. +* The name of Alexander I. Lebedev may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="sctp"></a>SCTP Licenses</h1> + + <p>These licenses apply to certain files in the directory + <span class="path">netwerk/sctp/src/</span>.</p> + +<pre> +Copyright (c) 2009-2010 Brad Penoff +Copyright (c) 2009-2010 Humaira Kamal +Copyright (c) 2011-2012 Irene Ruengeler +Copyright (c) 2010-2012, by Michael Tuexen. All rights reserved. +Copyright (c) 2010-2012, by Randall Stewart. All rights reserved. +Copyright (c) 2010-2012, by Robin Seggelmann. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +Copyright (c) 2001-2008, by Cisco Systems, Inc. All rights reserved. +Copyright (c) 2008-2012, by Randall Stewart. All rights reserved. +Copyright (c) 2008-2012, by Michael Tuexen. All rights reserved. +Copyright (c) 2008-2012, by Brad Penoff. All rights reserved. +Copyright (c) 1980, 1982, 1986, 1987, 1988, 1990, 1993 + The Regents of the University of California. +Copyright (c) 2005 Robert N. M. Watson All rights reserved. +Copyright (C) 1995, 1996, 1997, and 1998 WIDE Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +a) Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +b) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. +c) Neither the name of Cisco Systems, Inc, the name of the university, + the WIDE project, nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="skia"></a>Skia License</h1> + + <p>This license applies to certain files in the directory + <span class="path">gfx/skia/</span>.</p> + +<pre> +Copyright (c) 2011 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. +* Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="snappy"></a>Snappy License</h1> + + <p>This license applies to certain files in the directory + <span class="path">other-licenses/snappy/</span>.</p> + +<pre> +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="sprintf.js"></a>sprintf.js License</h1> + + <p>This license applies to + <span class="path">devtools/shared/sprintfjs/sprintf.js</span>.</p> + +<pre> +Copyright (c) 2007-2016, Alexandru Marasteanu <hello [at) alexei (dot] ro> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of this software nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="sunsoft"></a>SunSoft License</h1> + + <p>This license applies to the + <span class="path">ICC_H</span> block in + <span class="path">gfx/qcms/qcms.h</span>.</p> + +<pre> +Copyright (c) 1994-1996 SunSoft, Inc. + + Rights Reserved + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restrict- +ion, including without limitation the rights to use, copy, modify, +merge, publish distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON- +INFRINGEMENT. IN NO EVENT SHALL SUNSOFT, INC. OR ITS PARENT +COMPANY BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of SunSoft, Inc. +shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without written +authorization from SunSoft Inc. +</pre> + + + <hr> + + <h1><a id="superfasthash"></a>SuperFastHash License</h1> + + <p>This license applies to files in the directory + <span class="path">security/sandbox/chromium/base/third_party/superfasthash/</span>.</p> + +<pre> +Copyright (c) 2010, Paul Hsieh +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither my name, Paul Hsieh, nor the names of any other contributors to the + code use may not be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="unicode"></a>Unicode License</h1> + + <p>This license applies to files in the <span class="path">intl/icu</span> + and <span class="path">intl/tzdata</span> directories and certain files in + the <span class="path">js/src/vm</span> directory.</p> + </p> + +<pre> +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2016 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in http://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +--------------------- + +Third-Party Software Licenses + +This section contains third-party software notices and/or additional +terms for licensed third-party software components included within ICU +libraries. + +1. ICU License - ICU 1.8.1 to ICU 57.1 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1995-2016 International Business Machines Corporation and others +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, provided that the above +copyright notice(s) and this permission notice appear in all copies of +the Software and that both the above copyright notice(s) and this +permission notice appear in supporting documentation. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY +SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, use +or other dealings in this Software without prior written authorization +of the copyright holder. + +All trademarks and registered trademarks mentioned herein are the +property of their respective owners. + +2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt) + + # The Google Chrome software developed by Google is licensed under + # the BSD license. Other software included in this distribution is + # provided under other licenses, as set forth below. + # + # The BSD License + # http://opensource.org/licenses/bsd-license.php + # Copyright (C) 2006-2008, Google Inc. + # + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions are met: + # + # Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # Redistributions in binary form must reproduce the above + # copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided with + # the distribution. + # Neither the name of Google Inc. nor the names of its + # contributors may be used to endorse or promote products derived from + # this software without specific prior written permission. + # + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + # + # + # The word list in cjdict.txt are generated by combining three word lists + # listed below with further processing for compound word breaking. The + # frequency is generated with an iterative training against Google web + # corpora. + # + # * Libtabe (Chinese) + # - https://sourceforge.net/project/?group_id=1519 + # - Its license terms and conditions are shown below. + # + # * IPADIC (Japanese) + # - http://chasen.aist-nara.ac.jp/chasen/distribution.html + # - Its license terms and conditions are shown below. + # + # ---------COPYING.libtabe ---- BEGIN-------------------- + # + # /* + # * Copyrighy (c) 1999 TaBE Project. + # * Copyright (c) 1999 Pai-Hsiang Hsiao. + # * All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the TaBE Project nor the names of its + # * contributors may be used to endorse or promote products derived + # * from this software without specific prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # /* + # * Copyright (c) 1999 Computer Systems and Communication Lab, + # * Institute of Information Science, Academia + # * Sinica. All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the Computer Systems and Communication Lab + # * nor the names of its contributors may be used to endorse or + # * promote products derived from this software without specific + # * prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # Copyright 1996 Chih-Hao Tsai @ Beckman Institute, + # University of Illinois + # c-tsai4@uiuc.edu http://casper.beckman.uiuc.edu/~c-tsai4 + # + # ---------------COPYING.libtabe-----END-------------------------------- + # + # + # ---------------COPYING.ipadic-----BEGIN------------------------------- + # + # Copyright 2000, 2001, 2002, 2003 Nara Institute of Science + # and Technology. All Rights Reserved. + # + # Use, reproduction, and distribution of this software is permitted. + # Any copy of this software, whether in its original form or modified, + # must include both the above copyright notice and the following + # paragraphs. + # + # Nara Institute of Science and Technology (NAIST), + # the copyright holders, disclaims all warranties with regard to this + # software, including all implied warranties of merchantability and + # fitness, in no event shall NAIST be liable for + # any special, indirect or consequential damages or any damages + # whatsoever resulting from loss of use, data or profits, whether in an + # action of contract, negligence or other tortuous action, arising out + # of or in connection with the use or performance of this software. + # + # A large portion of the dictionary entries + # originate from ICOT Free Software. The following conditions for ICOT + # Free Software applies to the current dictionary as well. + # + # Each User may also freely distribute the Program, whether in its + # original form or modified, to any third party or parties, PROVIDED + # that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear + # on, or be attached to, the Program, which is distributed substantially + # in the same form as set out herein and that such intended + # distribution, if actually made, will neither violate or otherwise + # contravene any of the laws and regulations of the countries having + # jurisdiction over the User or the intended distribution itself. + # + # NO WARRANTY + # + # The program was produced on an experimental basis in the course of the + # research and development conducted during the project and is provided + # to users as so produced on an experimental basis. Accordingly, the + # program is provided without any warranty whatsoever, whether express, + # implied, statutory or otherwise. The term "warranty" used herein + # includes, but is not limited to, any warranty of the quality, + # performance, merchantability and fitness for a particular purpose of + # the program and the nonexistence of any infringement or violation of + # any right of any third party. + # + # Each user of the program will agree and understand, and be deemed to + # have agreed and understood, that there is no warranty whatsoever for + # the program and, accordingly, the entire risk arising from or + # otherwise connected with the program is assumed by the user. + # + # Therefore, neither ICOT, the copyright holder, or any other + # organization that participated in or was otherwise related to the + # development of the program and their respective officials, directors, + # officers and other employees shall be held liable for any and all + # damages, including, without limitation, general, special, incidental + # and consequential damages, arising out of or otherwise in connection + # with the use or inability to use the program or any product, material + # or result produced or otherwise obtained by using the program, + # regardless of whether they have been advised of, or otherwise had + # knowledge of, the possibility of such damages at any time during the + # project or thereafter. Each user will be deemed to have agreed to the + # foregoing by his or her commencement of use of the program. The term + # "use" as used herein includes, but is not limited to, the use, + # modification, copying and distribution of the program and the + # production of secondary products from the program. + # + # In the case where the program, whether in its original form or + # modified, was distributed or delivered to or received by a user from + # any person, organization or entity other than ICOT, unless it makes or + # grants independently of ICOT any specific warranty to the user in + # writing, such person, organization or entity, will also be exempted + # from and not be held liable to the user for any such damages as noted + # above as far as the program is concerned. + # + # ---------------COPYING.ipadic-----END---------------------------------- + +3. Lao Word Break Dictionary Data (laodict.txt) + + # Copyright (c) 2013 International Business Machines Corporation + # and others. All Rights Reserved. + # + # Project: http://code.google.com/p/lao-dictionary/ + # Dictionary: http://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt + # License: http://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt + # (copied below) + # + # This file is derived from the above dictionary, with slight + # modifications. + # ---------------------------------------------------------------------- + # Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, + # are permitted provided that the following conditions are met: + # + # + # Redistributions of source code must retain the above copyright notice, this + # list of conditions and the following disclaimer. Redistributions in + # binary form must reproduce the above copyright notice, this list of + # conditions and the following disclaimer in the documentation and/or + # other materials provided with the distribution. + # + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # OF THE POSSIBILITY OF SUCH DAMAGE. + # -------------------------------------------------------------------------- + +4. Burmese Word Break Dictionary Data (burmesedict.txt) + + # Copyright (c) 2014 International Business Machines Corporation + # and others. All Rights Reserved. + # + # This list is part of a project hosted at: + # github.com/kanyawtech/myanmar-karen-word-lists + # + # -------------------------------------------------------------------------- + # Copyright (c) 2013, LeRoy Benjamin Sharon + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: Redistributions of source code must retain the above + # copyright notice, this list of conditions and the following + # disclaimer. Redistributions in binary form must reproduce the + # above copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided + # with the distribution. + # + # Neither the name Myanmar Karen Word Lists, nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS + # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + # SUCH DAMAGE. + # -------------------------------------------------------------------------- + +5. Time Zone Database + + ICU uses the public domain data and code derived from Time Zone +Database for its time zone support. The ownership of the TZ database +is explained in BCP 175: Procedure for Maintaining the Time Zone +Database section 7. + + # 7. Database Ownership + # + # The TZ database itself is not an IETF Contribution or an IETF + # document. Rather it is a pre-existing and regularly updated work + # that is in the public domain, and is intended to remain in the + # public domain. Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do + # not apply to the TZ Database or contributions that individuals make + # to it. Should any claims be made and substantiated against the TZ + # Database, the organization that is providing the IANA + # Considerations defined in this RFC, under the memorandum of + # understanding with the IETF, currently ICANN, may act in accordance + # with all competent court orders. No ownership claims will be made + # by ICANN or the IETF Trust on the database or the code. Any person + # making a contribution to the database or code waives all rights to + # future claims in that contribution or in the TZ Database.</pre> + + + <hr> + + <h1><a id="ucal"></a>University of California License</h1> + + <p>This license applies to the following files or, in the case of + directories, certain files in those directories:</p> + + <ul> + <li class="path">dbm/</li> + <li class="path">db/mork/src/morkQuickSort.cpp</li> + <li class="path">xpcom/glue/nsQuickSort.cpp</li> + <li class="path">nsprpub/pr/src/misc/praton.c</li> + <li class="path">media/mtransport/third_party/nICEr/src/stun/addrs.c</li> + </ul> + +<pre> +Copyright (c) 1990, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +[3 Deleted as of 22nd July 1999; see + <a href="ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change">ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change</a> + for details] +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="hunspell-en-US"></a>US English Spellchecking Dictionary Licenses</h1> + + <p>These licenses apply to certain files in the directory + <span class="path">extensions/spellcheck/locales/en-US/hunspell/</span>. (This + code only ships in some localized versions of this product.)</p> + +<pre> +Different parts of the US English dictionary (SCOWL) are subject to the +following licenses as shown below. For additional details, sources, credits, +and public domain references, see <a href="https://dxr.mozilla.org/mozilla-central/source/extensions/spellcheck/locales/en-US/hunspell/README_en_US.txt">README.txt</a>. + +The collective work of the Spell Checking Oriented Word Lists (SCOWL) is under +the following copyright: + +Copyright 2000-2007 by Kevin Atkinson +Permission to use, copy, modify, distribute and sell these word lists, the +associated scripts, the output created from the scripts, and its documentation +for any purpose is hereby granted without fee, provided that the above +copyright notice appears in all copies and that both that copyright notice and +this permission notice appear in supporting documentation. Kevin Atkinson makes +no representations about the suitability of this array for any purpose. It is +provided "as is" without express or implied warranty. + +The WordNet database is under the following copyright: + +This software and database is being provided to you, the LICENSEE, by Princeton +University under the following license. By obtaining, using and/or copying +this software and database, you agree that you have read, understood, and will +comply with these terms and conditions: +Permission to use, copy, modify and distribute this software and database and +its documentation for any purpose and without fee or royalty is hereby granted, +provided that you agree to comply with the following copyright notice and +statements, including the disclaimer, and that the same appear on ALL copies of +the software, database and documentation, including modifications that you make +for internal use or for distribution. +WordNet 1.6 Copyright 1997 by Princeton University. All rights reserved. +THIS SOFTWARE AND DATABASE IS PROVIDED "AS IS" AND PRINCETON UNIVERSITY +MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF +EXAMPLE, BUT NOT LIMITATION, PRINCETON UNIVERSITY MAKES NO +REPRESENTATIONS OR WARRANTIES OF MERCHANT- ABILITY OR FITNESS FOR ANY +PARTICULAR PURPOSE OR THAT THE USE OF THE LICENSED SOFTWARE, DATABASE OR +DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, +TRADEMARKS OR OTHER RIGHTS. +The name of Princeton University or Princeton may not be used in advertising or +publicity pertaining to distribution of the software and/or database. Title to +copyright in this software, database and any associated documentation shall at +all times remain with Princeton University and LICENSEE agrees to preserve same. + +The "UK Advanced Cryptics Dictionary" is under the following copyright: + +Copyright (c) J Ross Beresford 1993-1999. All Rights Reserved. +The following restriction is placed on the use of this publication: if The UK +Advanced Cryptics Dictionary is used in a software package or redistributed in +any form, the copyright notice must be prominently displayed and the text of +this document must be included verbatim. There are no other restrictions: I +would like to see the list distributed as widely as possible. + +Various parts are under the Ispell copyright: + +Copyright 1993, Geoff Kuenning, Granada Hills, CA +All rights reserved. Redistribution and use in source and binary forms, with +or without modification, are permitted provided that the following conditions +are met: + 1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + 3. All modifications to the source code must be clearly marked as such. +Binary redistributions based on modified source code must be clearly marked as +modified versions in the documentation and/or other materials provided with +the distribution. + (clause 4 removed with permission from Geoff Kuenning) + 5. The name of Geoff Kuenning may not be used to endorse or promote products +derived from this software without specific prior written permission. + THIS SOFTWARE IS PROVIDED BY GEOFF KUENNING AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GEOFF KUENNING OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +Additional Contributors: + + Alan Beale <biljir@pobox.com> + M Cooper <thegrendel@theriver.com> +</pre> + + + + <hr> + + <h1><a id="v8"></a>V8 License</h1> + + <p>This license applies to certain files in the directories + <span class="path">js/src/irregexp</span>, + <span class="path">js/src/builtin</span>, + <span class="path">js/src/jit/arm</span> and + <span class="path">js/src/jit/mips</span>. + </p> +<pre> +Copyright 2006-2012 the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + +#if defined(XP_WIN) || defined(XP_MACOSX) || defined(XP_LINUX) + + <hr> + + <h1><a id="valve"></a>Valve BSD License</h1> + + <p>This license applies to certain files in the directory + <span class="path">gfx/vr/openvr</span>.</p> +<pre> +Copyright (c) 2015, Valve Corporation +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + +#endif + + <hr> + + <h1><a id="vtune"></a>VTune License</h1> + + <p>This license applies to certain files in the directory + <span class="path">js/src/vtune</span>.</p> +<pre> +Copyright (c) 2005-2012 Intel Corporation. All rights reserved. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Intel Corporation nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="webrtc"></a>WebRTC License</h1> + + <p>This license applies to certain files in the directory + <span class="path">media/webrtc/trunk</span>.</p> +<pre> +Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="x264"></a>x264 License</h1> + + <p>This license applies to the file <span class="path"> + media/webrtc/trunk/third_party/libyuv/source/x86inc.asm</span>. + </p> + +<pre> +Copyright (C) 2005-2012 x264 project + +Authors: Loren Merritt <lorenm@u.washington.edu> + Anton Mitrofanov <BugMaster@narod.ru> + Jason Garrett-Glaser <darkshikari@gmail.com> + Henrik Gramner <hengar-6@student.ltu.se> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + <hr> + + <h1><a id="xiph"></a>Xiph.org Foundation License</h1> + + <p>This license applies to files in the following directories + with the specified copyright year ranges:</p> + <ul> + <li><span class="path">media/libogg/</span>, 2002</li> + <li><span class="path">media/libtheora/</span>, 2002-2007</li> + <li><span class="path">media/libvorbis/</span>, 2002-2004</li> + <li><span class="path">media/libtremor/</span>, 2002-2010</li> + <li><span class="path">media/libspeex_resampler/</span>, 2002-2008</li> + </ul> + +<pre> +Copyright (c) <year>, Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="other-notices"></a>Other Required Notices</h1> + + <ul> + <li>This software is based in part on the work of the Independent + JPEG Group.</li> + <li>Portions of the OS/2 and Android versions + of this software are copyright ©1996-2012 + <a href="http://www.freetype.org/">The FreeType Project</a>. + All rights reserved.</li> + </ul> + + + <hr> + + <h1><a id="optional-notices"></a>Optional Notices</h1> + + <p>Some permissive software licenses request but do not require an + acknowledgement of the use of their software. We are very grateful + to the following people and projects for their contributions to + this product:</p> + + <ul> + <li>The <a href="http://www.zlib.net/">zlib</a> compression library + (Jean-loup Gailly, Mark Adler and team)</li> + <li>The <a href="http://www.bzip.org/">bzip2</a> compression library + (Julian Seward)</li> + <li>The <a href="http://www.libpng.org/pub/png/">libpng</a> graphics library + (Glenn Randers-Pehrson and team)</li> + <li>The <a href="http://www.sqlite.org/">sqlite</a> database engine + (D. Richard Hipp and team)</li> + <li>The <a href="http://nsis.sourceforge.net/">Nullsoft Scriptable Install System</a> + (Amir Szekely and team)</li> + </ul> + + + +#ifdef XP_WIN + + <hr> + + <h1><a id="proprietary-notices"></a>Proprietary Operating System Components</h1> + + <p>Under some circumstances, under our + <a href="http://www.mozilla.org/foundation/licensing/binary-components/">binary components policy</a>, + Mozilla may decide to include additional + operating system vendor code with the installer of our products designed + for that vendor's proprietary platform, to make our products work well on + that specific operating system. The following license statements + apply to such inclusions.</p> + + <h2><a id="directx"></a>Microsoft Windows: Terms for 'Microsoft Distributable Code'</h2> + + <p>These terms apply to the following files; + they are referred to below as "Distributable Code": + <ul> + <li><span class="path">d3d*.dll</span> (Direct3D libraries)</li> + <li><span class="path">msvc*.dll</span> (C and C++ runtime libraries)</li> + </ul> + </p> + +<pre> +Copyright (c) Microsoft Corporation. + +The Distributable Code may be used and distributed only if you comply with the +following terms: + +(i) You may use, copy, and distribute the Distributable Code only as part of + this product; +(ii) You may not use the Distributable Code on a platform other than Windows; +(iii) You may not alter any copyright, trademark or patent notice in the + Distributable Code; +(iv) You may not modify or distribute the source code of any Distributable + Code so that any part of the source code becomes subject to the MPL or + any other copyleft license; +(v) You must comply with any technical limitations in the Distributable Code + that only allow you to use it in certain ways; and +(vi) You must comply with all domestic and international export laws and + regulations that apply to the Distributable Code. +</pre> + +#endif + + + <hr> + + <p><a href="about:license#top">Return to top</a>.</p> + + </body> +</html> diff --git a/toolkit/content/macWindowMenu.inc b/toolkit/content/macWindowMenu.inc new file mode 100644 index 0000000000..c345ad8b7f --- /dev/null +++ b/toolkit/content/macWindowMenu.inc @@ -0,0 +1,40 @@ + <script type="application/javascript" src="chrome://global/content/macWindowMenu.js"/> + <commandset id="baseMenuCommandSet"> + <command id="minimizeWindow" + label="&minimizeWindow.label;" + oncommand="window.minimize();" /> + <command id="zoomWindow" + label="&zoomWindow.label;" + oncommand="zoomWindow();" /> + </commandset> + <keyset id="baseMenuKeyset"> + <key id="key_minimizeWindow" + command="minimizeWindow" + key="&minimizeWindow.key;" + modifiers="accel"/> + </keyset> + <menu id="windowMenu" + label="&windowMenu.label;" + datasources="rdf:window-mediator" ref="NC:WindowMediatorRoot" + onpopupshowing="macWindowMenuDidShow();" + hidden="false"> + <template> + <rule> + <menupopup> + <menuitem uri="rdf:*" + label="rdf:http://home.netscape.com/NC-rdf#Name" + type="radio" + name="windowList" + oncommand="ShowWindowFromResource(event.target)"/> + </menupopup> + </rule> + </template> + <menupopup id="windowPopup"> + <menuitem command="minimizeWindow" key="key_minimizeWindow"/> + <menuitem command="zoomWindow"/> + <!-- decomment when "BringAllToFront" is implemented + <menuseparator/> + <menuitem label="&bringAllToFront.label;" disabled="true"/> --> + <menuseparator id="sep-window-list"/> + </menupopup> + </menu> diff --git a/toolkit/content/macWindowMenu.js b/toolkit/content/macWindowMenu.js new file mode 100644 index 0000000000..46654c4f8a --- /dev/null +++ b/toolkit/content/macWindowMenu.js @@ -0,0 +1,51 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +function macWindowMenuDidShow() +{ + var windowManagerDS = + Components.classes['@mozilla.org/rdf/datasource;1?name=window-mediator'] + .getService(Components.interfaces.nsIWindowDataSource); + var sep = document.getElementById("sep-window-list"); + // Using double parens to avoid warning + while ((sep = sep.nextSibling)) { + var url = sep.getAttribute('id'); + var win = windowManagerDS.getWindowForResource(url); + if (win.document.documentElement.getAttribute("inwindowmenu") == "false") + sep.hidden = true; + else if (win == window) + sep.setAttribute("checked", "true"); + } +} + +function toOpenWindow( aWindow ) +{ + // deminiaturize the window, if it's in the Dock + if (aWindow.windowState == STATE_MINIMIZED) + aWindow.restore(); + aWindow.document.commandDispatcher.focusedWindow.focus(); +} + +function ShowWindowFromResource( node ) +{ + var windowManagerDS = + Components.classes['@mozilla.org/rdf/datasource;1?name=window-mediator'] + .getService(Components.interfaces.nsIWindowDataSource); + + var desiredWindow = null; + var url = node.getAttribute('id'); + desiredWindow = windowManagerDS.getWindowForResource( url ); + if (desiredWindow) + toOpenWindow(desiredWindow); +} + +function zoomWindow() +{ + if (window.windowState == STATE_NORMAL) + window.maximize(); + else + window.restore(); +} diff --git a/toolkit/content/menulist.css b/toolkit/content/menulist.css new file mode 100644 index 0000000000..ae6166d1b8 --- /dev/null +++ b/toolkit/content/menulist.css @@ -0,0 +1,11 @@ +/* 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/. */ + +@namespace html url("http://www.w3.org/1999/xhtml"); /* namespace for HTML elements */ + +html|*.menulist-editable-input { + -moz-appearance: none !important; + background: transparent ! important; + -moz-box-flex: 1; +} diff --git a/toolkit/content/minimal-xul.css b/toolkit/content/minimal-xul.css new file mode 100644 index 0000000000..0cd41922d1 --- /dev/null +++ b/toolkit/content/minimal-xul.css @@ -0,0 +1,133 @@ +/* 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 file should only contain a minimal set of rules for the XUL elements + * that may be implicitly created as part of HTML/SVG documents (e.g. + * scrollbars). Rules for everything else related to XUL can be found in + * xul.css. (This split of the XUL rules is to minimize memory use and improve + * performance in HTML/SVG documents.) + * + * This file should also not contain any app specific styling. Defaults for + * widgets of a particular application should be in that application's style + * sheet. For example style definitions for navigator can be found in + * navigator.css. + * + * THIS FILE IS LOCKED DOWN. YOU ARE NOT ALLOWED TO MODIFY IT WITHOUT FIRST + * HAVING YOUR CHANGES REVIEWED BY enndeakin@gmail.com + */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ +@namespace html url("http://www.w3.org/1999/xhtml"); /* namespace for HTML elements */ + +* { + -moz-user-focus: ignore; + -moz-user-select: none; + display: -moz-box; + box-sizing: border-box; +} + +:root { + text-rendering: optimizeLegibility; + -moz-binding: url("chrome://global/content/bindings/general.xml#root-element"); + -moz-control-character-visibility: visible; +} + +:root:-moz-locale-dir(rtl) { + direction: rtl; +} + +/* hide the content and destroy the frame */ +[hidden="true"] { + display: none; +} + +/* hide the content, but don't destroy the frames */ +[collapsed="true"], +[moz-collapsed="true"] { + visibility: collapse; +} + +/********** label **********/ + +description { + -moz-binding: url("chrome://global/content/bindings/text.xml#text-base"); +} + +label { + -moz-binding: url("chrome://global/content/bindings/text.xml#text-label"); +} + +label.text-link, label[onclick] { + -moz-binding: url("chrome://global/content/bindings/text.xml#text-link"); + -moz-user-focus: normal; +} + +label[control], label.radio-label, label.checkbox-label, label.toolbarbutton-multiline-text { + -moz-binding: url("chrome://global/content/bindings/text.xml#label-control"); +} + +html|span.accesskey { + text-decoration: underline; +} + +/********** resizer **********/ + +resizer { + -moz-binding: url("chrome://global/content/bindings/resizer.xml#resizer"); + position: relative; + z-index: 2147483647; +} + +/********** scrollbar **********/ + +/* Scrollbars are never flipped even if BiDI kicks in. */ +scrollbar[orient="horizontal"] { + direction: ltr; +} + +thumb { + -moz-binding: url(chrome://global/content/bindings/scrollbar.xml#thumb); + display: -moz-box !important; +} + +.scale-thumb { + -moz-binding: url(chrome://global/content/bindings/scale.xml#scalethumb); +} + +scrollbar, scrollbarbutton, scrollcorner, slider, thumb, scale { + -moz-user-select: none; +} + +scrollcorner { + display: -moz-box !important; +} + +scrollcorner[hidden="true"] { + display: none !important; +} + +scrollbar[value="hidden"] { + visibility: hidden; +} + +scale { + -moz-binding: url(chrome://global/content/bindings/scale.xml#scale); +} + +.scale-slider { + -moz-binding: url(chrome://global/content/bindings/scale.xml#scaleslider); + -moz-user-focus: normal; +} + +scrollbarbutton[sbattr="scrollbar-up-top"]:not(:-moz-system-metric(scrollbar-start-backward)), +scrollbarbutton[sbattr="scrollbar-down-top"]:not(:-moz-system-metric(scrollbar-start-forward)), +scrollbarbutton[sbattr="scrollbar-up-bottom"]:not(:-moz-system-metric(scrollbar-end-backward)), +scrollbarbutton[sbattr="scrollbar-down-bottom"]:not(:-moz-system-metric(scrollbar-end-forward)) { + display: none; +} + +thumb[sbattr="scrollbar-thumb"]:-moz-system-metric(scrollbar-thumb-proportional) { + -moz-box-flex: 1; +} diff --git a/toolkit/content/moz.build b/toolkit/content/moz.build new file mode 100644 index 0000000000..cc86890bfa --- /dev/null +++ b/toolkit/content/moz.build @@ -0,0 +1,34 @@ +# -*- 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/. + +TEST_DIRS += ['tests'] + +for var in ('target', 'MOZ_CONFIGURE_OPTIONS', 'CC', 'CC_VERSION', 'CXX'): + DEFINES[var] = CONFIG[var] + +DEFINES['CFLAGS'] = CONFIG['OS_CFLAGS'] + +if CONFIG['OS_TARGET'] == 'Android': + DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'] + +if CONFIG['MOZ_ANDROID_CXX_STL'] == 'libc++': + DEFINES['MOZ_USE_LIBCXX'] = True + +if CONFIG['MOZ_BUILD_APP'] == 'mobile/android': + DEFINES['MOZ_FENNEC'] = True + +JAR_MANIFESTS += ['jar.mn'] + +with Files('aboutTelemetry.*'): + BUG_COMPONENT = ('Toolkit', 'Telemetry') + +with Files('customizeToolbar.*'): + BUG_COMPONENT = ('Toolkit', 'Toolbars and Toolbar Customization') + +with Files('widgets/*'): + BUG_COMPONENT = ('Toolkit', 'XUL Widgets') + +DEFINES['TOPOBJDIR'] = TOPOBJDIR diff --git a/toolkit/content/mozilla.xhtml b/toolkit/content/mozilla.xhtml new file mode 100644 index 0000000000..1ffde19e43 --- /dev/null +++ b/toolkit/content/mozilla.xhtml @@ -0,0 +1,66 @@ +<!DOCTYPE html +[ + <!ENTITY % mozillaDTD SYSTEM "chrome://global/locale/mozilla.dtd" > + %mozillaDTD; + <!ENTITY % directionDTD SYSTEM "chrome://global/locale/global.dtd" > + %directionDTD; +]> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset='utf-8' /> + <title>&mozilla.title.15.1;</title> + +<style> +html { + background: maroon radial-gradient( circle, #a01010 0%, #800000 80%) center center / cover no-repeat; + color: white; + font-style: italic; + text-rendering: optimizeLegibility; + min-height: 100%; +} + +#moztext { + margin-top: 15%; + font-size: 1.1em; + font-family: serif; + text-align: center; + line-height: 1.5; +} + +#from { + font-size: 1.95em; + font-family: serif; + text-align: right; +} + +em { + font-size: 1.3em; + line-height: 0; +} + +a { + text-decoration: none; + color: white; +} +</style> +</head> + +<body dir="&locale.dir;"> + +<section> + <p id="moztext"> + &mozilla.quote.15.1; + </p> + + <p id="from"> + &mozilla.from.15.1; + </p> +</section> + +</body> +</html> diff --git a/toolkit/content/plugins.css b/toolkit/content/plugins.css new file mode 100644 index 0000000000..a599b5be46 --- /dev/null +++ b/toolkit/content/plugins.css @@ -0,0 +1,88 @@ +/* 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/. */ + +/* ===== plugins.css ===================================================== + == Styles used by the about:plugins page. + ======================================================================= */ + +body { + background-color: -moz-Field; + color: -moz-FieldText; + font: message-box; +} + +div#outside { + text-align: justify; + width: 90%; + margin-left: 5%; + margin-right: 5%; +} + +#plugs { + text-align: center; + font-size: xx-large; + font-weight: bold; +} + +#noplugs { + font-size: x-large; + font-weight: bold; +} + +.plugname { + margin-top: 2em; + margin-bottom: 1em; + font-size: large; + text-align: start; + font-weight: bold; +} + +dl { + margin: 0px 0px 3px 0px; +} + +table { + background-color: -moz-Dialog; + color: -moz-DialogText; + font: message-box; + text-align: start; + width: 100%; + border: 1px solid ThreeDShadow; + border-spacing: 0px; +} + +th, td { + border: none; + padding: 3px; +} + +th { + text-align: center; + background-color: Highlight; + color: HighlightText; +} + +th + th, +td + td { + border-inline-start: 1px dotted ThreeDShadow; +} + +td { + text-align: start; + border-top: 1px dotted ThreeDShadow; +} + +th.type, th.suff { + width: 25%; +} + +th.desc { + width: 50%; +} + +.notice { + background: -moz-cellhighlight; + border: 1px solid ThreeDShadow; + padding: 10px; +} diff --git a/toolkit/content/plugins.html b/toolkit/content/plugins.html new file mode 100644 index 0000000000..84cbba596a --- /dev/null +++ b/toolkit/content/plugins.html @@ -0,0 +1,217 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> + +<!-- 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/. --> + +<html> +<head> +<script type="application/javascript"> + "use strict"; + + Components.utils.import("resource://gre/modules/Services.jsm"); + + var Ci = Components.interfaces; + var strBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService); + var pluginsbundle = strBundleService.createBundle("chrome://global/locale/plugins.properties"); + + document.writeln("<title>" + pluginsbundle.GetStringFromName("title_label") + "<\/title>"); +</script> +<link rel="stylesheet" type="text/css" href="chrome://global/content/plugins.css"> +<link rel="stylesheet" type="text/css" href="chrome://global/skin/plugins.css"> +</head> +<body> +<div id="outside"> +<script type="application/javascript"> + "use strict"; + + function setDirection() { + var frame = document.getElementById("directionDetector"); + var direction = frame.contentDocument + .defaultView + .window + .getComputedStyle(frame.contentDocument.getElementById("target"), "") + .getPropertyValue("direction"); + document.body.removeChild(frame); + document.dir = direction; + } + + function setupDirection() { + var frame = document.createElement("iframe"); + frame.setAttribute("id", "directionDetector"); + frame.setAttribute("src", "chrome://global/content/directionDetector.html"); + frame.setAttribute("width", "0"); + frame.setAttribute("height", "0"); + frame.setAttribute("style", "visibility: hidden;"); + frame.setAttribute("onload", "setDirection();"); + document.body.appendChild(frame); + } + setupDirection(); + + /* JavaScript to enumerate and display all installed plug-ins + + * First, refresh plugins in case anything has been changed recently in + * prefs: (The "false" argument tells refresh not to reload or activate + * any plug-ins that would be active otherwise. In contrast, one would + * use "true" in the case of ASD instead of restarting) + */ + navigator.plugins.refresh(false); + + addMessageListener("PluginList", function({ data: aPlugins }) { + var fragment = document.createDocumentFragment(); + + // "Installed plugins" + var id, label; + if (aPlugins.length > 0) { + id = "plugs"; + label = "installedplugins_label"; + } else { + id = "noplugs"; + label = "nopluginsareinstalled_label"; + } + var enabledplugins = document.createElement("h1"); + enabledplugins.setAttribute("id", id); + enabledplugins.appendChild(document.createTextNode(pluginsbundle.GetStringFromName(label))); + fragment.appendChild(enabledplugins); + + var deprecation = document.createElement("p"); + deprecation.setAttribute("class", "notice"); + deprecation.textContent = pluginsbundle.GetStringFromName("deprecation_description") + " \u00A0 "; + var deprecationLink = document.createElement("a"); + deprecationLink.textContent = pluginsbundle.GetStringFromName("deprecation_learn_more"); + deprecationLink.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + "npapi"; + deprecation.appendChild(deprecationLink); + fragment.appendChild(deprecation); + + var stateNames = {}; + ["STATE_SOFTBLOCKED", + "STATE_BLOCKED", + "STATE_OUTDATED", + "STATE_VULNERABLE_UPDATE_AVAILABLE", + "STATE_VULNERABLE_NO_UPDATE"].forEach(function(label) { + stateNames[Ci.nsIBlocklistService[label]] = label; + }); + + for (var i = 0; i < aPlugins.length; i++) { + var plugin = aPlugins[i]; + if (plugin) { + // "Shockwave Flash" + var plugname = document.createElement("h2"); + plugname.setAttribute("class", "plugname"); + plugname.appendChild(document.createTextNode(plugin.name)); + fragment.appendChild(plugname); + + var dl = document.createElement("dl"); + fragment.appendChild(dl); + + // "File: Flash Player.plugin" + var fileDd = document.createElement("dd"); + var file = document.createElement("span"); + file.setAttribute("class", "label"); + file.appendChild(document.createTextNode(pluginsbundle.GetStringFromName("file_label") + " ")); + fileDd.appendChild(file); + fileDd.appendChild(document.createTextNode(plugin.pluginLibraries)); + dl.appendChild(fileDd); + + // "Path: /usr/lib/mozilla/plugins/libtotem-cone-plugin.so" + var pathDd = document.createElement("dd"); + var path = document.createElement("span"); + path.setAttribute("class", "label"); + path.appendChild(document.createTextNode(pluginsbundle.GetStringFromName("path_label") + " ")); + pathDd.appendChild(path); + pathDd.appendChild(document.createTextNode(plugin.pluginFullpath)); + dl.appendChild(pathDd); + + // "Version: " + var versionDd = document.createElement("dd"); + var version = document.createElement("span"); + version.setAttribute("class", "label"); + version.appendChild(document.createTextNode(pluginsbundle.GetStringFromName("version_label") + " ")); + versionDd.appendChild(version); + versionDd.appendChild(document.createTextNode(plugin.version)); + dl.appendChild(versionDd); + + // "State: " + var stateDd = document.createElement("dd"); + var state = document.createElement("span"); + state.setAttribute("label", "state"); + state.appendChild(document.createTextNode(pluginsbundle.GetStringFromName("state_label") + " ")); + stateDd.appendChild(state); + var status = plugin.isActive ? pluginsbundle.GetStringFromName("state_enabled") : pluginsbundle.GetStringFromName("state_disabled"); + if (plugin.blocklistState in stateNames) { + status += " (" + stateNames[plugin.blocklistState] + ")"; + } + stateDd.appendChild(document.createTextNode(status)); + dl.appendChild(stateDd); + + // Plugin Description + var descDd = document.createElement("dd"); + descDd.appendChild(document.createTextNode(plugin.description)); + dl.appendChild(descDd); + + // MIME Type table + var mimetypeTable = document.createElement("table"); + mimetypeTable.setAttribute("border", "1"); + mimetypeTable.setAttribute("class", "contenttable"); + fragment.appendChild(mimetypeTable); + + var thead = document.createElement("thead"); + mimetypeTable.appendChild(thead); + var tr = document.createElement("tr"); + thead.appendChild(tr); + + // "MIME Type" column header + var typeTh = document.createElement("th"); + typeTh.setAttribute("class", "type"); + typeTh.appendChild(document.createTextNode(pluginsbundle.GetStringFromName("mimetype_label"))); + tr.appendChild(typeTh); + + // "Description" column header + var descTh = document.createElement("th"); + descTh.setAttribute("class", "desc"); + descTh.appendChild(document.createTextNode(pluginsbundle.GetStringFromName("description_label"))); + tr.appendChild(descTh); + + // "Suffixes" column header + var suffixesTh = document.createElement("th"); + suffixesTh.setAttribute("class", "suff"); + suffixesTh.appendChild(document.createTextNode(pluginsbundle.GetStringFromName("suffixes_label"))); + tr.appendChild(suffixesTh); + + var tbody = document.createElement("tbody"); + mimetypeTable.appendChild(tbody); + + var mimeTypes = plugin.pluginMimeTypes; + for (var j = 0; j < mimeTypes.length; j++) { + var mimetype = mimeTypes[j]; + if (mimetype) { + var mimetypeRow = document.createElement("tr"); + tbody.appendChild(mimetypeRow); + + // "application/x-shockwave-flash" + var typename = document.createElement("td"); + typename.appendChild(document.createTextNode(mimetype.type)); + mimetypeRow.appendChild(typename); + + // "Shockwave Flash" + var description = document.createElement("td"); + description.appendChild(document.createTextNode(mimetype.description)); + mimetypeRow.appendChild(description); + + // "swf" + var suffixes = document.createElement("td"); + suffixes.appendChild(document.createTextNode(mimetype.suffixes)); + mimetypeRow.appendChild(suffixes); + } + } + } + } + + document.getElementById("outside").appendChild(fragment); + }); + + sendAsyncMessage("RequestPlugins"); +</script> +</div> +</body> +</html> diff --git a/toolkit/content/process-content.js b/toolkit/content/process-content.js new file mode 100644 index 0000000000..2ff8f908a9 --- /dev/null +++ b/toolkit/content/process-content.js @@ -0,0 +1,84 @@ +/* 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/. */ + +"use strict"; + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +// Creates a new PageListener for this process. This will listen for page loads +// and for those that match URLs provided by the parent process will set up +// a dedicated message port and notify the parent process. +Cu.import("resource://gre/modules/RemotePageManager.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const gInContentProcess = Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT; + +Services.cpmm.addMessageListener("gmp-plugin-crash", msg => { + let gmpservice = Cc["@mozilla.org/gecko-media-plugin-service;1"] + .getService(Ci.mozIGeckoMediaPluginService); + + gmpservice.RunPluginCrashCallbacks(msg.data.pluginID, msg.data.pluginName); +}); + +if (gInContentProcess) { + let ProcessObserver = { + TOPICS: [ + "inner-window-destroyed", + "xpcom-shutdown", + ], + + init() { + for (let topic of this.TOPICS) { + Services.obs.addObserver(this, topic, false); + Services.cpmm.addMessageListener("Memory:GetSummary", this); + } + }, + + uninit() { + for (let topic of this.TOPICS) { + Services.obs.removeObserver(this, topic); + Services.cpmm.removeMessageListener("Memory:GetSummary", this); + } + }, + + receiveMessage(msg) { + if (msg.name != "Memory:GetSummary") { + return; + } + let pid = Services.appinfo.processID; + let memMgr = Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager); + let rss = memMgr.resident; + let uss = memMgr.residentUnique; + Services.cpmm.sendAsyncMessage("Memory:Summary", { + pid, + summary: { + uss, + rss, + } + }); + }, + + observe(subject, topic, data) { + switch (topic) { + case "inner-window-destroyed": { + // Forward inner-window-destroyed notifications with the + // inner window ID, so that code in the parent that should + // do something when content windows go away can do it + let innerWindowID = + subject.QueryInterface(Ci.nsISupportsPRUint64).data; + Services.cpmm.sendAsyncMessage("Toolkit:inner-window-destroyed", + innerWindowID); + break; + } + case "xpcom-shutdown": { + this.uninit(); + break; + } + } + }, + }; + + ProcessObserver.init(); +} diff --git a/toolkit/content/resetProfile.css b/toolkit/content/resetProfile.css new file mode 100644 index 0000000000..a83171ff56 --- /dev/null +++ b/toolkit/content/resetProfile.css @@ -0,0 +1,15 @@ +/* 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/. */ + +#migratedItems { + margin-inline-start: 1.5em; +} + +#resetProfileFooter { + font-weight: bold; +} + +#resetProfileProgressDialog { + padding: 10px; +} diff --git a/toolkit/content/resetProfile.js b/toolkit/content/resetProfile.js new file mode 100644 index 0000000000..9a46a09a5c --- /dev/null +++ b/toolkit/content/resetProfile.js @@ -0,0 +1,20 @@ +/* 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/. */ + +"use strict"; + +// NB: this file can be loaded from aboutSupport.xhtml or from the +// resetProfile.xul dialog, and so Cu may or may not exist already. +// Proceed with caution: +if (!("Cu" in window)) { + window.Cu = Components.utils; +} + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/ResetProfile.jsm"); + +function onResetProfileAccepted() { + let retVals = window.arguments[0]; + retVals.reset = true; +} diff --git a/toolkit/content/resetProfile.xul b/toolkit/content/resetProfile.xul new file mode 100644 index 0000000000..845d473a49 --- /dev/null +++ b/toolkit/content/resetProfile.xul @@ -0,0 +1,35 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE prefwindow [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % resetProfileDTD SYSTEM "chrome://global/locale/resetProfile.dtd" > +%resetProfileDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://global/content/resetProfile.css"?> + +<dialog id="resetProfileDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&refreshProfile.dialog.title;" + buttons="accept,cancel" + defaultButton="cancel" + buttonlabelaccept="&refreshProfile.dialog.button.label;" + ondialogaccept="return onResetProfileAccepted();" + ondialogcancel="window.close();"> + + <script type="application/javascript" src="chrome://global/content/resetProfile.js"/> + + <description value="&refreshProfile.dialog.description1;"></description> + <label value="&refreshProfile.dialog.description2;"/> + + <vbox id="migratedItems"> + <label class="migratedLabel" value="&refreshProfile.dialog.items.label1;"/> + <label class="migratedLabel" value="&refreshProfile.dialog.items.label2;"/> + </vbox> +</dialog> diff --git a/toolkit/content/resetProfileProgress.xul b/toolkit/content/resetProfileProgress.xul new file mode 100644 index 0000000000..66585e2244 --- /dev/null +++ b/toolkit/content/resetProfileProgress.xul @@ -0,0 +1,25 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % resetProfileDTD SYSTEM "chrome://global/locale/resetProfile.dtd" > +%resetProfileDTD; +]> + +<?xml-stylesheet href="chrome://global/content/resetProfile.css"?> +<?xml-stylesheet href="chrome://global/skin/"?> + +<window id="resetProfileProgressDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&refreshProfile.dialog.title;" + style="min-width: 300px;"> + <vbox> + <description>&refreshProfile.cleaning.description;</description> + <progressmeter mode="undetermined"/> + </vbox> +</window> diff --git a/toolkit/content/select-child.js b/toolkit/content/select-child.js new file mode 100644 index 0000000000..d3690b7b80 --- /dev/null +++ b/toolkit/content/select-child.js @@ -0,0 +1,26 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SelectContentHelper", + "resource://gre/modules/SelectContentHelper.jsm"); + +addEventListener("mozshowdropdown", event => { + if (!event.isTrusted) + return; + + if (!SelectContentHelper.open) { + new SelectContentHelper(event.target, {isOpenedViaTouch: false}, this); + } +}); + +addEventListener("mozshowdropdown-sourcetouch", event => { + if (!event.isTrusted) + return; + + if (!SelectContentHelper.open) { + new SelectContentHelper(event.target, {isOpenedViaTouch: true}, this); + } +}); diff --git a/toolkit/content/tests/browser/.eslintrc.js b/toolkit/content/tests/browser/.eslintrc.js new file mode 100644 index 0000000000..c764b133dc --- /dev/null +++ b/toolkit/content/tests/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/browser.eslintrc.js" + ] +}; diff --git a/toolkit/content/tests/browser/audio.ogg b/toolkit/content/tests/browser/audio.ogg Binary files differnew file mode 100644 index 0000000000..7f1833508a --- /dev/null +++ b/toolkit/content/tests/browser/audio.ogg diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini new file mode 100644 index 0000000000..278b2ffe02 --- /dev/null +++ b/toolkit/content/tests/browser/browser.ini @@ -0,0 +1,75 @@ +[DEFAULT] +support-files = + head.js + file_contentTitle.html + audio.ogg + +[browser_audioCompeting.js] +tags = audiochannel +support-files = + file_multipleAudio.html +[browser_audioCompeting_onlyForActiveAgent.js] +tags = audiochannel +support-files = + file_multiplePlayingAudio.html +[browser_autoscroll_disabled.js] +[browser_block_autoplay_media.js] +tags = audiochannel +support-files = + file_multipleAudio.html +[browser_bug295977_autoscroll_overflow.js] +[browser_bug451286.js] +skip-if = !e10s +[browser_bug594509.js] +[browser_bug982298.js] +[browser_bug1198465.js] +[browser_contentTitle.js] +[browser_crash_previous_frameloader.js] +run-if = e10s && crashreporter +[browser_default_image_filename.js] +[browser_f7_caret_browsing.js] +[browser_findbar.js] +[browser_label_textlink.js] +[browser_isSynthetic.js] +support-files = + empty.png +[browser_keyevents_during_autoscrolling.js] +[browser_save_resend_postdata.js] +support-files = + common/mockTransfer.js + data/post_form_inner.sjs + data/post_form_outer.sjs +skip-if = e10s # Bug ?????? - test directly manipulates content (gBrowser.contentDocument.getElementById("postForm").submit();) +[browser_content_url_annotation.js] +skip-if = !e10s || !crashreporter +support-files = + file_redirect.html + file_redirect_to.html +[browser_bug1170531.js] +[browser_mediaPlayback.js] +tags = audiochannel +support-files = + file_mediaPlayback.html + file_mediaPlaybackFrame.html +[browser_mediaPlayback_mute.js] +tags = audiochannel +support-files = + file_mediaPlayback2.html + file_mediaPlaybackFrame2.html +[browser_mediaPlayback_suspended.js] +tags = audiochannel +support-files = + file_mediaPlayback2.html +[browser_mediaPlayback_suspended_multipleAudio.js] +tags = audiochannel +support-files = + file_multipleAudio.html +[browser_mute.js] +tags = audiochannel +[browser_mute2.js] +tags = audiochannel +[browser_quickfind_editable.js] +[browser_saveImageURL.js] +support-files = + image.jpg + image_page.html diff --git a/toolkit/content/tests/browser/browser_audioCompeting.js b/toolkit/content/tests/browser/browser_audioCompeting.js new file mode 100644 index 0000000000..7b6a76c1da --- /dev/null +++ b/toolkit/content/tests/browser/browser_audioCompeting.js @@ -0,0 +1,115 @@ +const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_multipleAudio.html"; + +function* wait_for_tab_playing_event(tab, expectPlaying) { + if (tab.soundPlaying == expectPlaying) { + ok(true, "The tab should " + (expectPlaying ? "" : "not ") + "be playing"); + } else { + yield BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, (event) => { + if (event.detail.changed.indexOf("soundplaying") >= 0) { + is(tab.soundPlaying, expectPlaying, "The tab should " + (expectPlaying ? "" : "not ") + "be playing"); + return true; + } + return false; + }); + } +} + +function play_audio_from_invisible_tab () { + return new Promise(resolve => { + var autoPlay = content.document.getElementById('autoplay'); + if (!autoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(autoPlay.paused, true, "Audio in tab 1 was paused by audio competing."); + autoPlay.play(); + autoPlay.onpause = function() { + autoPlay.onpause = null; + ok(true, "Audio in tab 1 can't playback when other tab is playing in foreground."); + resolve(); + }; + }); +} + +function audio_should_keep_playing_even_go_to_background () { + var autoPlay = content.document.getElementById('autoplay'); + if (!autoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(autoPlay.paused, false, "Audio in tab 2 is still playing in the background."); +} + +function play_non_autoplay_audio () { + return new Promise(resolve => { + var autoPlay = content.document.getElementById('autoplay'); + var nonAutoPlay = content.document.getElementById('nonautoplay'); + if (!autoPlay || !nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(nonAutoPlay.paused, true, "Non-autoplay audio isn't started playing yet."); + nonAutoPlay.play(); + + nonAutoPlay.onplay = function() { + nonAutoPlay.onplay = null; + is(nonAutoPlay.paused, false, "Start Non-autoplay audio."); + is(autoPlay.paused, false, "Autoplay audio is still playing."); + resolve(); + }; + }); +} + +add_task(function* setup_test_preference() { + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.audiochannel.audioCompeting", true], + ["dom.ipc.processCount", 1] + ]}, resolve); + }); +}); + +add_task(function* cross_tabs_audio_competing () { + info("- open tab 1 in foreground -"); + let tab1 = yield BrowserTestUtils.openNewForegroundTab(window.gBrowser, + "about:blank"); + tab1.linkedBrowser.loadURI(PAGE); + yield wait_for_tab_playing_event(tab1, true); + + info("- open tab 2 in foreground -"); + let tab2 = yield BrowserTestUtils.openNewForegroundTab(window.gBrowser, + "about:blank"); + tab2.linkedBrowser.loadURI(PAGE); + yield wait_for_tab_playing_event(tab1, false); + + info("- open tab 3 in foreground -"); + let tab3 = yield BrowserTestUtils.openNewForegroundTab(window.gBrowser, + "about:blank"); + yield ContentTask.spawn(tab2.linkedBrowser, null, + audio_should_keep_playing_even_go_to_background); + + info("- play audio from background tab 1 -"); + yield ContentTask.spawn(tab1.linkedBrowser, null, + play_audio_from_invisible_tab); + + info("- remove tabs -"); + yield BrowserTestUtils.removeTab(tab1); + yield BrowserTestUtils.removeTab(tab2); + yield BrowserTestUtils.removeTab(tab3); +}); + +add_task(function* within_one_tab_audio_competing () { + info("- open tab and play audio1 -"); + let tab = yield BrowserTestUtils.openNewForegroundTab(window.gBrowser, + "about:blank"); + tab.linkedBrowser.loadURI(PAGE); + yield wait_for_tab_playing_event(tab, true); + + info("- play audio2 in the same tab -"); + yield ContentTask.spawn(tab.linkedBrowser, null, + play_non_autoplay_audio); + + info("- remove tab -"); + yield BrowserTestUtils.removeTab(tab); +}); + diff --git a/toolkit/content/tests/browser/browser_audioCompeting_onlyForActiveAgent.js b/toolkit/content/tests/browser/browser_audioCompeting_onlyForActiveAgent.js new file mode 100644 index 0000000000..31cd3f6244 --- /dev/null +++ b/toolkit/content/tests/browser/browser_audioCompeting_onlyForActiveAgent.js @@ -0,0 +1,176 @@ +const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_multiplePlayingAudio.html"; + +var SuspendedType = { + NONE_SUSPENDED : 0, + SUSPENDED_PAUSE : 1, + SUSPENDED_BLOCK : 2, + SUSPENDED_PAUSE_DISPOSABLE : 3 +}; + +function wait_for_event(browser, event) { + return BrowserTestUtils.waitForEvent(browser, event, false, (event) => { + is(event.originalTarget, browser, "Event must be dispatched to correct browser."); + return true; + }); +} + +function check_all_audio_suspended(suspendedType) { + var audio1 = content.document.getElementById("audio1"); + var audio2 = content.document.getElementById("audio2"); + if (!audio1 || !audio2) { + ok(false, "Can't get the audio element!"); + } + + is(audio1.computedSuspended, suspendedType, + "The suspeded state of audio1 is correct."); + is(audio2.computedSuspended, suspendedType, + "The suspeded state of audio2 is correct."); +} + +function check_audio1_suspended(suspendedType) { + var audio1 = content.document.getElementById("audio1"); + if (!audio1) { + ok(false, "Can't get the audio element!"); + } + + is(audio1.computedSuspended, suspendedType, + "The suspeded state of audio1 is correct."); +} + +function check_audio2_suspended(suspendedType) { + var audio2 = content.document.getElementById("audio2"); + if (!audio2) { + ok(false, "Can't get the audio element!"); + } + + is(audio2.computedSuspended, suspendedType, + "The suspeded state of audio2 is correct."); +} + +function check_all_audio_pause_state(expectedPauseState) { + var audio1 = content.document.getElementById("audio1"); + var audio2 = content.document.getElementById("audio2"); + if (!audio1 | !audio2) { + ok(false, "Can't get the audio element!"); + } + + is(audio1.paused, expectedPauseState, + "The pause state of audio1 is correct."); + is(audio2.paused, expectedPauseState, + "The pause state of audio2 is correct."); +} + +function check_audio1_pause_state(expectedPauseState) { + var audio1 = content.document.getElementById("audio1"); + if (!audio1) { + ok(false, "Can't get the audio element!"); + } + + is(audio1.paused, expectedPauseState, + "The pause state of audio1 is correct."); +} + +function check_audio2_pause_state(expectedPauseState) { + var audio2 = content.document.getElementById("audio2"); + if (!audio2) { + ok(false, "Can't get the audio element!"); + } + + is(audio2.paused, expectedPauseState, + "The pause state of audio2 is correct."); +} + +function play_audio1_from_page() { + var audio1 = content.document.getElementById("audio1"); + if (!audio1) { + ok(false, "Can't get the audio element!"); + } + + is(audio1.paused, true, "Audio1 is paused."); + audio1.play(); + return new Promise(resolve => { + audio1.onplay = function() { + audio1.onplay = null; + ok(true, "Audio1 started playing."); + resolve(); + } + }); +} + +function stop_audio1_from_page() { + var audio1 = content.document.getElementById("audio1"); + if (!audio1) { + ok(false, "Can't get the audio element!"); + } + + is(audio1.paused, false, "Audio1 is playing."); + audio1.pause(); + return new Promise(resolve => { + audio1.onpause = function() { + audio1.onpause = null; + ok(true, "Audio1 stopped playing."); + resolve(); + } + }); +} + +function* audio_competing_for_active_agent(url, browser) { + browser.loadURI(url); + + info("- page should have playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + info("- the default suspended state of all audio should be non-suspened -"); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); + + info("- only pause playing audio in the page -"); + browser.pauseMedia(true /* disposable */); + + info("- page shouldn't have any playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStopped"); + yield ContentTask.spawn(browser, true /* expect for pause */, + check_all_audio_pause_state); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_PAUSE_DISPOSABLE, + check_all_audio_suspended); + + info("- resume audio1 from page -"); + yield ContentTask.spawn(browser, null, + play_audio1_from_page); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_audio1_suspended); + + info("- audio2 should still be suspended -"); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_PAUSE_DISPOSABLE, + check_audio2_suspended); + yield ContentTask.spawn(browser, true /* expect for pause */, + check_audio2_pause_state); + + info("- stop audio1 from page -"); + yield ContentTask.spawn(browser, null, + stop_audio1_from_page); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_audio1_suspended); + + info("- audio2 should still be suspended -"); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_PAUSE_DISPOSABLE, + check_audio2_suspended); + yield ContentTask.spawn(browser, true /* expect for pause */, + check_audio2_pause_state); + +} + +add_task(function* setup_test_preference() { + yield SpecialPowers.pushPrefEnv({"set": [ + ["media.useAudioChannelService.testing", true], + ["dom.audiochannel.audioCompeting", true], + ["dom.audiochannel.audioCompeting.allAgents", true] + ]}); +}); + +add_task(function* test_suspended_pause_disposable() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank" + }, audio_competing_for_active_agent.bind(this, PAGE)); +}); diff --git a/toolkit/content/tests/browser/browser_autoscroll_disabled.js b/toolkit/content/tests/browser/browser_autoscroll_disabled.js new file mode 100644 index 0000000000..07c6174abd --- /dev/null +++ b/toolkit/content/tests/browser/browser_autoscroll_disabled.js @@ -0,0 +1,67 @@ +add_task(function* () +{ + const kPrefName_AutoScroll = "general.autoScroll"; + Services.prefs.setBoolPref(kPrefName_AutoScroll, false); + + let dataUri = 'data:text/html,<html><body id="i" style="overflow-y: scroll"><div style="height: 2000px"></div>\ + <iframe id="iframe" style="display: none;"></iframe>\ +</body></html>'; + + let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + gBrowser.loadURI(dataUri); + yield loadedPromise; + + yield BrowserTestUtils.synthesizeMouse("#i", 50, 50, { button: 1 }, + gBrowser.selectedBrowser); + + yield ContentTask.spawn(gBrowser.selectedBrowser, { }, function* () { + var iframe = content.document.getElementById("iframe"); + + if (iframe) { + var e = new iframe.contentWindow.PageTransitionEvent("pagehide", + { bubbles: true, + cancelable: true, + persisted: false }); + iframe.contentDocument.dispatchEvent(e); + iframe.contentDocument.documentElement.dispatchEvent(e); + } + }); + + yield BrowserTestUtils.synthesizeMouse("#i", 100, 100, + { type: "mousemove", clickCount: "0" }, + gBrowser.selectedBrowser); + + // If scrolling didn't work, we wouldn't do any redraws and thus time out, so + // request and force redraws to get the chance to check for scrolling at all. + yield new Promise(resolve => window.requestAnimationFrame(resolve)); + + let msg = yield ContentTask.spawn(gBrowser.selectedBrowser, { }, function* () { + // Skip the first animation frame callback as it's the same callback that + // the browser uses to kick off the scrolling. + return new Promise(resolve => { + function checkScroll() { + let msg = ""; + let elem = content.document.getElementById('i'); + if (elem.scrollTop != 0) { + msg += "element should not have scrolled vertically"; + } + if (elem.scrollLeft != 0) { + msg += "element should not have scrolled horizontally"; + } + + resolve(msg); + } + + content.requestAnimationFrame(checkScroll); + }); + }); + + ok(!msg, "element scroll " + msg); + + // restore the changed prefs + if (Services.prefs.prefHasUserValue(kPrefName_AutoScroll)) + Services.prefs.clearUserPref(kPrefName_AutoScroll); + + // wait for focus to fix a failure in the next test if the latter runs too soon. + yield SimpleTest.promiseFocus(); +}); diff --git a/toolkit/content/tests/browser/browser_block_autoplay_media.js b/toolkit/content/tests/browser/browser_block_autoplay_media.js new file mode 100644 index 0000000000..3b2a309b9a --- /dev/null +++ b/toolkit/content/tests/browser/browser_block_autoplay_media.js @@ -0,0 +1,87 @@ +const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_multipleAudio.html"; + +var SuspendedType = { + NONE_SUSPENDED : 0, + SUSPENDED_PAUSE : 1, + SUSPENDED_BLOCK : 2, + SUSPENDED_PAUSE_DISPOSABLE : 3 +}; + +function* wait_for_tab_playing_event(tab, expectPlaying) { + if (tab.soundPlaying == expectPlaying) { + ok(true, "The tab should " + (expectPlaying ? "" : "not ") + "be playing"); + } else { + yield BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, (event) => { + if (event.detail.changed.indexOf("soundplaying") >= 0) { + is(tab.soundPlaying, expectPlaying, "The tab should " + (expectPlaying ? "" : "not ") + "be playing"); + return true; + } + return false; + }); + } +} + +function check_audio_suspended(suspendedType) { + var autoPlay = content.document.getElementById('autoplay'); + if (!autoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(autoPlay.computedSuspended, suspendedType, + "The suspeded state of autoplay audio is correct."); +} + +add_task(function* setup_test_preference() { + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["media.useAudioChannelService.testing", true], + ["media.block-autoplay-until-in-foreground", true] + ]}, resolve); + }); +}); + +add_task(function* block_autoplay_media() { + info("- open new background tab1 -"); + let tab1 = window.gBrowser.addTab("about:blank"); + tab1.linkedBrowser.loadURI(PAGE); + yield BrowserTestUtils.browserLoaded(tab1.linkedBrowser); + + info("- should block autoplay media for non-visited tab1 -"); + yield ContentTask.spawn(tab1.linkedBrowser, SuspendedType.SUSPENDED_BLOCK, + check_audio_suspended); + + info("- open new background tab2 -"); + let tab2 = window.gBrowser.addTab("about:blank"); + tab2.linkedBrowser.loadURI(PAGE); + yield BrowserTestUtils.browserLoaded(tab2.linkedBrowser); + + info("- should block autoplay for non-visited tab2 -"); + yield ContentTask.spawn(tab2.linkedBrowser, SuspendedType.SUSPENDED_BLOCK, + check_audio_suspended); + + info("- select tab1 as foreground tab -"); + yield BrowserTestUtils.switchTab(window.gBrowser, tab1); + + info("- media should be unblocked because the tab was visited -"); + yield wait_for_tab_playing_event(tab1, true); + yield ContentTask.spawn(tab1.linkedBrowser, SuspendedType.NONE_SUSPENDED, + check_audio_suspended); + + info("- open another new foreground tab3 -"); + let tab3 = yield BrowserTestUtils.openNewForegroundTab(window.gBrowser, + "about:blank"); + info("- should still play media from tab1 -"); + yield wait_for_tab_playing_event(tab1, true); + yield ContentTask.spawn(tab1.linkedBrowser, SuspendedType.NONE_SUSPENDED, + check_audio_suspended); + + info("- should still block media from tab2 -"); + yield wait_for_tab_playing_event(tab2, false); + yield ContentTask.spawn(tab2.linkedBrowser, SuspendedType.SUSPENDED_BLOCK, + check_audio_suspended); + + info("- remove tabs -"); + yield BrowserTestUtils.removeTab(tab1); + yield BrowserTestUtils.removeTab(tab2); + yield BrowserTestUtils.removeTab(tab3); +}); diff --git a/toolkit/content/tests/browser/browser_bug1170531.js b/toolkit/content/tests/browser/browser_bug1170531.js new file mode 100644 index 0000000000..49df5661aa --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug1170531.js @@ -0,0 +1,92 @@ +// Test for bug 1170531 +// https://bugzilla.mozilla.org/show_bug.cgi?id=1170531 + +add_task(function* () { + // Get a bunch of DOM nodes + let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + + let editMenu = document.getElementById("edit-menu"); + let menubar = editMenu.parentNode; + let menuPopup = editMenu.menupopup; + let editMenuIndex = -1; + for (let i = 0; i < menubar.children.length; i++) { + if (menubar.children[i] === editMenu) { + editMenuIndex = i; + break; + } + } + + let closeMenu = function(aCallback) { + if (OS.Constants.Sys.Name == "Darwin") { + executeSoon(aCallback); + return; + } + + menuPopup.addEventListener("popuphidden", function onPopupHidden() { + menuPopup.removeEventListener("popuphidden", onPopupHidden, false); + executeSoon(aCallback); + }, false); + + executeSoon(function() { + editMenu.open = false; + }); + }; + + let openMenu = function(aCallback) { + if (OS.Constants.Sys.Name == "Darwin") { + goUpdateGlobalEditMenuItems(); + // On OSX, we have a native menu, so it has to be updated. In single process browsers, + // this happens synchronously, but in e10s, we have to wait for the main thread + // to deal with it for us. 1 second should be plenty of time. + setTimeout(aCallback, 1000); + return; + } + + menuPopup.addEventListener("popupshown", function onPopupShown() { + menuPopup.removeEventListener("popupshown", onPopupShown, false); + executeSoon(aCallback); + }, false); + + executeSoon(function() { + editMenu.open = true; + }); + }; + + yield BrowserTestUtils.withNewTab({ gBrowser: gBrowser, url: "about:blank" }, function* (browser) { + let menu_cut_disabled, menu_copy_disabled; + + yield BrowserTestUtils.loadURI(browser, "data:text/html,<div>hello!</div>"); + yield BrowserTestUtils.browserLoaded(browser); + browser.focus(); + yield new Promise(resolve => waitForFocus(resolve, window)); + yield new Promise(openMenu); + menu_cut_disabled = menuPopup.querySelector("#menu_cut").getAttribute('disabled') == "true"; + is(menu_cut_disabled, false, "menu_cut should be enabled"); + menu_copy_disabled = menuPopup.querySelector("#menu_copy").getAttribute('disabled') == "true"; + is(menu_copy_disabled, false, "menu_copy should be enabled"); + yield new Promise(closeMenu); + + yield BrowserTestUtils.loadURI(browser, "data:text/html,<div contentEditable='true'>hello!</div>"); + yield BrowserTestUtils.browserLoaded(browser); + browser.focus(); + yield new Promise(resolve => waitForFocus(resolve, window)); + yield new Promise(openMenu); + menu_cut_disabled = menuPopup.querySelector("#menu_cut").getAttribute('disabled') == "true"; + is(menu_cut_disabled, false, "menu_cut should be enabled"); + menu_copy_disabled = menuPopup.querySelector("#menu_copy").getAttribute('disabled') == "true"; + is(menu_copy_disabled, false, "menu_copy should be enabled"); + yield new Promise(closeMenu); + + yield BrowserTestUtils.loadURI(browser, "about:preferences"); + yield BrowserTestUtils.browserLoaded(browser); + browser.focus(); + yield new Promise(resolve => waitForFocus(resolve, window)); + yield new Promise(openMenu); + menu_cut_disabled = menuPopup.querySelector("#menu_cut").getAttribute('disabled') == "true"; + is(menu_cut_disabled, true, "menu_cut should be disabled"); + menu_copy_disabled = menuPopup.querySelector("#menu_copy").getAttribute('disabled') == "true"; + is(menu_copy_disabled, true, "menu_copy should be disabled"); + yield new Promise(closeMenu); + }); +}); diff --git a/toolkit/content/tests/browser/browser_bug1198465.js b/toolkit/content/tests/browser/browser_bug1198465.js new file mode 100644 index 0000000000..a9cc83e121 --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug1198465.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var kPrefName = "accessibility.typeaheadfind.prefillwithselection"; +var kEmptyURI = "data:text/html,"; + +// This pref is false by default in OSX; ensure the test still works there. +Services.prefs.setBoolPref(kPrefName, true); + +registerCleanupFunction(function() { + Services.prefs.clearUserPref(kPrefName); +}); + +add_task(function* () { + let aTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, kEmptyURI); + ok(!gFindBarInitialized, "findbar isn't initialized yet"); + + // Note: the use case here is when the user types directly in the findbar + // _before_ it's prefilled with a text selection in the page. + + // So `yield BrowserTestUtils.sendChar()` can't be used here: + // - synthesizing a key in the browser won't actually send it to the + // findbar; the findbar isn't part of the browser content. + // - we need to _not_ wait for _startFindDeferred to be resolved; yielding + // a synthesized keypress on the browser implicitely happens after the + // browser has dispatched its return message with the prefill value for + // the findbar, which essentially nulls these tests. + + let findBar = gFindBar; + is(findBar._findField.value, "", "findbar is empty"); + + // Test 1 + // Any input in the findbar should erase a previous search. + + findBar._findField.value = "xy"; + findBar.startFind(); + is(findBar._findField.value, "xy", "findbar should have xy initial query"); + is(findBar._findField.mInputField, + document.activeElement, + "findbar is now focused"); + + EventUtils.sendChar("z", window); + is(findBar._findField.value, "z", "z erases xy"); + + findBar._findField.value = ""; + ok(!findBar._findField.value, "erase findbar after first test"); + + // Test 2 + // Prefilling the findbar should be ignored if a search has been run. + + findBar.startFind(); + ok(findBar._startFindDeferred, "prefilled value hasn't been fetched yet"); + is(findBar._findField.mInputField, + document.activeElement, + "findbar is still focused"); + + EventUtils.sendChar("a", window); + EventUtils.sendChar("b", window); + is(findBar._findField.value, "ab", "initial ab typed in the findbar"); + + // This resolves _startFindDeferred if it's still pending; let's just skip + // over waiting for the browser's return message that should do this as it + // doesn't really matter. + findBar.onCurrentSelection("foo", true); + ok(!findBar._startFindDeferred, "prefilled value fetched"); + is(findBar._findField.value, "ab", "ab kept instead of prefill value"); + + EventUtils.sendChar("c", window); + is(findBar._findField.value, "abc", "c is appended after ab"); + + // Clear the findField value to make the test run successfully + // for multiple runs in the same browser session. + findBar._findField.value = ""; + yield BrowserTestUtils.removeTab(aTab); +}); diff --git a/toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js b/toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js new file mode 100644 index 0000000000..958afc868e --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js @@ -0,0 +1,214 @@ +requestLongerTimeout(2); +add_task(function* () +{ + function pushPref(name, value) { + return new Promise(resolve => SpecialPowers.pushPrefEnv({"set": [[name, value]]}, resolve)); + } + + yield pushPref("general.autoScroll", true); + + const expectScrollNone = 0; + const expectScrollVert = 1; + const expectScrollHori = 2; + const expectScrollBoth = 3; + + var allTests = [ + {dataUri: 'data:text/html,<html><head><meta charset="utf-8"></head><body><style type="text/css">div { display: inline-block; }</style>\ + <div id="a" style="width: 100px; height: 100px; overflow: hidden;"><div style="width: 200px; height: 200px;"></div></div>\ + <div id="b" style="width: 100px; height: 100px; overflow: auto;"><div style="width: 200px; height: 200px;"></div></div>\ + <div id="c" style="width: 100px; height: 100px; overflow-x: auto; overflow-y: hidden;"><div style="width: 200px; height: 200px;"></div></div>\ + <div id="d" style="width: 100px; height: 100px; overflow-y: auto; overflow-x: hidden;"><div style="width: 200px; height: 200px;"></div></div>\ + <select id="e" style="width: 100px; height: 100px;" multiple="multiple"><option>aaaaaaaaaaaaaaaaaaaaaaaa</option><option>a</option><option>a</option>\ + <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option>\ + <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option></select>\ + <select id="f" style="width: 100px; height: 100px;"><option>a</option><option>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</option><option>a</option>\ + <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option>\ + <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option></select>\ + <div id="g" style="width: 99px; height: 99px; border: 10px solid black; margin: 10px; overflow: auto;"><div style="width: 100px; height: 100px;"></div></div>\ + <div id="h" style="width: 100px; height: 100px; overflow: -moz-hidden-unscrollable;"><div style="width: 200px; height: 200px;"></div></div>\ + <iframe id="iframe" style="display: none;"></iframe>\ + </body></html>'}, + {elem: 'a', expected: expectScrollNone}, + {elem: 'b', expected: expectScrollBoth}, + {elem: 'c', expected: expectScrollHori}, + {elem: 'd', expected: expectScrollVert}, + {elem: 'e', expected: expectScrollVert}, + {elem: 'f', expected: expectScrollNone}, + {elem: 'g', expected: expectScrollBoth}, + {elem: 'h', expected: expectScrollNone}, + {dataUri: 'data:text/html,<html><head><meta charset="utf-8"></head><body id="i" style="overflow-y: scroll"><div style="height: 2000px"></div>\ + <iframe id="iframe" style="display: none;"></iframe>\ + </body></html>'}, + {elem: 'i', expected: expectScrollVert}, // bug 695121 + {dataUri: 'data:text/html,<html><head><meta charset="utf-8"></head><style>html, body { width: 100%; height: 100%; overflow-x: hidden; overflow-y: scroll; }</style>\ + <body id="j"><div style="height: 2000px"></div>\ + <iframe id="iframe" style="display: none;"></iframe>\ + </body></html>'}, + {elem: 'j', expected: expectScrollVert}, // bug 914251 + {dataUri: 'data:text/html,<html><head><meta charset="utf-8">\ +<style>\ +body > div {scroll-behavior: smooth;width: 300px;height: 300px;overflow: scroll;}\ +body > div > div {width: 1000px;height: 1000px;}\ +</style>\ +</head><body><div id="t"><div></div></div></body></html>'}, + {elem: 't', expected: expectScrollBoth}, // bug 1308775 + {dataUri: 'data:text/html,<html><head><meta charset="utf-8"></head><body>\ +<div id="k" style="height: 150px; width: 200px; overflow: scroll; border: 1px solid black;">\ +<iframe style="height: 200px; width: 300px;"></iframe>\ +</div>\ +<div id="l" style="height: 150px; width: 300px; overflow: scroll; border: 1px dashed black;">\ +<iframe style="height: 200px; width: 200px;" src="data:text/html,<div style=\'border: 5px solid blue; height: 200%; width: 200%;\'></div>"></iframe>\ +</div>\ +<iframe id="m"></iframe>\ +<div style="height: 200%; border: 5px dashed black;">filler to make document overflow: scroll;</div>\ +</body></html>'}, + {elem: 'k', expected: expectScrollBoth}, + {elem: 'k', expected: expectScrollNone, testwindow: true}, + {elem: 'l', expected: expectScrollNone}, + {elem: 'm', expected: expectScrollVert, testwindow: true}, + {dataUri: 'data:text/html,<html><head><meta charset="utf-8"></head><body>\ +<img width="100" height="100" alt="image map" usemap="%23planetmap">\ +<map name="planetmap">\ + <area id="n" shape="rect" coords="0,0,100,100" href="javascript:void(null)">\ +</map>\ +<a href="javascript:void(null)" id="o" style="width: 100px; height: 100px; border: 1px solid black; display: inline-block; vertical-align: top;">link</a>\ +<input id="p" style="width: 100px; height: 100px; vertical-align: top;">\ +<textarea id="q" style="width: 100px; height: 100px; vertical-align: top;"></textarea>\ +<div style="height: 200%; border: 1px solid black;"></div>\ +</body></html>'}, + {elem: 'n', expected: expectScrollNone, testwindow: true}, + {elem: 'o', expected: expectScrollNone, testwindow: true}, + {elem: 'p', expected: expectScrollVert, testwindow: true, middlemousepastepref: false}, + {elem: 'q', expected: expectScrollVert, testwindow: true, middlemousepastepref: false}, + {dataUri: 'data:text/html,<html><head><meta charset="utf-8"></head><body>\ +<input id="r" style="width: 100px; height: 100px; vertical-align: top;">\ +<textarea id="s" style="width: 100px; height: 100px; vertical-align: top;"></textarea>\ +<div style="height: 200%; border: 1px solid black;"></div>\ +</body></html>'}, + {elem: 'r', expected: expectScrollNone, testwindow: true, middlemousepastepref: true}, + {elem: 's', expected: expectScrollNone, testwindow: true, middlemousepastepref: true} + ]; + + for (let test of allTests) { + if (test.dataUri) { + let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + gBrowser.loadURI(test.dataUri); + yield loadedPromise; + continue; + } + + let prefsChanged = (test.middlemousepastepref == false || test.middlemousepastepref == true); + if (prefsChanged) { + yield pushPref("middlemouse.paste", test.middlemousepastepref); + } + + yield BrowserTestUtils.synthesizeMouse("#" + test.elem, 50, 80, { button: 1 }, + gBrowser.selectedBrowser); + + // This ensures bug 605127 is fixed: pagehide in an unrelated document + // should not cancel the autoscroll. + yield ContentTask.spawn(gBrowser.selectedBrowser, { }, function* () { + var iframe = content.document.getElementById("iframe"); + + if (iframe) { + var e = new iframe.contentWindow.PageTransitionEvent("pagehide", + { bubbles: true, + cancelable: true, + persisted: false }); + iframe.contentDocument.dispatchEvent(e); + iframe.contentDocument.documentElement.dispatchEvent(e); + } + }); + + is(document.activeElement, gBrowser.selectedBrowser, "Browser still focused after autoscroll started"); + + yield BrowserTestUtils.synthesizeMouse("#" + test.elem, 100, 100, + { type: "mousemove", clickCount: "0" }, + gBrowser.selectedBrowser); + + if (prefsChanged) { + yield new Promise(resolve => SpecialPowers.popPrefEnv(resolve)); + } + + // Start checking for the scroll. + let firstTimestamp = undefined; + let timeCompensation; + do { + let timestamp = yield new Promise(resolve => window.requestAnimationFrame(resolve)); + if (firstTimestamp === undefined) { + firstTimestamp = timestamp; + } + + // This value is calculated similarly to the value of the same name in + // ClickEventHandler.autoscrollLoop, except here it's cumulative across + // all frames after the first one instead of being based only on the + // current frame. + timeCompensation = (timestamp - firstTimestamp) / 20; + info("timestamp=" + timestamp + " firstTimestamp=" + firstTimestamp + + " timeCompensation=" + timeCompensation); + + // Try to wait until enough time has passed to allow the scroll to happen. + // autoscrollLoop incrementally scrolls during each animation frame, but + // due to how its calculations work, when a frame is very close to the + // previous frame, no scrolling may actually occur during that frame. + // After 100ms's worth of frames, timeCompensation will be 1, making it + // more likely that the accumulated scroll in autoscrollLoop will be >= 1, + // although it also depends on acceleration, which here in this test + // should be > 1 due to how it synthesizes mouse events below. + } while (timeCompensation < 5); + + // Close the autoscroll popup by synthesizing Esc. + EventUtils.synthesizeKey("VK_ESCAPE", {}); + let scrollVert = test.expected & expectScrollVert; + let scrollHori = test.expected & expectScrollHori; + + yield ContentTask.spawn(gBrowser.selectedBrowser, + { scrollVert : scrollVert, + scrollHori: scrollHori, + elemid : test.elem, + checkWindow: test.testwindow }, + function* (args) { + let msg = ""; + if (args.checkWindow) { + if (!((args.scrollVert && content.scrollY > 0) || + (!args.scrollVert && content.scrollY == 0))) { + msg += "Failed: "; + } + msg += 'Window for ' + args.elemid + ' should' + (args.scrollVert ? '' : ' not') + ' have scrolled vertically\n'; + + if (!((args.scrollHori && content.scrollX > 0) || + (!args.scrollHori && content.scrollX == 0))) { + msg += "Failed: "; + } + msg += ' Window for ' + args.elemid + ' should' + (args.scrollHori ? '' : ' not') + ' have scrolled horizontally\n'; + } else { + let elem = content.document.getElementById(args.elemid); + if (!((args.scrollVert && elem.scrollTop > 0) || + (!args.scrollVert && elem.scrollTop == 0))) { + msg += "Failed: "; + } + msg += ' ' + args.elemid + ' should' + (args.scrollVert ? '' : ' not') + ' have scrolled vertically\n'; + if (!((args.scrollHori && elem.scrollLeft > 0) || + (!args.scrollHori && elem.scrollLeft == 0))) { + msg += "Failed: "; + } + msg += args.elemid + ' should' + (args.scrollHori ? '' : ' not') + ' have scrolled horizontally'; + } + + Assert.ok(msg.indexOf("Failed") == -1, msg); + } + ); + + // Before continuing the test, we need to ensure that the IPC + // message that stops autoscrolling has had time to arrive. + yield new Promise(resolve => executeSoon(resolve)); + } + + // remove 2 tabs that were opened by middle-click on links + while (gBrowser.visibleTabs.length > 1) { + gBrowser.removeTab(gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1]); + } + + // wait for focus to fix a failure in the next test if the latter runs too soon. + yield SimpleTest.promiseFocus(); +}); diff --git a/toolkit/content/tests/browser/browser_bug451286.js b/toolkit/content/tests/browser/browser_bug451286.js new file mode 100644 index 0000000000..a5dadeb849 --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug451286.js @@ -0,0 +1,152 @@ +Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js", this); + +add_task(function*() { + const SEARCH_TEXT = "text"; + const DATAURI = "data:text/html," + SEARCH_TEXT; + + // Bug 451286. An iframe that should be highlighted + let visible = "<iframe id='visible' src='" + DATAURI + "'></iframe>"; + + // Bug 493658. An invisible iframe that shouldn't interfere with + // highlighting matches lying after it in the document + let invisible = "<iframe id='invisible' style='display: none;' " + + "src='" + DATAURI + "'></iframe>"; + + let uri = DATAURI + invisible + SEARCH_TEXT + visible + SEARCH_TEXT; + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, uri); + let contentRect = tab.linkedBrowser.getBoundingClientRect(); + let noHighlightSnapshot = snapshotRect(window, contentRect); + ok(noHighlightSnapshot, "Got noHighlightSnapshot"); + + yield openFindBarAndWait(); + gFindBar._findField.value = SEARCH_TEXT; + yield findAgainAndWait(); + var matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) + matchCase.doCommand(); + + // Turn on highlighting + yield toggleHighlightAndWait(true); + yield closeFindBarAndWait(); + + // Take snapshot of highlighting + let findSnapshot = snapshotRect(window, contentRect); + ok(findSnapshot, "Got findSnapshot"); + + // Now, remove the highlighting, and take a snapshot to compare + // to our original state + yield openFindBarAndWait(); + yield toggleHighlightAndWait(false); + yield closeFindBarAndWait(); + + let unhighlightSnapshot = snapshotRect(window, contentRect); + ok(unhighlightSnapshot, "Got unhighlightSnapshot"); + + // Select the matches that should have been highlighted manually + yield ContentTask.spawn(tab.linkedBrowser, null, function*() { + let doc = content.document; + let win = doc.defaultView; + + // Create a manual highlight in the visible iframe to test bug 451286 + let iframe = doc.getElementById("visible"); + let ifBody = iframe.contentDocument.body; + let range = iframe.contentDocument.createRange(); + range.selectNodeContents(ifBody.childNodes[0]); + let ifWindow = iframe.contentWindow; + let ifDocShell = ifWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + + let ifController = ifDocShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + + let frameFindSelection = + ifController.getSelection(ifController.SELECTION_FIND); + frameFindSelection.addRange(range); + + // Create manual highlights in the main document (the matches that lie + // before/after the iframes + let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + + let docFindSelection = + controller.getSelection(ifController.SELECTION_FIND); + + range = doc.createRange(); + range.selectNodeContents(doc.body.childNodes[0]); + docFindSelection.addRange(range); + range = doc.createRange(); + range.selectNodeContents(doc.body.childNodes[2]); + docFindSelection.addRange(range); + range = doc.createRange(); + range.selectNodeContents(doc.body.childNodes[4]); + docFindSelection.addRange(range); + }); + + // Take snapshot of manual highlighting + let manualSnapshot = snapshotRect(window, contentRect); + ok(manualSnapshot, "Got manualSnapshot"); + + // Test 1: Were the matches in iframe correctly highlighted? + let res = compareSnapshots(findSnapshot, manualSnapshot, true); + ok(res[0], "Matches found in iframe correctly highlighted"); + + // Test 2: Were the matches in iframe correctly unhighlighted? + res = compareSnapshots(noHighlightSnapshot, unhighlightSnapshot, true); + ok(res[0], "Highlighting in iframe correctly removed"); + + yield BrowserTestUtils.removeTab(tab); +}); + +function toggleHighlightAndWait(shouldHighlight) { + return new Promise((resolve) => { + let listener = { + onFindResult() {}, + onHighlightFinished() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + }, + onMatchesCountResult() {} + }; + gFindBar.browser.finder.addResultListener(listener); + gFindBar.toggleHighlight(shouldHighlight); + }); +} + +function findAgainAndWait() { + return new Promise(resolve => { + let listener = { + onFindResult() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + }, + onHighlightFinished() {}, + onMatchesCountResult() {} + }; + gFindBar.browser.finder.addResultListener(listener); + gFindBar.onFindAgainCommand(); + }); +} + +function* openFindBarAndWait() { + let awaitTransitionEnd = BrowserTestUtils.waitForEvent(gFindBar, "transitionend"); + gFindBar.open(); + yield awaitTransitionEnd; +} + +// This test is comparing snapshots. It is necessary to wait for the gFindBar +// to close before taking the snapshot so the gFindBar does not take up space +// on the new snapshot. +function* closeFindBarAndWait() { + let awaitTransitionEnd = BrowserTestUtils.waitForEvent(gFindBar, "transitionend", false, event => { + return event.propertyName == "visibility"; + }); + gFindBar.close(); + yield awaitTransitionEnd; +} diff --git a/toolkit/content/tests/browser/browser_bug594509.js b/toolkit/content/tests/browser/browser_bug594509.js new file mode 100644 index 0000000000..e67b05f852 --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug594509.js @@ -0,0 +1,9 @@ +add_task(function* () { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:rights"); + + yield ContentTask.spawn(tab.linkedBrowser, null, function* () { + Assert.ok(content.document.getElementById("your-rights"), "about:rights content loaded"); + }); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_bug982298.js b/toolkit/content/tests/browser/browser_bug982298.js new file mode 100644 index 0000000000..047340c5ce --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug982298.js @@ -0,0 +1,70 @@ +const scrollHtml = + "<textarea id=\"textarea1\" row=2>Firefox\n\nFirefox\n\n\n\n\n\n\n\n\n\n" + + "</textarea><a href=\"about:blank\">blank</a>"; + +add_task(function*() { + let url = "data:text/html;base64," + btoa(scrollHtml); + yield BrowserTestUtils.withNewTab({gBrowser, url}, function*(browser) { + let awaitFindResult = new Promise(resolve => { + let listener = { + onFindResult(aData) { + info("got find result"); + browser.finder.removeResultListener(listener); + + ok(aData.result == Ci.nsITypeAheadFind.FIND_FOUND, "should find string"); + resolve(); + }, + onCurrentSelection() {}, + onMatchesCountResult() {} + }; + info("about to add results listener, open find bar, and send 'F' string"); + browser.finder.addResultListener(listener); + }); + gFindBar.onFindCommand(); + EventUtils.sendString("F"); + info("added result listener and sent string 'F'"); + yield awaitFindResult; + + let awaitScrollDone = BrowserTestUtils.waitForMessage(browser.messageManager, "ScrollDone"); + // scroll textarea to bottom + const scrollTest = + "var textarea = content.document.getElementById(\"textarea1\");" + + "textarea.scrollTop = textarea.scrollHeight;" + + "sendAsyncMessage(\"ScrollDone\", { });" + browser.messageManager.loadFrameScript("data:text/javascript;base64," + + btoa(scrollTest), false); + yield awaitScrollDone; + info("got ScrollDone event"); + yield BrowserTestUtils.loadURI(browser, "about:blank"); + yield BrowserTestUtils.browserLoaded(browser); + + ok(browser.currentURI.spec == "about:blank", "got load event for about:blank"); + + let awaitFindResult2 = new Promise(resolve => { + let listener = { + onFindResult(aData) { + info("got find result #2"); + browser.finder.removeResultListener(listener); + resolve(); + }, + onCurrentSelection() {}, + onMatchesCountResult() {} + }; + + browser.finder.addResultListener(listener); + info("added result listener"); + }); + // find again needs delay for crash test + setTimeout(function() { + // ignore exception if occured + try { + info("about to send find again command"); + gFindBar.onFindAgainCommand(false); + info("sent find again command"); + } catch (e) { + info("got exception from onFindAgainCommand: " + e); + } + }, 0); + yield awaitFindResult2; + }); +}); diff --git a/toolkit/content/tests/browser/browser_contentTitle.js b/toolkit/content/tests/browser/browser_contentTitle.js new file mode 100644 index 0000000000..e7966e5655 --- /dev/null +++ b/toolkit/content/tests/browser/browser_contentTitle.js @@ -0,0 +1,16 @@ +var url = "https://example.com/browser/toolkit/content/tests/browser/file_contentTitle.html"; + +add_task(function*() { + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + let browser = tab.linkedBrowser; + yield new Promise((resolve) => { + addEventListener("TestLocationChange", function listener() { + removeEventListener("TestLocationChange", listener); + resolve(); + }, true, true); + }); + + is(gBrowser.contentTitle, "Test Page", "Should have the right title."); + + gBrowser.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_content_url_annotation.js b/toolkit/content/tests/browser/browser_content_url_annotation.js new file mode 100644 index 0000000000..1a4cee4c6c --- /dev/null +++ b/toolkit/content/tests/browser/browser_content_url_annotation.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* global Services, requestLongerTimeout, TestUtils, BrowserTestUtils, + ok, info, dump, is, Ci, Cu, Components, ctypes, privateNoteIntentionalCrash, + gBrowser, add_task, addEventListener, removeEventListener, ContentTask */ + +"use strict"; + +// Running this test in ASAN is slow. +requestLongerTimeout(2); + +/** + * Removes a file from a directory. This is a no-op if the file does not + * exist. + * + * @param directory + * The nsIFile representing the directory to remove from. + * @param filename + * A string for the file to remove from the directory. + */ +function removeFile(directory, filename) { + let file = directory.clone(); + file.append(filename); + if (file.exists()) { + file.remove(false); + } +} + +/** + * Returns the directory where crash dumps are stored. + * + * @return nsIFile + */ +function getMinidumpDirectory() { + let dir = Services.dirsvc.get('ProfD', Ci.nsIFile); + dir.append("minidumps"); + return dir; +} + +/** + * Checks that the URL is correctly annotated on a content process crash. + */ +add_task(function* test_content_url_annotation() { + let url = "https://example.com/browser/toolkit/content/tests/browser/file_redirect.html"; + let redirect_url = "https://example.com/browser/toolkit/content/tests/browser/file_redirect_to.html"; + + yield BrowserTestUtils.withNewTab({ + gBrowser: gBrowser + }, function* (browser) { + ok(browser.isRemoteBrowser, "Should be a remote browser"); + + // file_redirect.html should send us to file_redirect_to.html + let promise = ContentTask.spawn(browser, {}, function* () { + dump('ContentTask starting...\n'); + yield new Promise((resolve) => { + addEventListener("RedirectDone", function listener() { + dump('Got RedirectDone\n'); + removeEventListener("RedirectDone", listener); + resolve(); + }, true, true); + }); + }); + browser.loadURI(url); + yield promise; + + // Crash the tab + let annotations = yield BrowserTestUtils.crashBrowser(browser); + + ok("URL" in annotations, "annotated a URL"); + is(annotations.URL, redirect_url, + "Should have annotated the URL after redirect"); + }); +}); diff --git a/toolkit/content/tests/browser/browser_crash_previous_frameloader.js b/toolkit/content/tests/browser/browser_crash_previous_frameloader.js new file mode 100644 index 0000000000..bd50c6ffde --- /dev/null +++ b/toolkit/content/tests/browser/browser_crash_previous_frameloader.js @@ -0,0 +1,108 @@ +"use strict"; + +/** + * Cleans up the .dmp and .extra file from a crash. + * + * @param subject (nsISupports) + * The subject passed through the ipc:content-shutdown + * observer notification when a content process crash has + * occurred. + */ +function cleanUpMinidump(subject) { + Assert.ok(subject instanceof Ci.nsIPropertyBag2, + "Subject needs to be a nsIPropertyBag2 to clean up properly"); + let dumpID = subject.getPropertyAsAString("dumpID"); + + Assert.ok(dumpID, "There should be a dumpID"); + if (dumpID) { + let dir = Services.dirsvc.get("ProfD", Ci.nsIFile); + dir.append("minidumps"); + + let file = dir.clone(); + file.append(dumpID + ".dmp"); + file.remove(true); + + file = dir.clone(); + file.append(dumpID + ".extra"); + file.remove(true); + } +} + +/** + * This test ensures that if a remote frameloader crashes after + * the frameloader owner swaps it out for a new frameloader, + * that a oop-browser-crashed event is not sent to the new + * frameloader's browser element. + */ +add_task(function* test_crash_in_previous_frameloader() { + // On debug builds, crashing tabs results in much thinking, which + // slows down the test and results in intermittent test timeouts, + // so we'll pump up the expected timeout for this test. + requestLongerTimeout(2); + + if (!gMultiProcessBrowser) { + Assert.ok(false, "This test should only be run in multi-process mode."); + return; + } + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://example.com", + }, function*(browser) { + // First, sanity check... + Assert.ok(browser.isRemoteBrowser, + "This browser needs to be remote if this test is going to " + + "work properly."); + + // We will wait for the oop-browser-crashed event to have + // a chance to appear. That event is fired when TabParents + // are destroyed, and that occurs _before_ ContentParents + // are destroyed, so we'll wait on the ipc:content-shutdown + // observer notification, which is fired when a ContentParent + // goes away. After we see this notification, oop-browser-crashed + // events should have fired. + let contentProcessGone = TestUtils.topicObserved("ipc:content-shutdown"); + let sawTabCrashed = false; + let onTabCrashed = () => { + sawTabCrashed = true; + }; + + browser.addEventListener("oop-browser-crashed", onTabCrashed); + + // The name of the game is to cause a crash in a remote browser, + // and then immediately swap out the browser for a non-remote one. + yield ContentTask.spawn(browser, null, function() { + const Cu = Components.utils; + Cu.import("resource://gre/modules/ctypes.jsm"); + Cu.import("resource://gre/modules/Timer.jsm"); + + let dies = function() { + privateNoteIntentionalCrash(); + let zero = new ctypes.intptr_t(8); + let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t)); + badptr.contents + }; + + // When the parent flips the remoteness of the browser, the + // page should receive the pagehide event, which we'll then + // use to crash the frameloader. + addEventListener("pagehide", function() { + dump("\nEt tu, Brute?\n"); + dies(); + }); + }); + + gBrowser.updateBrowserRemoteness(browser, false); + info("Waiting for content process to go away."); + let [subject, data] = yield contentProcessGone; + + // If we don't clean up the minidump, the harness will + // complain. + cleanUpMinidump(subject); + + info("Content process is gone!"); + Assert.ok(!sawTabCrashed, + "Should not have seen the oop-browser-crashed event."); + browser.removeEventListener("oop-browser-crashed", onTabCrashed); + }); +}); diff --git a/toolkit/content/tests/browser/browser_default_image_filename.js b/toolkit/content/tests/browser/browser_default_image_filename.js new file mode 100644 index 0000000000..2859d486fb --- /dev/null +++ b/toolkit/content/tests/browser/browser_default_image_filename.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +/** + * TestCase for bug 564387 + * <https://bugzilla.mozilla.org/show_bug.cgi?id=564387> + */ +add_task(function* () { + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + gBrowser.loadURI("data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7"); + yield loadPromise; + + let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown"); + + yield BrowserTestUtils.synthesizeMouseAtCenter("img", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser); + + yield popupShownPromise; + + let showFilePickerPromise = new Promise(resolve => { + MockFilePicker.showCallback = function(fp) { + is(fp.defaultString, "index.gif"); + resolve(); + } + }); + + registerCleanupFunction(function () { + MockFilePicker.cleanup(); + }); + + // Select "Save Image As" option from context menu + var saveImageAsCommand = document.getElementById("context-saveimage"); + saveImageAsCommand.doCommand(); + + yield showFilePickerPromise; + + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden"); + contextMenu.hidePopup(); + yield popupHiddenPromise; +}); diff --git a/toolkit/content/tests/browser/browser_f7_caret_browsing.js b/toolkit/content/tests/browser/browser_f7_caret_browsing.js new file mode 100644 index 0000000000..c4b6823d41 --- /dev/null +++ b/toolkit/content/tests/browser/browser_f7_caret_browsing.js @@ -0,0 +1,227 @@ +var gListener = null; +const kURL = "data:text/html;charset=utf-8,Caret browsing is fun.<input id='in'>"; + +const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled"; +const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret"; +const kPrefCaretBrowsingOn = "accessibility.browsewithcaret"; + +var oldPrefs = {}; +for (let pref of [kPrefShortcutEnabled, kPrefWarnOnEnable, kPrefCaretBrowsingOn]) { + oldPrefs[pref] = Services.prefs.getBoolPref(pref); +} + +Services.prefs.setBoolPref(kPrefShortcutEnabled, true); +Services.prefs.setBoolPref(kPrefWarnOnEnable, true); +Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false); + +registerCleanupFunction(function() { + for (let pref of [kPrefShortcutEnabled, kPrefWarnOnEnable, kPrefCaretBrowsingOn]) { + Services.prefs.setBoolPref(pref, oldPrefs[pref]); + } +}); + +// NB: not using BrowserTestUtils.domWindowOpened here because there's no way to +// undo waiting for a window open. If we don't want the window to be opened, and +// wait for it to verify that it indeed does not open, we need to be able to +// then "stop" waiting so that when we next *do* want it to open, our "old" +// listener doesn't fire and do things we don't want (like close the window...). +let gCaretPromptOpeningObserver; +function promiseCaretPromptOpened() { + return new Promise(resolve => { + function observer(subject, topic, data) { + if (topic == "domwindowopened") { + Services.ww.unregisterNotification(observer); + let win = subject.QueryInterface(Ci.nsIDOMWindow); + BrowserTestUtils.waitForEvent(win, "load", false, e => e.target.location.href != "about:blank").then(() => resolve(win)); + gCaretPromptOpeningObserver = null; + } + } + Services.ww.registerNotification(observer); + gCaretPromptOpeningObserver = observer; + }); +} + +function hitF7(async = true) { + let f7 = () => EventUtils.sendKey("F7"); + // Need to not stop execution inside this task: + if (async) { + executeSoon(f7); + } else { + f7(); + } +} + +function syncToggleCaretNoDialog(expected) { + let openedDialog = false; + promiseCaretPromptOpened().then(function(win) { + openedDialog = true; + win.close(); // This will eventually return focus here and allow the test to continue... + }); + // Cause the dialog to appear sync, if it still does. + hitF7(false); + + let expectedStr = expected ? "on." : "off."; + ok(!openedDialog, "Shouldn't open a dialog to turn caret browsing " + expectedStr); + // Need to clean up if the dialog wasn't opened, so the observer doesn't get + // re-triggered later on causing "issues". + if (!openedDialog) { + Services.ww.unregisterNotification(gCaretPromptOpeningObserver); + gCaretPromptOpeningObserver = null; + } + let prefVal = Services.prefs.getBoolPref(kPrefCaretBrowsingOn); + is(prefVal, expected, "Caret browsing should now be " + expectedStr); +} + +function waitForFocusOnInput(browser) +{ + return ContentTask.spawn(browser, null, function* () { + let textEl = content.document.getElementById("in"); + return ContentTaskUtils.waitForCondition(() => { + return content.document.activeElement == textEl; + }, "Input should get focused."); + }); +} + +function focusInput(browser) +{ + return ContentTask.spawn(browser, null, function* () { + let textEl = content.document.getElementById("in"); + textEl.focus(); + }); +} + +add_task(function* checkTogglingCaretBrowsing() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, kURL); + yield focusInput(tab.linkedBrowser); + + let promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + let prompt = yield promiseGotKey; + let doc = prompt.document; + is(doc.documentElement.defaultButton, "cancel", "No button should be the default"); + ok(!doc.getElementById("checkbox").checked, "Checkbox shouldn't be checked by default."); + let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + + doc.documentElement.cancelDialog(); + yield promiseDialogUnloaded; + info("Dialog unloaded"); + yield waitForFocusOnInput(tab.linkedBrowser); + ok(!Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should still be off after cancelling the dialog."); + + promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + prompt = yield promiseGotKey; + + doc = prompt.document; + is(doc.documentElement.defaultButton, "cancel", "No button should be the default"); + ok(!doc.getElementById("checkbox").checked, "Checkbox shouldn't be checked by default."); + promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + + doc.documentElement.acceptDialog(); + yield promiseDialogUnloaded; + info("Dialog unloaded"); + yield waitForFocusOnInput(tab.linkedBrowser); + ok(Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should be on after accepting the dialog."); + + syncToggleCaretNoDialog(false); + + promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + prompt = yield promiseGotKey; + doc = prompt.document; + + is(doc.documentElement.defaultButton, "cancel", "No button should be the default"); + ok(!doc.getElementById("checkbox").checked, "Checkbox shouldn't be checked by default."); + + promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + doc.documentElement.cancelDialog(); + yield promiseDialogUnloaded; + info("Dialog unloaded"); + yield waitForFocusOnInput(tab.linkedBrowser); + + ok(!Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should still be off after cancelling the dialog."); + + Services.prefs.setBoolPref(kPrefShortcutEnabled, true); + Services.prefs.setBoolPref(kPrefWarnOnEnable, true); + Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false); + + yield BrowserTestUtils.removeTab(tab); +}); + +add_task(function* toggleCheckboxNoCaretBrowsing() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, kURL); + yield focusInput(tab.linkedBrowser); + + let promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + let prompt = yield promiseGotKey; + let doc = prompt.document; + is(doc.documentElement.defaultButton, "cancel", "No button should be the default"); + let checkbox = doc.getElementById("checkbox"); + ok(!checkbox.checked, "Checkbox shouldn't be checked by default."); + + // Check the box: + checkbox.click(); + + let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + + // Say no: + doc.documentElement.getButton("cancel").click(); + + yield promiseDialogUnloaded; + info("Dialog unloaded"); + yield waitForFocusOnInput(tab.linkedBrowser); + ok(!Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should still be off."); + ok(!Services.prefs.getBoolPref(kPrefShortcutEnabled), "Shortcut should now be disabled."); + + syncToggleCaretNoDialog(false); + ok(!Services.prefs.getBoolPref(kPrefShortcutEnabled), "Shortcut should still be disabled."); + + Services.prefs.setBoolPref(kPrefShortcutEnabled, true); + Services.prefs.setBoolPref(kPrefWarnOnEnable, true); + Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false); + + yield BrowserTestUtils.removeTab(tab); +}); + + +add_task(function* toggleCheckboxWantCaretBrowsing() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, kURL); + yield focusInput(tab.linkedBrowser); + + let promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + let prompt = yield promiseGotKey; + let doc = prompt.document; + is(doc.documentElement.defaultButton, "cancel", "No button should be the default"); + let checkbox = doc.getElementById("checkbox"); + ok(!checkbox.checked, "Checkbox shouldn't be checked by default."); + + // Check the box: + checkbox.click(); + + let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + + // Say yes: + doc.documentElement.acceptDialog(); + yield promiseDialogUnloaded; + info("Dialog unloaded"); + yield waitForFocusOnInput(tab.linkedBrowser); + ok(Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should now be on."); + ok(Services.prefs.getBoolPref(kPrefShortcutEnabled), "Shortcut should still be enabled."); + ok(!Services.prefs.getBoolPref(kPrefWarnOnEnable), "Should no longer warn when enabling."); + + syncToggleCaretNoDialog(false); + syncToggleCaretNoDialog(true); + syncToggleCaretNoDialog(false); + + Services.prefs.setBoolPref(kPrefShortcutEnabled, true); + Services.prefs.setBoolPref(kPrefWarnOnEnable, true); + Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false); + + yield BrowserTestUtils.removeTab(tab); +}); + + + + diff --git a/toolkit/content/tests/browser/browser_findbar.js b/toolkit/content/tests/browser/browser_findbar.js new file mode 100644 index 0000000000..1ab06f6327 --- /dev/null +++ b/toolkit/content/tests/browser/browser_findbar.js @@ -0,0 +1,249 @@ +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +Components.utils.import("resource://gre/modules/Timer.jsm", this); + +const TEST_PAGE_URI = "data:text/html;charset=utf-8,The letter s."; +// Using 'javascript' schema to bypass E10SUtils.canLoadURIInProcess, because +// it does not allow 'data:' URI to be loaded in the parent process. +const E10S_PARENT_TEST_PAGE_URI = "javascript:document.write('The letter s.');"; + +/** + * Makes sure that the findbar hotkeys (' and /) event listeners + * are added to the system event group and do not get blocked + * by calling stopPropagation on a keypress event on a page. + */ +add_task(function* test_hotkey_event_propagation() { + info("Ensure hotkeys are not affected by stopPropagation."); + + // Opening new tab + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + let findbar = gBrowser.getFindBar(); + + // Pressing these keys open the findbar. + const HOTKEYS = ["/", "'"]; + + // Checking if findbar appears when any hotkey is pressed. + for (let key of HOTKEYS) { + is(findbar.hidden, true, "Findbar is hidden now."); + gBrowser.selectedTab = tab; + yield SimpleTest.promiseFocus(gBrowser.selectedBrowser); + yield BrowserTestUtils.sendChar(key, browser); + is(findbar.hidden, false, "Findbar should not be hidden."); + yield closeFindbarAndWait(findbar); + } + + // Stop propagation for all keyboard events. + let frameScript = () => { + const stopPropagation = e => e.stopImmediatePropagation(); + let window = content.document.defaultView; + window.removeEventListener("keydown", stopPropagation); + window.removeEventListener("keypress", stopPropagation); + window.removeEventListener("keyup", stopPropagation); + }; + + let mm = browser.messageManager; + mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", false); + + // Checking if findbar still appears when any hotkey is pressed. + for (let key of HOTKEYS) { + is(findbar.hidden, true, "Findbar is hidden now."); + gBrowser.selectedTab = tab; + yield SimpleTest.promiseFocus(gBrowser.selectedBrowser); + yield BrowserTestUtils.sendChar(key, browser); + is(findbar.hidden, false, "Findbar should not be hidden."); + yield closeFindbarAndWait(findbar); + } + + gBrowser.removeTab(tab); +}); + +add_task(function* test_not_found() { + info("Check correct 'Phrase not found' on new tab"); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + + // Search for the first word. + yield promiseFindFinished("--- THIS SHOULD NEVER MATCH ---", false); + let findbar = gBrowser.getFindBar(); + is(findbar._findStatusDesc.textContent, findbar._notFoundStr, + "Findbar status text should be 'Phrase not found'"); + + gBrowser.removeTab(tab); +}); + +add_task(function* test_found() { + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + + // Search for a string that WILL be found, with 'Highlight All' on + yield promiseFindFinished("S", true); + ok(!gBrowser.getFindBar()._findStatusDesc.textContent, + "Findbar status should be empty"); + + gBrowser.removeTab(tab); +}); + +// Setting first findbar to case-sensitive mode should not affect +// new tab find bar. +add_task(function* test_tabwise_case_sensitive() { + let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let findbar1 = gBrowser.getFindBar(); + + let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let findbar2 = gBrowser.getFindBar(); + + // Toggle case sensitivity for first findbar + findbar1.getElement("find-case-sensitive").click(); + + gBrowser.selectedTab = tab1; + + // Not found for first tab. + yield promiseFindFinished("S", true); + is(findbar1._findStatusDesc.textContent, findbar1._notFoundStr, + "Findbar status text should be 'Phrase not found'"); + + gBrowser.selectedTab = tab2; + + // But it didn't affect the second findbar. + yield promiseFindFinished("S", true); + ok(!findbar2._findStatusDesc.textContent, "Findbar status should be empty"); + + gBrowser.removeTab(tab1); + gBrowser.removeTab(tab2); +}); + +/** + * Navigating from a web page (for example mozilla.org) to an internal page + * (like about:addons) might trigger a change of browser's remoteness. + * 'Remoteness change' means that rendering page content moves from child + * process into the parent process or the other way around. + * This test ensures that findbar properly handles such a change. + */ +add_task(function* test_reinitialization_at_remoteness_change() { + // This test only makes sence in e10s evironment. + if (!gMultiProcessBrowser) { + info("Skipping this test because of non-e10s environment."); + return; + } + + info("Ensure findbar re-initialization at remoteness change."); + + // Load a remote page and trigger findbar construction. + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + let findbar = gBrowser.getFindBar(); + + // Findbar should operate normally. + yield promiseFindFinished("z", false); + is(findbar._findStatusDesc.textContent, findbar._notFoundStr, + "Findbar status text should be 'Phrase not found'"); + + yield promiseFindFinished("s", false); + ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty"); + + // Moving browser into the parent process and reloading sample data. + ok(browser.isRemoteBrowser, "Browser should be remote now."); + yield promiseRemotenessChange(tab, false); + yield BrowserTestUtils.loadURI(browser, E10S_PARENT_TEST_PAGE_URI); + ok(!browser.isRemoteBrowser, "Browser should not be remote any more."); + + // Findbar should keep operating normally after remoteness change. + yield promiseFindFinished("z", false); + is(findbar._findStatusDesc.textContent, findbar._notFoundStr, + "Findbar status text should be 'Phrase not found'"); + + yield promiseFindFinished("s", false); + ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty"); + + yield BrowserTestUtils.removeTab(tab); +}); + +/** + * Ensure that the initial typed characters aren't lost immediately after + * opening the find bar. + */ +add_task(function* () { + // This test only makes sence in e10s evironment. + if (!gMultiProcessBrowser) { + info("Skipping this test because of non-e10s environment."); + return; + } + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let browser = tab.linkedBrowser; + + ok(!gFindBarInitialized, "findbar isn't initialized yet"); + + let findBar = gFindBar; + let initialValue = findBar._findField.value; + + EventUtils.synthesizeKey("f", { accelKey: true }, window); + + let promises = [ + BrowserTestUtils.sendChar("a", browser), + BrowserTestUtils.sendChar("b", browser), + BrowserTestUtils.sendChar("c", browser) + ]; + + isnot(document.activeElement, findBar._findField.inputField, + "findbar is not yet focused"); + is(findBar._findField.value, initialValue, "still has initial find query"); + + yield Promise.all(promises); + is(document.activeElement, findBar._findField.inputField, + "findbar is now focused"); + is(findBar._findField.value, "abc", "abc fully entered as find query"); + + yield BrowserTestUtils.removeTab(tab); +}); + +function promiseFindFinished(searchText, highlightOn) { + let deferred = Promise.defer(); + + let findbar = gBrowser.getFindBar(); + findbar.startFind(findbar.FIND_NORMAL); + let highlightElement = findbar.getElement("highlight"); + if (highlightElement.checked != highlightOn) + highlightElement.click(); + executeSoon(() => { + findbar._findField.value = searchText; + + let resultListener; + // When highlighting is on the finder sends a second "FOUND" message after + // the search wraps. This causes timing problems with e10s. waitMore + // forces foundOrTimeout wait for the second "FOUND" message before + // resolving the promise. + let waitMore = highlightOn; + let findTimeout = setTimeout(() => foundOrTimedout(null), 2000); + let foundOrTimedout = function(aData) { + if (aData !== null && waitMore) { + waitMore = false; + return; + } + if (aData === null) + info("Result listener not called, timeout reached."); + clearTimeout(findTimeout); + findbar.browser.finder.removeResultListener(resultListener); + deferred.resolve(); + } + + resultListener = { + onFindResult: foundOrTimedout + }; + findbar.browser.finder.addResultListener(resultListener); + findbar._find(); + }); + + return deferred.promise; +} + +function promiseRemotenessChange(tab, shouldBeRemote) { + return new Promise((resolve) => { + let browser = gBrowser.getBrowserForTab(tab); + tab.addEventListener("TabRemotenessChange", function listener() { + tab.removeEventListener("TabRemotenessChange", listener); + resolve(); + }); + gBrowser.updateBrowserRemoteness(browser, shouldBeRemote); + }); +} diff --git a/toolkit/content/tests/browser/browser_isSynthetic.js b/toolkit/content/tests/browser/browser_isSynthetic.js new file mode 100644 index 0000000000..15a341461a --- /dev/null +++ b/toolkit/content/tests/browser/browser_isSynthetic.js @@ -0,0 +1,72 @@ +function LocationChangeListener(browser) { + this.browser = browser; + browser.addProgressListener(this); +} + +LocationChangeListener.prototype = { + wasSynthetic: false, + browser: null, + + destroy: function() { + this.browser.removeProgressListener(this); + }, + + onLocationChange: function(webProgress, request, location, flags) { + this.wasSynthetic = this.browser.isSyntheticDocument; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]) +} + +const FILES = gTestPath.replace("browser_isSynthetic.js", "") + .replace("chrome://mochitests/content/", "http://example.com/"); + +function waitForPageShow(browser) { + return ContentTask.spawn(browser, null, function*() { + Cu.import("resource://gre/modules/PromiseUtils.jsm"); + yield new Promise(resolve => { + let listener = () => { + removeEventListener("pageshow", listener, true); + resolve(); + } + addEventListener("pageshow", listener, true); + }); + }); +} + +add_task(function*() { + let tab = gBrowser.addTab("about:blank"); + let browser = tab.linkedBrowser; + yield BrowserTestUtils.browserLoaded(browser); + let listener = new LocationChangeListener(browser); + + is(browser.isSyntheticDocument, false, "Should not be synthetic"); + + let loadPromise = waitForPageShow(browser); + browser.loadURI("data:text/html;charset=utf-8,<html/>"); + yield loadPromise; + is(listener.wasSynthetic, false, "Should not be synthetic"); + is(browser.isSyntheticDocument, false, "Should not be synthetic"); + + loadPromise = waitForPageShow(browser); + browser.loadURI(FILES + "empty.png"); + yield loadPromise; + is(listener.wasSynthetic, true, "Should be synthetic"); + is(browser.isSyntheticDocument, true, "Should be synthetic"); + + loadPromise = waitForPageShow(browser); + browser.goBack(); + yield loadPromise; + is(listener.wasSynthetic, false, "Should not be synthetic"); + is(browser.isSyntheticDocument, false, "Should not be synthetic"); + + loadPromise = waitForPageShow(browser); + browser.goForward(); + yield loadPromise; + is(listener.wasSynthetic, true, "Should be synthetic"); + is(browser.isSyntheticDocument, true, "Should be synthetic"); + + listener.destroy(); + gBrowser.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js b/toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js new file mode 100644 index 0000000000..3fce471141 --- /dev/null +++ b/toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js @@ -0,0 +1,120 @@ +add_task(function * () +{ + const kPrefName_AutoScroll = "general.autoScroll"; + Services.prefs.setBoolPref(kPrefName_AutoScroll, true); + + const kNoKeyEvents = 0; + const kKeyDownEvent = 1; + const kKeyPressEvent = 2; + const kKeyUpEvent = 4; + const kAllKeyEvents = 7; + + var expectedKeyEvents; + var dispatchedKeyEvents; + var key; + var root; + + /** + * Encapsulates EventUtils.sendChar(). + */ + function sendChar(aChar) + { + key = aChar; + dispatchedKeyEvents = kNoKeyEvents; + EventUtils.sendChar(key); + is(dispatchedKeyEvents, expectedKeyEvents, + "unexpected key events were dispatched or not dispatched: " + key); + } + + /** + * Encapsulates EventUtils.sendKey(). + */ + function sendKey(aKey) + { + key = aKey; + dispatchedKeyEvents = kNoKeyEvents; + EventUtils.sendKey(key); + is(dispatchedKeyEvents, expectedKeyEvents, + "unexpected key events were dispatched or not dispatched: " + key); + } + + function onKey(aEvent) + { +// if (aEvent.target != root && aEvent.target != root.ownerDocument.body) { +// ok(false, "unknown target: " + aEvent.target.tagName); +// return; +// } + + var keyFlag; + switch (aEvent.type) { + case "keydown": + keyFlag = kKeyDownEvent; + break; + case "keypress": + keyFlag = kKeyPressEvent; + break; + case "keyup": + keyFlag = kKeyUpEvent; + break; + default: + ok(false, "Unknown events: " + aEvent.type); + return; + } + dispatchedKeyEvents |= keyFlag; + is(keyFlag, expectedKeyEvents & keyFlag, aEvent.type + " fired: " + key); + } + + var dataUri = 'data:text/html,<body style="height:10000px;"></body>'; + + let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + gBrowser.loadURI(dataUri); + yield loadedPromise; + + yield SimpleTest.promiseFocus(gBrowser.selectedBrowser); + + window.addEventListener("keydown", onKey, false); + window.addEventListener("keypress", onKey, false); + window.addEventListener("keyup", onKey, false); + + // Test whether the key events are handled correctly under normal condition + expectedKeyEvents = kAllKeyEvents; + sendChar("A"); + + // Start autoscrolling by middle button click on the page + let shownPromise = BrowserTestUtils.waitForEvent(window, "popupshown", false, + event => event.originalTarget.className == "autoscroller"); + yield BrowserTestUtils.synthesizeMouseAtPoint(10, 10, { button: 1 }, + gBrowser.selectedBrowser); + yield shownPromise; + + // Most key events should be eaten by the browser. + expectedKeyEvents = kNoKeyEvents; + sendChar("A"); + sendKey("DOWN"); + sendKey("RETURN"); + sendKey("RETURN"); + sendKey("HOME"); + sendKey("END"); + sendKey("TAB"); + sendKey("RETURN"); + + // Finish autoscrolling by ESC key. Note that only keydown and keypress + // events are eaten because keyup event is fired *after* the autoscrolling + // is finished. + expectedKeyEvents = kKeyUpEvent; + sendKey("ESCAPE"); + + // Test whether the key events are handled correctly under normal condition + expectedKeyEvents = kAllKeyEvents; + sendChar("A"); + + window.removeEventListener("keydown", onKey, false); + window.removeEventListener("keypress", onKey, false); + window.removeEventListener("keyup", onKey, false); + + // restore the changed prefs + if (Services.prefs.prefHasUserValue(kPrefName_AutoScroll)) + Services.prefs.clearUserPref(kPrefName_AutoScroll); + + finish(); +}); diff --git a/toolkit/content/tests/browser/browser_label_textlink.js b/toolkit/content/tests/browser/browser_label_textlink.js new file mode 100644 index 0000000000..861086707e --- /dev/null +++ b/toolkit/content/tests/browser/browser_label_textlink.js @@ -0,0 +1,38 @@ +add_task(function* () { + yield BrowserTestUtils.withNewTab({gBrowser, url: "about:config"}, function*(browser) { + let newTabURL = "http://www.example.com/"; + yield ContentTask.spawn(browser, newTabURL, function*(newTabURL) { + let doc = content.document; + let label = doc.createElement("label"); + label.href = newTabURL; + label.id = "textlink-test"; + label.className = "text-link"; + label.textContent = "click me"; + doc.documentElement.append(label); + }); + + // Test that click will open tab in foreground. + let awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL); + yield BrowserTestUtils.synthesizeMouseAtCenter("#textlink-test", {}, browser); + let newTab = yield awaitNewTab; + is(newTab.linkedBrowser, gBrowser.selectedBrowser, "selected tab should be example page"); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Test that ctrl+shift+click/meta+shift+click will open tab in background. + awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL); + yield BrowserTestUtils.synthesizeMouseAtCenter("#textlink-test", + {ctrlKey: true, metaKey: true, shiftKey: true}, + browser); + yield awaitNewTab; + is(gBrowser.selectedBrowser, browser, "selected tab should be original tab"); + yield BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + + // Middle-clicking should open tab in foreground. + awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL); + yield BrowserTestUtils.synthesizeMouseAtCenter("#textlink-test", + {button: 1}, browser); + newTab = yield awaitNewTab; + is(newTab.linkedBrowser, gBrowser.selectedBrowser, "selected tab should be example page"); + yield BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + }); +}); diff --git a/toolkit/content/tests/browser/browser_mediaPlayback.js b/toolkit/content/tests/browser/browser_mediaPlayback.js new file mode 100644 index 0000000000..1a6ebfcb83 --- /dev/null +++ b/toolkit/content/tests/browser/browser_mediaPlayback.js @@ -0,0 +1,30 @@ +const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_mediaPlayback.html"; +const FRAME = "https://example.com/browser/toolkit/content/tests/browser/file_mediaPlaybackFrame.html"; + +function wait_for_event(browser, event) { + return BrowserTestUtils.waitForEvent(browser, event, false, (event) => { + is(event.originalTarget, browser, "Event must be dispatched to correct browser."); + ok(!event.cancelable, "The event should not be cancelable"); + return true; + }); +} + +function* test_on_browser(url, browser) { + browser.loadURI(url); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + yield wait_for_event(browser, "DOMAudioPlaybackStopped"); +} + +add_task(function* test_page() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank", + }, test_on_browser.bind(undefined, PAGE)); +}); + +add_task(function* test_frame() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank", + }, test_on_browser.bind(undefined, FRAME)); +}); diff --git a/toolkit/content/tests/browser/browser_mediaPlayback_mute.js b/toolkit/content/tests/browser/browser_mediaPlayback_mute.js new file mode 100644 index 0000000000..852fc56fb1 --- /dev/null +++ b/toolkit/content/tests/browser/browser_mediaPlayback_mute.js @@ -0,0 +1,104 @@ +const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_mediaPlayback2.html"; +const FRAME = "https://example.com/browser/toolkit/content/tests/browser/file_mediaPlaybackFrame2.html"; + +function wait_for_event(browser, event) { + return BrowserTestUtils.waitForEvent(browser, event, false, (event) => { + is(event.originalTarget, browser, "Event must be dispatched to correct browser."); + return true; + }); +} + +function* test_audio_in_browser() { + function get_audio_element() { + var doc = content.document; + var list = doc.getElementsByTagName('audio'); + if (list.length == 1) { + return list[0]; + } + + // iframe? + list = doc.getElementsByTagName('iframe'); + + var iframe = list[0]; + list = iframe.contentDocument.getElementsByTagName('audio'); + return list[0]; + } + + var audio = get_audio_element(); + return { + computedVolume: audio.computedVolume, + computedMuted: audio.computedMuted + } +} + +function* test_on_browser(url, browser) { + browser.loadURI(url); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + var result = yield ContentTask.spawn(browser, null, test_audio_in_browser); + is(result.computedVolume, 1, "Audio volume is 1"); + is(result.computedMuted, false, "Audio is not muted"); + + ok(!browser.audioMuted, "Audio should not be muted by default"); + browser.mute(); + ok(browser.audioMuted, "Audio should be muted now"); + + yield wait_for_event(browser, "DOMAudioPlaybackStopped"); + + result = yield ContentTask.spawn(browser, null, test_audio_in_browser); + is(result.computedVolume, 0, "Audio volume is 0 when muted"); + is(result.computedMuted, true, "Audio is muted"); +} + +function* test_visibility(url, browser) { + browser.loadURI(url); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + var result = yield ContentTask.spawn(browser, null, test_audio_in_browser); + is(result.computedVolume, 1, "Audio volume is 1"); + is(result.computedMuted, false, "Audio is not muted"); + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank", + }, function() {}); + + ok(!browser.audioMuted, "Audio should not be muted by default"); + browser.mute(); + ok(browser.audioMuted, "Audio should be muted now"); + + yield wait_for_event(browser, "DOMAudioPlaybackStopped"); + + result = yield ContentTask.spawn(browser, null, test_audio_in_browser); + is(result.computedVolume, 0, "Audio volume is 0 when muted"); + is(result.computedMuted, true, "Audio is muted"); +} + +add_task(function*() { + yield new Promise((resolve) => { + SpecialPowers.pushPrefEnv({"set": [ + ["media.useAudioChannelService.testing", true] + ]}, resolve); + }); +}); + +add_task(function* test_page() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank", + }, test_on_browser.bind(undefined, PAGE)); +}); + +add_task(function* test_frame() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank", + }, test_on_browser.bind(undefined, FRAME)); +}); + +add_task(function* test_frame() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank", + }, test_visibility.bind(undefined, PAGE)); +}); diff --git a/toolkit/content/tests/browser/browser_mediaPlayback_suspended.js b/toolkit/content/tests/browser/browser_mediaPlayback_suspended.js new file mode 100644 index 0000000000..ef8bb9dc82 --- /dev/null +++ b/toolkit/content/tests/browser/browser_mediaPlayback_suspended.js @@ -0,0 +1,191 @@ +const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_mediaPlayback2.html"; + +var SuspendedType = { + NONE_SUSPENDED : 0, + SUSPENDED_PAUSE : 1, + SUSPENDED_BLOCK : 2, + SUSPENDED_PAUSE_DISPOSABLE : 3 +}; + +function wait_for_event(browser, event) { + return BrowserTestUtils.waitForEvent(browser, event, false, (event) => { + is(event.originalTarget, browser, "Event must be dispatched to correct browser."); + return true; + }); +} + +function check_audio_onplay() { + var list = content.document.getElementsByTagName('audio'); + if (list.length != 1) { + ok(false, "There should be only one audio element in page!") + } + + var audio = list[0]; + return new Promise((resolve, reject) => { + audio.onplay = () => { + ok(needToReceiveOnPlay, "Should not receive play event!"); + this.onplay = null; + reject(); + }; + + audio.pause(); + audio.play(); + + setTimeout(() => { + ok(true, "Doesn't receive play event when media was blocked."); + audio.onplay = null; + resolve(); + }, 1000) + }); +} + +function check_audio_suspended(suspendedType) { + var list = content.document.getElementsByTagName('audio'); + if (list.length != 1) { + ok(false, "There should be only one audio element in page!") + } + + var audio = list[0]; + is(audio.computedSuspended, suspendedType, + "The suspended state of MediaElement is correct."); +} + +function check_audio_pause_state(expectedPauseState) { + var list = content.document.getElementsByTagName('audio'); + if (list.length != 1) { + ok(false, "There should be only one audio element in page!") + } + + var audio = list[0]; + if (expectedPauseState) { + is(audio.paused, true, "Audio is paused correctly."); + } else { + is(audio.paused, false, "Audio is resumed correctly."); + } +} + +function* suspended_pause(url, browser) { + info("### Start test for suspended-pause ###"); + browser.loadURI(url); + + info("- page should have playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + info("- the suspended state of audio should be non-suspened -"); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_audio_suspended); + + info("- pause playing audio -"); + browser.pauseMedia(false /* non-disposable */); + yield ContentTask.spawn(browser, true /* expect for pause */, + check_audio_pause_state); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_PAUSE, + check_audio_suspended); + + info("- resume paused audio -"); + browser.resumeMedia(); + yield ContentTask.spawn(browser, false /* expect for playing */, + check_audio_pause_state); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_audio_suspended); +} + +function* suspended_pause_disposable(url, browser) { + info("### Start test for suspended-pause-disposable ###"); + browser.loadURI(url); + + info("- page should have playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + info("- the suspended state of audio should be non-suspened -"); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_audio_suspended); + + info("- pause playing audio -"); + browser.pauseMedia(true /* disposable */); + yield ContentTask.spawn(browser, true /* expect for pause */, + check_audio_pause_state); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_PAUSE_DISPOSABLE, + check_audio_suspended); + + info("- resume paused audio -"); + browser.resumeMedia(); + yield ContentTask.spawn(browser, false /* expect for playing */, + check_audio_pause_state); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_audio_suspended); +} + +function* suspended_stop_disposable(url, browser) { + info("### Start test for suspended-stop-disposable ###"); + browser.loadURI(url); + + info("- page should have playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + info("- the suspended state of audio should be non-suspened -"); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_audio_suspended); + + info("- stop playing audio -"); + browser.stopMedia(); + yield wait_for_event(browser, "DOMAudioPlaybackStopped"); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_audio_suspended); +} + +function* suspended_block(url, browser) { + info("### Start test for suspended-block ###"); + browser.loadURI(url); + + info("- page should have playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + info("- block playing audio -"); + browser.blockMedia(); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_BLOCK, + check_audio_suspended); + yield ContentTask.spawn(browser, null, + check_audio_onplay); + + info("- resume blocked audio -"); + browser.resumeMedia(); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_audio_suspended); +} + +add_task(function* setup_test_preference() { + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["media.useAudioChannelService.testing", true] + ]}, resolve); + }); +}); + +add_task(function* test_suspended_pause() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank" + }, suspended_pause.bind(this, PAGE)); +}); + +add_task(function* test_suspended_pause_disposable() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank" + }, suspended_pause_disposable.bind(this, PAGE)); +}); + +add_task(function* test_suspended_stop_disposable() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank" + }, suspended_stop_disposable.bind(this, PAGE)); +}); + +add_task(function* test_suspended_block() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank" + }, suspended_block.bind(this, PAGE)); +}); diff --git a/toolkit/content/tests/browser/browser_mediaPlayback_suspended_multipleAudio.js b/toolkit/content/tests/browser/browser_mediaPlayback_suspended_multipleAudio.js new file mode 100644 index 0000000000..12e2ec0774 --- /dev/null +++ b/toolkit/content/tests/browser/browser_mediaPlayback_suspended_multipleAudio.js @@ -0,0 +1,311 @@ +const PAGE = "https://example.com/browser/toolkit/content/tests/browser/file_multipleAudio.html"; + +var SuspendedType = { + NONE_SUSPENDED : 0, + SUSPENDED_PAUSE : 1, + SUSPENDED_BLOCK : 2, + SUSPENDED_PAUSE_DISPOSABLE : 3 +}; + +function wait_for_event(browser, event) { + return BrowserTestUtils.waitForEvent(browser, event, false, (event) => { + is(event.originalTarget, browser, "Event must be dispatched to correct browser."); + return true; + }); +} + +function check_all_audio_suspended(suspendedType) { + var autoPlay = content.document.getElementById('autoplay'); + var nonAutoPlay = content.document.getElementById('nonautoplay'); + if (!autoPlay || !nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(autoPlay.computedSuspended, suspendedType, + "The suspeded state of autoplay audio is correct."); + is(nonAutoPlay.computedSuspended, suspendedType, + "The suspeded state of non-autoplay audio is correct."); +} + +function check_autoplay_audio_suspended(suspendedType) { + var autoPlay = content.document.getElementById('autoplay'); + if (!autoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(autoPlay.computedSuspended, suspendedType, + "The suspeded state of autoplay audio is correct."); +} + +function check_nonautoplay_audio_suspended(suspendedType) { + var nonAutoPlay = content.document.getElementById('nonautoplay'); + if (!nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(nonAutoPlay.computedSuspended, suspendedType, + "The suspeded state of non-autoplay audio is correct."); +} + +function check_autoplay_audio_pause_state(expectedPauseState) { + var autoPlay = content.document.getElementById('autoplay'); + if (!autoPlay) { + ok(false, "Can't get the audio element!"); + } + + if (autoPlay.paused == expectedPauseState) { + if (expectedPauseState) { + ok(true, "Audio is paused correctly."); + } else { + ok(true, "Audio is resumed correctly."); + } + } else if (expectedPauseState) { + autoPlay.onpause = function () { + autoPlay.onpause = null; + ok(true, "Audio is paused correctly, checking from onpause."); + } + } else { + autoPlay.onplay = function () { + autoPlay.onplay = null; + ok(true, "Audio is resumed correctly, checking from onplay."); + } + } +} + +function play_nonautoplay_audio_should_be_paused() { + var nonAutoPlay = content.document.getElementById('nonautoplay'); + if (!nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + nonAutoPlay.play(); + return new Promise(resolve => { + nonAutoPlay.onpause = function () { + nonAutoPlay.onpause = null; + is(nonAutoPlay.ended, false, "Audio can't be playback."); + resolve(); + } + }); +} + +function all_audio_onresume() { + var autoPlay = content.document.getElementById('autoplay'); + var nonAutoPlay = content.document.getElementById('nonautoplay'); + if (!autoPlay || !nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(autoPlay.paused, false, "Autoplay audio is resumed."); + is(nonAutoPlay.paused, false, "Non-AutoPlay audio is resumed."); +} + +function all_audio_onpause() { + var autoPlay = content.document.getElementById('autoplay'); + var nonAutoPlay = content.document.getElementById('nonautoplay'); + if (!autoPlay || !nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(autoPlay.paused, true, "Autoplay audio is paused."); + is(nonAutoPlay.paused, true, "Non-AutoPlay audio is paused."); +} + +function play_nonautoplay_audio_should_play_until_ended() { + var nonAutoPlay = content.document.getElementById('nonautoplay'); + if (!nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + nonAutoPlay.play(); + return new Promise(resolve => { + nonAutoPlay.onended = function () { + nonAutoPlay.onended = null; + ok(true, "Audio can be playback until ended."); + resolve(); + } + }); +} + +function no_audio_resumed() { + var autoPlay = content.document.getElementById('autoplay'); + var nonAutoPlay = content.document.getElementById('nonautoplay'); + if (!autoPlay || !nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + is(autoPlay.paused && nonAutoPlay.paused, true, "No audio was resumed."); +} + +function play_nonautoplay_audio_should_be_blocked(suspendedType) { + var nonAutoPlay = content.document.getElementById('nonautoplay'); + if (!nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + nonAutoPlay.play(); + ok(nonAutoPlay.paused, "The blocked audio can't be playback."); +} + +function* suspended_pause(url, browser) { + info("### Start test for suspended-pause ###"); + browser.loadURI(url); + + info("- page should have playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + info("- the default suspended state of all audio should be non-suspened-"); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); + + info("- pause all audio in the page -"); + browser.pauseMedia(false /* non-disposable */); + yield ContentTask.spawn(browser, true /* expect for pause */, + check_autoplay_audio_pause_state); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_PAUSE, + check_autoplay_audio_suspended); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_nonautoplay_audio_suspended); + + info("- no audio can be playback during suspended-paused -"); + yield ContentTask.spawn(browser, null, + play_nonautoplay_audio_should_be_paused); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_PAUSE, + check_nonautoplay_audio_suspended); + + info("- both audio should be resumed at the same time -"); + browser.resumeMedia(); + yield ContentTask.spawn(browser, null, + all_audio_onresume); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); + + info("- both audio should be paused at the same time -"); + browser.pauseMedia(false /* non-disposable */); + yield ContentTask.spawn(browser, null, all_audio_onpause); +} + +function* suspended_pause_disposable(url, browser) { + info("### Start test for suspended-pause-disposable ###"); + browser.loadURI(url); + + info("- page should have playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + info("- the default suspended state of all audio should be non-suspened -"); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); + + info("- only pause playing audio in the page -"); + browser.pauseMedia(true /* non-disposable */); + yield ContentTask.spawn(browser, true /* expect for pause */, + check_autoplay_audio_pause_state); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_PAUSE_DISPOSABLE, + check_autoplay_audio_suspended); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_nonautoplay_audio_suspended); + + info("- new playing audio should be playback correctly -"); + yield ContentTask.spawn(browser, null, + play_nonautoplay_audio_should_play_until_ended); + + info("- should only resume one audio -"); + browser.resumeMedia(); + yield ContentTask.spawn(browser, false /* expect for playing */, + check_autoplay_audio_pause_state); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); +} + +function* suspended_stop_disposable(url, browser) { + info("### Start test for suspended-stop-disposable ###"); + browser.loadURI(url); + + info("- page should have playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + info("- the default suspended state of all audio should be non-suspened -"); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); + + info("- only stop playing audio in the page -"); + browser.stopMedia(); + yield wait_for_event(browser, "DOMAudioPlaybackStopped"); + yield ContentTask.spawn(browser, true /* expect for pause */, + check_autoplay_audio_pause_state); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); + + info("- new playing audio should be playback correctly -"); + yield ContentTask.spawn(browser, null, + play_nonautoplay_audio_should_play_until_ended); + + info("- no any audio can be resumed by page -"); + browser.resumeMedia(); + yield ContentTask.spawn(browser, null, no_audio_resumed); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); +} + +function* suspended_block(url, browser) { + info("### Start test for suspended-block ###"); + browser.loadURI(url); + + info("- page should have playing audio -"); + yield wait_for_event(browser, "DOMAudioPlaybackStarted"); + + info("- the default suspended state of all audio should be non-suspened-"); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); + + info("- block autoplay audio -"); + browser.blockMedia(); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_BLOCK, + check_autoplay_audio_suspended); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_nonautoplay_audio_suspended); + + info("- no audio can be playback during suspended-block -"); + yield ContentTask.spawn(browser, SuspendedType.SUSPENDED_BLOCK, + play_nonautoplay_audio_should_be_blocked); + + info("- both audio should be resumed at the same time -"); + browser.resumeMedia(); + yield ContentTask.spawn(browser, SuspendedType.NONE_SUSPENDED, + check_all_audio_suspended); +} + +add_task(function* setup_test_preference() { + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["media.useAudioChannelService.testing", true] + ]}, resolve); + }); +}); + +add_task(function* test_suspended_pause() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank" + }, suspended_pause.bind(this, PAGE)); +}); + +add_task(function* test_suspended_pause_disposable() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank" + }, suspended_pause_disposable.bind(this, PAGE)); +}); + +add_task(function* test_suspended_stop_disposable() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank" + }, suspended_stop_disposable.bind(this, PAGE)); +}); + +add_task(function* test_suspended_block() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "about:blank" + }, suspended_block.bind(this, PAGE)); +}); diff --git a/toolkit/content/tests/browser/browser_mute.js b/toolkit/content/tests/browser/browser_mute.js new file mode 100644 index 0000000000..f4829b8082 --- /dev/null +++ b/toolkit/content/tests/browser/browser_mute.js @@ -0,0 +1,16 @@ +const PAGE = "data:text/html,page"; + +function* test_on_browser(browser) { + ok(!browser.audioMuted, "Audio should not be muted by default"); + browser.mute(); + ok(browser.audioMuted, "Audio should be muted now"); + browser.unmute(); + ok(!browser.audioMuted, "Audio should be unmuted now"); +} + +add_task(function*() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: PAGE, + }, test_on_browser); +}); diff --git a/toolkit/content/tests/browser/browser_mute2.js b/toolkit/content/tests/browser/browser_mute2.js new file mode 100644 index 0000000000..38f415b71b --- /dev/null +++ b/toolkit/content/tests/browser/browser_mute2.js @@ -0,0 +1,26 @@ +const PAGE = "data:text/html,page"; + +function* test_on_browser(browser) { + ok(!browser.audioMuted, "Audio should not be muted by default"); + browser.mute(); + ok(browser.audioMuted, "Audio should be muted now"); + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: PAGE, + }, test_on_browser2); + + browser.unmute(); + ok(!browser.audioMuted, "Audio should be unmuted now"); +} + +function* test_on_browser2(browser) { + ok(!browser.audioMuted, "Audio should not be muted by default"); +} + +add_task(function*() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: PAGE, + }, test_on_browser); +}); diff --git a/toolkit/content/tests/browser/browser_quickfind_editable.js b/toolkit/content/tests/browser/browser_quickfind_editable.js new file mode 100644 index 0000000000..d4ab597448 --- /dev/null +++ b/toolkit/content/tests/browser/browser_quickfind_editable.js @@ -0,0 +1,47 @@ +const PAGE = "data:text/html,<div contenteditable>foo</div><input><textarea></textarea>"; +const DESIGNMODE_PAGE = "data:text/html,<body onload='document.designMode=\"on\";'>"; +const HOTKEYS = ["/", "'"]; + +function* test_hotkeys(browser, expected) { + let findbar = gBrowser.getFindBar(); + for (let key of HOTKEYS) { + is(findbar.hidden, true, "findbar is hidden"); + yield BrowserTestUtils.sendChar(key, gBrowser.selectedBrowser); + is(findbar.hidden, expected, "findbar should" + (expected ? "" : " not") + " be hidden"); + if (!expected) { + yield closeFindbarAndWait(findbar); + } + } +} + +function* focus_element(browser, query) { + yield ContentTask.spawn(browser, query, function* focus(query) { + let element = content.document.querySelector(query); + element.focus(); + }); +} + +add_task(function* test_hotkey_on_editable_element() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: PAGE + }, function* do_tests(browser) { + yield test_hotkeys(browser, false); + const ELEMENTS = ["div", "input", "textarea"]; + for (let elem of ELEMENTS) { + yield focus_element(browser, elem); + yield test_hotkeys(browser, true); + yield focus_element(browser, ":root"); + yield test_hotkeys(browser, false); + } + }); +}); + +add_task(function* test_hotkey_on_designMode_document() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: DESIGNMODE_PAGE + }, function* do_tests(browser) { + yield test_hotkeys(browser, true); + }); +}); diff --git a/toolkit/content/tests/browser/browser_saveImageURL.js b/toolkit/content/tests/browser/browser_saveImageURL.js new file mode 100644 index 0000000000..75e1cfdcdb --- /dev/null +++ b/toolkit/content/tests/browser/browser_saveImageURL.js @@ -0,0 +1,68 @@ +"use strict"; + +const IMAGE_PAGE = "https://example.com/browser/toolkit/content/tests/browser/image_page.html"; +const PREF_UNSAFE_FORBIDDEN = "dom.ipc.cpows.forbid-unsafe-from-browser"; + +MockFilePicker.init(window); +MockFilePicker.returnValue = MockFilePicker.returnCancel; + +registerCleanupFunction(function() { + MockFilePicker.cleanup(); +}); + +function waitForFilePicker() { + return new Promise((resolve) => { + MockFilePicker.showCallback = () => { + MockFilePicker.showCallback = null; + ok(true, "Saw the file picker"); + resolve(); + } + }) +} + +/** + * Test that saveImageURL works when we pass in the aIsContentWindowPrivate + * argument instead of a document. This is the preferred API. + */ +add_task(function* preferred_API() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: IMAGE_PAGE, + }, function*(browser) { + let url = yield ContentTask.spawn(browser, null, function*() { + let image = content.document.getElementById("image"); + return image.href; + }); + + saveImageURL(url, "image.jpg", null, true, false, null, null, null, null, false); + yield waitForFilePicker(); + }); +}); + +/** + * Test that saveImageURL will still work when passed a document instead + * of the aIsContentWindowPrivate argument. This is the deprecated API, and + * will not work in apps using remote browsers having PREF_UNSAFE_FORBIDDEN + * set to true. + */ +add_task(function* deprecated_API() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: IMAGE_PAGE, + }, function*(browser) { + yield pushPrefs([PREF_UNSAFE_FORBIDDEN, false]); + + let url = yield ContentTask.spawn(browser, null, function*() { + let image = content.document.getElementById("image"); + return image.href; + }); + + // Now get the document directly from content. If we run this test with + // e10s-enabled, this will be a CPOW, which is forbidden. We'll just + // pass the XUL document instead to test this interface. + let doc = document; + + saveImageURL(url, "image.jpg", null, true, false, null, doc, null, null); + yield waitForFilePicker(); + }); +}); diff --git a/toolkit/content/tests/browser/browser_save_resend_postdata.js b/toolkit/content/tests/browser/browser_save_resend_postdata.js new file mode 100644 index 0000000000..602a13d22b --- /dev/null +++ b/toolkit/content/tests/browser/browser_save_resend_postdata.js @@ -0,0 +1,145 @@ +/* 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/. */ + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +/** + * Test for bug 471962 <https://bugzilla.mozilla.org/show_bug.cgi?id=471962>: + * When saving an inner frame as file only, the POST data of the outer page is + * sent to the address of the inner page. + * + * Test for bug 485196 <https://bugzilla.mozilla.org/show_bug.cgi?id=485196>: + * Web page generated by POST is retried as GET when Save Frame As used, and the + * page is no longer in the cache. + */ +function test() { + waitForExplicitFinish(); + + gBrowser.loadURI("http://mochi.test:8888/browser/toolkit/content/tests/browser/data/post_form_outer.sjs"); + + gBrowser.addEventListener("pageshow", function pageShown(event) { + if (event.target.location == "about:blank") + return; + gBrowser.removeEventListener("pageshow", pageShown); + + // Submit the form in the outer page, then wait for both the outer + // document and the inner frame to be loaded again. + gBrowser.addEventListener("DOMContentLoaded", handleOuterSubmit); + gBrowser.contentDocument.getElementById("postForm").submit(); + }); + + var framesLoaded = 0; + var innerFrame; + + function handleOuterSubmit() { + if (++framesLoaded < 2) + return; + + gBrowser.removeEventListener("DOMContentLoaded", handleOuterSubmit); + + innerFrame = gBrowser.contentDocument.getElementById("innerFrame"); + + // Submit the form in the inner page. + gBrowser.addEventListener("DOMContentLoaded", handleInnerSubmit); + innerFrame.contentDocument.getElementById("postForm").submit(); + } + + function handleInnerSubmit() { + gBrowser.removeEventListener("DOMContentLoaded", handleInnerSubmit); + + // Create the folder the page will be saved into. + var destDir = createTemporarySaveDirectory(); + var file = destDir.clone(); + file.append("no_default_file_name"); + MockFilePicker.returnFiles = [file]; + MockFilePicker.showCallback = function(fp) { + MockFilePicker.filterIndex = 1; // kSaveAsType_URL + }; + + mockTransferCallback = onTransferComplete; + mockTransferRegisterer.register(); + + registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + destDir.remove(true); + }); + + var docToSave = innerFrame.contentDocument; + // We call internalSave instead of saveDocument to bypass the history + // cache. + internalSave(docToSave.location.href, docToSave, null, null, + docToSave.contentType, false, null, null, + docToSave.referrer ? makeURI(docToSave.referrer) : null, + docToSave, false, null); + } + + function onTransferComplete(downloadSuccess) { + ok(downloadSuccess, "The inner frame should have been downloaded successfully"); + + // Read the entire saved file. + var file = MockFilePicker.returnFiles[0]; + var fileContents = readShortFile(file); + + // Check if outer POST data is found (bug 471962). + is(fileContents.indexOf("inputfield=outer"), -1, + "The saved inner frame does not contain outer POST data"); + + // Check if inner POST data is found (bug 485196). + isnot(fileContents.indexOf("inputfield=inner"), -1, + "The saved inner frame was generated using the correct POST data"); + + finish(); + } +} + +Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this); + +function createTemporarySaveDirectory() { + var saveDir = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + return saveDir; +} + +/** + * Reads the contents of the provided short file (up to 1 MiB). + * + * @param aFile + * nsIFile object pointing to the file to be read. + * + * @return + * String containing the raw octets read from the file. + */ +function readShortFile(aFile) { + var inputStream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + inputStream.init(aFile, -1, 0, 0); + try { + var scrInputStream = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + scrInputStream.init(inputStream); + try { + // Assume that the file is much shorter than 1 MiB. + return scrInputStream.read(1048576); + } + finally { + // Close the scriptable stream after reading, even if the operation + // failed. + scrInputStream.close(); + } + } + finally { + // Close the stream after reading, if it is still open, even if the read + // operation failed. + inputStream.close(); + } +} diff --git a/toolkit/content/tests/browser/common/mockTransfer.js b/toolkit/content/tests/browser/common/mockTransfer.js new file mode 100644 index 0000000000..c8b8fc1616 --- /dev/null +++ b/toolkit/content/tests/browser/common/mockTransfer.js @@ -0,0 +1,67 @@ +/* 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/. */ + +Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://mochikit/content/tests/SimpleTest/MockObjects.js", this); + +var mockTransferCallback; + +/** + * This "transfer" object implementation continues the currently running test + * when the download is completed, reporting true for success or false for + * failure as the first argument of the testRunner.continueTest function. + */ +function MockTransfer() { + this._downloadIsSuccessful = true; +} + +MockTransfer.prototype = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIWebProgressListener, + Ci.nsIWebProgressListener2, + Ci.nsITransfer, + ]), + + /* nsIWebProgressListener */ + onStateChange: function MTFC_onStateChange(aWebProgress, aRequest, + aStateFlags, aStatus) { + // If at least one notification reported an error, the download failed. + if (!Components.isSuccessCode(aStatus)) + this._downloadIsSuccessful = false; + + // If the download is finished + if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && + (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) + // Continue the test, reporting the success or failure condition. + mockTransferCallback(this._downloadIsSuccessful); + }, + onProgressChange: function () {}, + onLocationChange: function () {}, + onStatusChange: function MTFC_onStatusChange(aWebProgress, aRequest, aStatus, + aMessage) { + // If at least one notification reported an error, the download failed. + if (!Components.isSuccessCode(aStatus)) + this._downloadIsSuccessful = false; + }, + onSecurityChange: function () {}, + + /* nsIWebProgressListener2 */ + onProgressChange64: function () {}, + onRefreshAttempted: function () {}, + + /* nsITransfer */ + init: function() {}, + setSha256Hash: function() {}, + setSignatureInfo: function() {} +}; + +// Create an instance of a MockObjectRegisterer whose methods can be used to +// temporarily replace the default "@mozilla.org/transfer;1" object factory with +// one that provides the mock implementation above. To activate the mock object +// factory, call the "register" method. Starting from that moment, all the +// transfer objects that are requested will be mock objects, until the +// "unregister" method is called. +var mockTransferRegisterer = + new MockObjectRegisterer("@mozilla.org/transfer;1", MockTransfer); diff --git a/toolkit/content/tests/browser/data/post_form_inner.sjs b/toolkit/content/tests/browser/data/post_form_inner.sjs new file mode 100644 index 0000000000..ce72159d81 --- /dev/null +++ b/toolkit/content/tests/browser/data/post_form_inner.sjs @@ -0,0 +1,31 @@ +/* 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/. */ + +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) +{ + var body = + '<html>\ + <body>\ + Inner POST data: '; + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bytes = [], avail = 0; + while ((avail = bodyStream.available()) > 0) + body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail)); + + body += + '<form id="postForm" action="post_form_inner.sjs" method="post">\ + <input type="text" name="inputfield" value="inner">\ + <input type="submit">\ + </form>\ + </body>\ + </html>'; + + response.bodyOutputStream.write(body, body.length); +} diff --git a/toolkit/content/tests/browser/data/post_form_outer.sjs b/toolkit/content/tests/browser/data/post_form_outer.sjs new file mode 100644 index 0000000000..89256fcfb2 --- /dev/null +++ b/toolkit/content/tests/browser/data/post_form_outer.sjs @@ -0,0 +1,34 @@ +/* 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/. */ + +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) +{ + var body = + '<html>\ + <body>\ + Outer POST data: '; + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bytes = [], avail = 0; + while ((avail = bodyStream.available()) > 0) + body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail)); + + body += + '<form id="postForm" action="post_form_outer.sjs" method="post">\ + <input type="text" name="inputfield" value="outer">\ + <input type="submit">\ + </form>\ + \ + <iframe id="innerFrame" src="post_form_inner.sjs" width="400" height="200">\ + \ + </body>\ + </html>'; + + response.bodyOutputStream.write(body, body.length); +} diff --git a/toolkit/content/tests/browser/empty.png b/toolkit/content/tests/browser/empty.png Binary files differnew file mode 100644 index 0000000000..17ddf0c3ee --- /dev/null +++ b/toolkit/content/tests/browser/empty.png diff --git a/toolkit/content/tests/browser/file_contentTitle.html b/toolkit/content/tests/browser/file_contentTitle.html new file mode 100644 index 0000000000..8d330aa0f2 --- /dev/null +++ b/toolkit/content/tests/browser/file_contentTitle.html @@ -0,0 +1,14 @@ +<html> +<head><title>Test Page</title></head> +<body> +<script type="text/javascript"> +dump("Script!\n"); +addEventListener("load", () => { + // Trigger an onLocationChange event. We want to make sure the title is still correct afterwards. + location.hash = "#x2"; + var event = new Event("TestLocationChange"); + document.dispatchEvent(event); +}, false); +</script> +</body> +</html> diff --git a/toolkit/content/tests/browser/file_mediaPlayback.html b/toolkit/content/tests/browser/file_mediaPlayback.html new file mode 100644 index 0000000000..5df0bc1542 --- /dev/null +++ b/toolkit/content/tests/browser/file_mediaPlayback.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<script type="text/javascript"> +var audio = new Audio(); +audio.oncanplay = function() { + audio.oncanplay = null; + audio.play(); +}; +audio.src = "audio.ogg"; +</script> diff --git a/toolkit/content/tests/browser/file_mediaPlayback2.html b/toolkit/content/tests/browser/file_mediaPlayback2.html new file mode 100644 index 0000000000..dffbd299b6 --- /dev/null +++ b/toolkit/content/tests/browser/file_mediaPlayback2.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<body> +<script type="text/javascript"> +var audio = new Audio(); +audio.oncanplay = function() { + audio.oncanplay = null; + audio.play(); +}; +audio.src = "audio.ogg"; +document.body.appendChild(audio); +</script> +</body> diff --git a/toolkit/content/tests/browser/file_mediaPlaybackFrame.html b/toolkit/content/tests/browser/file_mediaPlaybackFrame.html new file mode 100644 index 0000000000..119db62ecc --- /dev/null +++ b/toolkit/content/tests/browser/file_mediaPlaybackFrame.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<iframe src="file_mediaPlayback.html"></iframe> diff --git a/toolkit/content/tests/browser/file_mediaPlaybackFrame2.html b/toolkit/content/tests/browser/file_mediaPlaybackFrame2.html new file mode 100644 index 0000000000..d96a4cd4e9 --- /dev/null +++ b/toolkit/content/tests/browser/file_mediaPlaybackFrame2.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<iframe src="file_mediaPlayback2.html"></iframe> diff --git a/toolkit/content/tests/browser/file_multipleAudio.html b/toolkit/content/tests/browser/file_multipleAudio.html new file mode 100644 index 0000000000..5dc37febb4 --- /dev/null +++ b/toolkit/content/tests/browser/file_multipleAudio.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> +</head> +<body> +<audio id="autoplay" src="audio.ogg"></audio> +<audio id="nonautoplay" src="audio.ogg"></audio> +<script type="text/javascript"> + +// In linux debug on try server, sometimes the download process would fail, so +// we can't activate the "auto-play" or playing after receving "oncanplay". +// Therefore, we just call play here. +var audio = document.getElementById("autoplay"); +audio.loop = true; +audio.play(); + +</script> +</body> diff --git a/toolkit/content/tests/browser/file_multiplePlayingAudio.html b/toolkit/content/tests/browser/file_multiplePlayingAudio.html new file mode 100644 index 0000000000..ae122506fb --- /dev/null +++ b/toolkit/content/tests/browser/file_multiplePlayingAudio.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> +</head> +<body> +<audio id="audio1" src="audio.ogg" controls></audio> +<audio id="audio2" src="audio.ogg" controls></audio> +<script type="text/javascript"> + +// In linux debug on try server, sometimes the download process would fail, so +// we can't activate the "auto-play" or playing after receving "oncanplay". +// Therefore, we just call play here. +var audio1 = document.getElementById("audio1"); +audio1.loop = true; +audio1.play(); + +var audio2 = document.getElementById("audio2"); +audio2.loop = true; +audio2.play(); + +</script> +</body> diff --git a/toolkit/content/tests/browser/file_redirect.html b/toolkit/content/tests/browser/file_redirect.html new file mode 100644 index 0000000000..4d5fa9dfd1 --- /dev/null +++ b/toolkit/content/tests/browser/file_redirect.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>redirecting...</title> +<script> +window.addEventListener("load", + () => window.location = "file_redirect_to.html"); +</script> +<body> +redirectin u bro +</body> +</html> diff --git a/toolkit/content/tests/browser/file_redirect_to.html b/toolkit/content/tests/browser/file_redirect_to.html new file mode 100644 index 0000000000..28c0b53713 --- /dev/null +++ b/toolkit/content/tests/browser/file_redirect_to.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>redirected!</title> +<script> +window.addEventListener("load", () => { + var event = new Event("RedirectDone"); + document.dispatchEvent(event); +}); +</script> +<body> +u got redirected, bro +</body> +</html> diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js new file mode 100644 index 0000000000..1c6c2b54f7 --- /dev/null +++ b/toolkit/content/tests/browser/head.js @@ -0,0 +1,33 @@ +"use strict"; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + +/** + * A wrapper for the findbar's method "close", which is not synchronous + * because of animation. + */ +function closeFindbarAndWait(findbar) { + return new Promise((resolve) => { + if (findbar.hidden) { + resolve(); + return; + } + findbar.addEventListener("transitionend", function cont(aEvent) { + if (aEvent.propertyName != "visibility") { + return; + } + findbar.removeEventListener("transitionend", cont); + resolve(); + }); + findbar.close(); + }); +} + +function pushPrefs(...aPrefs) { + let deferred = Promise.defer(); + SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve); + return deferred.promise; +} diff --git a/toolkit/content/tests/browser/image.jpg b/toolkit/content/tests/browser/image.jpg Binary files differnew file mode 100644 index 0000000000..5031808ad2 --- /dev/null +++ b/toolkit/content/tests/browser/image.jpg diff --git a/toolkit/content/tests/browser/image_page.html b/toolkit/content/tests/browser/image_page.html new file mode 100644 index 0000000000..522a1d8cf9 --- /dev/null +++ b/toolkit/content/tests/browser/image_page.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>OHAI</title> +<body> +<img id="image" src="image.jpg" /> +</body> +</html> diff --git a/toolkit/content/tests/chrome/.eslintrc.js b/toolkit/content/tests/chrome/.eslintrc.js new file mode 100644 index 0000000000..2c669d844e --- /dev/null +++ b/toolkit/content/tests/chrome/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/chrome.eslintrc.js" + ] +}; diff --git a/toolkit/content/tests/chrome/RegisterUnregisterChrome.js b/toolkit/content/tests/chrome/RegisterUnregisterChrome.js new file mode 100644 index 0000000000..34f25d2f82 --- /dev/null +++ b/toolkit/content/tests/chrome/RegisterUnregisterChrome.js @@ -0,0 +1,161 @@ +/* This code is mostly copied from chrome/test/unit/head_crtestutils.js */ + +const NS_CHROME_MANIFESTS_FILE_LIST = "ChromeML"; +const XUL_CACHE_PREF = "nglayout.debug.disable_xul_cache"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; + +var gDirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIDirectoryService).QueryInterface(Ci.nsIProperties); +var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. + getService(Ci.nsIXULChromeRegistry); +var gPrefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + +// Create the temporary file in the profile, instead of in TmpD, because +// we know the mochitest harness kills off the profile when it's done. +function copyToTemporaryFile(f) +{ + let tmpd = gDirSvc.get("ProfD", Ci.nsIFile); + tmpf = tmpd.clone(); + tmpf.append("temp.manifest"); + tmpf.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + tmpf.remove(false); + f.copyTo(tmpd, tmpf.leafName); + return tmpf; +} + +function* dirIter(directory) +{ + var ioSvc = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var testsDir = ioSvc.newURI(directory, null, null) + .QueryInterface(Ci.nsIFileURL).file; + + let en = testsDir.directoryEntries; + while (en.hasMoreElements()) { + let file = en.getNext(); + yield file.QueryInterface(Ci.nsIFile); + } +} + +function getParent(path) { + let lastSlash = path.lastIndexOf("/"); + if (lastSlash == -1) { + lastSlash = path.lastIndexOf("\\"); + if (lastSlash == -1) { + return ""; + } + return '/' + path.substring(0, lastSlash).replace(/\\/g, '/'); + } + return path.substring(0, lastSlash); +} + +function copyDirToTempProfile(path, subdirname) { + + if (subdirname === undefined) { + subdirname = "mochikit-tmp"; + } + + let tmpdir = gDirSvc.get("ProfD", Ci.nsIFile); + tmpdir.append(subdirname); + tmpdir.createUnique(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o777); + + let rootDir = getParent(path); + if (rootDir == "") { + return tmpdir; + } + + // The SimpleTest directory is hidden + var files = Array.from(dirIter('file://' + rootDir)); + for (f in files) { + files[f].copyTo(tmpdir, ""); + } + return tmpdir; + +} + +function convertChromeURI(chromeURI) +{ + let uri = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService).newURI(chromeURI, null, null); + return gChromeReg.convertChromeURL(uri); +} + +function chromeURIToFile(chromeURI) +{ + var jar = getJar(chromeURI); + if (jar) { + var tmpDir = extractJarToTmp(jar); + let parts = chromeURI.split('/'); + if (parts[parts.length - 1] != '') { + tmpDir.append(parts[parts.length - 1]); + } + return tmpDir; + } + + return convertChromeURI(chromeURI). + QueryInterface(Ci.nsIFileURL).file; +} + +// Register a chrome manifest temporarily and return a function which un-does +// the registrarion when no longer needed. +function createManifestTemporarily(tempDir, manifestText) +{ + gPrefs.setBoolPref(XUL_CACHE_PREF, true); + + tempDir.append("temp.manifest"); + + let foStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + foStream.init(tempDir, + 0x02 | 0x08 | 0x20, 0o664, 0); // write, create, truncate + foStream.write(manifestText, manifestText.length); + foStream.close(); + let tempfile = copyToTemporaryFile(tempDir); + + Components.manager.QueryInterface(Ci.nsIComponentRegistrar). + autoRegister(tempfile); + + gChromeReg.refreshSkins(); + + return function() { + tempfile.fileSize = 0; // truncate the manifest + gChromeReg.checkForNewChrome(); + gChromeReg.refreshSkins(); + gPrefs.clearUserPref(XUL_CACHE_PREF); + } +} + +// Register a chrome manifest temporarily and return a function which un-does +// the registrarion when no longer needed. +function registerManifestTemporarily(manifestURI) +{ + gPrefs.setBoolPref(XUL_CACHE_PREF, true); + + let file = chromeURIToFile(manifestURI); + + let tempfile = copyToTemporaryFile(file); + Components.manager.QueryInterface(Ci.nsIComponentRegistrar). + autoRegister(tempfile); + + gChromeReg.refreshSkins(); + + return function() { + tempfile.fileSize = 0; // truncate the manifest + gChromeReg.checkForNewChrome(); + gChromeReg.refreshSkins(); + gPrefs.clearUserPref(XUL_CACHE_PREF); + } +} + +function registerManifestPermanently(manifestURI) +{ + var chromepath = chromeURIToFile(manifestURI); + + Components.manager.QueryInterface(Ci.nsIComponentRegistrar). + autoRegister(chromepath); + return chromepath; +} diff --git a/toolkit/content/tests/chrome/bug263683_window.xul b/toolkit/content/tests/chrome/bug263683_window.xul new file mode 100644 index 0000000000..46985a7adb --- /dev/null +++ b/toolkit/content/tests/chrome/bug263683_window.xul @@ -0,0 +1,210 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="263683test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="SimpleTest.executeSoon(startTest);" + title="263683 test"> + + <script type="application/javascript"><![CDATA[ + const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components; + Cu.import("resource://gre/modules/AppConstants.jsm"); + Cu.import("resource://gre/modules/Task.jsm"); + Cu.import("resource://testing-common/BrowserTestUtils.jsm"); + Cu.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.opener.wrappedJSObject); + + var gPrefsvc = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + var gFindBar = null; + var gBrowser; + + var imports = ["SimpleTest", "ok", "info", "is"]; + for (var name of imports) { + window[name] = window.opener.wrappedJSObject[name]; + } + + function startTest() { + Task.spawn(function* () { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + yield startTestWithBrowser(browserId); + } + }).then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + function* startTestWithBrowser(browserId) { + // We're bailing out when testing a remote browser on OSX 10.6, because it + // fails permanently. + if (browserId.endsWith("remote") && AppConstants.isPlatformAndVersionAtMost("macosx", 11)) { + return; + } + + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = BrowserTestUtils.browserLoaded(gBrowser); + gBrowser.loadURI('data:text/html,<h2>Text mozilla</h2><input id="inp" type="text" />'); + yield promise; + yield onDocumentLoaded(); + } + + function toggleHighlightAndWait(highlight) { + return new Promise(resolve => { + let listener = { + onHighlightFinished: function() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + gFindBar.toggleHighlight(highlight); + }); + } + + function* onDocumentLoaded() { + gFindBar.open(); + var search = "mozilla"; + gFindBar._findField.focus(); + gFindBar._findField.value = search; + var matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + matchCase.doCommand(); + } + + yield toggleHighlightAndWait(true); + gFindBar._find(); + + yield ContentTask.spawn(gBrowser, { search }, function* (args) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + Assert.ok("SELECTION_FIND" in controller, "Correctly detects new selection type"); + let selection = controller.getSelection(controller.SELECTION_FIND); + + Assert.equal(selection.rangeCount, 1, + "Correctly added a match to the selection type"); + Assert.equal(selection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + }); + + yield toggleHighlightAndWait(false); + + yield ContentTask.spawn(gBrowser, { search }, function* (args) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + let selection = controller.getSelection(controller.SELECTION_FIND); + Assert.equal(selection.rangeCount, 0, "Correctly removed the range"); + + let input = content.document.getElementById("inp"); + input.value = args.search; + }); + + yield toggleHighlightAndWait(true); + + yield ContentTask.spawn(gBrowser, { search }, function* (args) { + let input = content.document.getElementById("inp"); + let inputController = input.editor.selectionController; + let inputSelection = inputController.getSelection(inputController.SELECTION_FIND); + + Assert.equal(inputSelection.rangeCount, 1, + "Correctly added a match from input to the selection type"); + Assert.equal(inputSelection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + }); + + yield toggleHighlightAndWait(false); + + yield ContentTask.spawn(gBrowser, null, function* () { + let input = content.document.getElementById("inp"); + let inputController = input.editor.selectionController; + let inputSelection = inputController.getSelection(inputController.SELECTION_FIND); + + Assert.equal(inputSelection.rangeCount, 0, "Correctly removed the range"); + }); + + // For posterity, test iframes too. + + let promise = BrowserTestUtils.browserLoaded(gBrowser); + gBrowser.loadURI('data:text/html,<h2>Text mozilla</h2><iframe id="leframe" ' + + 'src="data:text/html,Text mozilla"></iframe>'); + yield promise; + + yield toggleHighlightAndWait(true); + + yield ContentTask.spawn(gBrowser, { search }, function* (args) { + function getSelection(docShell) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + return controller.getSelection(controller.SELECTION_FIND); + } + + let selection = getSelection(docShell); + Assert.equal(selection.rangeCount, 1, + "Correctly added a match to the selection type"); + Assert.equal(selection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + + // Check the iframe too: + let frame = content.document.getElementById("leframe"); + // Hoops! Get the docShell first, then the selection. + selection = getSelection(frame.contentWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell)); + Assert.equal(selection.rangeCount, 1, + "Correctly added a match to the selection type"); + Assert.equal(selection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + }); + + yield toggleHighlightAndWait(false); + + let matches = gFindBar._foundMatches.value.match(/([\d]*)\sof\s([\d]*)/); + is(matches[1], "2", "Found correct amount of matches") + + yield ContentTask.spawn(gBrowser, null, function* (args) { + function getSelection(docShell) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + return controller.getSelection(controller.SELECTION_FIND); + } + + let selection = getSelection(docShell); + Assert.equal(selection.rangeCount, 0, "Correctly removed the range"); + + // Check the iframe too: + let frame = content.document.getElementById("leframe"); + // Hoops! Get the docShell first, then the selection. + selection = getSelection(frame.contentWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell)); + Assert.equal(selection.rangeCount, 0, "Correctly removed the range"); + + content.document.documentElement.focus(); + }); + + gFindBar.close(true); + } + ]]></script> + + <browser type="content-primary" flex="1" id="content" src="about:blank"/> + <browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug304188_window.xul b/toolkit/content/tests/chrome/bug304188_window.xul new file mode 100644 index 0000000000..931fd5c735 --- /dev/null +++ b/toolkit/content/tests/chrome/bug304188_window.xul @@ -0,0 +1,94 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="FindbarTest for bug 304188 - +find-menu appears in editor element which has had makeEditable() called but designMode not set"> + + <script type="application/javascript"><![CDATA[ + const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components; + Cu.import("resource://gre/modules/Task.jsm"); + Cu.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.opener.wrappedJSObject); + + var gFindBar = null; + var gBrowser; + + var imports = ["SimpleTest", "ok", "info"]; + for (var name of imports) { + window[name] = window.opener.wrappedJSObject[name]; + } + + function onLoad() { + Task.spawn(function* () { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + yield startTestWithBrowser(browserId); + } + }).then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + function* startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = ContentTask.spawn(gBrowser, null, function* () { + return new Promise(resolve => { + addEventListener("DOMContentLoaded", function listener() { + removeEventListener("DOMContentLoaded", listener); + resolve(); + }); + }); + }); + gBrowser.loadURI("data:text/html;charset=utf-8,some%20random%20text"); + yield promise; + yield onDocumentLoaded(); + } + + function* onDocumentLoaded() { + yield ContentTask.spawn(gBrowser, null, function* () { + var edsession = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + edsession.makeWindowEditable(content, "html", false, true, false); + content.focus(); + }); + + yield enterStringIntoEditor("'"); + yield enterStringIntoEditor("/"); + + ok(gFindBar.hidden, + "Findfield should have stayed hidden after entering editor test"); + } + + function* enterStringIntoEditor(aString) { + for (let i = 0; i < aString.length; i++) { + yield ContentTask.spawn(gBrowser, { charCode: aString.charCodeAt(i) }, function* (args) { + let event = content.document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, args.charCode); + content.document.body.dispatchEvent(event); + }); + } + } + ]]></script> + + <browser id="content" flex="1" src="about:blank" type="content-primary"/> + <browser id="content-remote" remote="true" flex="1" src="about:blank" type="content-primary"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug331215_window.xul b/toolkit/content/tests/chrome/bug331215_window.xul new file mode 100644 index 0000000000..757ce61b8a --- /dev/null +++ b/toolkit/content/tests/chrome/bug331215_window.xul @@ -0,0 +1,102 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="331215test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="SimpleTest.executeSoon(startTest);" + title="331215 test"> + + <script type="application/javascript"><![CDATA[ + const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components; + Cu.import("resource://gre/modules/Task.jsm"); + Cu.import("resource://testing-common/BrowserTestUtils.jsm"); + Cu.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.opener.wrappedJSObject); + + var gFindBar = null; + var gBrowser; + + var imports = ["SimpleTest", "ok", "info"]; + for (var name of imports) { + window[name] = window.opener.wrappedJSObject[name]; + } + SimpleTest.requestLongerTimeout(2); + + function startTest() { + Task.spawn(function* () { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + yield startTestWithBrowser(browserId); + } + }).then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + function* startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = BrowserTestUtils.browserLoaded(gBrowser); + gBrowser.loadURI("data:text/plain,latest"); + yield promise; + yield onDocumentLoaded(); + } + + function* onDocumentLoaded() { + document.getElementById("cmd_find").doCommand(); + yield promiseEnterStringIntoFindField("test"); + document.commandDispatcher + .getControllerForCommand("cmd_moveTop") + .doCommand("cmd_moveTop"); + yield promiseEnterStringIntoFindField("l"); + ok(gFindBar._findField.getAttribute("status") == "notfound", + "Findfield status attribute should have been 'notfound' after entering test"); + yield promiseEnterStringIntoFindField("a"); + ok(gFindBar._findField.getAttribute("status") != "notfound", + "Findfield status attribute should not have been 'notfound' after entering latest"); + } + + function promiseEnterStringIntoFindField(aString) { + return new Promise(resolve => { + let listener = { + onFindResult: function(result) { + if (result.result == Ci.nsITypeAheadFind.FIND_FOUND && result.searchString != aString) + return; + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + + for (let c of aString) { + let code = c.charCodeAt(0); + let ev = new KeyboardEvent("keypress", { + keyCode: code, + charCode: code, + bubbles: true + }); + gFindBar._findField.inputField.dispatchEvent(ev); + } + }); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content-primary" flex="1" id="content" src="about:blank"/> + <browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug360437_window.xul b/toolkit/content/tests/chrome/bug360437_window.xul new file mode 100644 index 0000000000..08498b58b2 --- /dev/null +++ b/toolkit/content/tests/chrome/bug360437_window.xul @@ -0,0 +1,120 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="360437Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="startTest();" + title="360437 test"> + + <script type="application/javascript"><![CDATA[ + const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components; + Cu.import("resource://gre/modules/Task.jsm"); + Cu.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.opener.wrappedJSObject); + + var gFindBar = null; + var gBrowser; + + var imports = ["SimpleTest", "ok", "is", "info"]; + for (var name of imports) { + window[name] = window.opener.wrappedJSObject[name]; + } + + function startTest() { + Task.spawn(function* () { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + yield startTestWithBrowser(browserId); + } + }).then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + function* startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = ContentTask.spawn(gBrowser, null, function* () { + return new Promise(resolve => { + addEventListener("DOMContentLoaded", function listener() { + removeEventListener("DOMContentLoaded", listener); + resolve(); + }); + }); + }); + gBrowser.loadURI("data:text/html,<form><input id='input' type='text' value='text inside an input element'></form>"); + yield promise; + yield onDocumentLoaded(); + } + + function* onDocumentLoaded() { + gFindBar.onFindCommand(); + + // Make sure the findfield is correctly focused on open + var searchStr = "text inside an input element"; + yield promiseEnterStringIntoFindField(searchStr); + is(document.commandDispatcher.focusedElement, + gFindBar._findField.inputField, "Find field isn't focused"); + + // Make sure "find again" correctly transfers focus to the content element + // when the find bar is closed. + gFindBar.close(); + gFindBar.onFindAgainCommand(false); + yield ContentTask.spawn(gBrowser, null, function* () { + Assert.equal(content.document.activeElement, + content.document.getElementById("input"), "Input Element isn't focused"); + }); + + // Make sure "find again" doesn't focus the content element if focus + // isn't in the content document. + var textbox = document.getElementById("textbox"); + textbox.focus(); + gFindBar.close(); + gFindBar.onFindAgainCommand(false); + ok(textbox.hasAttribute("focused"), + "Focus was stolen from a chrome element"); + } + + function promiseFindResult(str = null) { + return new Promise(resolve => { + let listener = { + onFindResult: function({ searchString }) { + if (str !== null && str != searchString) { + return; + } + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + function promiseEnterStringIntoFindField(str) { + let promise = promiseFindResult(str); + for (let i = 0; i < str.length; i++) { + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, str.charCodeAt(i)); + gFindBar._findField.inputField.dispatchEvent(event); + } + return promise; + } + ]]></script> + <textbox id="textbox"/> + <browser type="content-primary" flex="1" id="content" src="about:blank"/> + <browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug366992_window.xul b/toolkit/content/tests/chrome/bug366992_window.xul new file mode 100644 index 0000000000..a1e2ae1af1 --- /dev/null +++ b/toolkit/content/tests/chrome/bug366992_window.xul @@ -0,0 +1,57 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> + +<window id="366992 test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="onLoad();" + width="600" + height="600" + title="366992 test"> + + <commandset id="editMenuCommands"/> + + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript"><![CDATA[ + // Without the fix for bug 366992, the delete command would be enabled + // for the textbox even though the textbox's controller for this command + // disables it. + var gShouldNotBeReachedController = { + supportsCommand: function(aCommand) { + return aCommand == "cmd_delete"; + }, + isCommandEnabled: function(aCommand) { + return aCommand == "cmd_delete"; + }, + doCommand: function(aCommand) { } + } + + function ok(condition, message) { + window.opener.wrappedJSObject.SimpleTest.ok(condition, message); + } + function finish() { + window.controllers.removeController(gShouldNotBeReachedController); + window.close(); + window.opener.wrappedJSObject.SimpleTest.finish(); + } + + function onLoad() { + document.getElementById("textbox").focus(); + var deleteDisabled = document.getElementById("cmd_delete") + .getAttribute("disabled") == "true"; + ok(deleteDisabled, + "cmd_delete should be disabled when the empty textbox is focused"); + finish(); + } + + window.controllers.appendController(gShouldNotBeReachedController); + ]]></script> + + <textbox id="textbox"/> +</window> diff --git a/toolkit/content/tests/chrome/bug409624_window.xul b/toolkit/content/tests/chrome/bug409624_window.xul new file mode 100644 index 0000000000..002cbe0421 --- /dev/null +++ b/toolkit/content/tests/chrome/bug409624_window.xul @@ -0,0 +1,98 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="409624test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + title="409624 test"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript"><![CDATA[ + var gFindBar = null; + var gBrowser; + + var imports = ["SimpleTest", "ok", "is"]; + for (var name of imports) { + window[name] = window.opener.wrappedJSObject[name]; + } + + function finish() { + window.close(); + SimpleTest.finish(); + } + + function startTest() { + gFindBar = document.getElementById("FindToolbar"); + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", onPageShow, false); + gBrowser.loadURI('data:text/html,<h2>Text mozilla</h2><input id="inp" type="text" />'); + } + + function onPageShow() { + gBrowser.removeEventListener("pageshow", onPageShow, false); + gFindBar.clear(); + let textbox = gFindBar.getElement("findbar-textbox"); + + // Clear should work regardless of whether the editor has been lazily + // initialised yet + ok(!gFindBar.hasTransactions, "No transactions when findbar empty"); + textbox.value = "mozilla"; + ok(gFindBar.hasTransactions, "Has transactions when findbar value set without editor init"); + gFindBar.clear(); + is(textbox.value, '', "findbar input value cleared after clear() call without editor init"); + ok(!gFindBar.hasTransactions, "No transactions after clear() call"); + + gFindBar.open(); + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden && matchCaseCheckbox.checked) + matchCaseCheckbox.click(); + ok(!matchCaseCheckbox.checked, "case-insensitivity correctly set"); + + // Simulate typical input + textbox.focus(); + gFindBar.clear(); + sendChar("m"); + ok(gFindBar.hasTransactions, "Has transactions after input"); + let preSelection = gBrowser.contentWindow.getSelection(); + ok(!preSelection.isCollapsed, "Found item and selected range"); + gFindBar.clear(); + is(textbox.value, '', "findbar input value cleared after clear() call"); + let postSelection = gBrowser.contentWindow.getSelection(); + ok(postSelection.isCollapsed, "item found deselected after clear() call"); + let fp = gFindBar.getElement("find-previous"); + ok(fp.disabled, "find-previous button disabled after clear() call"); + let fn = gFindBar.getElement("find-next"); + ok(fn.disabled, "find-next button disabled after clear() call"); + + // Test status updated after a search for text not in page + textbox.focus(); + sendChar("x"); + gFindBar.clear(); + let ftext = gFindBar.getElement("find-status"); + is(ftext.textContent, "", "status text disabled after clear() call"); + + // Test input empty with undo stack non-empty + textbox.focus(); + sendChar("m"); + sendKey("BACK_SPACE"); + ok(gFindBar.hasTransactions, "Has transactions when undo available"); + gFindBar.clear(); + gFindBar.close(); + + finish(); + } + + SimpleTest.waitForFocus(startTest, window); + ]]></script> + + <browser type="content-primary" flex="1" id="content" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug429723_window.xul b/toolkit/content/tests/chrome/bug429723_window.xul new file mode 100644 index 0000000000..28439ae8e7 --- /dev/null +++ b/toolkit/content/tests/chrome/bug429723_window.xul @@ -0,0 +1,84 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="429723Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="429723 test"> + + <script type="application/javascript"><![CDATA[ + var gFindBar = null; + var gBrowser; + + function ok(condition, message) { + window.opener.wrappedJSObject.SimpleTest.ok(condition, message); + } + + function finish() { + window.close(); + window.opener.wrappedJSObject.SimpleTest.finish(); + } + + function onLoad() { + var _delayedOnLoad = function() { + gFindBar = document.getElementById("FindToolbar"); + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", onPageShow, false); + gBrowser.loadURI("data:text/html,<h2 id='h2'>mozilla</h2>"); + } + setTimeout(_delayedOnLoad, 1000); + } + + function enterStringIntoFindField(aString) { + for (var i=0; i < aString.length; i++) { + var event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, aString.charCodeAt(i)); + gFindBar._findField.inputField.dispatchEvent(event); + } + } + + function onPageShow() { + gBrowser.removeEventListener("pageshow", onPageShow, false); + var findField = gFindBar._findField; + document.getElementById("cmd_find").doCommand(); + + var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden & matchCaseCheckbox.checked) + matchCaseCheckbox.click(); + + // Perform search + var searchStr = "z"; + enterStringIntoFindField(searchStr); + + // Highlight search term + var highlight = gFindBar.getElement("highlight"); + if (!highlight.checked) + highlight.click(); + + // Delete search term + var event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, KeyEvent.DOM_VK_BACK_SPACE, 0); + gFindBar._findField.inputField.dispatchEvent(event); + + var notRed = !findField.hasAttribute("status") || + (findField.getAttribute("status") != "notfound"); + ok(notRed, "Find Bar textbox is correct colour"); + finish(); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content-primary" flex="1" id="content" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug451540_window.xul b/toolkit/content/tests/chrome/bug451540_window.xul new file mode 100644 index 0000000000..3c08c95c9f --- /dev/null +++ b/toolkit/content/tests/chrome/bug451540_window.xul @@ -0,0 +1,248 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"?> + +<window id="451540test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + title="451540 test"> + + <script type="application/javascript"><![CDATA[ + const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components; + Cu.import("resource://gre/modules/Task.jsm"); + Cu.import("resource://testing-common/BrowserTestUtils.jsm"); + Cu.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.opener.wrappedJSObject); + const SEARCH_TEXT = "minefield"; + + let gFindBar = null; + let gPrefsvc = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + let gBrowser; + + let sendCtrl = true; + let sendMeta = false; + if (navigator.platform.indexOf("Mac") >= 0) { + sendCtrl = false; + sendMeta = true; + } + + let imports = [ "SimpleTest", "ok", "is", "info"]; + for (let name of imports) { + window[name] = window.opener.wrappedJSObject[name]; + } + + SimpleTest.requestLongerTimeout(2); + + function startTest() { + gFindBar = document.getElementById("FindToolbar"); + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", onPageShow, false); + let data = `data:text/html,<input id="inp" type="text" /> + <textarea id="tarea"/>`; + gBrowser.loadURI(data); + } + + function promiseHighlightFinished() { + return new Promise(resolve => { + let listener = { + onHighlightFinished() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + function* resetForNextTest(elementId, aText) { + if (!aText) + aText = SEARCH_TEXT; + + // Turn off highlighting + let highlightButton = gFindBar.getElement("highlight"); + if (highlightButton.checked) { + highlightButton.click(); + } + + // Initialise input + info(`setting element value to ${aText}`); + yield ContentTask.spawn(gBrowser, {elementId, aText}, function*(args) { + let {elementId, aText} = args; + let doc = content.document; + let element = doc.getElementById(elementId); + element.value = aText; + element.focus(); + }); + info(`just set element value to ${aText}`); + gFindBar._findField.value = SEARCH_TEXT; + + // Perform search and turn on highlighting + gFindBar._find(); + highlightButton.click(); + yield promiseHighlightFinished(); + + // Move caret to start of element + info(`focusing element`); + yield ContentTask.spawn(gBrowser, elementId, function*(elementId) { + let doc = content.document; + let element = doc.getElementById(elementId); + element.focus(); + }); + info(`focused element`); + if (navigator.platform.indexOf("Mac") >= 0) { + yield BrowserTestUtils.synthesizeKey("VK_LEFT", { metaKey: true }, gBrowser); + } else { + yield BrowserTestUtils.synthesizeKey("VK_HOME", {}, gBrowser); + } + } + + function* testSelection(elementId, expectedRangeCount, message) { + yield ContentTask.spawn(gBrowser, {elementId, expectedRangeCount, message}, function*(args) { + let {elementId, expectedRangeCount, message} = args; + let doc = content.document; + let element = doc.getElementById(elementId); + let controller = element.editor.selectionController; + let selection = controller.getSelection(controller.SELECTION_FIND); + Assert.equal(selection.rangeCount, expectedRangeCount, message); + }); + } + + function* testInput(elementId, testTypeText) { + let isEditableElement = yield ContentTask.spawn(gBrowser, elementId, function*(elementId) { + let doc = content.document; + let element = doc.getElementById(elementId); + return element instanceof Ci.nsIDOMNSEditableElement; + }); + if (!isEditableElement) { + return; + } + + // Initialize the findbar + let matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + matchCase.doCommand(); + } + + // First check match has been correctly highlighted + yield resetForNextTest(elementId); + + yield testSelection(elementId, 1, testTypeText + " correctly highlighted match"); + + // Test 2: check highlight removed when text added within the highlight + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + yield BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + + yield testSelection(elementId, 0, testTypeText + " correctly removed highlight on text insertion"); + + // Test 3: check highlighting remains when text added before highlight + yield resetForNextTest(elementId); + yield BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + yield testSelection(elementId, 1, testTypeText + " highlight correctly remained on text insertion at start"); + + // Test 4: check highlighting remains when text added after highlight + yield resetForNextTest(elementId); + for (let x = 0; x < SEARCH_TEXT.length; x++) { + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + } + yield BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + yield testSelection(elementId, 1, testTypeText + " highlight correctly remained on text insertion at end"); + + // Test 5: deleting text within the highlight + yield resetForNextTest(elementId); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + yield testSelection(elementId, 0, testTypeText + " correctly removed highlight on text deletion"); + + // Test 6: deleting text at end of highlight + yield resetForNextTest(elementId, SEARCH_TEXT + "A"); + for (let x = 0; x < (SEARCH_TEXT + "A").length; x++) { + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + } + yield BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + yield testSelection(elementId, 1, testTypeText + " highlight correctly remained on text deletion at end"); + + // Test 7: deleting text at start of highlight + yield resetForNextTest(elementId, "A" + SEARCH_TEXT); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + yield testSelection(elementId, 1, testTypeText + " highlight correctly remained on text deletion at start"); + + // Test 8: deleting selection + yield resetForNextTest(elementId); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { shiftKey: true }, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { shiftKey: true }, gBrowser); + yield BrowserTestUtils.synthesizeKey("x", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + yield testSelection(elementId, 0, testTypeText + " correctly removed highlight on selection deletion"); + + // Test 9: Multiple matches within one editor (part 1) + // Check second match remains highlighted after inserting text into + // first match, and that its highlighting gets removed when the + // second match is edited + yield resetForNextTest(elementId, SEARCH_TEXT + " " + SEARCH_TEXT); + yield testSelection(elementId, 2, testTypeText + " correctly highlighted both matches"); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + yield BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + yield testSelection(elementId, 1, testTypeText + " correctly removed only the first highlight on text insertion"); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_LEFT", {}, gBrowser); + yield BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + yield testSelection(elementId, 0, testTypeText + " correctly removed second highlight on text insertion"); + + // Test 10: Multiple matches within one editor (part 2) + // Check second match remains highlighted after deleting text in + // first match, and that its highlighting gets removed when the + // second match is edited + yield resetForNextTest(elementId, SEARCH_TEXT + " " + SEARCH_TEXT); + yield testSelection(elementId, 2, testTypeText + " correctly highlighted both matches"); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + yield testSelection(elementId, 1, testTypeText + " correctly removed only the first highlight on text deletion"); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_LEFT", {}, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + yield testSelection(elementId, 0, testTypeText + " correctly removed second highlight on text deletion"); + + // Test 11: Multiple matches within one editor (part 3) + // Check second match remains highlighted after deleting selection + // in first match, and that second match highlighting gets correctly + // removed when it has a selection deleted from it + yield resetForNextTest(elementId, SEARCH_TEXT + " " + SEARCH_TEXT); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { shiftKey: true }, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { shiftKey: true }, gBrowser); + yield BrowserTestUtils.synthesizeKey("x", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + yield testSelection(elementId, 1, testTypeText + " correctly removed only first highlight on selection deletion"); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_LEFT", { shiftKey: true }, gBrowser); + yield BrowserTestUtils.synthesizeKey("VK_LEFT", { shiftKey: true }, gBrowser); + yield BrowserTestUtils.synthesizeKey("x", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + yield testSelection(elementId, 0, testTypeText + " correctly removed second highlight on selection deletion"); + } + + function onPageShow() { + gBrowser.removeEventListener("load", onPageShow, true); + Task.spawn(function*() { + gFindBar.open(); + yield testInput("inp", "Input:"); + yield testInput("tarea", "Textarea:"); + }).then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForFocus(startTest, window); + ]]></script> + + <browser type="content-primary" flex="1" id="content" src="about:blank"/> + <browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug624329_window.xul b/toolkit/content/tests/chrome/bug624329_window.xul new file mode 100644 index 0000000000..efca39d3bf --- /dev/null +++ b/toolkit/content/tests/chrome/bug624329_window.xul @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Test for bug 624329 context menu position" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + context="menu"> + + <script> + opener.SimpleTest.waitForFocus(opener.childFocused, window); + </script> + + <menupopup id="menu"> + <!-- The bug demonstrated only when the accesskey was presented separately + from the label. + e.g. because the accesskey is not a letter in the label. + + The bug demonstrates only on the first show of the context menu + unless menu items are removed/added each time the menu is + constructed. --> + <menuitem label="Long label to ensure the popup would hit the right of the screen" accesskey="1"/> + </menupopup> +</window> diff --git a/toolkit/content/tests/chrome/chrome.ini b/toolkit/content/tests/chrome/chrome.ini new file mode 100644 index 0000000000..2b9be4c8ea --- /dev/null +++ b/toolkit/content/tests/chrome/chrome.ini @@ -0,0 +1,199 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + ../widgets/popup_shared.js + ../widgets/tree_shared.js + RegisterUnregisterChrome.js + bug263683_window.xul + bug304188_window.xul + bug331215_window.xul + bug360437_window.xul + bug366992_window.xul + bug409624_window.xul + bug429723_window.xul + bug624329_window.xul + dialog_dialogfocus.xul + file_about_networking_wsh.py + file_autocomplete_with_composition.js + findbar_entireword_window.xul + findbar_events_window.xul + findbar_window.xul + frame_popup_anchor.xul + frame_popupremoving_frame.xul + frame_subframe_origin_subframe1.xul + frame_subframe_origin_subframe2.xul + popup_childframe_node.xul + popup_trigger.js + sample_entireword_latin1.html + window_browser_drop.xul + window_keys.xul + window_largemenu.xul + window_panel.xul + window_popup_anchor.xul + window_popup_anchoratrect.xul + window_popup_attribute.xul + window_popup_button.xul + window_popup_preventdefault_chrome.xul + window_preferences.xul + window_preferences2.xul + window_preferences3.xul + window_preferences_commandretarget.xul + window_screenPosSize.xul + window_showcaret.xul + window_subframe_origin.xul + window_titlebar.xul + window_tooltip.xul + xul_selectcontrol.js + rtlchrome/rtl.css + rtlchrome/rtl.dtd + rtlchrome/rtl.manifest + rtltest/righttoleft.manifest + rtltest/content/dirtest.xul + +[test_about_networking.html] +[test_arrowpanel.xul] +[test_autocomplete2.xul] +[test_autocomplete3.xul] +[test_autocomplete4.xul] +[test_autocomplete5.xul] +[test_autocomplete_delayOnPaste.xul] +subsuite = clipboard +[test_autocomplete_emphasis.xul] +[test_autocomplete_with_composition_on_input.html] +[test_autocomplete_with_composition_on_textbox.xul] +[test_autocomplete_placehold_last_complete.xul] +[test_browser_drop.xul] +[test_bug253481.xul] +subsuite = clipboard +[test_bug263683.xul] +[test_bug304188.xul] +[test_bug331215.xul] +[test_bug360220.xul] +[test_bug360437.xul] +skip-if = os == 'linux' # Bug 1264604 +[test_bug365773.xul] +[test_bug366992.xul] +[test_bug382990.xul] +[test_bug409624.xul] +[test_bug418874.xul] +[test_bug429723.xul] +[test_bug437844.xul] +[test_bug457632.xul] +[test_bug460942.xul] +[test_bug471776.xul] +[test_bug509732.xul] +[test_bug554279.xul] +[test_bug557987.xul] +[test_bug562554.xul] +[test_bug570192.xul] +[test_bug585946.xul] +[test_bug624329.xul] +skip-if = (os == 'mac' && os_version == '10.10') # Unexpectedly perma-passes on OSX 10.10 +[test_bug792324.xul] +[test_bug1048178.xul] +skip-if = toolkit == "cocoa" +[test_button.xul] +[test_closemenu_attribute.xul] +[test_colorpicker_popup.xul] +[test_contextmenu_list.xul] +[test_datepicker.xul] +[test_deck.xul] +[test_dialogfocus.xul] +[test_findbar.xul] +subsuite = clipboard +[test_findbar_entireword.xul] +[test_findbar_events.xul] +[test_focus_anons.xul] +[test_hiddenitems.xul] +[test_hiddenpaging.xul] +[test_keys.xul] +[test_labelcontrol.xul] +[test_largemenu.xul] +skip-if = os == 'linux' && !debug #Bug 1207174 +[test_menu.xul] +[test_menu_anchored.xul] +[test_menu_hide.xul] +[test_menuchecks.xul] +[test_menuitem_blink.xul] +[test_menuitem_commands.xul] +[test_menulist.xul] +[test_menulist_keynav.xul] +[test_menulist_null_value.xul] +[test_menulist_paging.xul] +[test_menulist_position.xul] +[test_mousescroll.xul] +[test_notificationbox.xul] +[test_panel.xul] +[test_panelfrommenu.xul] +[test_popup_anchor.xul] +[test_popup_anchoratrect.xul] +skip-if = os == 'linux' # 1167694 +[test_popup_attribute.xul] +skip-if = os == 'linux' && asan #Bug 1131634 +[test_popup_button.xul] +skip-if = os == 'linux' && asan # Bug 1281360 +[test_popup_coords.xul] +[test_popup_keys.xul] +[test_popup_moveToAnchor.xul] +[test_popup_preventdefault.xul] +[test_popup_preventdefault_chrome.xul] +[test_popup_recreate.xul] +[test_popup_scaled.xul] +[test_popup_tree.xul] +[test_popuphidden.xul] +[test_popupincontent.xul] +[test_popupremoving.xul] +[test_popupremoving_frame.xul] +[test_position.xul] +[test_preferences.xul] +[test_preferences_beforeaccept.xul] +support-files = window_preferences_beforeaccept.xul +[test_preferences_onsyncfrompreference.xul] +support-files = window_preferences_onsyncfrompreference.xul +[test_progressmeter.xul] +[test_props.xul] +[test_radio.xul] +[test_richlist_direction.xul] +[test_righttoleft.xul] +[test_scale.xul] +[test_scaledrag.xul] +[test_screenPersistence.xul] +[test_scrollbar.xul] +[test_showcaret.xul] +[test_sorttemplate.xul] +[test_statusbar.xul] +[test_subframe_origin.xul] +[test_tabbox.xul] +[test_tabindex.xul] +[test_textbox_dictionary.xul] +[test_textbox_emptytext.xul] +[test_textbox_number.xul] +[test_textbox_search.xul] +[test_timepicker.xul] +[test_titlebar.xul] +skip-if = os == "linux" +[test_toolbar.xul] +[test_tooltip.xul] +skip-if = (os == 'mac' && os_version == '10.10') # Bug 1141245, frequent timeouts on OSX 10.10 +[test_tooltip_noautohide.xul] +[test_tree.xul] +[test_tree_hier.xul] +[test_tree_hier_cell.xul] +[test_tree_single.xul] +[test_tree_view.xul] +# test_panel_focus.xul won't work if the Full Keyboard Access preference is set to +# textboxes and lists only, so skip this test on Mac +[test_panel_focus.xul] +support-files = window_panel_focus.xul +skip-if = toolkit == "cocoa" +[test_chromemargin.xul] +support-files = window_chromemargin.xul +skip-if = toolkit == "cocoa" +[test_bug451540.xul] +support-files = bug451540_window.xul +[test_autocomplete_mac_caret.xul] +skip-if = toolkit != "cocoa" +[test_cursorsnap.xul] +disabled = +#skip-if = os != "win" +support-files = window_cursorsnap_dialog.xul window_cursorsnap_wizard.xul diff --git a/toolkit/content/tests/chrome/dialog_dialogfocus.xul b/toolkit/content/tests/chrome/dialog_dialogfocus.xul new file mode 100644 index 0000000000..770695ed3e --- /dev/null +++ b/toolkit/content/tests/chrome/dialog_dialogfocus.xul @@ -0,0 +1,57 @@ +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<dialog buttons="extra2,accept,cancel" onload="loaded()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<tabbox id="tabbox" hidden="true"> + <tabs> + <tab id="tab" label="Tab"/> + </tabs> + <tabpanels> + <tabpanel> + <button id="tabbutton" label="Tab Button"/> + <button id="tabbutton2" label="Tab Button 2"/> + </tabpanel> + </tabpanels> +</tabbox> + +<textbox id="textbox-yes" value="textbox-yes" hidden="true"/> +<textbox id="textbox-no" value="textbox-no" noinitialfocus="true" hidden="true"/> +<button id="one" label="One"/> +<button id="two" label="Two" hidden="true"/> + +<script> +function loaded() +{ + if (window.arguments) { + var step = window.arguments[0]; + switch (step) { + case 2: + document.getElementById("one").setAttribute("noinitialfocus", "true"); + break; + case 3: + document.getElementById("one").hidden = true; + case 4: + document.getElementById("tabbutton2").setAttribute("noinitialfocus", "true"); + case 5: + document.getElementById("tabbutton").setAttribute("noinitialfocus", "true"); + case 6: + document.getElementById("tabbox").hidden = false; + break; + case 7: + var two = document.getElementById("two"); + two.hidden = false; + two.focus(); + break; + case 8: + document.getElementById("textbox-yes").hidden = false; + break; + case 9: + document.getElementById("textbox-no").hidden = false; + break; + } + } +} +</script> + +</dialog> diff --git a/toolkit/content/tests/chrome/file_about_networking_wsh.py b/toolkit/content/tests/chrome/file_about_networking_wsh.py new file mode 100644 index 0000000000..17ad250e56 --- /dev/null +++ b/toolkit/content/tests/chrome/file_about_networking_wsh.py @@ -0,0 +1,9 @@ +from mod_pywebsocket import msgutil + +def web_socket_do_extra_handshake(request): + pass + +def web_socket_transfer_data(request): + while not request.client_terminated: + msgutil.receive_message(request) + diff --git a/toolkit/content/tests/chrome/file_autocomplete_with_composition.js b/toolkit/content/tests/chrome/file_autocomplete_with_composition.js new file mode 100644 index 0000000000..881e772ad3 --- /dev/null +++ b/toolkit/content/tests/chrome/file_autocomplete_with_composition.js @@ -0,0 +1,540 @@ +// nsDoTestsForAutoCompleteWithComposition tests autocomplete with composition. +// Users must include SimpleTest.js and EventUtils.js. + +function waitForCondition(condition, nextTest) { + var tries = 0; + var interval = setInterval(function() { + if (condition() || tries >= 30) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} + +function nsDoTestsForAutoCompleteWithComposition(aDescription, + aWindow, + aTarget, + aAutoCompleteController, + aIsFunc, + aGetTargetValueFunc, + aOnFinishFunc) +{ + this._description = aDescription; + this._window = aWindow; + this._target = aTarget; + this._controller = aAutoCompleteController; + + this._is = aIsFunc; + this._getTargetValue = aGetTargetValueFunc; + this._onFinish = aOnFinishFunc; + + this._target.focus(); + + this._DefaultCompleteDefaultIndex = + this._controller.input.completeDefaultIndex; + + this._doTests(); +} + +nsDoTestsForAutoCompleteWithComposition.prototype = { + _window: null, + _target: null, + _controller: null, + _DefaultCompleteDefaultIndex: false, + _description: "", + + _is: null, + _getTargetValue: function () { return "not initialized"; }, + _onFinish: null, + + _doTests: function () + { + if (++this._testingIndex == this._tests.length) { + this._controller.input.completeDefaultIndex = + this._DefaultCompleteDefaultIndex; + this._onFinish(); + return; + } + + var test = this._tests[this._testingIndex]; + if (this._controller.input.completeDefaultIndex != test.completeDefaultIndex) { + this._controller.input.completeDefaultIndex = test.completeDefaultIndex; + } + test.execute(this._window); + + waitForCondition(() => { + return this._controller.searchStatus >= + Components.interfaces.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH; + }, + this._checkResult.bind(this)); + }, + + _checkResult: function () + { + var test = this._tests[this._testingIndex]; + this._is(this._getTargetValue(), test.value, + this._description + ", " + test.description + ": value"); + this._is(this._controller.searchString, test.searchString, + this._description + ", " + test.description +": searchString"); + this._is(this._controller.input.popupOpen, test.popup, + this._description + ", " + test.description + ": popupOpen"); + this._doTests(); + }, + + _testingIndex: -1, + _tests: [ + // Simple composition when popup hasn't been shown. + // The autocomplete popup should not be shown during composition, but + // after compositionend, the popup should be shown. + { description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "M", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "M", code: "KeyM", keyCode: KeyboardEvent.DOM_VK_M, + shiftKey: true }, + }, aWindow); + }, popup: false, value: "M", searchString: "" + }, + { description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "Mo", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "o", code: "KeyO", keyCode: KeyboardEvent.DOM_VK_O }, + }, aWindow); + }, popup: false, value: "Mo", searchString: "" + }, + { description: "compositionend should open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Enter", code: "Enter" } }, aWindow); + }, popup: true, value: "Mo", searchString: "Mo" + }, + // If composition starts when popup is shown, the compositionstart event + // should cause closing the popup. + { description: "compositionstart should close the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "z", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "z", code: "KeyZ", keyCode: KeyboardEvent.DOM_VK_Z }, + }, aWindow); + }, popup: false, value: "Moz", searchString: "Mo" + }, + { description: "modifying composition string shouldn't reopen the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "zi", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "i", code: "KeyI", keyCode: KeyboardEvent.DOM_VK_I }, + }, aWindow); + }, popup: false, value: "Mozi", searchString: "Mo" + }, + { description: "compositionend should research the result and open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Enter", code: "Enter" } }, aWindow); + }, popup: true, value: "Mozi", searchString: "Mozi" + }, + // If composition is cancelled, the value shouldn't be changed. + { description: "compositionstart should reclose the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "l", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "l", code: "KeyL", keyCode: KeyboardEvent.DOM_VK_L }, + }, aWindow); + }, popup: false, value: "Mozil", searchString: "Mozi" + }, + { description: "modifying composition string shouldn't reopen the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "ll", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "l", code: "KeyL", keyCode: KeyboardEvent.DOM_VK_L }, + }, aWindow); + }, popup: false, value: "Mozill", searchString: "Mozi" + }, + { description: "modifying composition string to empty string shouldn't reopen the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 } + }, aWindow); + }, popup: false, value: "Mozi", searchString: "Mozi" + }, + { description: "cancled compositionend should reopen the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeComposition({ type: "compositioncommit", data: "", + key: { key: "KEY_Escape", code: "Escape" } }, aWindow); + }, popup: true, value: "Mozi", searchString: "Mozi" + }, + // But if composition replaces some characters and canceled, the search + // string should be the latest value. + { description: "compositionstart with selected string should close the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeKey("VK_LEFT", { shiftKey: true }, aWindow); + synthesizeKey("VK_LEFT", { shiftKey: true }, aWindow); + synthesizeCompositionChange( + { "composition": + { "string": "z", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "z", code: "KeyZ", keyCode: KeyboardEvent.DOM_VK_Z }, + }, aWindow); + }, popup: false, value: "Moz", searchString: "Mozi" + }, + { description: "modifying composition string shouldn't reopen the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "zi", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "i", code: "KeyI", keyCode: KeyboardEvent.DOM_VK_I }, + }, aWindow); + }, popup: false, value: "Mozi", searchString: "Mozi" + }, + { description: "modifying composition string to empty string shouldn't reopen the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 } + }, aWindow); + }, popup: false, value: "Mo", searchString: "Mozi" + }, + { description: "canceled compositionend should search the result with the latest value", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Escape", code: "Escape" } }, aWindow); + }, popup: true, value: "Mo", searchString: "Mo" + }, + // If all characters are removed, the popup should be closed. + { description: "the value becomes empty by backspace, the popup should be closed", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + }, popup: false, value: "", searchString: "" + }, + // composition which is canceled shouldn't cause opening the popup. + { description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "M", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "m", code: "KeyM", keyCode: KeyboardEvent.DOM_VK_M, + shiftKey: true }, + }, aWindow); + }, popup: false, value: "M", searchString: "" + }, + { description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "Mo", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "o", code: "KeyO", keyCode: KeyboardEvent.DOM_VK_O }, + }, aWindow); + }, popup: false, value: "Mo", searchString: "" + }, + { description: "modifying composition string to empty string shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 } + }, aWindow); + }, popup: false, value: "", searchString: "" + }, + { description: "canceled compositionend shouldn't open the popup if it was closed", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Escape", code: "Escape" } }, aWindow); + }, popup: false, value: "", searchString: "" + }, + // Down key should open the popup even if the editor is empty. + { description: "DOWN key should open the popup even if the value is empty", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeKey("VK_DOWN", {}, aWindow); + }, popup: true, value: "", searchString: "" + }, + // If popup is open at starting composition, the popup should be reopened + // after composition anyway. + { description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "M", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "M", code: "KeyM", keyCode: KeyboardEvent.DOM_VK_M, + shiftKey: true }, + }, aWindow); + }, popup: false, value: "M", searchString: "" + }, + { description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "Mo", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "o", code: "KeyO", keyCode: KeyboardEvent.DOM_VK_O }, + }, aWindow); + }, popup: false, value: "Mo", searchString: "" + }, + { description: "modifying composition string to empty string shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 } + }, aWindow); + }, popup: false, value: "", searchString: "" + }, + { description: "canceled compositionend should open the popup if it was opened", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Escape", code: "Escape" } }, aWindow); + }, popup: true, value: "", searchString: "" + }, + // Type normally, and hit escape, the popup should be closed. + { description: "ESCAPE should close the popup after typing something", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeKey("M", { shiftKey: true }, aWindow); + synthesizeKey("o", { shiftKey: true }, aWindow); + synthesizeKey("VK_ESCAPE", {}, aWindow); + }, popup: false, value: "Mo", searchString: "Mo" + }, + // Even if the popup is closed, composition which is canceled should open + // the popup if the value isn't empty. + // XXX This might not be good behavior, but anyway, this is minor issue... + { description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "z", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "z", code: "KeyZ", keyCode: KeyboardEvent.DOM_VK_Z }, + }, aWindow); + }, popup: false, value: "Moz", searchString: "Mo" + }, + { description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "zi", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "i", code: "KeyI", keyCode: KeyboardEvent.DOM_VK_I }, + }, aWindow); + }, popup: false, value: "Mozi", searchString: "Mo" + }, + { description: "modifying composition string to empty string shouldn't open the popup", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "", + "clauses": + [ + { "length": 0, "attr": 0 } + ] + }, + "caret": { "start": 0, "length": 0 } + }, aWindow); + }, popup: false, value: "Mo", searchString: "Mo" + }, + { description: "canceled compositionend shouldn't open the popup if the popup was closed", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Escape", code: "Escape" } }, aWindow); + }, popup: true, value: "Mo", searchString: "Mo" + }, + // House keeping... + { description: "house keeping for next tests", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + }, popup: false, value: "", searchString: "" + }, + // Testing for nsIAutoCompleteInput.completeDefaultIndex being true. + { description: "compositionstart shouldn't open the popup (completeDefaultIndex is true)", + completeDefaultIndex: true, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "M", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 1, "length": 0 }, + "key": { key: "M", code: "KeyM", keyCode: KeyboardEvent.DOM_VK_M, + shiftKey: true }, + }, aWindow); + }, popup: false, value: "M", searchString: "" + }, + { description: "modifying composition string shouldn't open the popup (completeDefaultIndex is true)", + completeDefaultIndex: true, + execute: function (aWindow) { + synthesizeCompositionChange( + { "composition": + { "string": "Mo", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + "key": { key: "o", code: "KeyO", keyCode: KeyboardEvent.DOM_VK_O }, + }, aWindow); + }, popup: false, value: "Mo", searchString: "" + }, + { description: "compositionend should open the popup (completeDefaultIndex is true)", + completeDefaultIndex: true, + execute: function (aWindow) { + synthesizeComposition({ type: "compositioncommitasis", + key: { key: "KEY_Enter", code: "Enter" } }, aWindow); + }, popup: true, value: "Mozilla", searchString: "Mo" + }, + // House keeping... + { description: "house keeping for next tests", + completeDefaultIndex: false, + execute: function (aWindow) { + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + synthesizeKey("VK_BACK_SPACE", {}, aWindow); + }, popup: false, value: "", searchString: "" + } + ] +}; diff --git a/toolkit/content/tests/chrome/findbar_entireword_window.xul b/toolkit/content/tests/chrome/findbar_entireword_window.xul new file mode 100644 index 0000000000..f0da61081d --- /dev/null +++ b/toolkit/content/tests/chrome/findbar_entireword_window.xul @@ -0,0 +1,275 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"?> + +<window id="FindbarEntireWordTest" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="findbar test - entire words only"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js"/> + + <script type="application/javascript"><![CDATA[ + const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components; + Cu.import("resource://gre/modules/Task.jsm"); + Cu.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.opener.wrappedJSObject); + + var gFindBar = null; + var gBrowser; + + var imports = ["SimpleTest", "SpecialPowers", "ok", "is", "isnot", "info"]; + for (var name of imports) { + window[name] = window.opener.wrappedJSObject[name]; + } + SimpleTest.requestLongerTimeout(2); + + const kBaseURL = "chrome://mochitests/content/chrome/toolkit/content/tests/chrome"; + const kTests = { + latin1: { + testSimpleEntireWord: { + "and": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'and' should've been found"); + is(results.matches.total, 6, "should've found 6 matches"); + }, + "an": results => { + is(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'an' shouldn't have been found"); + is(results.matches.total, 0, "should've found 0 matches"); + }, + "darkness": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'darkness' should've been found"); + is(results.matches.total, 3, "should've found 3 matches"); + }, + "mammon": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'mammon' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + }, + testCaseSensitive: { + "And": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'And' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + }, + "and": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'and' should've been found"); + is(results.matches.total, 5, "should've found 5 matches"); + }, + "Mammon": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'mammon' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + }, + testWordBreakChars: { + "a": results => { + // 'a' is a common charactar, but there should only be one occurrence + // separated by word boundaries. + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'a' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + }, + "quarrelled": results => { + // 'quarrelled' is denoted as a word by a period char. + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'quarrelled' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + }, + testQuickfind: { + "and": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'and' should've been found"); + is(results.matches.total, 6, "should've found 6 matches"); + }, + "an": results => { + is(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'an' shouldn't have been found"); + is(results.matches.total, 0, "should've found 0 matches"); + }, + "darkness": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'darkness' should've been found"); + is(results.matches.total, 3, "should've found 3 matches"); + }, + "mammon": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'mammon' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + } + } + }; + + function onLoad() { + Task.spawn(function* () { + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { set: [["findbar.entireword", true]] }, resolve); + }); + + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + // XXXmikedeboer: when multiple test samples are available, make this + // a nested loop that iterates over them. For now, only + // latin is available. + yield startTestWithBrowser("latin1", browserId); + } + }).then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + function* startTestWithBrowser(testName, browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + + let promise = ContentTask.spawn(gBrowser, null, function* () { + return new Promise(resolve => { + addEventListener("DOMContentLoaded", function listener() { + removeEventListener("DOMContentLoaded", listener); + resolve(); + }); + }); + }); + gBrowser.loadURI(kBaseURL + "/sample_entireword_" + testName + ".html"); + yield promise; + yield onDocumentLoaded(testName); + } + + function* onDocumentLoaded(testName) { + let suite = kTests[testName]; + yield testSimpleEntireWord(suite.testSimpleEntireWord); + yield testCaseSensitive(suite.testCaseSensitive); + yield testWordBreakChars(suite.testWordBreakChars); + yield testQuickfind(suite.testQuickfind); + } + + var enterStringIntoFindField = Task.async(function* (str, waitForResult = true) { + for (let promise, i = 0; i < str.length; i++) { + if (waitForResult) { + promise = promiseFindResult(); + } + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, str.charCodeAt(i)); + gFindBar._findField.inputField.dispatchEvent(event); + if (waitForResult) { + yield promise; + } + } + }); + + function openFindbar() { + document.getElementById("cmd_find").doCommand(); + return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise; + } + + function promiseFindResult(searchString) { + return new Promise(resolve => { + let data = {}; + let listener = { + onFindResult: res => { + if (searchString && res.searchString != searchString) + return; + + gFindBar.browser.finder.removeResultListener(listener); + data.find = res; + if (res.result == Ci.nsITypeAheadFind.FIND_NOTFOUND) { + data.matches = { total: 0, current: 0 }; + resolve(data); + return; + } + listener = { + onMatchesCountResult: res => { + gFindBar.browser.finder.removeResultListener(listener); + data.matches = res; + resolve(data); + } + }; + gFindBar.browser.finder.addResultListener(listener); + } + }; + + gFindBar.browser.finder.addResultListener(listener); + }); + } + + function* testIterator(tests) { + for (let searchString of Object.getOwnPropertyNames(tests)) { + gFindBar.clear(); + + let promise = promiseFindResult(searchString); + + yield enterStringIntoFindField(searchString, false); + + let result = yield promise; + tests[searchString](result); + } + } + + function* testSimpleEntireWord(tests) { + yield openFindbar(); + ok(!gFindBar.hidden, "testSimpleEntireWord: findbar should be open"); + + yield* testIterator(tests); + + gFindBar.close(); + } + + function* testCaseSensitive(tests) { + yield openFindbar(); + ok(!gFindBar.hidden, "testCaseSensitive: findbar should be open"); + + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden && !matchCaseCheckbox.checked) + matchCaseCheckbox.click(); + + yield* testIterator(tests); + + if (!matchCaseCheckbox.hidden) + matchCaseCheckbox.click(); + gFindBar.close(); + } + + function* testWordBreakChars(tests) { + yield openFindbar(); + ok(!gFindBar.hidden, "testWordBreakChars: findbar should be open"); + + yield* testIterator(tests); + + gFindBar.close(); + } + + function* testQuickfind(tests) { + yield ContentTask.spawn(gBrowser, null, function* () { + let document = content.document; + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, "/".charCodeAt(0)); + document.documentElement.dispatchEvent(event); + }); + + ok(!gFindBar.hidden, "testQuickfind: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField, + "testQuickfind: find field is not focused"); + ok(!gFindBar.getElement("entire-word-status").hidden, + "testQuickfind: entire word mode status text should be visible"); + + yield* testIterator(tests); + + gFindBar.close(); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content-primary" flex="1" id="content" src="about:blank"/> + <browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/findbar_events_window.xul b/toolkit/content/tests/chrome/findbar_events_window.xul new file mode 100644 index 0000000000..2bfc52c146 --- /dev/null +++ b/toolkit/content/tests/chrome/findbar_events_window.xul @@ -0,0 +1,173 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="FindbarTest" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="SimpleTest.executeSoon(startTest);" + title="findbar events test"> + + <script type="application/javascript"><![CDATA[ + const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components; + Cu.import("resource://gre/modules/Task.jsm"); + Cu.import("resource://testing-common/BrowserTestUtils.jsm"); + Cu.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.opener.wrappedJSObject); + + var gFindBar = null; + var gBrowser; + const kTimeout = 5000; // 5 seconds. + + var imports = ["SimpleTest", "ok", "is", "info"]; + for (var name of imports) { + window[name] = window.opener.wrappedJSObject[name]; + } + SimpleTest.requestLongerTimeout(2); + + function startTest() { + Task.spawn(function* () { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + yield startTestWithBrowser(browserId); + } + }).then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + function* startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = BrowserTestUtils.browserLoaded(gBrowser); + gBrowser.loadURI("data:text/html,hello there"); + yield promise; + yield onDocumentLoaded(); + } + + function* onDocumentLoaded() { + gFindBar.open(); + gFindBar.onFindCommand(); + + yield testFind(); + yield testFindAgain(); + yield testCaseSensitivity(); + yield testHighlight(); + } + + function checkSelection() { + return new Promise(resolve => { + SimpleTest.executeSoon(() => { + ContentTask.spawn(gBrowser, null, function* () { + let selected = content.getSelection(); + Assert.equal(String(selected), "", "No text is selected"); + + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + let selection = controller.getSelection(controller.SELECTION_FIND); + Assert.equal(selection.rangeCount, 0, "No text is highlighted"); + }).then(resolve); + }); + }); + } + + function once(node, eventName, preventDefault = true) { + return new Promise((resolve, reject) => { + let timeout = window.setTimeout(() => { + reject("Event wasn't fired within " + kTimeout + "ms for event '" + + eventName + "'."); + }, kTimeout); + + node.addEventListener(eventName, function clb(e) { + window.clearTimeout(timeout); + node.removeEventListener(eventName, clb); + if (preventDefault) + e.preventDefault(); + resolve(e); + }); + }); + } + + function* testFind() { + info("Testing normal find."); + let query = "t"; + let promise = once(gFindBar, "find"); + + // Put some text in the find box. + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, query.charCodeAt(0)); + gFindBar._findField.inputField.dispatchEvent(event); + + let e = yield promise; + ok(e.detail.query === query, "find event query should match '" + query + "'"); + // Since we're preventing the default make sure nothing was selected. + yield checkSelection(); + } + + function testFindAgain() { + info("Testing repeating normal find."); + let promise = once(gFindBar, "findagain"); + + gFindBar.onFindAgainCommand(); + + yield promise; + // Since we're preventing the default make sure nothing was selected. + yield checkSelection(); + } + + function* testCaseSensitivity() { + info("Testing normal case sensitivity."); + let promise = once(gFindBar, "findcasesensitivitychange", false); + + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + matchCaseCheckbox.click(); + + let e = yield promise; + ok(e.detail.caseSensitive, "find should be case sensitive"); + + // Toggle it back to the original setting. + matchCaseCheckbox.click(); + + // Changing case sensitivity does the search so clear the selected text + // before the next test. + yield ContentTask.spawn(gBrowser, null, () => content.getSelection().removeAllRanges()); + } + + function* testHighlight() { + info("Testing find with highlight all."); + // Update the find state so the highlight button is clickable. + gFindBar.updateControlState(Ci.nsITypeAheadFind.FIND_FOUND, false); + + let promise = once(gFindBar, "findhighlightallchange"); + + let highlightButton = gFindBar.getElement("highlight"); + if (!highlightButton.checked) + highlightButton.click(); + + let e = yield promise; + ok(e.detail.highlightAll, "find event should have highlight all set"); + // Since we're preventing the default make sure nothing was highlighted. + yield checkSelection(); + + // Toggle it back to the original setting. + if (highlightButton.checked) + highlightButton.click(); + } + ]]></script> + + <browser type="content-primary" flex="1" id="content" src="about:blank"/> + <browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/findbar_window.xul b/toolkit/content/tests/chrome/findbar_window.xul new file mode 100644 index 0000000000..f17f760fe0 --- /dev/null +++ b/toolkit/content/tests/chrome/findbar_window.xul @@ -0,0 +1,756 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="FindbarTest" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="findbar test"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript"><![CDATA[ + const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components; + Cu.import("resource://gre/modules/AppConstants.jsm"); + Cu.import("resource://gre/modules/Task.jsm"); + Cu.import("resource://testing-common/BrowserTestUtils.jsm"); + Cu.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.opener.wrappedJSObject); + + var gPrefsvc = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + const SAMPLE_URL = "http://www.mozilla.org/"; + const SAMPLE_TEXT = "Some text in a text field."; + const SEARCH_TEXT = "Text Test"; + const NOT_FOUND_TEXT = "This text is not on the page." + const ITERATOR_TIMEOUT = gPrefsvc.getIntPref("findbar.iteratorTimeout"); + + var gFindBar = null; + var gBrowser; + + var gClipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); + var gHasFindClipboard = gClipboard.supportsFindClipboard(); + + var gStatusText; + var gXULBrowserWindow = { + QueryInterface: function(aIID) { + if (aIID.Equals(Ci.nsIXULBrowserWindow) || + aIID.Equals(Ci.nsISupports)) + return this; + + throw Cr.NS_NOINTERFACE; + }, + + setJSStatus: function() { }, + + setOverLink: function(aStatusText, aLink) { + gStatusText = aStatusText; + }, + + onBeforeLinkTraversal: function() { } + }; + + var imports = ["SimpleTest", "ok", "is", "info"]; + for (var name of imports) { + window[name] = window.opener.wrappedJSObject[name]; + } + SimpleTest.requestLongerTimeout(2); + + function onLoad() { + Task.spawn(function* () { + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow) + .XULBrowserWindow = gXULBrowserWindow; + + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + yield startTestWithBrowser(browserId); + } + }).then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + function* startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + + // Tests delays the loading of a document for one second. + yield new Promise(resolve => setTimeout(resolve, 1000)); + + let promise = BrowserTestUtils.browserLoaded(gBrowser); + gBrowser.loadURI("data:text/html,<h2 id='h2'>" + SEARCH_TEXT + + "</h2><h2><a href='" + SAMPLE_URL + "'>Link Test</a></h2><input id='text' type='text' value='" + + SAMPLE_TEXT + "'></input><input id='button' type='button'></input><img id='img' width='50' height='50'/>"); + yield promise; + yield onDocumentLoaded(); + } + + function* onDocumentLoaded() { + yield testNormalFind(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testNormalFind"); + yield openFindbar(); + yield testNormalFindWithComposition(); + gFindBar.close(); + ok(gFindBar.hidden, "findbar should be hidden after testNormalFindWithComposition"); + yield openFindbar(); + yield testAutoCaseSensitivityUI(); + yield testQuickFindText(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testQuickFindText"); + // TODO: `testFindWithHighlight` tests fastFind integrity, which can not + // be accessed with RemoteFinder. We're skipping it for now. + if (gFindBar._browser.finder._fastFind) { + yield testFindWithHighlight(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testFindWithHighlight"); + } + yield testFindbarSelection(); + ok(gFindBar.hidden, "Failed to close findbar after testFindbarSelection"); + // TODO: I don't know how to drop a content element on a chrome input. + if (!gBrowser.hasAttribute("remote")) + testDrop(); + yield testQuickFindLink(); + if (gHasFindClipboard) { + yield testStatusText(); + } + + if (!AppConstants.DEBUG) { + yield testFindCountUI(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testFindCountUI"); + yield testFindCountUI(true); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testFindCountUI - linksOnly"); + } + + yield openFindbar(); + yield testFindAfterCaseChanged(); + gFindBar.close(); + yield openFindbar(); + yield testFailedStringReset(); + gFindBar.close(); + yield testQuickFindClose(); + // TODO: This doesn't seem to work when the findbar is connected to a + // remote browser element. + if (!gBrowser.hasAttribute("remote")) + yield testFindAgainNotFound(); + yield testToggleEntireWord(); + } + + function* testFindbarSelection() { + function checkFindbarState(aTestName, aExpSelection) { + ok(!gFindBar.hidden, "testFindbarSelection: failed to open findbar: " + aTestName); + ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField, + "testFindbarSelection: find field is not focused: " + aTestName); + if (!gHasFindClipboard) { + ok(gFindBar._findField.value == aExpSelection, + "Incorrect selection in testFindbarSelection: " + aTestName + + ". Selection: " + gFindBar._findField.value); + } + + // Clear the value, close the findbar. + gFindBar._findField.value = ""; + gFindBar.close(); + } + + // Test normal selected text. + yield ContentTask.spawn(gBrowser, null, function* () { + let document = content.document; + let cH2 = document.getElementById("h2"); + let cSelection = content.getSelection(); + let cRange = document.createRange(); + cRange.setStart(cH2, 0); + cRange.setEnd(cH2, 1); + cSelection.removeAllRanges(); + cSelection.addRange(cRange); + }); + yield openFindbar(); + checkFindbarState("plain text", SEARCH_TEXT); + + // Test nsIDOMNSEditableElement with selection. + yield ContentTask.spawn(gBrowser, null, function* () { + let textInput = content.document.getElementById("text"); + textInput.focus(); + textInput.select(); + }); + yield openFindbar(); + checkFindbarState("text input", SAMPLE_TEXT); + + // Test non-editable nsIDOMNSEditableElement (button). + yield ContentTask.spawn(gBrowser, null, function* () { + content.document.getElementById("button").focus(); + }); + yield openFindbar(); + checkFindbarState("button", ""); + } + + function testDrop() { + gFindBar.open(); + // use an dummy image to start the drag so it doesn't get interrupted by a selection + var img = gBrowser.contentDocument.getElementById("img"); + synthesizeDrop(img, gFindBar._findField, [[ {type: "text/plain", data: "Rabbits" } ]], "copy", window); + is(gFindBar._findField.inputField.value, "Rabbits", "drop on findbar"); + gFindBar.close(); + } + + function testQuickFindClose() { + return new Promise(resolve => { + var _isClosedCallback = function() { + ok(gFindBar.hidden, + "_isClosedCallback: Failed to auto-close quick find bar after " + + gFindBar._quickFindTimeoutLength + "ms"); + resolve(); + }; + setTimeout(_isClosedCallback, gFindBar._quickFindTimeoutLength + 100); + }); + } + + function testStatusText() { + return new Promise(resolve => { + var _delayedCheckStatusText = function() { + ok(gStatusText == SAMPLE_URL, "testStatusText: Failed to set status text of found link"); + resolve(); + }; + setTimeout(_delayedCheckStatusText, 100); + }); + } + + function promiseFindResult() { + return new Promise(resolve => { + let listener = { + onFindResult: function(result) { + gFindBar.browser.finder.removeResultListener(listener); + resolve(result); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + function promiseMatchesCountResult() { + return new Promise(resolve => { + let listener = { + onMatchesCountResult: function() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + // Make sure we resolve _at least_ after five times the find iterator timeout. + setTimeout(resolve, (ITERATOR_TIMEOUT * 5) + 20); + }); + } + + function promiseHighlightFinished() { + return new Promise(resolve => { + let listener = { + onHighlightFinished() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + var enterStringIntoFindField = Task.async(function* (str, waitForResult = true) { + for (let promise, i = 0; i < str.length; i++) { + if (waitForResult) { + promise = promiseFindResult(); + } + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, str.charCodeAt(i)); + gFindBar._findField.inputField.dispatchEvent(event); + if (waitForResult) { + yield promise; + } + } + }); + + function promiseExpectRangeCount(rangeCount) { + return ContentTask.spawn(gBrowser, { rangeCount }, function* (args) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND); + Assert.equal(sel.rangeCount, args.rangeCount, + "Expected the correct amount of ranges inside the Find selection"); + }); + } + + // also test match-case + function* testNormalFind() { + document.getElementById("cmd_find").doCommand(); + + ok(!gFindBar.hidden, "testNormalFind: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField, + "testNormalFind: find field is not focused"); + + let promise; + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden && matchCaseCheckbox.checked) { + promise = promiseFindResult(); + matchCaseCheckbox.click(); + yield promise; + } + + var searchStr = "text tes"; + yield enterStringIntoFindField(searchStr); + + let sel = yield ContentTask.spawn(gBrowser, { searchStr }, function* (args) { + let sel = content.getSelection().toString(); + Assert.equal(sel.toLowerCase(), args.searchStr, + "testNormalFind: failed to find '" + args.searchStr + "'"); + return sel; + }); + testClipboardSearchString(sel); + + if (!matchCaseCheckbox.hidden) { + promise = promiseFindResult(); + matchCaseCheckbox.click(); + yield promise; + enterStringIntoFindField("t"); + yield ContentTask.spawn(gBrowser, { searchStr }, function* (args) { + Assert.notEqual(content.getSelection().toString(), args.searchStr, + "testNormalFind: Case-sensitivy is broken '" + args.searchStr + "'"); + }); + promise = promiseFindResult(); + matchCaseCheckbox.click(); + yield promise; + } + } + + function openFindbar() { + document.getElementById("cmd_find").doCommand(); + return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise; + } + + function* testNormalFindWithComposition() { + ok(!gFindBar.hidden, "testNormalFindWithComposition: findbar should be open"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField, + "testNormalFindWithComposition: find field should be focused"); + + var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + var clicked = false; + if (!matchCaseCheckbox.hidden & matchCaseCheckbox.checked) { + matchCaseCheckbox.click(); + clicked = true; + } + + gFindBar._findField.inputField.focus(); + + var searchStr = "text"; + + synthesizeCompositionChange( + { "composition": + { "string": searchStr, + "clauses": + [ + { "length": searchStr.length, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": searchStr.length, "length": 0 } + }); + + yield ContentTask.spawn(gBrowser, { searchStr }, function* (args) { + Assert.notEqual(content.getSelection().toString().toLowerCase(), args.searchStr, + "testNormalFindWithComposition: text shouldn't be found during composition"); + }); + + synthesizeComposition({ type: "compositioncommitasis" }); + + let sel = yield ContentTask.spawn(gBrowser, { searchStr }, function* (args) { + let sel = content.getSelection().toString(); + Assert.equal(sel.toLowerCase(), args.searchStr, + "testNormalFindWithComposition: text should be found after committing composition"); + return sel; + }); + testClipboardSearchString(sel); + + if (clicked) { + matchCaseCheckbox.click(); + } + } + + function* testAutoCaseSensitivityUI() { + var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + var matchCaseLabel = gFindBar.getElement("match-case-status"); + ok(!matchCaseCheckbox.hidden, "match case box is hidden in manual mode"); + ok(matchCaseLabel.hidden, "match case label is visible in manual mode"); + + gPrefsvc.setIntPref("accessibility.typeaheadfind.casesensitive", 2); + + ok(matchCaseCheckbox.hidden, + "match case box is visible in automatic mode"); + ok(!matchCaseLabel.hidden, + "match case label is hidden in automatic mode"); + + yield enterStringIntoFindField("a"); + var insensitiveLabel = matchCaseLabel.value; + yield enterStringIntoFindField("A"); + var sensitiveLabel = matchCaseLabel.value; + ok(insensitiveLabel != sensitiveLabel, + "Case Sensitive label was not correctly updated"); + + // bug 365551 + gFindBar.onFindAgainCommand(); + ok(matchCaseCheckbox.hidden && !matchCaseLabel.hidden, + "bug 365551: case sensitivity UI is broken after find-again"); + gPrefsvc.setIntPref("accessibility.typeaheadfind.casesensitive", 0); + gFindBar.close(); + } + + function* clearFocus() { + document.commandDispatcher.focusedElement = null; + document.commandDispatcher.focusedWindow = null; + yield ContentTask.spawn(gBrowser, null, function* () { + content.focus(); + }); + } + + function* testQuickFindLink() { + yield clearFocus(); + + yield ContentTask.spawn(gBrowser, null, function* () { + let document = content.document; + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, "'".charCodeAt(0)); + document.documentElement.dispatchEvent(event); + }); + + ok(!gFindBar.hidden, "testQuickFindLink: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField, + "testQuickFindLink: find field is not focused"); + + var searchStr = "Link Test"; + yield enterStringIntoFindField(searchStr); + yield ContentTask.spawn(gBrowser, { searchStr }, function* (args) { + Assert.equal(content.getSelection().toString(), args.searchStr, + "testQuickFindLink: failed to find sample link"); + }); + testClipboardSearchString(searchStr); + } + + // See bug 963925 for more details on this test. + function* testFindWithHighlight() { + gFindBar._findField.value = ""; + + // For this test, we want to closely control the selection. The easiest + // way to do so is to replace the implementation of + // Finder.getInitialSelection with a no-op and call the findbar's callback + // (onCurrentSelection(..., true)) ourselves with our hand-picked + // selection. + let oldGetInitialSelection = gFindBar.browser.finder.getInitialSelection; + let searchStr; + gFindBar.browser.finder.getInitialSelection = function(){}; + + let findCommand = document.getElementById("cmd_find"); + findCommand.doCommand(); + + gFindBar.onCurrentSelection("", true); + + searchStr = "e"; + yield enterStringIntoFindField(searchStr); + + let a = gFindBar._findField.value; + let b = gFindBar._browser.finder._fastFind.searchString; + let c = gFindBar._browser.finder.searchString; + ok(a == b && b == c, "testFindWithHighlight 1: " + a + ", " + b + ", " + c + "."); + + searchStr = "t"; + findCommand.doCommand(); + + gFindBar.onCurrentSelection(searchStr, true); + gFindBar.browser.finder.getInitialSelection = oldGetInitialSelection; + + a = gFindBar._findField.value; + b = gFindBar._browser.finder._fastFind.searchString; + c = gFindBar._browser.finder.searchString; + ok(a == searchStr && b == c, "testFindWithHighlight 2: " + searchStr + + ", " + a + ", " + b + ", " + c + "."); + + let highlightButton = gFindBar.getElement("highlight"); + highlightButton.click(); + ok(highlightButton.checked, "testFindWithHighlight 3: Highlight All should be checked."); + + a = gFindBar._findField.value; + b = gFindBar._browser.finder._fastFind.searchString; + c = gFindBar._browser.finder.searchString; + ok(a == searchStr && b == c, "testFindWithHighlight 4: " + a + ", " + b + ", " + c + "."); + + gFindBar.onFindAgainCommand(); + a = gFindBar._findField.value; + b = gFindBar._browser.finder._fastFind.searchString; + c = gFindBar._browser.finder.searchString; + ok(a == b && b == c, "testFindWithHighlight 5: " + a + ", " + b + ", " + c + "."); + + highlightButton.click(); + ok(!highlightButton.checked, "testFindWithHighlight: Highlight All should be unchecked."); + + // Regression test for bug 1316515. + searchStr = "e"; + gFindBar.clear(); + yield enterStringIntoFindField(searchStr); + yield promiseExpectRangeCount(0); + + highlightButton.click(); + ok(highlightButton.checked, "testFindWithHighlight: Highlight All should be checked."); + yield promiseHighlightFinished(); + yield promiseExpectRangeCount(3); + + synthesizeKey("VK_BACK_SPACE", {}); + yield promiseExpectRangeCount(0); + + // Regression test for bug 1316513. + highlightButton.click(); + ok(!highlightButton.checked, "testFindWithHighlight - 1316513: Highlight All should be unchecked."); + yield enterStringIntoFindField(searchStr); + + highlightButton.click(); + ok(highlightButton.checked, "testFindWithHighlight - 1316513: Highlight All should be checked."); + yield promiseHighlightFinished(); + yield promiseExpectRangeCount(3); + + let promise = BrowserTestUtils.browserLoaded(gBrowser); + gBrowser.reload(); + yield promise; + + ok(highlightButton.checked, "testFindWithHighlight - 1316513: Highlight All " + + "should still be checked after a reload."); + synthesizeKey("VK_RETURN", {}); + yield promiseHighlightFinished(); + yield promiseExpectRangeCount(3); + + // Uncheck at test end to not interfere with other test functions that are + // run after this one. + highlightButton.click(); + } + + function* testQuickFindText() { + yield clearFocus(); + + yield ContentTask.spawn(gBrowser, null, function* () { + let document = content.document; + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, "/".charCodeAt(0)); + document.documentElement.dispatchEvent(event); + }); + + ok(!gFindBar.hidden, "testQuickFindText: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField, + "testQuickFindText: find field is not focused"); + + yield enterStringIntoFindField(SEARCH_TEXT); + yield ContentTask.spawn(gBrowser, { SEARCH_TEXT }, function* (args) { + Assert.equal(content.getSelection().toString(), args.SEARCH_TEXT, + "testQuickFindText: failed to find '" + args.SEARCH_TEXT + "'"); + }); + testClipboardSearchString(SEARCH_TEXT); + } + + function* testFindCountUI(linksOnly = false) { + yield clearFocus(); + + if (linksOnly) { + yield ContentTask.spawn(gBrowser, null, function* () { + let document = content.document; + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, "'".charCodeAt(0)); + document.documentElement.dispatchEvent(event); + }); + } else { + document.getElementById("cmd_find").doCommand(); + } + + ok(!gFindBar.hidden, "testFindCountUI: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField, + "testFindCountUI: find field is not focused"); + + let promise; + let matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + promise = promiseFindResult(); + matchCase.click(); + yield new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); + yield promise; + } + + let foundMatches = gFindBar._foundMatches; + let tests = [{ + text: "t", + current: linksOnly ? 1 : 5, + total: linksOnly ? 2 : 10, + }, { + text: "te", + current: linksOnly ? 1 : 3, + total: linksOnly ? 1 : 5, + }, { + text: "tes", + current: 1, + total: linksOnly ? 1 : 2, + }, { + text: "texxx", + current: 0, + total: 0 + }]; + let regex = /([\d]*)\sof\s([\d]*)/; + + function assertMatches(aTest, aMatches) { + is(aMatches[1], String(aTest.current), + `${linksOnly ? "[Links-only] " : ""}Currently highlighted match should be at ${aTest.current} for '${aTest.text}'`); + is(aMatches[2], String(aTest.total), + `${linksOnly ? "[Links-only] " : ""}Total amount of matches should be ${aTest.total} for '${aTest.text}'`); + } + + for (let test of tests) { + gFindBar._findField.select(); + gFindBar._findField.focus(); + + let timeout = ITERATOR_TIMEOUT; + if (test.text.length == 1) + timeout *= 4; + else if (test.text.length == 2) + timeout *= 2; + timeout += 20; + yield new Promise(resolve => setTimeout(resolve, timeout)); + yield enterStringIntoFindField(test.text, false); + yield promiseMatchesCountResult(); + let matches = foundMatches.value.match(regex); + if (!test.total) { + ok(!matches, "No message should be shown when 0 matches are expected"); + } else { + assertMatches(test, matches); + for (let i = 1; i < test.total; i++) { + yield new Promise(resolve => setTimeout(resolve, timeout)); + gFindBar.onFindAgainCommand(); + yield promiseMatchesCountResult(); + // test.current + 1, test.current + 2, ..., test.total, 1, ..., test.current + let current = (test.current + i - 1) % test.total + 1; + assertMatches({ + text: test.text, + current: current, + total: test.total + }, foundMatches.value.match(regex)); + } + } + } + } + + // See bug 1051187. + function* testFindAfterCaseChanged() { + // Search to set focus on "Text Test" so that searching for "t" selects first + // (upper case!) "T". + yield enterStringIntoFindField(SEARCH_TEXT); + gFindBar.clear(); + + gPrefsvc.setIntPref("accessibility.typeaheadfind.casesensitive", 0); + + yield enterStringIntoFindField("t"); + yield ContentTask.spawn(gBrowser, null, function* () { + Assert.equal(content.getSelection().toString(), "T", "First T should be selected."); + }); + + gPrefsvc.setIntPref("accessibility.typeaheadfind.casesensitive", 1); + yield ContentTask.spawn(gBrowser, null, function* () { + Assert.equal(content.getSelection().toString(), "t", "First t should be selected."); + }); + } + + // Make sure that _findFailedString is cleared: + // 1. Do a search that fails with case sensitivity but matches with no case sensitivity. + // 2. Uncheck case sensitivity button to match the string. + function* testFailedStringReset() { + gPrefsvc.setIntPref("accessibility.typeaheadfind.casesensitive", 1); + + yield enterStringIntoFindField(SEARCH_TEXT.toUpperCase(), false); + yield ContentTask.spawn(gBrowser, null, function* () { + Assert.equal(content.getSelection().toString(), "", "Not found."); + }); + + gPrefsvc.setIntPref("accessibility.typeaheadfind.casesensitive", 0); + yield ContentTask.spawn(gBrowser, { SEARCH_TEXT }, function* (args) { + Assert.equal(content.getSelection().toString(), args.SEARCH_TEXT, + "Search text should be selected."); + }); + } + + function testClipboardSearchString(aExpected) { + if (!gHasFindClipboard) + return; + + if (!aExpected) + aExpected = ""; + var searchStr = gFindBar.browser.finder.clipboardSearchString; + ok(searchStr.toLowerCase() == aExpected.toLowerCase(), + "testClipboardSearchString: search string not set to '" + aExpected + + "', instead found '" + searchStr + "'"); + } + + // See bug 967982. + function* testFindAgainNotFound() { + yield openFindbar(); + yield enterStringIntoFindField(NOT_FOUND_TEXT, false); + gFindBar.close(); + ok(gFindBar.hidden, "The findbar is closed."); + let promise = promiseFindResult(); + gFindBar.onFindAgainCommand(); + yield promise; + ok(!gFindBar.hidden, "Unsuccessful Find Again opens the find bar."); + + yield enterStringIntoFindField(SEARCH_TEXT); + gFindBar.close(); + ok(gFindBar.hidden, "The findbar is closed."); + promise = promiseFindResult(); + gFindBar.onFindAgainCommand(); + yield promise; + ok(gFindBar.hidden, "Successful Find Again leaves the find bar closed."); + } + + function* testToggleEntireWord() { + yield openFindbar(); + let promise = promiseFindResult(); + yield enterStringIntoFindField("Tex", false); + let result = yield promise; + is(result.result, Ci.nsITypeAheadFind.FIND_FOUND, "Text should be found"); + + yield new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); + promise = promiseFindResult(); + let check = gFindBar.getElement("find-entire-word"); + check.click(); + result = yield promise; + is(result.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "Text should NOT be found"); + + check.click(); + gFindBar.close(true); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content-primary" flex="1" id="content" src="about:blank"/> + <browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/frame_popup_anchor.xul b/toolkit/content/tests/chrome/frame_popup_anchor.xul new file mode 100644 index 0000000000..be6254ce01 --- /dev/null +++ b/toolkit/content/tests/chrome/frame_popup_anchor.xul @@ -0,0 +1,82 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<menupopup id="popup" onpopupshowing="if (isSecondTest) popupShowing(event)" onpopupshown="popupShown()" + onpopuphidden="nextTest()"> + <menuitem label="One"/> + <menuitem label="Two"/> +</menupopup> + +<button id="button" label="OK" popup="popup"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var isSecondTest = false; + +function openPopup() +{ + document.getElementById("popup").openPopup(parent.document.getElementById("outerbutton"), "after_start", 3, 1); +} + +function popupShowing(event) +{ + var buttonrect = document.getElementById("button").getBoundingClientRect(); + parent.opener.wrappedJSObject.SimpleTest.is(event.clientX, buttonrect.left + 6, "popup clientX with mouse"); + parent.opener.wrappedJSObject.SimpleTest.is(event.clientY, buttonrect.top + 6, "popup clientY with mouse"); +} + +function popupShown() +{ + var left, top; + var popuprect = document.getElementById("popup").getBoundingClientRect(); + if (isSecondTest) { + var buttonrect = document.getElementById("button").getBoundingClientRect(); + left = buttonrect.left + 6; + top = buttonrect.top + 6; + } + else { + var iframerect = parent.document.getElementById("frame").getBoundingClientRect(); + var buttonrect = parent.document.getElementById("outerbutton").getBoundingClientRect(); + + // The popup should appear anchored on the bottom left edge of the button, however + // the client rectangle is relative to the iframe's document. Thus the coordinates + // are: + // left = iframe's left - anchor button's left - 3 pixel offset passed to openPopup + + // iframe border (17px) + iframe padding (0) + // top = iframe's top - anchor button's bottom - 1 pixel offset passed to openPopup + + // iframe border (0) + iframe padding (3px); + left = -(Math.round(iframerect.left) - Math.round(buttonrect.left) + 14); + top = -(Math.round(iframerect.top) - Math.round(buttonrect.bottom) + 2); + } + + var testid = isSecondTest ? "with mouse" : "anchored to parent frame"; + parent.opener.wrappedJSObject.SimpleTest.is(Math.round(popuprect.left), left, "popup left " + testid); + parent.opener.wrappedJSObject.SimpleTest.is(Math.round(popuprect.top), top, "popup top " + testid); + + document.getElementById("popup").hidePopup(); +} + +function nextTest() +{ + if (isSecondTest) { + parent.opener.wrappedJSObject.SimpleTest.finish(); + parent.close(); + } + else { + // this second test ensures that the popupshowing coordinates when a popup in + // a frame is opened are correct + isSecondTest = true; + synthesizeMouse(document.getElementById("button"), 6, 6, { }); + } +} + +]]> +</script> + +</page> diff --git a/toolkit/content/tests/chrome/frame_popupremoving_frame.xul b/toolkit/content/tests/chrome/frame_popupremoving_frame.xul new file mode 100644 index 0000000000..e8f00ce7a9 --- /dev/null +++ b/toolkit/content/tests/chrome/frame_popupremoving_frame.xul @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Removing Frame Tests" + onload="setTimeout(init, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<hbox> + +<menu id="separatemenu1" label="Menu"> + <menupopup id="separatepopup1" onpopupshown="document.getElementById('separatemenu2').open = true"> + <menuitem label="L1 One"/> + <menuitem label="L1 Two"/> + <menuitem label="L1 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu2" label="Menu"> + <menupopup id="separatepopup2" onpopupshown="document.getElementById('separatemenu3').open = true"> + <menuitem label="L2 One"/> + <menuitem label="L2 Two"/> + <menuitem label="L2 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu3" label="Menu" onpopupshown="document.getElementById('separatemenu4').open = true"> + <menupopup id="separatepopup3"> + <menuitem label="L3 One"/> + <menuitem label="L3 Two"/> + <menuitem label="L3 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu4" label="Menu" onpopupshown="document.getElementById('nestedmenu1').open = true"> + <menupopup id="separatepopup3"> + <menuitem label="L4 One"/> + <menuitem label="L4 Two"/> + <menuitem label="L4 Three"/> + </menupopup> +</menu> + +</hbox> + +<menu id="nestedmenu1" label="Menu"> + <menupopup id="nestedpopup1" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu2" label="Menu"> + <menupopup id="nestedpopup2" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu3" label="Menu"> + <menupopup id="nestedpopup3" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu4" label="Menu" onpopupshown="parent.popupsOpened()"> + <menupopup id="nestedpopup4"> + <menuitem label="Nested One"/> + <menuitem label="Nested Two"/> + <menuitem label="Nested Three"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> +</menu> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function init() +{ + document.getElementById("separatemenu1").open = true; +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xul b/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xul new file mode 100644 index 0000000000..c85083cb79 --- /dev/null +++ b/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xul @@ -0,0 +1,43 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<page id="frame1" + style="background-color:green;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<spacer height="10px"/> +<iframe + style="margin:10px; min-height:170px; max-width:200px; max-height:200px; border:solid 1px white;" + src="frame_subframe_origin_subframe2.xul"></iframe> +<spacer height="3px"/> +<caption id="cap1" style="min-width:200px; max-width:200px; background-color:white;" label=""/> +<script class="testbody" type="application/javascript"> + +// Fire a mouse move event aimed at this window, and check to be +// sure the client coords translate from widget to the dom correctly. + +function runTests() +{ + synthesizeMouse(document.getElementById("frame1"), 3, 4, { type: "mousemove" }); +} + +function mouseMove(e) { + e.stopPropagation(); + var element = e.target; + var el = document.getElementById("cap1"); + el.label = "client: (" + e.clientX + "," + e.clientY + ")"; + parent.opener.wrappedJSObject.SimpleTest.is(e.clientX, 3, "mouse event clientX on sub frame 1"); + parent.opener.wrappedJSObject.SimpleTest.is(e.clientY, 4, "mouse event clientY on sub frame 1"); + // fire the next test on the sub frame + frames[0].runTests(); +} + +window.addEventListener("mousemove", mouseMove, false); + +</script> +</page> diff --git a/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xul b/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xul new file mode 100644 index 0000000000..92ef64b898 --- /dev/null +++ b/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xul @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<page id="frame2" + style="background-color:red;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<spacer height="10px"/> +<caption id="cap2" style="background-color:white;" label=""/> +<script class="testbody" type="application/javascript"> + +// Fire a mouse move event aimed at this window, and check to be +// sure the client coords translate from widget to the dom correctly. + +function runTests() +{ + synthesizeMouse(document.getElementById("frame2"), 6, 5, { type: "mousemove" }); +} + +function mouseMove(e) { + e.stopPropagation(); + var element = e.target; + var el = document.getElementById("cap2"); + el.label = "client: (" + e.clientX + "," + e.clientY + ")"; + parent.parent.opener.wrappedJSObject.SimpleTest.is(e.clientX, 6, "mouse event clientX on sub frame 2"); + parent.parent.opener.wrappedJSObject.SimpleTest.is(e.clientY, 5, "mouse event clientY on sub frame 2"); + parent.parent.opener.wrappedJSObject.SimpleTest.finish(); + parent.parent.close(); +} + +window.addEventListener("mousemove",mouseMove, false); + +</script> +</page> diff --git a/toolkit/content/tests/chrome/popup_childframe_node.xul b/toolkit/content/tests/chrome/popup_childframe_node.xul new file mode 100644 index 0000000000..512f5f8c26 --- /dev/null +++ b/toolkit/content/tests/chrome/popup_childframe_node.xul @@ -0,0 +1,2 @@ +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" width="80" height="80" + onclick="document.documentElement.setAttribute('data', 'x' + document.popupNode)"/> diff --git a/toolkit/content/tests/chrome/popup_trigger.js b/toolkit/content/tests/chrome/popup_trigger.js new file mode 100644 index 0000000000..920d4d0702 --- /dev/null +++ b/toolkit/content/tests/chrome/popup_trigger.js @@ -0,0 +1,859 @@ +var gMenuPopup = null; +var gTrigger = null; +var gIsMenu = false; +var gScreenX = -1, gScreenY = -1; +var gCachedEvent = null; +var gCachedEvent2 = null; + +function cacheEvent(modifiers) +{ + var cachedEvent = null; + + var mouseFn = function(event) { + cachedEvent = event; + } + + window.addEventListener("mousedown", mouseFn, false); + synthesizeMouse(document.documentElement, 0, 0, modifiers); + window.removeEventListener("mousedown", mouseFn, false); + + return cachedEvent; +} + +function runTests() +{ + if (screen.height < 768) { + ok(false, "popup tests are likely to fail for screen heights less than 768 pixels"); + } + + gMenuPopup = document.getElementById("thepopup"); + gTrigger = document.getElementById("trigger"); + + gIsMenu = gTrigger.boxObject instanceof MenuBoxObject; + + // a hacky way to get the screen position of the document. Cache the event + // so that we can use it in calls to openPopup. + gCachedEvent = cacheEvent({ shiftKey: true }); + gScreenX = gCachedEvent.screenX; + gScreenY = gCachedEvent.screenY; + gCachedEvent2 = cacheEvent({ altKey: true, ctrlKey: true, shiftKey: true, metaKey: true }); + + startPopupTests(popupTests); +} + +var popupTests = [ +{ + testname: "mouse click on trigger", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + test: function() { + // for menus, no trigger will be set. For non-menus using the popup + // attribute, the trigger will be set to the node with the popup attribute + gExpectedTriggerNode = gIsMenu ? "notset" : gTrigger; + synthesizeMouse(gTrigger, 4, 4, { }); + }, + result: function (testname) { + gExpectedTriggerNode = null; + // menus are the anchor but non-menus are opened at screen coordinates + is(gMenuPopup.anchorNode, gIsMenu ? gTrigger : null, testname + " anchorNode"); + // menus are opened internally, but non-menus have a mouse event which + // triggered them + is(gMenuPopup.triggerNode, gIsMenu ? null : gTrigger, testname + " triggerNode"); + is(document.popupNode, gIsMenu ? null : gTrigger, testname + " document.popupNode"); + is(document.tooltipNode, null, testname + " document.tooltipNode"); + // check to ensure the popup node for a different document isn't used + if (window.opener) + is(window.opener.document.popupNode, null, testname + " opener.document.popupNode"); + + // this will be used in some tests to ensure the size doesn't change + var popuprect = gMenuPopup.getBoundingClientRect(); + gPopupWidth = Math.round(popuprect.width); + gPopupHeight = Math.round(popuprect.height); + + checkActive(gMenuPopup, "", testname); + checkOpen("trigger", testname); + // if a menu, the popup should be opened underneath the menu in the + // 'after_start' position, otherwise it is opened at the mouse position + if (gIsMenu) + compareEdge(gTrigger, gMenuPopup, "after_start", 0, 0, testname); + } +}, +{ + // check that pressing cursor down while there is no selection + // highlights the first item + testname: "cursor down no selection", + events: [ "DOMMenuItemActive item1" ], + test: function() { synthesizeKey("VK_DOWN", { }); }, + result: function(testname) { checkActive(gMenuPopup, "item1", testname); } +}, +{ + // check that pressing cursor up wraps and highlights the last item + testname: "cursor up wrap", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive last" ], + test: function() { synthesizeKey("VK_UP", { }); }, + result: function(testname) { + checkActive(gMenuPopup, "last", testname); + } +}, +{ + // check that pressing cursor down wraps and highlights the first item + testname: "cursor down wrap", + events: [ "DOMMenuItemInactive last", "DOMMenuItemActive item1" ], + test: function() { synthesizeKey("VK_DOWN", { }); }, + result: function(testname) { checkActive(gMenuPopup, "item1", testname); } +}, +{ + // check that pressing cursor down highlights the second item + testname: "cursor down", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive item2" ], + test: function() { synthesizeKey("VK_DOWN", { }); }, + result: function(testname) { checkActive(gMenuPopup, "item2", testname); } +}, +{ + // check that pressing cursor up highlights the second item + testname: "cursor up", + events: [ "DOMMenuItemInactive item2", "DOMMenuItemActive item1" ], + test: function() { synthesizeKey("VK_UP", { }); }, + result: function(testname) { checkActive(gMenuPopup, "item1", testname); } +}, +{ + // cursor left should not do anything + testname: "cursor left", + test: function() { synthesizeKey("VK_LEFT", { }); }, + result: function(testname) { checkActive(gMenuPopup, "item1", testname); } +}, +{ + // cursor right should not do anything + testname: "cursor right", + test: function() { synthesizeKey("VK_RIGHT", { }); }, + result: function(testname) { checkActive(gMenuPopup, "item1", testname); } +}, +{ + // check cursor down when a disabled item exists in the menu + testname: "cursor down disabled", + events: function() { + // On Windows, disabled items are included when navigating, but on + // other platforms, disabled items are skipped over + if (navigator.platform.indexOf("Win") == 0) { + return [ "DOMMenuItemInactive item1", "DOMMenuItemActive item2" ]; + } + return [ "DOMMenuItemInactive item1", "DOMMenuItemActive amenu" ]; + }, + test: function() { + document.getElementById("item2").disabled = true; + synthesizeKey("VK_DOWN", { }); + } +}, +{ + // check cursor up when a disabled item exists in the menu + testname: "cursor up disabled", + events: function() { + if (navigator.platform.indexOf("Win") == 0) { + return [ "DOMMenuItemInactive item2", "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", "DOMMenuItemActive item2", + "DOMMenuItemInactive item2", "DOMMenuItemActive item1" ]; + } + return [ "DOMMenuItemInactive amenu", "DOMMenuItemActive item1" ]; + }, + test: function() { + if (navigator.platform.indexOf("Win") == 0) + synthesizeKey("VK_DOWN", { }); + synthesizeKey("VK_UP", { }); + if (navigator.platform.indexOf("Win") == 0) + synthesizeKey("VK_UP", { }); + } +}, +{ + testname: "mouse click outside", + events: [ "popuphiding thepopup", "popuphidden thepopup", + "DOMMenuItemInactive item1", "DOMMenuInactive thepopup" ], + test: function() { + gMenuPopup.hidePopup(); + // XXXndeakin event simulation fires events outside of the platform specific + // widget code so the popup capturing isn't handled. Thus, the menu won't + // rollup this way. + // synthesizeMouse(gTrigger, 0, -12, { }); + }, + result: function(testname, step) { + is(gMenuPopup.anchorNode, null, testname + " anchorNode"); + is(gMenuPopup.triggerNode, null, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + checkClosed("trigger", testname); + } +}, +{ + // these tests check to ensure that passing an anchor and position + // puts the popup in the right place + testname: "open popup anchored", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + steps: ["before_start", "before_end", "after_start", "after_end", + "start_before", "start_after", "end_before", "end_after", "after_pointer", "overlap", + "topleft topleft", "topcenter topleft", "topright topleft", + "leftcenter topright", "rightcenter topright", + "bottomleft bottomleft", "bottomcenter bottomleft", "bottomright bottomleft", + "topleft bottomright", "bottomcenter bottomright", "rightcenter topright"], + test: function(testname, step) { + gExpectedTriggerNode = "notset"; + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result: function(testname, step) { + // no triggerNode because it was opened without passing an event + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, gTrigger, testname + " anchorNode"); + is(gMenuPopup.triggerNode, null, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + compareEdge(gTrigger, gMenuPopup, step, 0, 0, testname); + } +}, +{ + // these tests check the same but with a 10 pixel margin on the popup + testname: "open popup anchored with margin", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + steps: ["before_start", "before_end", "after_start", "after_end", + "start_before", "start_after", "end_before", "end_after", "after_pointer", "overlap", + "topleft topleft", "topcenter topleft", "topright topleft", + "leftcenter topright", "rightcenter topright", + "bottomleft bottomleft", "bottomcenter bottomleft", "bottomright bottomleft", + "topleft bottomright", "bottomcenter bottomright", "rightcenter topright"], + test: function(testname, step) { + gMenuPopup.setAttribute("style", "margin: 10px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result: function(testname, step) { + var rightmod = step == "before_end" || step == "after_end" || + step == "start_before" || step == "start_after" || + step.match(/topright$/) || step.match(/bottomright$/); + var bottommod = step == "before_start" || step == "before_end" || + step == "start_after" || step == "end_after" || + step.match(/bottomleft$/) || step.match(/bottomright$/); + compareEdge(gTrigger, gMenuPopup, step, rightmod ? -10 : 10, bottommod ? -10 : 10, testname); + gMenuPopup.removeAttribute("style"); + } +}, +{ + // these tests check the same but with a -8 pixel margin on the popup + testname: "open popup anchored with negative margin", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + steps: ["before_start", "before_end", "after_start", "after_end", + "start_before", "start_after", "end_before", "end_after", "after_pointer", "overlap"], + test: function(testname, step) { + gMenuPopup.setAttribute("style", "margin: -8px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result: function(testname, step) { + var rightmod = step == "before_end" || step == "after_end" || + step == "start_before" || step == "start_after"; + var bottommod = step == "before_start" || step == "before_end" || + step == "start_after" || step == "end_after"; + compareEdge(gTrigger, gMenuPopup, step, rightmod ? 8 : -8, bottommod ? 8 : -8, testname); + gMenuPopup.removeAttribute("style"); + } +}, + { + testname: "open popup with large positive margin", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + steps: ["before_start", "before_end", "after_start", "after_end", + "start_before", "start_after", "end_before", "end_after", "after_pointer", "overlap"], + test: function(testname, step) { + gMenuPopup.setAttribute("style", "margin: 1000px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result: function(testname, step) { + var popuprect = gMenuPopup.getBoundingClientRect(); + // as there is more room on the 'end' or 'after' side, popups will always + // appear on the right or bottom corners, depending on which side they are + // allowed to be flipped by. + var expectedleft = step == "before_end" || step == "after_end" ? + 0 : Math.round(window.innerWidth - gPopupWidth); + var expectedtop = step == "start_after" || step == "end_after" ? + 0 : Math.round(window.innerHeight - gPopupHeight); + is(Math.round(popuprect.left), expectedleft, testname + " x position " + step); + is(Math.round(popuprect.top), expectedtop, testname + " y position " + step); + gMenuPopup.removeAttribute("style"); + } +}, +{ + testname: "open popup with large negative margin", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + steps: ["before_start", "before_end", "after_start", "after_end", + "start_before", "start_after", "end_before", "end_after", "after_pointer", "overlap"], + test: function(testname, step) { + gMenuPopup.setAttribute("style", "margin: -1000px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result: function(testname, step) { + var popuprect = gMenuPopup.getBoundingClientRect(); + // using negative margins causes the reverse of positive margins, and + // popups will appear on the left or top corners. + var expectedleft = step == "before_end" || step == "after_end" ? + Math.round(window.innerWidth - gPopupWidth) : 0; + var expectedtop = step == "start_after" || step == "end_after" ? + Math.round(window.innerHeight - gPopupHeight) : 0; + is(Math.round(popuprect.left), expectedleft, testname + " x position " + step); + is(Math.round(popuprect.top), expectedtop, testname + " y position " + step); + gMenuPopup.removeAttribute("style"); + } +}, +{ + testname: "popup with unknown step", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + test: function() { + gMenuPopup.openPopup(gTrigger, "other", 0, 0, false, false); + }, + result: function (testname) { + var triggerrect = gMenuPopup.getBoundingClientRect(); + var popuprect = gMenuPopup.getBoundingClientRect(); + is(Math.round(popuprect.left), triggerrect.left, testname + " x position "); + is(Math.round(popuprect.top), triggerrect.top, testname + " y position "); + } +}, +{ + // these tests check to ensure that the position attribute can be used + // to set the position of a popup instead of passing it as an argument + testname: "open popup anchored with attribute", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + steps: ["before_start", "before_end", "after_start", "after_end", + "start_before", "start_after", "end_before", "end_after", "after_pointer", "overlap", + "topcenter topleft", "topright bottomright", "leftcenter topright"], + test: function(testname, step) { + gMenuPopup.setAttribute("position", step); + gMenuPopup.openPopup(gTrigger, "", 0, 0, false, false); + }, + result: function(testname, step) { compareEdge(gTrigger, gMenuPopup, step, 0, 0, testname); } +}, +{ + // this test checks to ensure that the attributes override flag to openPopup + // can be used to override the popup's position. This test also passes an + // event to openPopup to check the trigger node. + testname: "open popup anchored with override", + events: [ "popupshowing thepopup 0010", "popupshown thepopup" ], + test: function(testname, step) { + // attribute overrides the position passed in + gMenuPopup.setAttribute("position", "end_after"); + gExpectedTriggerNode = gCachedEvent.target; + gMenuPopup.openPopup(gTrigger, "before_start", 0, 0, false, true, gCachedEvent); + }, + result: function(testname, step) { + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, gTrigger, testname + " anchorNode"); + is(gMenuPopup.triggerNode, gCachedEvent.target, testname + " triggerNode"); + is(document.popupNode, gCachedEvent.target, testname + " document.popupNode"); + compareEdge(gTrigger, gMenuPopup, "end_after", 0, 0, testname); + } +}, +{ + testname: "close popup with escape", + events: [ "popuphiding thepopup", "popuphidden thepopup", + "DOMMenuInactive thepopup", ], + test: function(testname, step) { + synthesizeKey("VK_ESCAPE", { }); + checkClosed("trigger", testname); + } +}, +{ + // check that offsets may be supplied to the openPopup method + testname: "open popup anchored with offsets", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + test: function(testname, step) { + // attribute is empty so does not override + gMenuPopup.setAttribute("position", ""); + gMenuPopup.openPopup(gTrigger, "before_start", 5, 10, true, true); + }, + result: function(testname, step) { compareEdge(gTrigger, gMenuPopup, "before_start", 5, 10, testname); } +}, +{ + // these tests check to ensure that passing an anchor and position + // puts the popup in the right place + testname: "show popup anchored", + condition: function() { + // only perform this test for popups not in a menu, such as those using + // the popup attribute, as the showPopup implementation in popup.xml + // calls openMenu if the popup is inside a menu + return !gIsMenu; + }, + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + steps: [["topleft", "topleft"], + ["topleft", "topright"], ["topleft", "bottomleft"], + ["topright", "topleft"], ["topright", "bottomright"], + ["bottomleft", "bottomright"], ["bottomleft", "topleft"], + ["bottomright", "bottomleft"], ["bottomright", "topright"]], + test: function(testname, step) { + // the attributes should be ignored + gMenuPopup.setAttribute("popupanchor", "topright"); + gMenuPopup.setAttribute("popupalign", "bottomright"); + gMenuPopup.setAttribute("position", "end_after"); + gMenuPopup.showPopup(gTrigger, -1, -1, "popup", step[0], step[1]); + }, + result: function(testname, step) { + var pos = convertPosition(step[0], step[1]); + compareEdge(gTrigger, gMenuPopup, pos, 0, 0, testname); + gMenuPopup.removeAttribute("popupanchor"); + gMenuPopup.removeAttribute("popupalign"); + gMenuPopup.removeAttribute("position"); + } +}, +{ + testname: "show popup with position", + condition: function() { return !gIsMenu; }, + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + test: function(testname, step) { + gMenuPopup.showPopup(gTrigger, gScreenX + 60, gScreenY + 15, + "context", "topleft", "bottomright"); + }, + result: function(testname, step) { + var rect = gMenuPopup.getBoundingClientRect(); + ok(true, gScreenX + "," + gScreenY); + is(rect.left, 60, testname + " left"); + is(rect.top, 15, testname + " top"); + ok(rect.right, testname + " right is " + rect.right); + ok(rect.bottom, testname + " bottom is " + rect.bottom); + } +}, +{ + // if no anchor is supplied to openPopup, it should be opened relative + // to the viewport. + testname: "open popup unanchored", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + test: function(testname, step) { gMenuPopup.openPopup(null, "after_start", 6, 8, false); }, + result: function(testname, step) { + var rect = gMenuPopup.getBoundingClientRect(); + ok(rect.left == 6 && rect.top == 8 && rect.right && rect.bottom, testname); + } +}, +{ + testname: "activate menuitem with mouse", + events: [ "DOMMenuInactive thepopup", "command item3", + "popuphiding thepopup", "popuphidden thepopup", + "DOMMenuItemInactive item3" ], + test: function(testname, step) { + var item3 = document.getElementById("item3"); + synthesizeMouse(item3, 4, 4, { }); + }, + result: function(testname, step) { checkClosed("trigger", testname); } +}, +{ + testname: "close popup", + condition: function() { return false; }, + events: [ "popuphiding thepopup", "popuphidden thepopup", + "DOMMenuInactive thepopup" ], + test: function(testname, step) { gMenuPopup.hidePopup(); } +}, +{ + testname: "open popup at screen", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + test: function(testname, step) { + gExpectedTriggerNode = "notset"; + gMenuPopup.openPopupAtScreen(gScreenX + 24, gScreenY + 20, false); + }, + result: function(testname, step) { + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, null, testname + " anchorNode"); + is(gMenuPopup.triggerNode, null, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + var rect = gMenuPopup.getBoundingClientRect(); + is(rect.left, 24, testname + " left"); + is(rect.top, 20, testname + " top"); + ok(rect.right, testname + " right is " + rect.right); + ok(rect.bottom, testname + " bottom is " + rect.bottom); + } +}, +{ + // check that pressing a menuitem's accelerator selects it. Note that + // the menuitem with the M accesskey overrides the earlier menuitem that + // begins with M. + testname: "menuitem accelerator", + events: [ "DOMMenuItemActive amenu", "DOMMenuItemInactive amenu", + "DOMMenuInactive thepopup", + "command amenu", "popuphiding thepopup", "popuphidden thepopup", + "DOMMenuItemInactive amenu" + ], + test: function() { synthesizeKey("M", { }); }, + result: function(testname) { checkClosed("trigger", testname); } +}, +{ + testname: "open context popup at screen", + events: [ "popupshowing thepopup 0010", "popupshown thepopup" ], + test: function(testname, step) { + gExpectedTriggerNode = gCachedEvent.target; + gMenuPopup.openPopupAtScreen(gScreenX + 8, gScreenY + 16, true, gCachedEvent); + }, + result: function(testname, step) { + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, null, testname + " anchorNode"); + is(gMenuPopup.triggerNode, gCachedEvent.target, testname + " triggerNode"); + is(document.popupNode, gCachedEvent.target, testname + " document.popupNode"); + + var childframe = document.getElementById("childframe"); + if (childframe) { + for (var t = 0; t < 2; t++) { + var child = childframe.contentDocument; + var evt = child.createEvent("Event"); + evt.initEvent("click", true, true); + child.documentElement.dispatchEvent(evt); + is(child.documentElement.getAttribute("data"), "xnull", + "cannot get popupNode from other document"); + child.documentElement.setAttribute("data", "none"); + // now try again with document.popupNode set explicitly + document.popupNode = gCachedEvent.target; + } + } + + var openX = 8; + var openY = 16; + var rect = gMenuPopup.getBoundingClientRect(); + is(rect.left, openX + (platformIsMac() ? 1 : 2), testname + " left"); + is(rect.top, openY + (platformIsMac() ? -6 : 2), testname + " top"); + ok(rect.right, testname + " right is " + rect.right); + ok(rect.bottom, testname + " bottom is " + rect.bottom); + } +}, +{ + // pressing a letter that doesn't correspond to an accelerator, but does + // correspond to the first letter in a menu's label. The menu should not + // close because there is more than one item corresponding to that letter + testname: "menuitem with non accelerator", + events: [ "DOMMenuItemActive one" ], + test: function() { synthesizeKey("O", { }); }, + result: function(testname) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "one", testname); + } +}, +{ + // pressing the letter again should select the next one that starts with + // that letter + testname: "menuitem with non accelerator again", + events: [ "DOMMenuItemInactive one", "DOMMenuItemActive submenu" ], + test: function() { synthesizeKey("O", { }); }, + result: function(testname) { + // 'submenu' is a menu but it should not be open + checkOpen("trigger", testname); + checkClosed("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + } +}, +{ + // open the submenu with the cursor right key + testname: "open submenu with cursor right", + events: [ "popupshowing submenupopup", "DOMMenuItemActive submenuitem", + "popupshown submenupopup" ], + test: function() { synthesizeKey("VK_RIGHT", { }); }, + result: function(testname) { + checkOpen("trigger", testname); + checkOpen("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive(document.getElementById("submenupopup"), "submenuitem", testname); + } +}, +{ + // close the submenu with the cursor left key + testname: "close submenu with cursor left", + events: [ "popuphiding submenupopup", "popuphidden submenupopup", + "DOMMenuItemInactive submenuitem", "DOMMenuInactive submenupopup", + "DOMMenuItemActive submenu" ], + test: function() { synthesizeKey("VK_LEFT", { }); }, + result: function(testname) { + checkOpen("trigger", testname); + checkClosed("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive(document.getElementById("submenupopup"), "", testname); + } +}, +{ + // open the submenu with the enter key + testname: "open submenu with enter", + events: [ "popupshowing submenupopup", "DOMMenuItemActive submenuitem", + "popupshown submenupopup" ], + test: function() { synthesizeKey("VK_RETURN", { }); }, + result: function(testname) { + checkOpen("trigger", testname); + checkOpen("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive(document.getElementById("submenupopup"), "submenuitem", testname); + } +}, +{ + // close the submenu with the escape key + testname: "close submenu with escape", + events: [ "popuphiding submenupopup", "popuphidden submenupopup", + "DOMMenuItemInactive submenuitem", "DOMMenuInactive submenupopup", + "DOMMenuItemActive submenu" ], + test: function() { synthesizeKey("VK_ESCAPE", { }); }, + result: function(testname) { + checkOpen("trigger", testname); + checkClosed("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive(document.getElementById("submenupopup"), "", testname); + } +}, +{ + // pressing the letter again when the next item is disabled should still + // select the disabled item on Windows, but select the next item on other + // platforms + testname: "menuitem with non accelerator disabled", + events: function() { + if (navigator.platform.indexOf("Win") == 0) { + return [ "DOMMenuItemInactive submenu", "DOMMenuItemActive other", + "DOMMenuItemInactive other", "DOMMenuItemActive item1" ]; + } + return [ "DOMMenuItemInactive submenu", "DOMMenuItemActive last", + "DOMMenuItemInactive last", "DOMMenuItemActive item1" ]; + }, + test: function() { synthesizeKey("O", { }); synthesizeKey("F", { }); }, + result: function(testname) { + checkActive(gMenuPopup, "item1", testname); + } +}, +{ + // pressing a letter that doesn't correspond to an accelerator nor the + // first letter of a menu. This should have no effect. + testname: "menuitem with keypress no accelerator found", + test: function() { synthesizeKey("G", { }); }, + result: function(testname) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "item1", testname); + } +}, +{ + // when only one menuitem starting with that letter exists, it should be + // selected and the menu closed + testname: "menuitem with non accelerator single", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", "DOMMenuInactive thepopup", + "command amenu", "popuphiding thepopup", "popuphidden thepopup", + "DOMMenuItemInactive amenu", + ], + test: function() { synthesizeKey("M", { }); }, + result: function(testname) { + checkClosed("trigger", testname); + checkActive(gMenuPopup, "", testname); + } +}, +{ + testname: "open context popup at screen with all modifiers set", + events: [ "popupshowing thepopup 1111", "popupshown thepopup" ], + autohide: "thepopup", + test: function(testname, step) { + gMenuPopup.openPopupAtScreen(gScreenX + 8, gScreenY + 16, true, gCachedEvent2); + } +}, +{ + testname: "open popup with open property", + events: [ "popupshowing thepopup", "popupshown thepopup" ], + test: function(testname, step) { openMenu(gTrigger); }, + result: function(testname, step) { + checkOpen("trigger", testname); + if (gIsMenu) + compareEdge(gTrigger, gMenuPopup, "after_start", 0, 0, testname); + } +}, +{ + testname: "open submenu with open property", + events: [ "popupshowing submenupopup", "DOMMenuItemActive submenu", + "popupshown submenupopup" ], + test: function(testname, step) { openMenu(document.getElementById("submenu")); }, + result: function(testname, step) { + checkOpen("trigger", testname); + checkOpen("submenu", testname); + // XXXndeakin + // getBoundingClientRect doesn't seem to working right for submenus + // so disable this test for now + // compareEdge(document.getElementById("submenu"), + // document.getElementById("submenupopup"), "end_before", 0, 0, testname); + } +}, +{ + testname: "hidePopup hides entire chain", + events: [ "popuphiding submenupopup", "popuphidden submenupopup", + "popuphiding thepopup", "popuphidden thepopup", + "DOMMenuInactive submenupopup", + "DOMMenuItemInactive submenu", "DOMMenuItemInactive submenu", + "DOMMenuInactive thepopup", ], + test: function() { gMenuPopup.hidePopup(); }, + result: function(testname, step) { + checkClosed("trigger", testname); + checkClosed("submenu", testname); + } +}, +{ + testname: "open submenu with open property without parent open", + test: function(testname, step) { openMenu(document.getElementById("submenu")); }, + result: function(testname, step) { + checkClosed("trigger", testname); + checkClosed("submenu", testname); + } +}, +{ + testname: "open popup with open property and position", + condition: function() { return gIsMenu; }, + events: [ "popupshowing thepopup", "popupshown thepopup" ], + test: function(testname, step) { + gMenuPopup.setAttribute("position", "before_start"); + openMenu(gTrigger); + }, + result: function(testname, step) { + compareEdge(gTrigger, gMenuPopup, "before_start", 0, 0, testname); + } +}, +{ + testname: "close popup with open property", + condition: function() { return gIsMenu; }, + events: [ "popuphiding thepopup", "popuphidden thepopup", + "DOMMenuInactive thepopup" ], + test: function(testname, step) { closeMenu(gTrigger, gMenuPopup); }, + result: function(testname, step) { checkClosed("trigger", testname); } +}, +{ + testname: "open popup with open property, position, anchor and alignment", + condition: function() { return gIsMenu; }, + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + test: function(testname, step) { + gMenuPopup.setAttribute("position", "start_after"); + gMenuPopup.setAttribute("popupanchor", "topright"); + gMenuPopup.setAttribute("popupalign", "bottomright"); + openMenu(gTrigger); + }, + result: function(testname, step) { + compareEdge(gTrigger, gMenuPopup, "start_after", 0, 0, testname); + } +}, +{ + testname: "open popup with open property, anchor and alignment", + condition: function() { return gIsMenu; }, + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + test: function(testname, step) { + gMenuPopup.removeAttribute("position"); + gMenuPopup.setAttribute("popupanchor", "bottomright"); + gMenuPopup.setAttribute("popupalign", "topright"); + openMenu(gTrigger); + }, + result: function(testname, step) { + compareEdge(gTrigger, gMenuPopup, "after_end", 0, 0, testname); + gMenuPopup.removeAttribute("popupanchor"); + gMenuPopup.removeAttribute("popupalign"); + } +}, +{ + testname: "focus and cursor down on trigger", + condition: function() { return gIsMenu; }, + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + test: function(testname, step) { + gTrigger.focus(); + synthesizeKey("VK_DOWN", { altKey: !platformIsMac() }); + }, + result: function(testname, step) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "", testname); + } +}, +{ + testname: "focus and cursor up on trigger", + condition: function() { return gIsMenu; }, + events: [ "popupshowing thepopup", "popupshown thepopup" ], + test: function(testname, step) { + gTrigger.focus(); + synthesizeKey("VK_UP", { altKey: !platformIsMac() }); + }, + result: function(testname, step) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "", testname); + } +}, +{ + testname: "select and enter on menuitem", + condition: function() { return gIsMenu; }, + events: [ "DOMMenuItemActive item1", "DOMMenuItemInactive item1", + "DOMMenuInactive thepopup", "command item1", + "popuphiding thepopup", "popuphidden thepopup", + "DOMMenuItemInactive item1" ], + test: function(testname, step) { + synthesizeKey("VK_DOWN", { }); + synthesizeKey("VK_RETURN", { }); + }, + result: function(testname, step) { checkClosed("trigger", testname); } +}, +{ + testname: "focus trigger and key to open", + condition: function() { return gIsMenu; }, + events: [ "popupshowing thepopup", "popupshown thepopup" ], + autohide: "thepopup", + test: function(testname, step) { + gTrigger.focus(); + synthesizeKey(platformIsMac() ? " " : "VK_F4", { }); + }, + result: function(testname, step) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "", testname); + } +}, +{ + // the menu should only open when the meta or alt key is not pressed + testname: "focus trigger and key wrong modifier", + condition: function() { return gIsMenu; }, + test: function(testname, step) { + gTrigger.focus(); + if (platformIsMac()) + synthesizeKey("VK_F4", { altKey: true }); + else + synthesizeKey("", { metaKey: true }); + }, + result: function(testname, step) { + checkClosed("trigger", testname); + } +}, +{ + testname: "mouse click on disabled menu", + condition: function() { return gIsMenu; }, + test: function(testname, step) { + gTrigger.setAttribute("disabled", "true"); + synthesizeMouse(gTrigger, 4, 4, { }); + }, + result: function(testname, step) { + checkClosed("trigger", testname); + gTrigger.removeAttribute("disabled"); + } +}, +{ + // openPopup should open the menu synchronously, however popupshown + // is fired asynchronously + testname: "openPopup synchronous", + events: [ "popupshowing thepopup", "popupshowing submenupopup", + "popupshown thepopup", "DOMMenuItemActive submenu", + "popupshown submenupopup" ], + test: function(testname, step) { + gMenuPopup.openPopup(gTrigger, "after_start", 0, 0, false, true); + document.getElementById("submenupopup"). + openPopup(gTrigger, "end_before", 0, 0, false, true); + checkOpen("trigger", testname); + checkOpen("submenu", testname); + } +}, +{ + // remove the content nodes for the popup + testname: "remove content", + test: function(testname, step) { + var submenupopup = document.getElementById("submenupopup"); + submenupopup.parentNode.removeChild(submenupopup); + var popup = document.getElementById("thepopup"); + popup.parentNode.removeChild(popup); + } +} + +]; + +function platformIsMac() +{ + return navigator.platform.indexOf("Mac") > -1; +} diff --git a/toolkit/content/tests/chrome/rtlchrome/rtl.css b/toolkit/content/tests/chrome/rtlchrome/rtl.css new file mode 100644 index 0000000000..0fea010019 --- /dev/null +++ b/toolkit/content/tests/chrome/rtlchrome/rtl.css @@ -0,0 +1,2 @@ +/* Imitate RTL UI */
+window { direction: rtl; }
diff --git a/toolkit/content/tests/chrome/rtlchrome/rtl.dtd b/toolkit/content/tests/chrome/rtlchrome/rtl.dtd new file mode 100644 index 0000000000..8b32de6746 --- /dev/null +++ b/toolkit/content/tests/chrome/rtlchrome/rtl.dtd @@ -0,0 +1 @@ +<!ENTITY locale.dir "rtl"> diff --git a/toolkit/content/tests/chrome/rtlchrome/rtl.manifest b/toolkit/content/tests/chrome/rtlchrome/rtl.manifest new file mode 100644 index 0000000000..a4cc6929be --- /dev/null +++ b/toolkit/content/tests/chrome/rtlchrome/rtl.manifest @@ -0,0 +1,5 @@ +content rtlchrome /
+
+# Override intl.css with our own CSS file
+override chrome://global/locale/intl.css chrome://rtlchrome/rtl.css
+override chrome://global/locale/global.dtd chrome://rtlchrome/rtl.dtd
diff --git a/toolkit/content/tests/chrome/rtltest/content/dirtest.xul b/toolkit/content/tests/chrome/rtltest/content/dirtest.xul new file mode 100644 index 0000000000..b75d41eaa3 --- /dev/null +++ b/toolkit/content/tests/chrome/rtltest/content/dirtest.xul @@ -0,0 +1,25 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<html:style> +hbox, vbox { background-color: white; } +hbox:-moz-locale-dir(ltr) { background-color: yellow; } +vbox:-moz-locale-dir(rtl) { background-color: green; } +</html:style> + +<hbox id="hbox"> + <button label="One"/> + <button label="Two"/> + <button label="Three"/> +</hbox> +<vbox id="vbox"> + <button label="One"/> + <button label="Two"/> + <button label="Three"/> +</vbox> + +</window> diff --git a/toolkit/content/tests/chrome/rtltest/righttoleft.manifest b/toolkit/content/tests/chrome/rtltest/righttoleft.manifest new file mode 100644 index 0000000000..db98656bce --- /dev/null +++ b/toolkit/content/tests/chrome/rtltest/righttoleft.manifest @@ -0,0 +1,3 @@ +content ltrtest content/ +content rtltest content/ +locale rtltest ar-QA content/ diff --git a/toolkit/content/tests/chrome/sample_entireword_latin1.html b/toolkit/content/tests/chrome/sample_entireword_latin1.html new file mode 100644 index 0000000000..b2d66fa3c4 --- /dev/null +++ b/toolkit/content/tests/chrome/sample_entireword_latin1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head><title>Latin entire-word find test page</title></head> + <body> + <!-- Feel free to extend the contents of this page with more comprehensive + - Latin punctuation and/ or word markers. + --> + <p>The twins of Mammon quarrelled. Their warring plunged the world into a new darkness, and the beast abhorred the darkness. So it began to move swiftly, and grew more powerful, and went forth and multiplied. And the beasts brought fire and light to the darkness.</p> + <p>from The Book of Mozilla, 15:1</p> + </body> +</html> diff --git a/toolkit/content/tests/chrome/test_about_networking.html b/toolkit/content/tests/chrome/test_about_networking.html new file mode 100644 index 0000000000..6ffaf2ba79 --- /dev/null +++ b/toolkit/content/tests/chrome/test_about_networking.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=912103 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + const Cc = Components.classes; + const Ci = Components.interfaces; + + var dashboard = Cc['@mozilla.org/network/dashboard;1'] + .getService(Ci.nsIDashboard); + dashboard.enableLogging = true; + + var wsURI = "ws://mochi.test:8888/chrome/toolkit/content/tests/chrome/file_about_networking"; + var websocket = new WebSocket(wsURI); + + websocket.addEventListener("open", function() { + dashboard.requestWebsocketConnections(function(data) { + var found = false; + for (var i = 0; i < data.websockets.length; i++) { + if (data.websockets[i].hostport == "mochi.test:8888") { + found = true; + break; + } + } + isnot(found, false, "tested websocket entry not found"); + websocket.close(); + SimpleTest.finish(); + }); + }); + } + + window.addEventListener("DOMContentLoaded", function run() { + window.removeEventListener("DOMContentLoaded", run); + runTest(); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=912103">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_arrowpanel.xul b/toolkit/content/tests/chrome/test_arrowpanel.xul new file mode 100644 index 0000000000..671c33a15c --- /dev/null +++ b/toolkit/content/tests/chrome/test_arrowpanel.xul @@ -0,0 +1,327 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Arrow Panels" + style="padding: 10px;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<stack flex="1"> + <label id="topleft" value="Top Left Corner" left="15" top="15"/> + <label id="topright" value="Top Right" right="15" top="15"/> + <label id="bottomleft" value="Bottom Left Corner" left="15" bottom="15"/> + <label id="bottomright" value="Bottom Right" right="15" bottom="15"/> + <!-- Our SimpleTest/TestRunner.js runs tests inside an iframe which sizes are W=500 H=300. + 'left' and 'top' values need to be set so that the panel (popup) has enough room to display on its 4 sides. --> + <label id="middle" value="+/- Centered" left="225" top="135"/> + <iframe id="frame" type="content" + src="data:text/html,<input id='input'>" width="100" height="100" left="225" top="120"/> +</stack> + +<panel id="panel" type="arrow" animate="false" + onpopupshown="checkPanelPosition(this)" onpopuphidden="runNextTest.next()"> + <box width="115" height="65"/> +</panel> + +<panel id="bigpanel" type="arrow" animate="false" + onpopupshown="checkBigPanel(this)" onpopuphidden="runNextTest.next()"> + <box width="125" height="3000"/> +</panel> + +<panel id="animatepanel" type="arrow" + onpopupshown="animatedPopupShown = true;" + onpopuphidden="animatedPopupHidden = true; runNextTest.next();"> + <label value="Animate Closed" height="40"/> +</panel> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +const isOSXYosemite = navigator.userAgent.indexOf("Mac OS X 10.10") != -1; + +var expectedAnchor = null; +var expectedSide = "", expectedAnchorEdge = "", expectedPack = "", expectedAlignment = ""; +var zoomFactor = 1; +var animatedPopupShown = false; +var animatedPopupHidden = false; +var runNextTest; + +function startTest() +{ + runNextTest = nextTest(); + runNextTest.next(); +} + +function nextTest() +{ + var panel = $("panel"); + + function openPopup(position, anchor, expected, anchorEdge, pack, alignment) + { + expectedAnchor = anchor instanceof Node ? anchor : $(anchor); + expectedSide = expected; + expectedAnchorEdge = anchorEdge; + expectedPack = pack; + expectedAlignment = alignment == undefined ? position : alignment; + + panel.removeAttribute("side"); + panel.openPopup(expectedAnchor, position, 0, 0, false, false, null); + } + + for (var iter = 0; iter < 2; iter++) { + openPopup("after_start", "topleft", "top", "left", "start"); + yield; + openPopup("after_start", "bottomleft", "bottom", "left", "start", "before_start"); + yield; + openPopup("before_start", "topleft", "top", "left", "start", "after_start"); + yield; + openPopup("before_start", "bottomleft", "bottom", "left", "start"); + yield; + openPopup("after_start", "middle", "top", "left", "start"); + yield; + openPopup("before_start", "middle", "bottom", "left", "start"); + yield; + + openPopup("after_start", "topright", "top", "right", "end", "after_end"); + yield; + openPopup("after_start", "bottomright", "bottom", "right", "end", "before_end"); + yield; + openPopup("before_start", "topright", "top", "right", "end", "after_end"); + yield; + openPopup("before_start", "bottomright", "bottom", "right", "end", "before_end"); + yield; + + openPopup("after_end", "middle", "top", "right", "end"); + yield; + openPopup("before_end", "middle", "bottom", "right", "end"); + yield; + + openPopup("start_before", "topleft", "left", "top", "start", "end_before"); + yield; + openPopup("start_before", "topright", "right", "top", "start"); + yield; + openPopup("end_before", "topleft", "left", "top", "start"); + yield; + openPopup("end_before", "topright", "right", "top", "start", "start_before"); + yield; + openPopup("start_before", "middle", "right", "top", "start"); + yield; + openPopup("end_before", "middle", "left", "top", "start"); + yield; + + openPopup("start_before", "bottomleft", "left", "bottom", "end", "end_after"); + yield; + openPopup("start_before", "bottomright", "right", "bottom", "end", "start_after"); + yield; + openPopup("end_before", "bottomleft", "left", "bottom", "end", "end_after"); + yield; + openPopup("end_before", "bottomright", "right", "bottom", "end", "start_after"); + yield; + + openPopup("start_after", "middle", "right", "bottom", "end"); + yield; + openPopup("end_after", "middle", "left", "bottom", "end"); + yield; + + openPopup("topcenter bottomleft", "bottomleft", "bottom", "center left", "start", "before_start"); + yield; + openPopup("bottomcenter topleft", "topleft", "top", "center left", "start", "after_start"); + yield; + openPopup("topcenter bottomright", "bottomright", "bottom", "center right", "end", "before_end"); + yield; + openPopup("bottomcenter topright", "topright", "top", "center right", "end", "after_end"); + yield; + openPopup("topcenter bottomleft", "middle", "bottom", "center left", "start", "before_start"); + yield; + openPopup("bottomcenter topleft", "middle", "top", "center left", "start", "after_start"); + yield; + + openPopup("leftcenter topright", "middle", "right", "center top", "start", "start_before"); + yield; + openPopup("rightcenter bottomleft", "middle", "left", "center bottom", "end", "end_after"); + yield; + +/* + XXXndeakin disable these parts of the test which often cause problems, see bug 626563 + + openPopup("after_start", frames[0].document.getElementById("input"), "top", "left", "start"); + yield; + + setScale(frames[0], 1.5); + openPopup("after_start", frames[0].document.getElementById("input"), "top", "left", "start"); + yield; + + setScale(frames[0], 2.5); + openPopup("before_start", frames[0].document.getElementById("input"), "bottom", "left", "start"); + yield; + + setScale(frames[0], 1); +*/ + + $("bigpanel").openPopup($("topleft"), "after_start", 0, 0, false, false, null, "start"); + yield; + + // switch to rtl mode + document.documentElement.style.direction = "rtl"; + $("topleft").setAttribute("right", "15"); + $("topright").setAttribute("left", "15"); + $("bottomleft").setAttribute("right", "15"); + $("bottomright").setAttribute("left", "15"); + $("topleft").removeAttribute("left"); + $("topright").removeAttribute("right"); + $("bottomleft").removeAttribute("left"); + $("bottomright").removeAttribute("right"); + } + + // Test that a transition occurs when opening or closing the popup. The transition is + // disabled on Linux. + if (navigator.platform.indexOf("Linux") == -1) { + function transitionEnded(event) { + if ($("animatepanel").state != "open") { + is($("animatepanel").state, "showing", "state is showing during transitionend"); + ok(!animatedPopupShown, "popupshown not fired yet") + } else { + is($("animatepanel").state, "open", "state is open after transitionend"); + ok(animatedPopupShown, "popupshown now fired") + SimpleTest.executeSoon(() => runNextTest.next()); + } + } + + // Check that the transition occurs for an arrow panel with animate="true" + window.addEventListener("transitionend", transitionEnded, false); + $("animatepanel").openPopup($("topleft"), "after_start", 0, 0, false, false, null, "start"); + is($("animatepanel").state, "showing", "state is showing"); + yield; + window.removeEventListener("transitionend", transitionEnded, false); + + synthesizeKey("VK_ESCAPE", { }); + ok(!animatedPopupHidden, "animated popup not hidden yet"); + yield; + } + + SimpleTest.finish() + yield; +} + +function setScale(win, scale) +{ + var wn = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation); + var shell = wn.QueryInterface(Components.interfaces.nsIDocShell); + var docViewer = shell.contentViewer; + docViewer.fullZoom = scale; + zoomFactor = scale; +} + +function checkPanelPosition(panel) +{ + let anchor = panel.anchorNode; + let adj = 0, hwinpos = 0, vwinpos = 0; + if (anchor.ownerDocument != document) { + var framerect = anchor.ownerDocument.defaultView.frameElement.getBoundingClientRect(); + hwinpos = framerect.left; + vwinpos = framerect.top; + } + + // Positions are reversed in rtl yet the coordinates used in the computations + // are not, so flip the expected label side and anchor edge. + var isRTL = (window.getComputedStyle(panel).direction == "rtl"); + if (isRTL) { + var flipLeftRight = val => val == "left" ? "right" : "left"; + expectedAnchorEdge = expectedAnchorEdge.replace(/(left|right)/, flipLeftRight); + expectedSide = expectedSide.replace(/(left|right)/, flipLeftRight); + } + + var panelRect = panel.getBoundingClientRect(); + var anchorRect = anchor.getBoundingClientRect(); + var contentBO = panel.firstChild.boxObject; + var contentRect = { top: contentBO.y, + left: contentBO.x, + bottom: contentBO.y + contentBO.height, + right: contentBO.x + contentBO.width }; + switch (expectedSide) { + case "top": + ok(contentRect.top > vwinpos + anchorRect.bottom * zoomFactor + 5, "panel content is below"); + break; + case "bottom": + ok(contentRect.bottom < vwinpos + anchorRect.top * zoomFactor - 5, "panel content is above"); + break; + case "left": + ok(contentRect.left > hwinpos + anchorRect.right * zoomFactor + 5, "panel content is right"); + break; + case "right": + ok(contentRect.right < hwinpos + anchorRect.left * zoomFactor - 5, "panel content is left"); + break; + } + + let iscentered = false; + if (expectedAnchorEdge.indexOf("center ") == 0) { + expectedAnchorEdge = expectedAnchorEdge.substring(7); + iscentered = true; + } + + switch (expectedAnchorEdge) { + case "top": + adj = vwinpos + parseInt(getComputedStyle(panel, "").marginTop); + if (iscentered) + adj += Math.round(anchorRect.height) / 2; + isWithinHalfPixel(panelRect.top, anchorRect.top * zoomFactor + adj, "anchored on top"); + break; + case "bottom": + adj = vwinpos + parseInt(getComputedStyle(panel, "").marginBottom); + if (iscentered) + adj += Math.round(anchorRect.height) / 2; + isWithinHalfPixel(panelRect.bottom, anchorRect.bottom * zoomFactor - adj, "anchored on bottom"); + break; + case "left": + adj = hwinpos + parseInt(getComputedStyle(panel, "").marginLeft); + if (iscentered) + adj += Math.round(anchorRect.width) / 2; + isWithinHalfPixel(panelRect.left, anchorRect.left * zoomFactor + adj, "anchored on left "); + break; + case "right": + adj = hwinpos + parseInt(getComputedStyle(panel, "").marginRight); + if (iscentered) + adj += Math.round(anchorRect.width) / 2; + if (!isOSXYosemite) + isWithinHalfPixel(panelRect.right, anchorRect.right * zoomFactor - adj, "anchored on right"); + break; + } + + is(anchor, expectedAnchor, "anchor"); + + var arrow = document.getAnonymousElementByAttribute(panel, "anonid", "arrow"); + is(arrow.getAttribute("side"), expectedSide, "panel arrow side"); + is(arrow.hidden, false, "panel hidden"); + is(arrow.parentNode.pack, expectedPack, "panel arrow pack"); + is(panel.alignmentPosition, expectedAlignment, "panel alignmentPosition"); + + panel.hidePopup(); +} + +function isWithinHalfPixel(a, b, desc) +{ + ok(Math.abs(a - b) <= 0.5, desc); +} + +function checkBigPanel(panel) +{ + ok(panel.firstChild.getBoundingClientRect().height < 2800, "big panel height"); + panel.hidePopup(); +} + +SimpleTest.waitForFocus(startTest); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"/> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete2.xul b/toolkit/content/tests/chrome/test_autocomplete2.xul new file mode 100644 index 0000000000..875cddd07a --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete2.xul @@ -0,0 +1,197 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 2" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<textbox id="autocomplete" type="autocomplete" + autocompletesearch="simple" + onsearchcomplete="checkResult();"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +// Set to indicate whether or not we want autoCompleteSimple to return a result +var returnResult = false; + +const ACR = Components.interfaces.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (returnResult) { + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "SUCCESS"; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: 0, + getValueAt: function() { return this._param; }, + getCommentAt: function() { return null; }, + getStyleAt: function() { return null; }, + getImageAt: function() { return null; }, + getFinalCompleteValueAt: function() { return this.getValueAt(); }, + getLabelAt: function() { return null; }, + removeValueAt: function() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIFactory) || + iid.equals(Components.interfaces.nsIAutoCompleteSearch)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + createInstance: function(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch: function(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch: function() {} +}; + +var componentManager = Components.manager + .QueryInterface(Components.interfaces.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + + +// Test Bug 441530 - correctly setting "nomatch" +// Test Bug 441526 - correctly setting style with "highlightnonmatches" + +SimpleTest.waitForExplicitFinish(); +setTimeout(startTest, 0); + +function startTest() { + var autocomplete = $("autocomplete"); + + // Ensure highlightNonMatches can be set correctly. + + // This should not be set by default. + is(autocomplete.hasAttribute("highlightnonmatches"), false, + "highlight nonmatches not set by default"); + + autocomplete.highlightNonMatches = "true"; + + is(autocomplete.getAttribute("highlightnonmatches"), "true", + "highlight non matches attribute set correctly"); + is(autocomplete.highlightNonMatches, true, + "highlight non matches getter returned correctly"); + + autocomplete.highlightNonMatches = "false"; + + is(autocomplete.getAttribute("highlightnonmatches"), "false", + "highlight non matches attribute set to false correctly"); + is(autocomplete.highlightNonMatches, false, + "highlight non matches getter returned false correctly"); + + ok(!autocomplete.popup.hasAttribute("autocompleteinput"), + "autocompleteinput on popup not set by default"); + + check(); +} + +function check() { + var autocomplete = $("autocomplete"); + + // Toggle this value, so we can re-use the one function. + returnResult = !returnResult; + + // blur the field to ensure that the popup is closed and that the previous + // search has stopped, then start a new search. + autocomplete.blur(); + autocomplete.focus(); + synthesizeKey("r", {}); +} + +function checkResult() { + var autocomplete = $("autocomplete"); + var style = window.getComputedStyle(autocomplete, ""); + + if (returnResult) { + // Result was returned, so there should not be a nomatch attribute + is(autocomplete.hasAttribute("nomatch"), false, + "nomatch attribute shouldn't be present here"); + + // Ensure that the style is set correctly whichever way highlightNonMatches + // is set. + autocomplete.highlightNonMatches = "true"; + + isnot(style.getPropertyCSSValue("color").cssText, "rgb(255, 0, 0)", + "not nomatch and highlightNonMatches - should not be red"); + + autocomplete.highlightNonMatches = "false"; + + isnot(style.getPropertyCSSValue("color").cssText, "rgb(255, 0, 0)", + "not nomatch and not highlightNonMatches - should not be red"); + + is (autocomplete.popup.getAttribute("autocompleteinput"), "autocomplete", + "The popup's autocompleteinput attribute is set to the ID of the textbox"); + + setTimeout(check, 0); + } + else { + // No result was returned, so there should be nomatch attribute + is(autocomplete.getAttribute("nomatch"), "true", + "nomatch attribute not correctly set when expected"); + + // Ensure that the style is set correctly whichever way highlightNonMatches + // is set. + autocomplete.highlightNonMatches = "true"; + + is(style.getPropertyCSSValue("color").cssText, "rgb(255, 0, 0)", + "nomatch and highlightNonMatches - should be red"); + + autocomplete.highlightNonMatches = "false"; + + isnot(style.getPropertyCSSValue("color").cssText, "rgb(255, 0, 0)", + "nomatch and not highlightNonMatches - should not be red"); + + ok(!autocomplete.popup.hasAttribute("autocompleteinput"), + "autocompleteinput on popup not set when closed"); + + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete3.xul b/toolkit/content/tests/chrome/test_autocomplete3.xul new file mode 100644 index 0000000000..953fd15c86 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete3.xul @@ -0,0 +1,188 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 3" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<textbox id="autocomplete" type="autocomplete" + autocompletesearch="simple" + onsearchcomplete="checkResult();"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +// Set to indicate whether or not we want autoCompleteSimple to return a result +var returnResult = true; + +const ACR = Components.interfaces.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (returnResult) { + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "Result"; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt: function() { return this._param; }, + getCommentAt: function() { return null; }, + getStyleAt: function() { return null; }, + getImageAt: function() { return null; }, + getFinalCompleteValueAt: function() { return this.getValueAt(); }, + getLabelAt: function() { return null; }, + removeValueAt: function() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIFactory) || + iid.equals(Components.interfaces.nsIAutoCompleteSearch)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + createInstance: function(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch: function(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch: function() {} +}; + +var componentManager = Components.manager + .QueryInterface(Components.interfaces.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + + +// Test Bug 325842 - completeDefaultIndex + +SimpleTest.waitForExplicitFinish(); +setTimeout(startTest, 0); + +var currentTest = 0; + +// Note the entries for these tests (key) are incremental. +const tests = [ + { completeDefaultIndex: "false", key: "r", result: "r", + start: 1, end: 1 }, + { completeDefaultIndex: "true", key: "e", result: "result", + start: 2, end: 6 }, + { completeDefaultIndex: "true", key: "t", result: "ret >> Result", + start: 3, end: 13 } +]; + +function startTest() { + var autocomplete = $("autocomplete"); + + // These should not be set by default. + is(autocomplete.hasAttribute("completedefaultindex"), false, + "completedefaultindex not set by default"); + + autocomplete.completeDefaultIndex = "true"; + + is(autocomplete.getAttribute("completedefaultindex"), "true", + "completedefaultindex attribute set correctly"); + is(autocomplete.completeDefaultIndex, true, + "autoFill getter returned correctly"); + + autocomplete.completeDefaultIndex = "false"; + + is(autocomplete.getAttribute("completedefaultindex"), "false", + "completedefaultindex attribute set to false correctly"); + is(autocomplete.completeDefaultIndex, false, + "completeDefaultIndex getter returned false correctly"); + + checkNext(); +} + +function checkNext() { + var autocomplete = $("autocomplete"); + + autocomplete.completeDefaultIndex = tests[currentTest].completeDefaultIndex; + autocomplete.focus(); + + synthesizeKey(tests[currentTest].key, {}); +} + +function checkResult() { + var autocomplete = $("autocomplete"); + var style = window.getComputedStyle(autocomplete, ""); + + is(autocomplete.value, tests[currentTest].result, + "Test " + currentTest + ": autocomplete.value should equal '" + + tests[currentTest].result + "'"); + + is(autocomplete.selectionStart, tests[currentTest].start, + "Test " + currentTest + ": autocomplete selection should start at " + + tests[currentTest].start); + + is(autocomplete.selectionEnd, tests[currentTest].end, + "Test " + currentTest + ": autocomplete selection should end at " + + tests[currentTest].end); + + ++currentTest; + + if (currentTest < tests.length) + setTimeout(checkNext, 0); + else { + // TODO (bug 494809): Autocomplete-in-the-middle should take in count RTL + // and complete on VK_RIGHT or VK_LEFT based on that. It should also revert + // what user has typed to far if he moves in the opposite direction. + if (autocomplete.value.indexOf(">>") == -1) { + // Test result if user accepts autocomplete suggestion. + synthesizeKey("VK_RIGHT", {}); + is(autocomplete.value, "Result", + "Test complete: autocomplete.value should equal 'Result'"); + is(autocomplete.selectionStart, 6, + "Test complete: autocomplete selection should start at 6"); + is(autocomplete.selectionEnd, 6, + "Test complete: autocomplete selection should end at 6"); + } + + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete4.xul b/toolkit/content/tests/chrome/test_autocomplete4.xul new file mode 100644 index 0000000000..007e956612 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete4.xul @@ -0,0 +1,280 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 4" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<textbox id="autocomplete" + type="autocomplete" + completedefaultindex="true" + + onsearchcomplete="searchComplete();" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +// Set to indicate whether or not we want autoCompleteSimple to return a result +var returnResult = true; + +const ACR = Components.interfaces.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (returnResult) { + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "Result"; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt: function() { return this._param; }, + getCommentAt: function() { return null; }, + getStyleAt: function() { return null; }, + getImageAt: function() { return null; }, + getFinalCompleteValueAt: function() { return this.getValueAt(); }, + getLabelAt: function() { return null; }, + removeValueAt: function() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIFactory) || + iid.equals(Components.interfaces.nsIAutoCompleteSearch)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + createInstance: function(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch: function(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch: function() {} +}; + +var componentManager = Components.manager + .QueryInterface(Components.interfaces.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + + +// Test Bug 325842 - completeDefaultIndex + +SimpleTest.waitForExplicitFinish(); +setTimeout(nextTest, 0); + +var currentTest = null; + +// Note the entries for these tests (key) are incremental. +const tests = [ + { + desc: "HOME key remove selection", + key: "VK_HOME", + removeSelection: true, + result: "re", + start: 0, end: 0 + }, + { + desc: "LEFT key remove selection", + key: "VK_LEFT", + removeSelection: true, + result: "re", + start: 1, end: 1 + }, + { desc: "RIGHT key remove selection", + key: "VK_RIGHT", + removeSelection: true, + result: "re", + start: 2, end: 2 + }, + { desc: "ENTER key remove selection", + key: "VK_RETURN", + removeSelection: true, + result: "re", + start: 2, end: 2 + }, + { + desc: "HOME key", + key: "VK_HOME", + removeSelection: false, + result: "Result", + start: 0, end: 0 + }, + { + desc: "LEFT key", + key: "VK_LEFT", + removeSelection: false, + result: "Result", + start: 5, end: 5 + }, + { desc: "RIGHT key", + key: "VK_RIGHT", + removeSelection: false, + result: "Result", + start: 6, end: 6 + }, + { desc: "RETURN key", + key: "VK_RETURN", + removeSelection: false, + result: "Result", + start: 6, end: 6 + }, + { desc: "TAB key should confirm suggestion when forcecomplete is set", + key: "VK_TAB", + removeSelection: false, + forceComplete: true, + result: "Result", + start: 6, end: 6 + }, + + { desc: "RIGHT key complete from middle", + key: "VK_RIGHT", + forceComplete: true, + completeFromMiddle: true, + result: "Result", + start: 6, end: 6 + }, + { + desc: "RIGHT key w/ minResultsForPopup=2", + key: "VK_RIGHT", + removeSelection: false, + minResultsForPopup: 2, + result: "Result", + start: 6, end: 6 + }, +]; + +function nextTest() { + if (!tests.length) { + // No more tests to run, finish. + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); + return; + } + + var autocomplete = $("autocomplete"); + autocomplete.value = ""; + currentTest = tests.shift(); + + // HOME key works differently on Mac, so we skip tests using it. + if (currentTest.key == "VK_HOME" && navigator.platform.indexOf("Mac") != -1) + nextTest(); + else + setTimeout(runCurrentTest, 0); +} + +function runCurrentTest() { + var autocomplete = $("autocomplete"); + if ("minResultsForPopup" in currentTest) + autocomplete.setAttribute("minresultsforpopup", currentTest.minResultsForPopup) + else + autocomplete.removeAttribute("minresultsforpopup"); + + autocomplete.focus(); + + if (!currentTest.completeFromMiddle) { + synthesizeKey("r", {}); + synthesizeKey("e", {}); + } + else { + synthesizeKey("l", {}); + synthesizeKey("t", {}); + } +} + +function searchComplete() { + var autocomplete = $("autocomplete"); + autocomplete.setAttribute("forcecomplete", currentTest.forceComplete ? true : false); + + if (currentTest.completeFromMiddle) { + if (!currentTest.forceComplete) { + synthesizeKey(currentTest.key, {}); + } + else if (!/ >> /.test(autocomplete.value)) { + // At this point we should have a value like "lt >> Result" showing. + throw new Error("Expected an middle-completed value, got " + autocomplete.value); + } + + // For forceComplete a blur should cause a value from the results to get + // completed to. E.g. "lt >> Result" will turn into "Result". + if (currentTest.forceComplete) + autocomplete.blur(); + + checkResult(); + return; + } + + is(autocomplete.value, "result", + "Test '" + currentTest.desc + "': autocomplete.value should equal 'result'"); + + if (autocomplete.selectionStart == 2) { // Finished inserting "re" string. + if (currentTest.removeSelection) { + // remove current selection + synthesizeKey("VK_DELETE", {}); + } + + synthesizeKey(currentTest.key, {}); + + checkResult(); + } +} + +function checkResult() { + var autocomplete = $("autocomplete"); + + is(autocomplete.value, currentTest.result, + "Test '" + currentTest.desc + "': autocomplete.value should equal '" + + currentTest.result + "'"); + + is(autocomplete.selectionStart, currentTest.start, + "Test '" + currentTest.desc + "': autocomplete selection should start at " + + currentTest.start); + + is(autocomplete.selectionEnd, currentTest.end, + "Test '" + currentTest.desc + "': autocomplete selection should end at " + + currentTest.end); + + setTimeout(nextTest, 0); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete5.xul b/toolkit/content/tests/chrome/test_autocomplete5.xul new file mode 100644 index 0000000000..2f6dc5a307 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete5.xul @@ -0,0 +1,152 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 5" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<textbox id="autocomplete" type="autocomplete" + autocompletesearch="simple" + ontextentered="checkTextEntered();" + ontextreverted="checkTextReverted();" + onsearchbegin="checkSearchBegin();" + onsearchcomplete="checkSearchCompleted();"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +const ACR = Components.interfaces.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "SUCCESS"; +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: 0, + getValueAt: function() { return this._param; }, + getCommentAt: function() { return null; }, + getStyleAt: function() { return null; }, + getImageAt: function() { return null; }, + getFinalCompleteValueAt: function() { return this.getValueAt(); }, + getLabelAt: function() { return null; }, + removeValueAt: function() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIFactory) || + iid.equals(Components.interfaces.nsIAutoCompleteSearch)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + createInstance: function(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch: function(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch: function() {} +}; + +var componentManager = Components.manager + .QueryInterface(Components.interfaces.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +SimpleTest.waitForExplicitFinish(); +setTimeout(startTest, 0); + +function startTest() { + let autocomplete = $("autocomplete"); + + // blur the field to ensure that the popup is closed and that the previous + // search has stopped, then start a new search. + autocomplete.blur(); + autocomplete.focus(); + synthesizeKey("r", {}); +} + +let hasTextEntered = false; +let hasSearchBegun = false; + +function checkSearchBegin() { + hasSearchBegun = true; +} + +let test = 0; +function checkSearchCompleted() { + is(hasSearchBegun, true, "onsearchbegin handler has been correctly called."); + + if (test == 0) { + hasSearchBegun = false; + synthesizeKey("VK_RETURN", { }); + } else if (test == 1) { + hasSearchBegun = false; + synthesizeKey("VK_ESCAPE", { }); + } else { + throw "checkSearchCompleted should only be called twice."; + } +} + +function checkTextEntered() { + is(test, 0, "checkTextEntered should be reached from first test."); + is(hasSearchBegun, false, "onsearchbegin handler should not be called on text revert."); + + // fire second test + test++; + + let autocomplete = $("autocomplete"); + autocomplete.textValue = ""; + autocomplete.blur(); + autocomplete.focus(); + synthesizeKey("r", {}); +} + +function checkTextReverted() { + is(test, 1, "checkTextReverted should be the second test reached."); + is(hasSearchBegun, false, "onsearchbegin handler should not be called on text revert."); + + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_delayOnPaste.xul b/toolkit/content/tests/chrome/test_autocomplete_delayOnPaste.xul new file mode 100644 index 0000000000..19f54ac217 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_delayOnPaste.xul @@ -0,0 +1,128 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 4" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + +<textbox id="autocomplete" + type="autocomplete" + completedefaultindex="true" + onsearchcomplete="searchComplete();" + timeout="0" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +function autoCompleteSimpleResult(aString) { + this.searchString = aString; + this.searchResult = Components.interfaces.nsIAutoCompleteResult.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "Result"; +} +autoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: Components.interfaces.nsIAutoCompleteResult.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt: function() { return this._param; }, + getCommentAt: function() { return null; }, + getStyleAt: function() { return null; }, + getImageAt: function() { return null; }, + getFinalCompleteValueAt: function() { return this.getValueAt(); }, + getLabelAt: function() { return null; }, + removeValueAt: function() {} +}; + +// A basic autocomplete implementation that returns one result. +let autoCompleteSimple = { + classID: Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"), + contractID: "@mozilla.org/autocomplete/search;1?name=simple", + QueryInterface: XPCOMUtils.generateQI([ + Components.interfaces.nsIFactory, + Components.interfaces.nsIAutoCompleteSearch + ]), + createInstance: function (outer, iid) { + return this.QueryInterface(iid); + }, + + registerFactory: function () { + let registrar = + Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar); + registrar.registerFactory(this.classID, "Test Simple Autocomplete", + this.contractID, this); + }, + unregisterFactory: function () { + let registrar = + Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar); + registrar.unregisterFactory(this.classID, this); + }, + + startSearch: function (aString, aParam, aResult, aListener) { + let result = new autoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + stopSearch: function () {} +}; + +SimpleTest.waitForExplicitFinish(); + +// XPFE AutoComplete needs to register early. +autoCompleteSimple.registerFactory(); + +let gACTimer; +let gAutoComplete; + +function searchComplete() { + is(gAutoComplete.value, "result", "Value should be autocompleted now"); + ok(Date.now() - gACTimer > 500, "There should be a delay before autocomplete"); + + // Unregister the factory so that we don't get in the way of other tests + autoCompleteSimple.unregisterFactory(); + SimpleTest.finish(); +} + +function runTest() { + gAutoComplete = $("autocomplete"); + + const SEARCH_STRING = "res"; + + function cbCallback() { + gAutoComplete.focus(); + synthesizeKey("v", { accelKey: true }); + is(gAutoComplete.value, SEARCH_STRING, "Value should not be autocompleted immediately"); + } + + SimpleTest.waitForClipboard(SEARCH_STRING, function () { + gACTimer = Date.now(); + Components.classes["@mozilla.org/widget/clipboardhelper;1"] + .getService(Components.interfaces.nsIClipboardHelper) + .copyStringToClipboard(SEARCH_STRING, Components.interfaces.nsIClipboard.kGlobalClipboard); + }, cbCallback, cbCallback); +} +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul new file mode 100644 index 0000000000..b162742f12 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul @@ -0,0 +1,175 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete emphasis test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<textbox id="richautocomplete" type="autocomplete" + autocompletesearch="simple" + onsearchcomplete="checkSearchCompleted();" + autocompletepopup="richpopup"/> +<panel id="richpopup" type="autocomplete-richlistbox" noautofocus="true"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +const ACR = Components.interfaces.nsIAutoCompleteResult; + +// A global variable to hold the search result for the current search. +var resultText = ""; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; +} + +nsAutoCompleteSimpleResult.prototype = { + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: 0, + getValueAt: function() { return resultText; }, + getCommentAt: function() { return this.getValueAt(); }, + getStyleAt: function() { return null; }, + getImageAt: function() { return null; }, + getFinalCompleteValueAt: function() { return this.getValueAt(); }, + getLabelAt: function() { return this.getValueAt(); }, + removeValueAt: function() {} +}; + +// A basic autocomplete implementation that returns the string contained in 'resultText'. +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIFactory) || + iid.equals(Components.interfaces.nsIAutoCompleteSearch)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + createInstance: function(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch: function(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch: function() {} +}; + +var componentManager = Components.manager + .QueryInterface(Components.interfaces.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +SimpleTest.waitForExplicitFinish(); +setTimeout(nextTest, 0); + +/* Test cases have the following attributes: + * - search: A search string, to be emphasized in the result. + * - result: A fixed result string, so we can hardcode the expected emphasis. + * - emphasis: A list of chunks that should be emphasized or not, in strict alternation. + * - emphasizeFirst: Whether the first element of 'emphasis' should be emphasized; + * The emphasis of the other elements is defined by the strict alternation rule. + */ +let testcases = [ + { search: "test", + result: "A test string", + emphasis: ["A ", "test", " string"], + emphasizeFirst: false + }, + { search: "tea two", + result: "Tea for two, and two for tea...", + emphasis: ["Tea", " for ", "two", ", and ", "two", " for ", "tea", "..."], + emphasizeFirst: true + }, + { search: "tat", + result: "tatatat", + emphasis: ["tatatat"], + emphasizeFirst: true + }, + { search: "cheval valise", + result: "chevalise", + emphasis: ["chevalise"], + emphasizeFirst: true + } +]; +let test = -1; +let currentTest = null; + +function nextTest() { + test++; + + if (test >= testcases.length) { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + return; + } + + // blur the field to ensure that the popup is closed and that the previous + // search has stopped, then start a new search. + let autocomplete = $("richautocomplete"); + autocomplete.blur(); + autocomplete.focus(); + + currentTest = testcases[test]; + resultText = currentTest.result; + autocomplete.value = currentTest.search; + synthesizeKey("VK_DOWN", {}); +} + +function checkSearchCompleted() { + let autocomplete = $("richautocomplete"); + let result = autocomplete.popup.richlistbox.firstChild; + + for (let attribute of [result._titleText, result._urlText]) { + is(attribute.childNodes.length, currentTest.emphasis.length, + "The element should have the expected number of children."); + for (let i = 0; i < currentTest.emphasis.length; i++) { + let node = attribute.childNodes[i]; + // Emphasized parts strictly alternate. + if ((i % 2 == 0) == currentTest.emphasizeFirst) { + // Check that this part is correctly emphasized. + is(node.nodeName, "span", ". That child should be a span node"); + ok(node.classList.contains("ac-emphasize-text"), ". That child should be emphasized"); + is(node.textContent, currentTest.emphasis[i], ". That emphasis should be as expected."); + } else { + // Check that this part is _not_ emphasized. + is(node.nodeName, "#text", ". That child should be a text node"); + is(node.textContent, currentTest.emphasis[i], ". That text should be as expected."); + } + } + } + + setTimeout(nextTest, 0); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xul b/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xul new file mode 100644 index 0000000000..21670215d4 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xul @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test" + onload="setTimeout(keyCaretTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<textbox id="autocomplete" type="autocomplete"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function keyCaretTest() +{ + var autocomplete = $("autocomplete"); + + autocomplete.focus(); + checkKeyCaretTest("VK_UP", 0, 0, false, "no value up"); + checkKeyCaretTest("VK_DOWN", 0, 0, false, "no value down"); + + autocomplete.value = "Sample"; + + autocomplete.selectionStart = 3; + autocomplete.selectionEnd = 3; + checkKeyCaretTest("VK_UP", 0, 0, true, "value up with caret in middle"); + checkKeyCaretTest("VK_UP", 0, 0, false, "value up with caret in middle again"); + + autocomplete.selectionStart = 2; + autocomplete.selectionEnd = 2; + checkKeyCaretTest("VK_DOWN", 6, 6, true, "value down with caret in middle"); + checkKeyCaretTest("VK_DOWN", 6, 6, false, "value down with caret in middle again"); + + autocomplete.selectionStart = 1; + autocomplete.selectionEnd = 4; + checkKeyCaretTest("VK_UP", 0, 0, true, "value up with selection"); + + autocomplete.selectionStart = 1; + autocomplete.selectionEnd = 4; + checkKeyCaretTest("VK_DOWN", 6, 6, true, "value down with selection"); + + SimpleTest.finish(); +} + +function checkKeyCaretTest(key, expectedStart, expectedEnd, result, testid) +{ + var autocomplete = $("autocomplete"); + + var event = result ? "keypress" : "!keypress"; + synthesizeKeyExpectEvent(key, { }, autocomplete.inputField, event, testid); + is(autocomplete.selectionStart, expectedStart, testid + " selectionStart"); + is(autocomplete.selectionEnd, expectedEnd, testid + " selectionEnd"); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xul b/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xul new file mode 100644 index 0000000000..01004327de --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xul @@ -0,0 +1,309 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + +<textbox id="autocomplete" + type="autocomplete" + completedefaultindex="true" + timeout="0" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +function autoCompleteSimpleResult(aString, searchId) { + this.searchString = aString; + this.searchResult = Components.interfaces.nsIAutoCompleteResult.RESULT_SUCCESS; + this.matchCount = 1; + if (aString.startsWith('ret')) { + this._param = autoCompleteSimpleResult.retireCompletion; + } else { + this._param = "Result"; + } + this._searchId = searchId; +} +autoCompleteSimpleResult.retireCompletion = "Retire"; +autoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: Components.interfaces.nsIAutoCompleteResult.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt: function() { return this._param; }, + getCommentAt: function() { return null; }, + getStyleAt: function() { return null; }, + getImageAt: function() { return null; }, + getLabelAt: function() { return null; }, + removeValueAt: function() {} +}; + +var searchCounter = 0; + +// A basic autocomplete implementation that returns one result. +let autoCompleteSimple = { + classID: Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"), + contractID: "@mozilla.org/autocomplete/search;1?name=simple", + searchAsync: false, + pendingSearch: null, + + QueryInterface: XPCOMUtils.generateQI([ + Components.interfaces.nsIFactory, + Components.interfaces.nsIAutoCompleteSearch + ]), + createInstance: function (outer, iid) { + return this.QueryInterface(iid); + }, + + registerFactory: function () { + let registrar = + Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar); + registrar.registerFactory(this.classID, "Test Simple Autocomplete", + this.contractID, this); + }, + unregisterFactory: function () { + let registrar = + Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar); + registrar.unregisterFactory(this.classID, this); + }, + + startSearch: function (aString, aParam, aResult, aListener) { + let result = new autoCompleteSimpleResult(aString); + + if (this.searchAsync) { + // Simulate an async search by using a timeout before invoking the + // |onSearchResult| callback. + // Store the searchTimeout such that it can be canceled if stopSearch is called. + this.pendingSearch = setTimeout(() => { + this.pendingSearch = null; + + aListener.onSearchResult(this, result); + + // Move to the next step in the async test. + asyncTest.next(); + }, 0); + } else { + aListener.onSearchResult(this, result); + } + }, + stopSearch: function () { + clearTimeout(this.pendingSearch); + } +}; + +SimpleTest.waitForExplicitFinish(); + +// XPFE AutoComplete needs to register early. +autoCompleteSimple.registerFactory(); + +let gACTimer; +let gAutoComplete; +let asyncTest; + +let searchCompleteTimeoutId = null; + +function finishTest() { + // Unregister the factory so that we don't get in the way of other tests + autoCompleteSimple.unregisterFactory(); + SimpleTest.finish(); +} + +function runTest() { + gAutoComplete = $("autocomplete"); + gAutoComplete.focus(); + + // Return the search results synchronous, which also makes the completion + // happen synchronous. + autoCompleteSimple.searchAsync = false; + + synthesizeKey("r", {}); + is(gAutoComplete.value, "result", "Value should be autocompleted immediately"); + + synthesizeKey("e", {}); + is(gAutoComplete.value, "result", "Value should be autocompleted immediately"); + + synthesizeKey("VK_DELETE", {}); + is(gAutoComplete.value, "re", "Deletion should not complete value"); + + synthesizeKey("VK_BACK_SPACE", {}); + is(gAutoComplete.value, "r", "Backspace should not complete value"); + + synthesizeKey("VK_LEFT", {}); + is(gAutoComplete.value, "r", "Value should stay same when navigating with cursor"); + + runAsyncTest(); +} + +function* asyncTestGenerator() { + synthesizeKey("r", {}); + synthesizeKey("e", {}); + is(gAutoComplete.value, "re", "Value should not be autocompleted immediately"); + + // Calling |yield undefined| makes this generator function wait until + // |asyncTest.next();| is called. This happens from within the + // |autoCompleteSimple.startSearch()| function once the simulated async + // search has finished. + // Therefore, the effect of the |yield undefined;| here (and the ones) below + // is to wait until the async search result comes back. + yield undefined; + + is(gAutoComplete.value, "result", "Value should be autocompleted"); + + // Test if typing the `s` character completes directly based on the last + // completion + synthesizeKey("s", {}); + is(gAutoComplete.value, "result", "Value should be completed immediately"); + + yield undefined; + + is(gAutoComplete.value, "result", "Value should be autocompleted to same value"); + synthesizeKey("VK_DELETE", {}); + is(gAutoComplete.value, "res", "Deletion should not complete value"); + + // No |yield undefined| needed here as no completion is triggered by the deletion. + + is(gAutoComplete.value, "res", "Still no complete value after deletion"); + + synthesizeKey("VK_BACK_SPACE", {}); + is(gAutoComplete.value, "re", "Backspace should not complete value"); + + yield undefined; + + is(gAutoComplete.value, "re", "Value after search due to backspace should stay the same"); (3) + + // Typing a character that is not like the previous match. In this case, the + // completion cannot happen directly and therefore the value will be completed + // only after the search has finished. + synthesizeKey("t", {}); + is(gAutoComplete.value, "ret", "Value should not be autocompleted immediately"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + synthesizeKey("i", {}); + is(gAutoComplete.value, "retire", "Value should be autocompleted immediately"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted to the same value"); + + // Setup the scene to test how the completion behaves once the placeholder + // completion and the result from the search do not agree with each other. + gAutoComplete.value = 'r'; + // Need to type two characters as the input was reset and the autocomplete + // controller things, ther user hit the backspace button, in which case + // no completion is performed. But as a completion is desired, another + // character `t` is typed afterwards. + synthesizeKey("e", {}); + yield undefined; + synthesizeKey("t", {}); + is(gAutoComplete.value, "ret", "Value should not be autocompleted"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + // The placeholder string is now set to "retire". Changing the completion + // string to "retirement" and see what the completion will turn out like. + autoCompleteSimpleResult.retireCompletion = "Retirement"; + synthesizeKey("i", {}); + is(gAutoComplete.value, "retire", "Value should be autocompleted based on placeholder"); + + yield undefined; + + is(gAutoComplete.value, "retirement", "Value should be autocompleted based on search result"); + + // Change the search result to `Retire` again and see if the new result is + // complited. + autoCompleteSimpleResult.retireCompletion = "Retire"; + synthesizeKey("r", {}); + is(gAutoComplete.value, "retirement", "Value should be autocompleted based on placeholder"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted based on search result"); + + // Complete the value + gAutoComplete.value = 're'; + synthesizeKey("t", {}); + yield undefined; + synthesizeKey("i", {}); + is(gAutoComplete.value, "reti", "Value should not be autocompleted"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + // Remove the selected text "re" (1) and the "et" (2). Afterwards, add it again (3). + // This should not cause the completion to kick in. + synthesizeKey("VK_DELETE", {}); // (1) + + is(gAutoComplete.value, "reti", "Value should not complete after deletion"); + + gAutoComplete.selectionStart = 1; + gAutoComplete.selectionEnd = 3; + synthesizeKey("VK_DELETE", {}); // (2) + + is(gAutoComplete.value, "ri", "Value should stay unchanged after removing character in the middle"); + + yield undefined; + + synthesizeKey("e", {}); // (3.1) + is(gAutoComplete.value, "rei", "Inserting a character in the middle should not complete the value"); + + yield undefined; + + synthesizeKey("t", {}); // (3.2) + is(gAutoComplete.value, "reti", "Inserting a character in the middle should not complete the value"); + + yield undefined; + + // Adding a new character at the end should not cause the completion to happen again + // as the completion failed before. + gAutoComplete.selectionStart = 4; + gAutoComplete.selectionEnd = 4; + synthesizeKey("r", {}); + is(gAutoComplete.value, "retir", "Value should not be autocompleted immediately"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + finishTest(); + yield undefined; +} + +function runAsyncTest() { + gAutoComplete.value = ''; + autoCompleteSimple.searchAsync = true; + + asyncTest = asyncTestGenerator(); + asyncTest.next(); +} +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html new file mode 100644 index 0000000000..3f57a0d6e4 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>autocomplete with composition tests on HTML input element</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="file_autocomplete_with_composition.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <iframe id="formTarget" name="formTarget"></iframe> + <form action="data:text/html," target="formTarget"> + <input name="test" id="input"><input type="submit"> + </form> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + var formFillController = + SpecialPowers.getFormFillController() + .QueryInterface(Components.interfaces.nsIAutoCompleteInput); + var originalFormFillTimeout = formFillController.timeout; + + SpecialPowers.attachFormFillControllerTo(window); + var target = document.getElementById("input"); + + // Register a word to the form history. + target.focus(); + target.value = "Mozilla"; + synthesizeKey("VK_RETURN", {}); + target.value = ""; + + var test1 = new nsDoTestsForAutoCompleteWithComposition( + "Testing on HTML input (asynchronously search)", + window, target, formFillController.controller, is, + function () { return target.value; }, + function () { + target.setAttribute("timeout", 0); + var test2 = new nsDoTestsForAutoCompleteWithComposition( + "Testing on HTML input (synchronously search)", + window, target, formFillController.controller, is, + function () { return target.value; }, + function () { + formFillController.timeout = originalFormFillTimeout; + SpecialPowers.detachFormFillControllerFrom(window); + SimpleTest.finish(); + }); + }); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_textbox.xul b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_textbox.xul new file mode 100644 index 0000000000..90972b8dab --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_textbox.xul @@ -0,0 +1,124 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window title="Testing autocomplete with composition" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + <script type="text/javascript" + src="file_autocomplete_with_composition.js" /> + + <textbox id="textbox" type="autocomplete" + autocompletesearch="simpleForComposition"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +const nsIAutoCompleteResult = Components.interfaces.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (aString == "" || aString == "Mozilla".substr(0, aString.length)) { + this.searchResult = nsIAutoCompleteResult.RESULT_SUCCESS; + this.matchCount = 1; + this._value = "Mozilla"; + } else { + this.searchResult = nsIAutoCompleteResult.RESULT_NOMATCH; + this.matchCount = 0; + this._value = ""; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _value: "", + searchString: null, + searchResult: nsIAutoCompleteResult.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt: function(aIndex) { return aIndex == 0 ? this._value : null; }, + getCommentAt: function() { return null; }, + getStyleAt: function() { return null; }, + getImageAt: function() { return null; }, + getFinalCompleteValueAt: function(aIndex) { return this.getValueAt(aIndex); }, + getLabelAt: function() { return null; }, + removeValueAt: function() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = + Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = + "@mozilla.org/autocomplete/search;1?name=simpleForComposition" +var autoCompleteSimple = { + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIFactory) || + iid.equals(Components.interfaces.nsIAutoCompleteSearch)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + createInstance: function(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch: function(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch: function() {} +}; + +var componentManager = + Components.manager + .QueryInterface(Components.interfaces.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, + "Test Simple Autocomplete for composition", + autoCompleteSimpleName, autoCompleteSimple); + +function runTests() +{ + var target = document.getElementById("textbox"); + target.setAttribute("timeout", 1); + var test1 = new nsDoTestsForAutoCompleteWithComposition( + "Testing on XUL textbox (asynchronously search)", + window, target, target.controller, is, + function () { return target.value; }, + function () { + target.setAttribute("timeout", 0); + var test2 = new nsDoTestsForAutoCompleteWithComposition( + "Testing on XUL textbox (synchronously search)", + window, target, target.controller, is, + function () { return target.value; }, + function () { + // Unregister the factory so that we don't get in the way of other + // tests + componentManager.unregisterFactory(autoCompleteSimpleID, + autoCompleteSimple); + SimpleTest.finish(); + }); + }); +} + +SimpleTest.waitForFocus(runTests); +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_browser_drop.xul b/toolkit/content/tests/chrome/test_browser_drop.xul new file mode 100644 index 0000000000..4ba21c514c --- /dev/null +++ b/toolkit/content/tests/chrome/test_browser_drop.xul @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Browser Drop Test" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"/> + + <script><![CDATA[ +SimpleTest.waitForExplicitFinish(); +function runTest() { + add_task(function*() { + let win = window.open("window_browser_drop.xul", "_blank", "chrome,width=200,height=200"); + yield SimpleTest.promiseFocus(win); + for (let browserType of ["content", "remote-content"]) { + yield win.dropLinksOnBrowser(win.document.getElementById(browserType + "child"), browserType); + } + yield win.dropLinksOnBrowser(win.document.getElementById("chromechild"), "chrome"); + }); +} +//]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug1048178.xul b/toolkit/content/tests/chrome/test_bug1048178.xul new file mode 100644 index 0000000000..79f3acad5d --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug1048178.xul @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1048178 +--> +<window title="Mozilla Bug 1048178" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"/> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1048178" + target="_blank">Mozilla Bug 1048178</a> + + <hbox> + <scrollbar id="scroller" + orient="horizontal" + curpos="0" + maxpos="500" + pageincrement="500" + width="500" + style="margin:0"/> + </hbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 1048178 **/ +var scrollbarTester = { + scrollbar: null, + startTest: function() { + this.scrollbar = $("scroller"); + this.setScrollToClick(false); + this.testThumbDragging(); + SimpleTest.finish(); + }, + testThumbDragging: function() { + var x = 400; // on the right half of the scroolbar + var y = 5; + + this.mousedown(x, y, 0); + this.mousedown(x, y, 2); + this.mouseup(x, y, 2); + this.mouseup(x, y, 0); + + var newPos = this.getPos(); // sould be '500' + + this.mousedown(x, y, 0); + this.mousemove(x-1, y, 0); + this.mouseup(x-1, y, 0); + + var newPos2 = this.getPos(); + ok(newPos2 < newPos, + "Scrollbar thumb should follow the mouse when dragged."); + }, + setScrollToClick: function(value) { + var prefService = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + var uiBranch = prefService.getBranch("ui."); + uiBranch.setIntPref("scrollToClick", value ? 1 : 0); + }, + getPos: function() { + return this.scrollbar.getAttribute("curpos"); + }, + mousedown: function(x, y, button) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousedown", 'button': button }); + }, + mousemove: function(x, y, button) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousemove", 'button': button }); + }, + mouseup: function(x, y, button) { + synthesizeMouse(this.scrollbar, x, y, { type: "mouseup", 'button': button }); + } +} + +function doTest() { + setTimeout(function() { scrollbarTester.startTest(); }, 0); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug253481.xul b/toolkit/content/tests/chrome/test_bug253481.xul new file mode 100644 index 0000000000..aa5c017c68 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug253481.xul @@ -0,0 +1,90 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=253481 +--> +<window title="Mozilla Bug 253481" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=253481">Mozilla Bug 253481</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +</body> + +<description> + Tests pasting of multi-line content into a single-line xul:textbox. +</description> + +<vbox> +<textbox id="pasteintact" newlines="pasteintact"/> +<textbox id="pastetofirst" newlines="pastetofirst"/> +<textbox id="replacewithspaces" newlines="replacewithspaces"/> +<textbox id="strip" newlines="strip"/> +<textbox id="replacewithcommas" newlines="replacewithcommas"/> +<textbox id="stripsurroundingwhitespace" newlines="stripsurroundingwhitespace"/> +</vbox> +<script class="testbody" type="application/javascript;version=1.7"> +<![CDATA[ +/** Test for Bug 253481 **/ +function testPaste(name, element, expected) { + element.value = ""; + element.focus(); + synthesizeKey("v", { accelKey: true }); + is(element.value, expected, name); +} + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { +setTimeout(function() { +var testString = "\n hello hello \n world\nworld \n"; +var expectedResults = { +// even "pasteintact" strips leading/trailing newlines +"pasteintact": testString.replace(/^\n/, '').replace(/\n$/, ''), +// "pastetofirst" strips leading newlines +"pastetofirst": testString.replace(/^\n/, '').split(/\n/)[0], +// "replacewithspaces" strips trailing newlines first - bug 432415 +"replacewithspaces": testString.replace(/\n$/, '').replace(/\n/g,' '), +// "strip" is pretty straightforward +"strip": testString.replace(/\n/g,''), +// "replacewithcommas" strips leading and trailing newlines first +"replacewithcommas": testString.replace(/^\n/, '').replace(/\n$/, '').replace(/\n/g,','), +// "stripsurroundingwhitespace" strips all newlines and whitespace around them +"stripsurroundingwhitespace": testString.replace(/\s*\n\s*/g,'') +}; + +// Put a multi-line string in the clipboard +SimpleTest.waitForClipboard(testString, function() { + var clip = Components.classes["@mozilla.org/widget/clipboardhelper;1"] + .getService(Components.interfaces.nsIClipboardHelper); + clip.copyString(testString); +}, function() { + for (let [item, expected] of Object.entries(expectedResults)) { + testPaste(item, $(item), expected); + } + + SimpleTest.finish(); +}, function() { + ok(false, "Could not copy the string to clipboard, giving up"); + + SimpleTest.finish(); +}); +}, 0); +}); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug263683.xul b/toolkit/content/tests/chrome/test_bug263683.xul new file mode 100644 index 0000000000..c5755c9f17 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug263683.xul @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=263683 +--> +<window title="Mozilla Bug 263683" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=263683"> + Mozilla Bug 263683 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 263683 **/ + SimpleTest.waitForExplicitFinish(); + window.open("bug263683_window.xul", "263683test", + "chrome,width=600,height=600"); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug304188.xul b/toolkit/content/tests/chrome/test_bug304188.xul new file mode 100644 index 0000000000..f41d24f9bb --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug304188.xul @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=304188 +--> +<window title="Mozilla Bug 304188" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=304188">Mozilla Bug 304188</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 304188 **/ +SimpleTest.waitForExplicitFinish(); +window.open("bug304188_window.xul", "findbartest", + "chrome,width=600,height=600"); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug331215.xul b/toolkit/content/tests/chrome/test_bug331215.xul new file mode 100644 index 0000000000..e0d0d1e0a9 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug331215.xul @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=331215 +--> +<window title="Mozilla Bug 331215" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=331215">Mozilla Bug 331215</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 331215 **/ + +SimpleTest.waitForExplicitFinish(); +window.open("bug331215_window.xul", "331215test", + "chrome,width=600,height=600"); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug360220.xul b/toolkit/content/tests/chrome/test_bug360220.xul new file mode 100644 index 0000000000..3fcb2bf139 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug360220.xul @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=360220 +--> +<window title="Mozilla Bug 360220" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=360220">Mozilla Bug 360220</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<menulist id="menulist"> + <menupopup> + <menuitem id="firstItem" label="foo" selected="true"/> + <menuitem id="secondItem" label="bar"/> + </menupopup> +</menulist> +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +/** Test for Bug 360220 **/ + +var menulist = document.getElementById("menulist"); +var secondItem = document.getElementById("secondItem"); +menulist.selectedItem = secondItem; + +is(menulist.label, "bar", "second item was not selected"); + +let mutObserver = new MutationObserver(() => { + is(menulist.label, "new label", "menulist label was not updated to the label of its selected item"); + done(); +}); +mutObserver.observe(menulist, { attributeFilter: ['label'] }); +secondItem.label = "new label"; + +let failureTimeout = setTimeout(function() { + ok(false, "menulist label should have updated"); + done(); +}, 2000); + +function done() { + mutObserver.disconnect(); + clearTimeout(failureTimeout); + SimpleTest.finish(); +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug360437.xul b/toolkit/content/tests/chrome/test_bug360437.xul new file mode 100644 index 0000000000..eb17adcf5d --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug360437.xul @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=360437 +--> +<window title="Mozilla Bug 360437" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=360437">Mozilla Bug 360437</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 360437 **/ +SimpleTest.waitForExplicitFinish(); +window.open("bug360437_window.xul", "360437test", + "chrome,width=600,height=600"); + + + + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug365773.xul b/toolkit/content/tests/chrome/test_bug365773.xul new file mode 100644 index 0000000000..17385365ad --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug365773.xul @@ -0,0 +1,67 @@ +<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=365773
+-->
+<window title="Mozilla Bug 365773"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=365773">Mozilla Bug 365773</a>
+<p id="display">
+ <radiogroup id="group" collapsed="true" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <radio id="item" label="Item"/>
+ </radiogroup>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 365773 **/
+
+function selectItem(item, isIndex, testName) {
+ var exception = null;
+ try {
+ if (isIndex)
+ document.getElementById("group").selectedIndex = item;
+ else
+ document.getElementById("group").selectedItem = item;
+ }
+ catch(e) {
+ exception = e;
+ }
+
+ ok(exception == null, testName);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+window.onload = function runTests() {
+ var item = document.getElementById("item");
+
+ selectItem(item, false, "Radio button selected with selectedItem (not focused)");
+ selectItem(null, false, "Radio button deselected with selectedItem (not focused)");
+ selectItem(0, true, "Radio button selected with selectedIndex (not focused)");
+ selectItem(-1, true, "Radio button deselected with selectedIndex (not focused)");
+
+ document.getElementById("group").focus();
+
+ selectItem(item, false, "Radio button selected with selectedItem (focused)");
+ selectItem(null, false, "Radio button deselected with selectedItem (focused)");
+ selectItem(0, true, "Radio button selected with selectedIndex (focused)");
+ selectItem(-1, true, "Radio button deselected with selectedIndex (focused)");
+
+ SimpleTest.finish();
+};
+]]>
+</script>
+
+</window>
diff --git a/toolkit/content/tests/chrome/test_bug366992.xul b/toolkit/content/tests/chrome/test_bug366992.xul new file mode 100644 index 0000000000..2c92defc54 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug366992.xul @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=366992 +--> +<window title="Mozilla Bug 366992" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=366992">Mozilla Bug 366992</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 366992 **/ +SimpleTest.waitForExplicitFinish(); +window.open("bug366992_window.xul", "findbartest", + "chrome,width=600,height=600"); + + + + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug382990.xul b/toolkit/content/tests/chrome/test_bug382990.xul new file mode 100644 index 0000000000..aa3b00431d --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug382990.xul @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=382990 +--> +<window title="Mozilla Bug 382990" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="startThisTest()"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=382990" + target="_blank">Mozilla Bug 382990</a> + </body> + + <tree id="testTree" height="200px"> + <treecols> + <treecol flex="1" label="Name" id="name"/> + </treecols> + <treechildren> + <treeitem><treerow><treecell label="a"/></treerow></treeitem> + <treeitem><treerow><treecell label="z"/></treerow></treeitem> + </treechildren> + </tree> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + /** Test for Bug 382990 **/ + + SimpleTest.waitForExplicitFinish(); + function startThisTest() + { + var treeElem = document.getElementById("testTree"); + treeElem.view.selection.select(0); + treeElem.focus(); + synthesizeKey("z", {ctrlKey: true}); + ok(!treeElem.view.selection.isSelected(1), "Tree selection should not change for key events with ctrl pressed."); + SimpleTest.finish(); + } + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug409624.xul b/toolkit/content/tests/chrome/test_bug409624.xul new file mode 100644 index 0000000000..59a862cad7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug409624.xul @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=409624 +--> +<window title="Mozilla Bug 409624" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=409624"> + Mozilla Bug 409624 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 409624 **/ + SimpleTest.waitForExplicitFinish(); + window.open("bug409624_window.xul", "409624test", + "chrome,width=600,height=600"); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug418874.xul b/toolkit/content/tests/chrome/test_bug418874.xul new file mode 100644 index 0000000000..13f0a14530 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug418874.xul @@ -0,0 +1,71 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Textbox with placeholder test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + + <hbox> + <textbox id="t1" placeholder="empty"/> + </hbox> + + <hbox> + <textbox id="t2" placeholder="empty"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + var t2 = $("t2"); + setTextboxValue(t1, "1"); + var t1Enabled = {}; + var t1CanUndo = {}; + t1.editor.canUndo(t1Enabled, t1CanUndo); + is(t1CanUndo.value, true, + "undo correctly enabled when placeholder was not changed through property"); + + t2.placeholder = "reallyempty"; + setTextboxValue(t2, "2"); + var t2Enabled = {}; + var t2CanUndo = {}; + t2.editor.canUndo(t2Enabled, t2CanUndo); + is(t2CanUndo.value, true, + "undo correctly enabled when placeholder explicitly changed through property"); + + SimpleTest.finish(); + } + + function setTextboxValue(textbox, value) { + textbox.focus(); + for (var i = 0; i < value.length; ++i) { + synthesizeKey(value.charAt(i), {}); + } + textbox.blur(); + } + + SimpleTest.waitForFocus(doTest); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug429723.xul b/toolkit/content/tests/chrome/test_bug429723.xul new file mode 100644 index 0000000000..99ee8acd97 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug429723.xul @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=429723 +--> +<window title="Mozilla Bug 429723" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=429723">Mozilla Bug 429723</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 429723 **/ +SimpleTest.waitForExplicitFinish(); +window.open("bug429723_window.xul", "429723test", + "chrome,width=600,height=600"); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug437844.xul b/toolkit/content/tests/chrome/test_bug437844.xul new file mode 100644 index 0000000000..b194b30419 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug437844.xul @@ -0,0 +1,95 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=437844 +https://bugzilla.mozilla.org/show_bug.cgi?id=348233 +--> +<window title="Mozilla Bug 437844 and Bug 348233" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="chrome://mochikit/content/chrome-harness.js"></script> + <script type="application/javascript" + src="RegisterUnregisterChrome.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=437844"> + Mozilla Bug 437844 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=348233"> + Mozilla Bug 348233 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.expectAssertions(18, 22); + + /** Test for Bug 437844 and Bug 348233 **/ + SimpleTest.waitForExplicitFinish(); + + let prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + prefs.setCharPref("intl.uidirection.en-US", "rtl"); + + let rootDir = getRootDirectory(window.location.href); + let manifest = rootDir + "rtlchrome/rtl.manifest"; + + //copy rtlchrome to profile/rtlchrome and generate .manifest + let filePath = chromeURIToFile(manifest); + let tempProfileDir = copyDirToTempProfile(filePath.path, 'rtlchrome'); + if (tempProfileDir.path.lastIndexOf('\\') >= 0) { + manifest = "content rtlchrome /" + tempProfileDir.path.replace(/\\/g, '/') + "\n"; + } else { + manifest = "content rtlchrome " + tempProfileDir.path + "\n"; + } + manifest += "override chrome://global/locale/intl.css chrome://rtlchrome/content/rtlchrome/rtl.css\n"; + manifest += "override chrome://global/locale/global.dtd chrome://rtlchrome/content/rtlchrome/rtl.dtd\n"; + + let cleanupFunc = createManifestTemporarily(tempProfileDir, manifest); + + // Load about:plugins in an iframe + let frame = document.createElement("iframe"); + frame.setAttribute("src", "about:plugins"); + frame.addEventListener("load", function () { + frame.removeEventListener("load", arguments.callee, false); + is(frame.contentDocument.dir, "rtl", "about:plugins should be RTL in RTL locales"); + + let gDirSvc = Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIDirectoryService). + QueryInterface(Components.interfaces.nsIProperties); + let tmpd = gDirSvc.get("ProfD", Components.interfaces.nsIFile); + + frame = document.createElement("iframe"); + frame.setAttribute("src", "file://" + tmpd.path); // a file:// URI, bug 348233 + frame.addEventListener("load", function () { + frame.removeEventListener("load", arguments.callee, false); + + is(frame.contentDocument.body.dir, "rtl", "file:// listings should be RTL in RTL locales"); + + cleanupFunc(); + prefs.clearUserPref("intl.uidirection.en-US"); + SimpleTest.finish(); + }, false); + document.documentElement.appendChild(frame); + }, false); + document.documentElement.appendChild(frame); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug451540.xul b/toolkit/content/tests/chrome/test_bug451540.xul new file mode 100644 index 0000000000..64ae43c3c8 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug451540.xul @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=451540 +--> +<window title="Mozilla Bug 451540" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=451540"> + Mozilla Bug 451540 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 451540 **/ + SimpleTest.waitForExplicitFinish(); + window.open("bug451540_window.xul", "451540test", + "chrome,width=600,height=600"); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug457632.xul b/toolkit/content/tests/chrome/test_bug457632.xul new file mode 100644 index 0000000000..7bc70f2cc3 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug457632.xul @@ -0,0 +1,178 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 457632 + --> +<window title="Bug 457632" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <notificationbox id="nb"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" + onload="test()"/> + + <!-- test code goes here --> +<script type="application/javascript"> +<![CDATA[ +var gNotificationBox; + +function completeAnimation(nextTest) { + if (!gNotificationBox._animating) { + nextTest(); + return; + } + + setTimeout(completeAnimation, 50, nextTest); +} + +function test() { + SimpleTest.waitForExplicitFinish(); + gNotificationBox = document.getElementById("nb"); + + is(gNotificationBox.allNotifications.length, 0, "There should be no initial notifications"); + gNotificationBox.appendNotification("Test notification", + "notification1", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + is(gNotificationBox.allNotifications.length, 1, "Notification exists while animating in"); + let notification = gNotificationBox.getNotificationWithValue("notification1"); + ok(notification, "Notification should exist while animating in"); + + // Wait for the notificaton to finish displaying + completeAnimation(test1); +} + +// Tests that a notification that is fully animated in gets removed immediately +function test1() { + let notification = gNotificationBox.getNotificationWithValue("notification1"); + gNotificationBox.removeNotification(notification); + notification = gNotificationBox.getNotificationWithValue("notification1"); + ok(!notification, "Test 1 showed notification was still present"); + ok(!gNotificationBox.currentNotification, "Test 1 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 1 should show no notifications present"); + + // Wait for the notificaton to finish hiding + completeAnimation(test2); +} + +// Tests that a notification that is animating in gets removed immediately +function test2() { + let notification = gNotificationBox.appendNotification("Test notification", + "notification2", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + gNotificationBox.removeNotification(notification); + notification = gNotificationBox.getNotificationWithValue("notification2"); + ok(!notification, "Test 2 showed notification was still present"); + ok(!gNotificationBox.currentNotification, "Test 2 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 2 should show no notifications present"); + + // Get rid of the hiding notifications + gNotificationBox.removeAllNotifications(true); + test3(); +} + +// Tests that a background notification goes away immediately +function test3() { + let notification = gNotificationBox.appendNotification("Test notification", + "notification3", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + let notification2 = gNotificationBox.appendNotification("Test notification", + "notification4", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + is(gNotificationBox.allNotifications.length, 2, "Test 3 should show 2 notifications present"); + gNotificationBox.removeNotification(notification); + is(gNotificationBox.allNotifications.length, 1, "Test 3 should show 1 notifications present"); + notification = gNotificationBox.getNotificationWithValue("notification3"); + ok(!notification, "Test 3 showed notification was still present"); + gNotificationBox.removeNotification(notification2); + is(gNotificationBox.allNotifications.length, 0, "Test 3 should show 0 notifications present"); + notification2 = gNotificationBox.getNotificationWithValue("notification4"); + ok(!notification2, "Test 3 showed notification2 was still present"); + ok(!gNotificationBox.currentNotification, "Test 3 said there was still a current notification"); + + // Get rid of the hiding notifications + gNotificationBox.removeAllNotifications(true); + test4(); +} + +// Tests that a foreground notification hiding a background one goes away +function test4() { + let notification = gNotificationBox.appendNotification("Test notification", + "notification5", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + let notification2 = gNotificationBox.appendNotification("Test notification", + "notification6", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + gNotificationBox.removeNotification(notification2); + notification2 = gNotificationBox.getNotificationWithValue("notification6"); + ok(!notification2, "Test 4 showed notification2 was still present"); + is(gNotificationBox.currentNotification, notification, "Test 4 said the current notification was wrong"); + is(gNotificationBox.allNotifications.length, 1, "Test 4 should show 1 notifications present"); + gNotificationBox.removeNotification(notification); + notification = gNotificationBox.getNotificationWithValue("notification5"); + ok(!notification, "Test 4 showed notification was still present"); + ok(!gNotificationBox.currentNotification, "Test 4 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 4 should show 0 notifications present"); + + // Get rid of the hiding notifications + gNotificationBox.removeAllNotifications(true); + test5(); +} + +// Tests that removeAllNotifications gets rid of everything +function test5() { + let notification = gNotificationBox.appendNotification("Test notification", + "notification7", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + let notification2 = gNotificationBox.appendNotification("Test notification", + "notification8", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + gNotificationBox.removeAllNotifications(); + notification = gNotificationBox.getNotificationWithValue("notification7"); + notification2 = gNotificationBox.getNotificationWithValue("notification8"); + ok(!notification, "Test 5 showed notification was still present"); + ok(!notification2, "Test 5 showed notification2 was still present"); + ok(!gNotificationBox.currentNotification, "Test 5 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 5 should show 0 notifications present"); + + gNotificationBox.appendNotification("Test notification", + "notification9", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + + // Wait for the notificaton to finish displaying + completeAnimation(test6); +} + +// Tests whether removing an already removed notification doesn't break things +function test6() { + let notification = gNotificationBox.getNotificationWithValue("notification9"); + ok(notification, "Test 6 should have an initial notification"); + gNotificationBox.removeNotification(notification); + gNotificationBox.removeNotification(notification); + + ok(!gNotificationBox.currentNotification, "Test 6 shouldn't be any current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 6 allNotifications.length should be 0"); + notification = gNotificationBox.appendNotification("Test notification", + "notification10", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + is(notification, gNotificationBox.currentNotification, "Test 6 should have made the current notification"); + gNotificationBox.removeNotification(notification); + + SimpleTest.finish(); +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug460942.xul b/toolkit/content/tests/chrome/test_bug460942.xul new file mode 100644 index 0000000000..dae10da57a --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug460942.xul @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=460942 +--> +<window title="Mozilla Bug 460942" + onload="runTests()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=460942" + target="_blank">Mozilla Bug 460942</a> + </body> + + <!-- test code goes here --> + + <richlistbox> + <richlistitem id="item1"> + <label value="one"/> + <box> + <label value="two"/> + </box> + </richlistitem> + <richlistitem id="item2"><description>one</description><description>two</description></richlistitem> + </richlistbox> + + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug 460942 **/ + function runTests() { + is ($("item1").label, "one two"); + is ($("item2").label, ""); + SimpleTest.finish(); + } + SimpleTest.waitForExplicitFinish(); + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug471776.xul b/toolkit/content/tests/chrome/test_bug471776.xul new file mode 100644 index 0000000000..6002c691ac --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug471776.xul @@ -0,0 +1,47 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Textbox with placeholder undo test" width="500" height="600" + onload="doTest();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + + <hbox> + <textbox id="t1" placeholder="empty"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + t1.focus(); + var t1Enabled = {}; + var t1CanUndo = {}; + t1.editor.canUndo(t1Enabled, t1CanUndo); + ok(!t1CanUndo.value, "undo correctly disabled when no user edits"); + SimpleTest.finish(); + } + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug509732.xul b/toolkit/content/tests/chrome/test_bug509732.xul new file mode 100644 index 0000000000..cc7ce6807f --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug509732.xul @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 509732 + --> +<window title="Bug 509732" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <notificationbox id="nb" hidden="true"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" + onload="test()"/> + + <!-- test code goes here --> +<script type="application/javascript"> +<![CDATA[ +var gNotificationBox; + +// Tests that a notification that is added in an hidden box didn't throw the animation +function test() { + SimpleTest.waitForExplicitFinish(); + gNotificationBox = document.getElementById("nb"); + + is(gNotificationBox.allNotifications.length, 0, "There should be no initial notifications"); + + gNotificationBox.appendNotification("Test notification", + "notification1", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + + is(gNotificationBox.allNotifications.length, 1, "Notification exists"); + is(gNotificationBox._animating, false, "Notification shouldn't be animating"); + + test1(); +} + +// Tests that a notification that is removed from an hidden box didn't throw the animation +function test1() { + let notification = gNotificationBox.getNotificationWithValue("notification1"); + gNotificationBox.removeNotification(notification); + ok(!gNotificationBox.currentNotification, "Test 1 should show no current animation"); + is(gNotificationBox._animating, false, "Notification shouldn't be animating"); + is(gNotificationBox.allNotifications.length, 0, "Test 1 should show no notifications present"); + + SimpleTest.finish(); +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug554279.xul b/toolkit/content/tests/chrome/test_bug554279.xul new file mode 100644 index 0000000000..d4057b8907 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug554279.xul @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Toolbar" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="startTest();"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <toolbox> + <toolbarpalette id="palette"/> + + <toolbar id="tb1" currentset="p1"/> + </toolbox> + + <!-- test resuls are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" + style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="text/javascript"><![CDATA[ + var toolbar = $("tb1"); + + ok(toolbar, "got the toolbar, triggering the xbl constructor"); + + var palette = $("palette"); + ok(palette, "palette is still in the document"); + + var button = document.createElement("p1"); + button.id = button.label = "p1"; + palette.appendChild(button); + + SimpleTest.waitForExplicitFinish(); + function startTest() { + is(button.parentNode, toolbar, "button has been added to the toolbar"); + SimpleTest.finish(); + } + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug557987.xul b/toolkit/content/tests/chrome/test_bug557987.xul new file mode 100644 index 0000000000..ba680568ff --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug557987.xul @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 557987 + --> +<window title="Bug 557987" width="400" height="400" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + + <toolbarbutton id="button" type="menu-button" label="Test bug 557987" + onclick="eventReceived('click');" + oncommand="eventReceived('command');"> + <menupopup onpopupshowing="eventReceived('popupshowing'); return false;" /> + </toolbarbutton> + <menulist id="menulist" editable="true" value="Test bug 557987" + onfocus="eventReceived('focus')" /> + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(test); + +// Tests that mouse events are correctly dispatched to <toolbarbutton type="menu-button"/> +function test() { + + disableNonTestMouseEvents(true); + + let button = $("button"); + let rightEdge = button.getBoundingClientRect().width - 2; + let centerX = button.getBoundingClientRect().width / 2; + let centerY = button.getBoundingClientRect().height / 2; + + synthesizeMouse(button, rightEdge, centerY, {}, window); + synthesizeMouse(button, centerX, centerY, {}, window); + + let menulist = $("menulist"); + centerX = menulist.getBoundingClientRect().width / 2; + centerY = menulist.getBoundingClientRect().height / 2; + synthesizeMouse(menulist, centerX, centerY, {}, window); + + synthesizeMouse(document.getElementsByTagName("body")[0], 0, 0, {}, window); + + disableNonTestMouseEvents(false); + SimpleTest.executeSoon(finishTest); + +} + +function finishTest() { + is(eventCount.command, 1, "Correct number of command events received"); + is(eventCount.popupshowing, 1, "Correct number of popupshowing events received"); + is(eventCount.click, 2, "Correct number of click events received"); + is(eventCount.focus, 1, "Correct number of focus events received"); + + SimpleTest.finish(); +} + +let eventCount = { + command: 0, + popupshowing: 0, + click: 0, + focus: 0 +}; + +function eventReceived(eventName) { + eventCount[eventName]++; +} + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug562554.xul b/toolkit/content/tests/chrome/test_bug562554.xul new file mode 100644 index 0000000000..7ee9ef03d6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug562554.xul @@ -0,0 +1,92 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 562554 + --> +<window title="Bug 562554" width="400" height="400" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<xbl:bindings xmlns:xbl="http://www.mozilla.org/xbl"> + <xbl:binding id="menu" display="xul:menu" + extends="chrome://global/content/bindings/button.xml#button-base"> + <xbl:content> + <xbl:children includes="menupopup"/> + <xul:stack> + <xul:button width="100" left="0" top="0" height="30" allowevents="true" + onclick="eventReceived('clickbutton1'); return false;"/> + <xul:button width="100" left="70" top="0" height="30" + onclick="eventReceived('clickbutton2'); return false;"/> + </xul:stack> + </xbl:content> + </xbl:binding> +</xbl:bindings> + + <toolbarbutton type="menu" id="toolbarmenu" height="200" style="-moz-binding: url(#menu);"> + <menupopup id="menupopup" onpopupshowing="eventReceived('popupshowing'); return false;"/> + </toolbarbutton> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(test); + +// Tests that mouse events are correctly dispatched to <toolbarbutton type="menu"/> +function test() { + disableNonTestMouseEvents(true); + nextTest(); +} + +let tests = [ + // Click on the toolbarbutton itself - should call popupshowing + () => synthesizeMouse($("toolbarmenu"), 10, 50, {}, window), + + // Click on button1 which has allowevents="true" - should call clickbutton1 + () => synthesizeMouse($("toolbarmenu"), 10, 15, {}, window), + + // Click on button2 where it intersects with button1 - should call popupshowing + () => synthesizeMouse($("toolbarmenu"), 85, 15, {}, window), + + // Click on button2 outside of intersection - should call popupshowing + () => synthesizeMouse($("toolbarmenu"), 150, 15, {}, window) +]; + +function nextTest() { + if (tests.length) { + let func = tests.shift(); + func(); + SimpleTest.executeSoon(nextTest); + } else { + disableNonTestMouseEvents(false); + SimpleTest.executeSoon(finishTest); + } +} + +function finishTest() { + is(eventCount.clickbutton1, 1, "Correct number of clicks on button 1"); + is(eventCount.clickbutton2, 0, "Correct number of clicks on button 2"); + is(eventCount.popupshowing, 3, "Correct number of popupshowing events received"); + + SimpleTest.finish(); +} + +let eventCount = { + popupshowing: 0, + clickbutton1: 0, + clickbutton2: 0 +}; + +function eventReceived(eventName) { + eventCount[eventName]++; +} + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug570192.xul b/toolkit/content/tests/chrome/test_bug570192.xul new file mode 100644 index 0000000000..09f73e932b --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug570192.xul @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=570192 +--> +<window title="Mozilla Bug 558406" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script> + <script type="application/javascript" + src="RegisterUnregisterChrome.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570192"> + Mozilla Bug 570192 + </a> + + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script type="application/javascript"> + <![CDATA[ + + addLoadEvent(function() { + try { + var content = document.getElementById("content"); + content.innerHTML = '<textbox newlines="pasteintact" ' + + 'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>'; + var textbox = content.firstChild; + ok(textbox, "created the textbox"); + ok(!textbox.editor, "do we have an editor?"); + } catch (e) { + ok(false, "Got an exception: " + e); + } + SimpleTest.finish(); + }); + SimpleTest.waitForExplicitFinish(); + + ]]> + </script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug585946.xul b/toolkit/content/tests/chrome/test_bug585946.xul new file mode 100644 index 0000000000..738e46b1b1 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug585946.xul @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Toolbar" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="startTest();"> + + <script type="application/javascript" src="chrome://mochikit/content/MochiKit/packed.js"></script> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <toolbox> + <toolbarpalette/> + <toolbar id="toolbar" defaultset="node1,node2"> + <toolbarbutton id="node1" label="node1" removable="true"/> + <toolbarbutton id="node2" label="node2" removable="true"/> + </toolbar> + </toolbox> + + <!-- test resuls are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" + style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function startTest() { + var toolbar = $("toolbar"); + + var splitter = document.createElement("splitter"); + splitter.setAttribute("id", "dynsplitter"); + splitter.setAttribute("skipintoolbarset", "true"); + + toolbar.insertBefore(splitter, $("node2")); + + function checkPos() { + is($("dynsplitter").previousSibling, $("node1")); + is($("dynsplitter").nextSibling, $("node2")); + } + + checkPos(); + toolbar.style.MozBinding = "url(chrome://global/content/bindings/toolbar.xml#toolbar-drag)"; + toolbar.clientTop; // style flush + checkPos(); + + SimpleTest.finish(); +} + + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug624329.xul b/toolkit/content/tests/chrome/test_bug624329.xul new file mode 100644 index 0000000000..893b386870 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug624329.xul @@ -0,0 +1,160 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=624329 +--> +<window title="Mozilla Bug 624329 context menu position" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" + onload="openTestWindow()"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=624329" + target="_blank">Mozilla Bug 624329</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug 624329 **/ + +SimpleTest.waitForExplicitFinish(); + +var win; +var timeoutID; +var menu; + +function openTestWindow() { + win = open("bug624329_window.xul", "_blank", "width=300,resizable=yes,chrome"); + // 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 listenOnce(event, callback) { + win.addEventListener(event, function listener() { + win.removeEventListener(event, listener, false); + callback(); + }, false); +} + +function childFocused() { + // maximizing the window is a simple way to ensure that the menu is near + // the right edge of the screen. + + listenOnce("resize", childResized); + win.maximize(); +} + +function childResized() { + const isOSXLion = navigator.userAgent.indexOf("Mac OS X 10.7") != -1; + const isOSXMtnLion = navigator.userAgent.indexOf("Mac OS X 10.8") != -1; + const isOSXMavericks = navigator.userAgent.indexOf("Mac OS X 10.9") != -1; + const isOSXYosemite = navigator.userAgent.indexOf("Mac OS X 10.10") != -1; + if (isOSXLion || isOSXMtnLion || isOSXMavericks || isOSXYosemite) { + todo_is(win.windowState, win.STATE_MAXIMIZED, + "A resize before being maximized breaks this test on 10.7 and 10.8 and 10.9 and 10.10"); + finish(); + return; + } + + is(win.windowState, win.STATE_MAXIMIZED, + "window should be maximized"); + + isnot(win.innerWidth, 300, + "window inner width should have changed"); + + openContextMenu(); +} + +function openContextMenu() { + var mouseX = win.innerWidth - 10; + var mouseY = 10; + + menu = win.document.getElementById("menu"); + var screenX = menu.boxObject.screenX; + var screenY = menu.boxObject.screenY; + var utils = + win.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindowUtils); + + utils.sendMouseEvent("contextmenu", mouseX, mouseY, 2, 0, 0); + + var interval = setInterval(checkMoved, 200); + function checkMoved() { + if (menu.boxObject.screenX != screenX || + menu.boxObject.screenY != screenY) { + clearInterval(interval); + // Wait further to check that the window does not move again. + setTimeout(checkPosition, 1000); + } + } + + function checkPosition() { + var menubox = menu.boxObject; + var winbox = win.document.documentElement.boxObject; + var platformIsMac = navigator.userAgent.indexOf("Mac") > -1; + + var x = menubox.screenX - winbox.screenX; + var y = menubox.screenY - winbox.screenY; + + if (platformIsMac) + { + // This check is alterered slightly for OSX which adds padding to the top + // and bottom of its context menus. The menu position calculation must + // be changed to allow for the pointer to be outside this padding + // when the menu opens. + // (Bug 1075089) + ok(y + 6 >= mouseY, + "menu top " + (y + 6) + " should be below click point " + mouseY); + } + else + { + ok(y >= mouseY, + "menu top " + y + " should be below click point " + mouseY); + } + + ok(y <= mouseY + 20, + "menu top " + y + " should not be too far below click point " + mouseY); + + ok(x < mouseX, + "menu left " + x + " should be left of click point " + mouseX); + var right = x + menubox.width; + + if (platformIsMac) { + // Rather than be constrained by the right hand screen edge, OSX menus flip + // horizontally and appear to the left of the mouse pointer + ok(right < mouseX, + "menu right " + right + " should be left of click point " + mouseX); + } + else { + ok(right > mouseX, + "menu right " + right + " should be right of click point " + mouseX); + } + + clearTimeout(timeoutID); + finish(); + } + +} + +function finish() { + if (menu && navigator.platform.indexOf("Win") >= 0) { + todo(false, "Should not have to hide popup before closing its window"); + // This avoids mochitest "Unable to restore focus" errors (bug 670053). + menu.hidePopup(); + } + win.close(); + SimpleTest.finish(); +} + + ]]> + </script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug792324.xul b/toolkit/content/tests/chrome/test_bug792324.xul new file mode 100644 index 0000000000..a6fa425056 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug792324.xul @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=792324 +--> +<window title="Mozilla Bug 792324" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +<body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=792324">Mozilla Bug 792324</a> + + <p id="display"></p> +<div id="content" style="display: none"> +</div> +</body> + +<panel id="panel-1"> + <button label="just a normal button"/> + <button id="button-1" + accesskey="X" + oncommand="clicked(event)" + label="Button in panel 1" + /> +</panel> + +<panel id="panel-2"> + <button label="just a normal button"/> + <button id="button-2" + accesskey="X" + oncommand="clicked(event)" + label="Button in panel 2" + /> +</panel> + +<script class="testbody" type="application/javascript;version=1.7"><![CDATA[ + +/** Test for Bug 792324 **/ +let after_click; + +function clicked(event) { + after_click(event); +} + +function checkAccessKeyOnPanel(panelid, buttonid, cb) { + let panel = document.getElementById(panelid); + panel.addEventListener("popupshown", function onpopupshown() { + panel.removeEventListener("popupshown", onpopupshown); + panel.firstChild.focus(); + after_click = function(event) { + is(event.target.id, buttonid, "Accesskey was directed to the button '" + buttonid + "'"); + panel.hidePopup(); + cb(); + } + synthesizeKey("X", {}); + }); + panel.openPopup(null, "", 100, 100, false, false); +} + +function test() { + checkAccessKeyOnPanel("panel-1", "button-1", function() { + checkAccessKeyOnPanel("panel-2", "button-2", function() { + SimpleTest.finish(); + }); + }); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(test, window); + +]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_button.xul b/toolkit/content/tests/chrome/test_button.xul new file mode 100644 index 0000000000..fa4e7b0035 --- /dev/null +++ b/toolkit/content/tests/chrome/test_button.xul @@ -0,0 +1,71 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for button + --> +<window title="Button Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<button id="one" label="One" /> +<button id="two" label="Two"/> +<hbox> + <button id="three" label="Three" open="true"/> +</hbox> +<hbox> + <button id="four" type="menu" label="Four"/> + <button id="five" type="panel" label="Five"/> + <button id="six" label="Six"/> +</hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_button() +{ + synthesizeMouseExpectEvent($("one"), 2, 2, {}, $("one"), "command", "button press"); + $("one").focus(); + synthesizeKeyExpectEvent("VK_SPACE", { }, $("one"), "command", "key press"); + $("two").disabled = true; + synthesizeMouseExpectEvent($("two"), 2, 2, {}, $("two"), "!command", "button press command when disabled"); + synthesizeMouseExpectEvent($("two"), 2, 2, {}, $("two"), "click", "button press click when disabled"); + + if (navigator.platform.indexOf("Mac") == -1) { + $("one").focus(); + synthesizeKey("VK_DOWN", { }); + is(document.activeElement, $("three"), "key cursor down on button"); + + synthesizeKey("VK_RIGHT", { }); + is(document.activeElement, $("four"), "key cursor right on button"); + synthesizeKey("VK_DOWN", { }); + is(document.activeElement, $("four"), "key cursor down on menu button"); + $("five").focus(); + synthesizeKey("VK_DOWN", { }); + is(document.activeElement, $("five"), "key cursor down on panel button"); + + $("three").focus(); + synthesizeKey("VK_UP", { }); + is(document.activeElement, $("one"), "key cursor up on button"); + } + + $("two").focus(); + ok(document.activeElement != $("two"), "focus disabled button"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(test_button); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_chromemargin.xul b/toolkit/content/tests/chrome/test_chromemargin.xul new file mode 100644 index 0000000000..79c4f7525e --- /dev/null +++ b/toolkit/content/tests/chrome/test_chromemargin.xul @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Custom chrome margin tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> + +// Tests parsing of the chrome margin attrib on a window. + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_chromemargin.xul", "_blank", "chrome,width=600,height=600"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_closemenu_attribute.xul b/toolkit/content/tests/chrome/test_closemenu_attribute.xul new file mode 100644 index 0000000000..c1e93734fc --- /dev/null +++ b/toolkit/content/tests/chrome/test_closemenu_attribute.xul @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu closemenu Attribute Tests" + onload="setTimeout(nextTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<button id="menu" type="menu" label="Menu" onpopuphidden="popupHidden(event)"> + <menupopup id="p1" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="l1" label="One"> + <menupopup id="p2" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="l2" label="Two"> + <menupopup id="p3" onpopupshown="executeMenuItem()"> + <menuitem id="l3" label="Three"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> +</button> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gExpectedId = "p3"; +var gMode = -1; +var gModes = ["", "auto", "single", "none"]; + +function nextTest() +{ + gMode++; + if (gModes[gMode] != "none") + gExpectedId = "p3"; + + if (gMode != 0) + $("l3").setAttribute("closemenu", gModes[gMode]); + if (gModes[gMode] == "none") + $("l2").open = true; + else + $("menu").open = true; +} + +function executeMenuItem() +{ + synthesizeKey("VK_DOWN", { }); + synthesizeKey("VK_RETURN", { }); + // after a couple of seconds, end the test, as the 'none' closemenu value + // should not hide any popups + if (gModes[gMode] == "none") + setTimeout(function() { $("menu").open = false; }, 2000); +} + +function popupHidden(event) +{ + if (gModes[gMode] == "none") { + if (event.target.id == "p1") + SimpleTest.finish() + return; + } + + is(event.target.id, gExpectedId, + "Expected event " + gModes[gMode] + " " + gExpectedId); + + gExpectedId = ""; + if (event.target.id == "p3") { + if (gModes[gMode] == "" || gModes[gMode] == "auto") + gExpectedId = "p2"; + } + else if (event.target.id == "p2") { + if (gModes[gMode] == "" || gModes[gMode] == "auto") + gExpectedId = "p1"; + } + + if (!gExpectedId) + nextTest(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_colorpicker_popup.xul b/toolkit/content/tests/chrome/test_colorpicker_popup.xul new file mode 100644 index 0000000000..3ac84260b2 --- /dev/null +++ b/toolkit/content/tests/chrome/test_colorpicker_popup.xul @@ -0,0 +1,148 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Colorpicker Tests" + onload="setTimeout(runTests, 0);" + onpopupshown="popupShown();" + onpopuphidden="popupHiding();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<colorpicker id="colorpicker-popup" type="button" color="#FF0000" tabindex="1"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var gTestPhase = -1; +var gCp = null; + +SimpleTest.waitForExplicitFinish(); + +function preventDefault(event) { + event.preventDefault(); +} + +function runTests() +{ + gCp = document.getElementById("colorpicker-popup"); + is(gCp.color, "#FF0000", "popup color is initialized"); + is(gCp.tabIndex, 1, "button tabindex is initialized"); + is(gCp.disabled, false, "button is not disabled"); + + document.addEventListener("keypress", preventDefault, false); + + goNext(); +} + +var phases = [ "mouse click", "showPopup", + "key left", "key right", "key up", "key down", "key space" ]; + +function popupShown() +{ + if (gTestPhase >= phases.length) + return; + + var phase = phases[gTestPhase]; + + is(gCp.open, true, phase + " popup shown, open property is true"); + + switch (phase) { + case "mouse click": + synthesizeMouse(gCp, 2, 2, { }); + break; + case "showPopup": + gCp.hidePopup(); + break; + case "key left": + synthesizeKey("VK_LEFT", { }); + synthesizeKeyExpectEvent("VK_RETURN", { }); + is(gCp.color, "#C0C0C0", "key left while open"); + break; + case "key right": + synthesizeKey("VK_RIGHT", { }); + synthesizeKeyExpectEvent("VK_SPACE", { }); + is(gCp.color, "#FF0000", "key right while open"); + break; + case "key up": + synthesizeKey("VK_UP", { }); + synthesizeKeyExpectEvent("VK_RETURN", { }); + is(gCp.color, "#FF6666", "key up while open"); + break; + case "key down": + synthesizeKey("VK_DOWN", { }); + synthesizeKeyExpectEvent("VK_SPACE", { }); + is(gCp.color, "#FF0000", "key down while open"); + break; + default: + synthesizeMouse(gCp, 2, 2, { }); +// this breaks on the Mac, so disable for now +// synthesizeKey("VK_ESCAPE", { }); + break; + } +} + +function popupHiding() +{ + var phase = phases[gTestPhase]; + if (phase == "showPopup") + phase = "hidePopup"; + if (phase == "key left") + phase = "escape"; + is(gCp.open, false, phase + " popup hidden, open property is false"); + + goNext(); +} + +function goNext() +{ + gTestPhase++; + if (gTestPhase >= phases.length) { + document.removeEventListener("keypress", preventDefault, false); + SimpleTest.finish(); + return; + } + + gCp.focus(); + + var phase = phases[gTestPhase]; + switch (phase) { + case "mouse click": + synthesizeMouse(gCp, 2, 2, { }); + break; + case "showPopup": + gCp.showPopup(); + break; + case "key left": + synthesizeKey("VK_LEFT", { }); + break; + case "key right": + synthesizeKey("VK_RIGHT", { }); + break; + case "key down": + synthesizeKey("VK_UP", { }); + break; + case "key up": + synthesizeKey("VK_DOWN", { }); + break; + case "key space": + synthesizeKey("VK_SPACE", { }); + break; + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_contextmenu_list.xul b/toolkit/content/tests/chrome/test_contextmenu_list.xul new file mode 100644 index 0000000000..157831a581 --- /dev/null +++ b/toolkit/content/tests/chrome/test_contextmenu_list.xul @@ -0,0 +1,288 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Context Menu on List Tests" + onload="setTimeout(startTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<spacer height="5"/> + +<hbox style="padding-left: 10px;"> + <spacer width="5"/> + <richlistbox id="list" context="themenu" style="padding: 0;" oncontextmenu="checkContextMenu(event)"> + <richlistitem id="item1" style="padding-top: 3px; margin: 0;"><button label="One"/></richlistitem> + <richlistitem id="item2" height="22"><checkbox label="Checkbox"/></richlistitem> + <richlistitem id="item3"><button label="Three"/></richlistitem> + <richlistitem id="item4"><checkbox label="Four"/></richlistitem> + </richlistbox> + + <tree id="tree" rows="5" flex="1" context="themenu" style="-moz-appearance: none; border: 0"> + <treecols> + <treecol label="Name" flex="1"/> + <splitter class="tree-splitter"/> + <treecol label="Moons"/> + </treecols> + <treechildren id="treechildren"> + <treeitem> + <treerow> + <treecell label="Mercury"/> + <treecell label="0"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Venus"/> + <treecell label="0"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Earth"/> + <treecell label="1"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mars"/> + <treecell label="2"/> + </treerow> + </treeitem> + </treechildren> + </tree> + + <menu id="menu" label="Menu"> + <menupopup id="menupopup" onpopupshown="menuTests()" onpopuphidden="nextTest()" + oncontextmenu="checkContextMenuForMenu(event)"> + <menuitem id="menu1" label="Menu 1"/> + <menuitem id="menu2" label="Menu 2"/> + <menuitem id="menu3" label="Menu 3"/> + </menupopup> + </menu> + +</hbox> + +<menupopup id="themenu" onpopupshowing="if (gTestId == -1) event.preventDefault()" + onpopupshown="checkPopup()" onpopuphidden="setTimeout(nextTest, 0);"> + <menuitem label="Item"/> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gTestId = -1; +var gTestElement = "list"; +var gSelectionStep = 0; +var gContextMenuFired = false; + +function startTest() +{ + // first, check if the richlistbox selection changes on a contextmenu mouse event + var element = $("list"); + synthesizeMouse(element.getItemAtIndex(3), 7, 1, { type : "mousedown", button: 2, ctrlKey: true }); + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + + gSelectionStep++; + synthesizeMouse(element.getItemAtIndex(1), 7, 1, { type : "mousedown", button: 2, ctrlKey: true, shiftKey: true }); + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + + gSelectionStep++; + synthesizeMouse(element.getItemAtIndex(1), 7, 1, { type : "mousedown", button: 2 }); + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + + $("menu").open = true; +} + +function menuTests() +{ + gSelectionStep = 0; + var element = $("menu"); + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + is(gContextMenuFired, true, "context menu fired when menu open"); + + gSelectionStep = 1; + $("menu").boxObject.activeChild = $("menu2"); + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + + $("menu").open = false; +} + +function nextTest() +{ + gTestId++; + if (gTestId > 2) { + if (gTestElement == "list") { + gTestElement = "tree"; + gTestId = 0; + } + else { + SimpleTest.finish(); + return; + } + } + var element = $(gTestElement); + element.focus(); + if (gTestId == 0) { + if (gTestElement == "list") + element.selectedIndex = 2; + element.currentIndex = 2; + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + } + else if (gTestId == 1) { + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + } + else { + element.currentIndex = -1; + element.selectedIndex = -1; + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + } +} + +// This is nasty so I'd better explain what's going on. +// The basic problem is that the synthetic mouse coordinate generated +// by DOMWindowUtils.sendMouseEvent and also the synthetic mouse coordinate +// generated internally when contextmenu events are redirected to the focused +// element are rounded to the nearest device pixel. But this rounding is done +// while the coordinates are relative to the nearest widget. When this test +// is run in the mochitest harness, the nearest widget is the main mochitest +// window, and our document can have a fractional position within that +// mochitest window. So when we round coordinates for comparison in this +// test, we need to do so very carefully, especially if the target element +// also has a fractional position within our document. +// +// For example, if the y-offset of our containing IFRAME is 100.4px, +// and the offset of our expected point is 10.3px in our document, the actual +// mouse event is dispatched to round(110.7) == 111px. This comes back +// with a clientY of round(111 - 100.4) == round(10.6) == 11. This is not +// equal to round(10.3) as you might expect. + +function isRoundedX(a, b, msg) +{ + is(Math.round(a + mozInnerScreenX), Math.round(b + mozInnerScreenX), msg); +} + +function isRoundedY(a, b, msg) +{ + is(Math.round(a + mozInnerScreenY), Math.round(b + mozInnerScreenY), msg); +} + +function checkContextMenu(event) +{ + var rect = $(gTestElement).getBoundingClientRect(); + + var frombase = (gTestId == -1 || gTestId == 1); + if (!frombase) + rect = event.originalTarget.getBoundingClientRect(); + var left = frombase ? rect.left + 7 : rect.left; + var top = frombase ? rect.top + 4 : rect.bottom; + + isRoundedX(event.clientX, left, gTestElement + " clientX " + gSelectionStep + " " + gTestId + "," + frombase); + isRoundedY(event.clientY, top, gTestElement + " clientY " + gSelectionStep + " " + gTestId); + ok(event.screenX > left, gTestElement + " screenX " + gSelectionStep + " " + gTestId); + ok(event.screenY > top, gTestElement + " screenY " + gSelectionStep + " " + gTestId); + + // context menu from mouse click + switch (gTestId) { + case -1: + var expected = gSelectionStep == 2 ? 1 : (platformIsMac() ? 3 : 0); + is($(gTestElement).selectedIndex, expected, "index after click " + gSelectionStep); + break; + case 0: + if (gTestElement == "list") + is(event.originalTarget, $("item3"), "list selection target"); + else + is(event.originalTarget, $("treechildren"), "tree selection target"); + break; + case 1: + is(event.originalTarget.id, $("item1").id, "list mouse selection target"); + break; + case 2: + is(event.originalTarget, $("list"), "list no selection target"); + break; + } +} + +function checkContextMenuForMenu(event) +{ + gContextMenuFired = true; + + var popuprect = (gSelectionStep ? $("menu2") : $("menupopup")).getBoundingClientRect(); + is(event.clientX, Math.round(popuprect.left), "menu left " + gSelectionStep); + // the clientY is off by one sometimes on Windows (when loaded in the testing iframe + // but not when loaded separately) so just check for both cases for now + ok(event.clientY == Math.round(popuprect.bottom) || + event.clientY - 1 == Math.round(popuprect.bottom), "menu top " + gSelectionStep); +} + +function checkPopup() +{ + var menurect = $("themenu").getBoundingClientRect(); + + // Context menus are offset by a number of pixels from the mouse click + // which activates them. This is so that they don't appear exactly + // under the mouse which can cause them to be mistakenly dismissed. + // The number of pixels depends on the platform and is defined in + // each platform's nsLookAndFeel + var contextMenuOffsetX = platformIsMac() ? 1 : 2; + var contextMenuOffsetY = platformIsMac() ? -6 : 2; + + if (gTestId == 0) { + if (gTestElement == "list") { + var itemrect = $("item3").getBoundingClientRect(); + isRoundedX(menurect.left, itemrect.left + contextMenuOffsetX, + "list selection keyboard left"); + isRoundedY(menurect.top, itemrect.bottom + contextMenuOffsetY, + "list selection keyboard top"); + } + else { + var tree = $("tree"); + var bodyrect = $("treechildren").getBoundingClientRect(); + isRoundedX(menurect.left, bodyrect.left + contextMenuOffsetX, + "tree selection keyboard left"); + isRoundedY(menurect.top, bodyrect.top + + tree.treeBoxObject.rowHeight * 3 + contextMenuOffsetY, + "tree selection keyboard top"); + } + } + else if (gTestId == 1) { + // activating a context menu with the mouse from position (7, 4). + var elementrect = $(gTestElement).getBoundingClientRect(); + isRoundedX(menurect.left, elementrect.left + 7 + contextMenuOffsetX, + gTestElement + " mouse left"); + isRoundedY(menurect.top, elementrect.top + 4 + contextMenuOffsetY, + gTestElement + " mouse top"); + } + else { + var elementrect = $(gTestElement).getBoundingClientRect(); + isRoundedX(menurect.left, elementrect.left + contextMenuOffsetX, + gTestElement + " no selection keyboard left"); + isRoundedY(menurect.top, elementrect.bottom + contextMenuOffsetY, + gTestElement + " no selection keyboard top"); + } + + $("themenu").hidePopup(); +} + +function platformIsMac() +{ + return navigator.platform.indexOf("Mac") > -1; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_cursorsnap.xul b/toolkit/content/tests/chrome/test_cursorsnap.xul new file mode 100644 index 0000000000..de153e7040 --- /dev/null +++ b/toolkit/content/tests/chrome/test_cursorsnap.xul @@ -0,0 +1,127 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window title="Cursor snapping test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +const kMaxRetryCount = 4; +const kTimeoutTime = [ + 100, 100, 1000, 1000, 5000 +]; + +var gRetryCount; + +var gTestingCount = 0; +var gTestingIndex = -1; +var gDisable = false; +var gHidden = false; + +function canRetryTest() +{ + return gRetryCount <= kMaxRetryCount; +} + +function getTimeoutTime() +{ + return kTimeoutTime[gRetryCount]; +} + +function runNextTest() +{ + gRetryCount = 0; + gTestingIndex++; + runCurrentTest(); +} + +function retryCurrentTest() +{ + ok(canRetryTest(), "retry the current test..."); + gRetryCount++; + runCurrentTest(); +} + +function runCurrentTest() +{ + var position = "top=" + gTestingCount + ",left=" + gTestingCount + ","; + gTestingCount++; + switch (gTestingIndex) { + case 0: + gDisable = false; + gHidden = false; + window.open("window_cursorsnap_dialog.xul", "_blank", + position + "chrome,width=100,height=100"); + break; + case 1: + gDisable = true; + gHidden = false; + window.open("window_cursorsnap_dialog.xul", "_blank", + position + "chrome,width=100,height=100"); + break; + case 2: + gDisable = false; + gHidden = true; + window.open("window_cursorsnap_dialog.xul", "_blank", + position + "chrome,width=100,height=100"); + break; + case 3: + gDisable = false; + gHidden = false; + window.open("window_cursorsnap_wizard.xul", "_blank", + position + "chrome,width=100,height=100"); + break; + case 4: + gDisable = true; + gHidden = false; + window.open("window_cursorsnap_wizard.xul", "_blank", + position + "chrome,width=100,height=100"); + break; + case 5: + gDisable = false; + gHidden = true; + window.open("window_cursorsnap_wizard.xul", "_blank", + position + "chrome,width=100,height=100"); + break; + default: + SetPrefs(false); + SimpleTest.finish(); + return; + } +} + +function SetPrefs(aSet) +{ + var prefSvc = Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch); + const kPrefName = "ui.cursor_snapping.always_enabled"; + if (aSet) { + prefSvc.setBoolPref(kPrefName, true); + } else if (prefSvc.prefHasUserValue(kPrefName)) { + prefSvc.clearUserPref(kPrefName); + } +} + +SetPrefs(true); +runNextTest(); + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_datepicker.xul b/toolkit/content/tests/chrome/test_datepicker.xul new file mode 100644 index 0000000000..e7a61f43bc --- /dev/null +++ b/toolkit/content/tests/chrome/test_datepicker.xul @@ -0,0 +1,415 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for datepicker + --> +<window title="datepicker" width="500" height="600" + onload="setTimeout(testtag_datepickers, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<hbox onpopupshown="testtag_datepicker_UI_popup()" + onpopuphidden="testtag_finish()"> +<datepicker id="datepicker"/> +<datepicker id="datepicker-popup" type="popup"/> +<hbox onDOMMouseScroll="mouseScrolled = event.defaultPrevented;"> + <datepicker id="datepicker-grid" type="grid" value="2007-04-21"/> +</hbox> +</hbox> + +<!-- Test-only key bindings, but must not conflict with the application. --> +<keyset id="mainKeyset"> + <key id="key_alt_z" key="Z" oncommand="return" modifiers="alt"/> + <key id="key_ctrl_q" key="Q" oncommand="return" modifiers="control"/> + <key id="key_meta_e" key="E" oncommand="return" modifiers="meta"/> +</keyset> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +var mouseScrolled = false; + +SimpleTest.waitForExplicitFinish(); + +function testtag_datepickers() +{ + var dppopup = document.getElementById("datepicker-popup"); + testtag_datepicker(document.getElementById("datepicker"), "", "datepicker"); + testtag_datepicker(dppopup, "popup", "datepicker popup"); + + var gridpicker = document.getElementById("datepicker-grid"); + is(gridpicker.monthField.selectedIndex, "3", "datepicker grid correct month is initially selected"); + testtag_datepicker(gridpicker, "grid", "datepicker grid"); + dppopup.open = true; +} + +function testtag_finish() +{ + ok(!document.getElementById("datepicker-popup").open, "datepicker popup open false again"); + + var dpgrid = document.getElementById("datepicker-grid"); + synthesizeWheel(dpgrid, 5, 5, { deltaY: 10.0, + deltaMode: WheelEvent.DOM_DELTA_LINE }); + is(mouseScrolled, true, "mouse scrolled"); + is(dpgrid.displayedMonth, 2, "mouse scroll changed month"); + + SimpleTest.finish(); +} + +function testtag_datepicker(dp, type, testid) +{ + testid += " "; + + var today = new Date(); + var tyear = today.getFullYear(); + var tmonth = today.getMonth(); + var tdate = today.getDate(); + + // testtag_comparedate(dp, testid + "initial", tyear, tmonth, tdate); + + // check that setting the value property works + dp.value = testtag_getdatestring(tyear, tmonth, tdate); + testtag_comparedate(dp, testid + "set value", tyear, tmonth, tdate); + + // check that setting the dateValue property works + dp.dateValue = today; + testtag_comparedate(dp, testid + "set dateValue", tyear, tmonth, tdate); + ok(dp.value !== today, testid + " set dateValue different date"); + + ok(!dp.readOnly, testid + "readOnly"); + dp.readOnly = true; + ok(dp.readOnly, testid + "set readOnly"); + dp.readOnly = false; + ok(!dp.readOnly, testid + "clear readOnly"); + + var setDateField = function(field, value, expectException, + expectedYear, expectedMonth, expectedDate) + { + var exh = false; + try { + dp[field] = value; + } catch (ex) { exh = true; } + is(exh, expectException, testid + "set " + field + " " + value); + testtag_comparedate(dp, testid + "set " + field + " " + value, + expectedYear, expectedMonth, expectedDate); + } + + // check the value property + setDateField("value", "2003-1-27", false, 2003, 0, 27); + setDateField("value", "2002-11-8", false, 2002, 10, 8); + setDateField("value", "2001-07-02", false, 2001, 6, 2); + setDateField("value", "2002-10-25", false, 2002, 9, 25); + + // check that the year, month and date fields can be set properly + setDateField("year", 2002, false, 2002, 9, 25); + setDateField("year", 0, true, 2002, 9, 25); + + setDateField("month", 6, false, 2002, 6, 25); + setDateField("month", 9, false, 2002, 9, 25); + setDateField("month", 10, false, 2002, 10, 25); + setDateField("month", -1, true, 2002, 10, 25); + setDateField("month", 12, true, 2002, 10, 25); + + setDateField("date", 9, false, 2002, 10, 9); + setDateField("date", 10, false, 2002, 10, 10); + setDateField("date", 15, false, 2002, 10, 15); + setDateField("date", 0, true, 2002, 10, 15); + setDateField("date", 32, true, 2002, 10, 15); + + // check leap year handling + setDateField("value", "1600-2-29", false, 1600, 1, 29); + setDateField("value", "2000-2-29", false, 2000, 1, 29); + setDateField("value", "2003-2-29", false, 2003, 2, 1); + setDateField("value", "2004-2-29", false, 2004, 1, 29); + setDateField("value", "2100-2-29", false, 2100, 2, 1); + + // check invalid values for the value and dateValue properties + dp.value = "2002-07-15"; + setDateField("value", "", true, 2002, 6, 15); + setDateField("value", "2-2", true, 2002, 6, 15); + setDateField("value", "2000-5-6-6", true, 2002, 6, 15); + setDateField("value", "2000-a-19", true, 2002, 6, 15); + setDateField("dateValue", "none", true, 2002, 6, 15); + + // grid and popup types can display a different month than the current one + var isGridOrPopup = (type == "grid" || type == "popup"); + dp.displayedMonth = 3; + testtag_comparedate(dp, testid + "set displayedMonth", + 2002, isGridOrPopup ? 6 : 3, 15, 3); + + dp.displayedYear = 2009; + testtag_comparedate(dp, testid + "set displayedYear", + isGridOrPopup ? 2002 : 2009, isGridOrPopup ? 6 : 3, 15, 3, 2009); + + if (isGridOrPopup) { + dp.value = "2008-02-29"; + dp.displayedYear = 2009; + is(dp.displayedMonth, 1, "set displayedYear during leap year"); + } + + is(dp.open, false, testid + "open false"); + if (type != "popup") { + dp.open = true; + ok(!dp.open, testid + "open still false"); + } + + // check the fields + if (type != "grid") { + ok(dp.yearField instanceof HTMLInputElement, testid + "yearField"); + ok(dp.monthField instanceof HTMLInputElement, testid + "monthField"); + ok(dp.dateField instanceof HTMLInputElement, testid + "dateField"); + + testtag_datepicker_UI_fields(dp, testid); + + dp.readOnly = true; + + // check that keyboard usage doesn't change the value when the datepicker + // is read only + testtag_datepicker_UI_key(dp, testid + "readonly ", "2003-01-29", + dp.yearField, 2003, 0, 29, 2003, 0, 29); + testtag_datepicker_UI_key(dp, testid + "readonly ", "2003-04-29", + dp.monthField, 2003, 3, 29, 2003, 3, 29); + testtag_datepicker_UI_key(dp, testid + "readonly ", "2003-06-15", + dp.dateField, 2003, 5, 15, 2003, 5, 15); + + dp.readOnly = false; + } + else { + testtag_datepicker_UI_grid(dp, "grid", testid); + } +} + +function testtag_datepicker_UI_fields(dp, testid) +{ + testid += "UI"; + dp.focus(); + + // test adjusting the date with the up and down keys + testtag_datepicker_UI_key(dp, testid, "2003-01-29", dp.yearField, 2004, 0, 29, 2003, 0, 29); + testtag_datepicker_UI_key(dp, testid, "1600-02-29", dp.yearField, 1601, 1, 28, 1600, 1, 28); + testtag_datepicker_UI_key(dp, testid, "2000-02-29", dp.yearField, 2001, 1, 28, 2000, 1, 28); + testtag_datepicker_UI_key(dp, testid, "2004-02-29", dp.yearField, 2005, 1, 28, 2004, 1, 28); + + testtag_datepicker_UI_key(dp, testid, "2003-04-29", dp.monthField, 2003, 4, 29, 2003, 3, 29); + testtag_datepicker_UI_key(dp, testid, "2003-01-15", dp.monthField, 2003, 1, 15, 2003, 0, 15); + testtag_datepicker_UI_key(dp, testid, "2003-12-29", dp.monthField, 2003, 0, 29, 2003, 11, 29); + testtag_datepicker_UI_key(dp, testid, "2003-03-31", dp.monthField, 2003, 3, 30, 2003, 2, 30); + + testtag_datepicker_UI_key(dp, testid, "2003-06-15", dp.dateField, 2003, 5, 16, 2003, 5, 15); + testtag_datepicker_UI_key(dp, testid, "2003-06-01", dp.dateField, 2003, 5, 2, 2003, 5, 1); + testtag_datepicker_UI_key(dp, testid, "2003-06-30", dp.dateField, 2003, 5, 1, 2003, 5, 30); + testtag_datepicker_UI_key(dp, testid, "1600-02-28", dp.dateField, 1600, 1, 29, 1600, 1, 28); + testtag_datepicker_UI_key(dp, testid, "2000-02-28", dp.dateField, 2000, 1, 29, 2000, 1, 28); + testtag_datepicker_UI_key(dp, testid, "2003-02-28", dp.dateField, 2003, 1, 1, 2003, 1, 28); + testtag_datepicker_UI_key(dp, testid, "2004-02-28", dp.dateField, 2004, 1, 29, 2004, 1, 28); + testtag_datepicker_UI_key(dp, testid, "2100-02-28", dp.dateField, 2100, 1, 1, 2100, 1, 28); + + synthesizeKeyExpectEvent('Z', { altKey: true }, $("key_alt_z"), "command", testid + " alt shortcut"); + synthesizeKeyExpectEvent('Q', { ctrlKey: true }, $("key_ctrl_q"), "command", testid + " ctrl shortcut"); + synthesizeKeyExpectEvent('E', { metaKey: true }, $("key_meta_e"), "command", testid + " meta shortcut"); +} + +function testtag_datepicker_UI_grid(dp, type, testid) +{ + testid += "UI "; + + // check that pressing the cursor keys moves the date properly. For grid + // types, focus the grid first. For popup types, the grid should be focused + // automatically when opening the popup. + var ktarget = dp; + if (type == "grid") + dp.focus(); + else + ktarget = dp.attachedControl; + + dp.value = "2003-02-22"; + + synthesizeKeyExpectEvent("VK_LEFT", { }, ktarget, "change", testid + "key left"); + is(dp.value, "2003-02-21", testid + "key left"); + + synthesizeKeyExpectEvent("VK_RIGHT", { }, ktarget, "change", testid + "key right"); + is(dp.value, "2003-02-22", testid + "key right"); + synthesizeKeyExpectEvent("VK_RIGHT", { }, ktarget, "change", testid + "key right next week"); + is(dp.value, "2003-02-23", testid + "key right next week"); + synthesizeKeyExpectEvent("VK_LEFT", { }, ktarget, "change", testid + "key left previous week"); + is(dp.value, "2003-02-22", testid + "key left previous week"); + + synthesizeKeyExpectEvent("VK_UP", { }, ktarget, "change", testid + "key up"); + is(dp.value, "2003-02-15", testid + "key up"); + synthesizeKeyExpectEvent("VK_DOWN", { }, ktarget, "change", testid + "key down"); + is(dp.value, "2003-02-22", testid + "key down"); + synthesizeKeyExpectEvent("VK_DOWN", { }, ktarget, "change"); + is(dp.value, "2003-03-01", testid + "key down next month", testid + "key down next month"); + synthesizeKeyExpectEvent("VK_UP", { }, ktarget, "change"); + is(dp.value, "2003-02-22", testid + "key up previous month", testid + "key up previous month"); + + // the displayed month may be changed with the page up and page down keys, + // however this only changes the displayed month, not the current value. + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { }, ktarget, "monthchange", testid + "key page down"); + is(dp.value, "2003-02-22", testid + "key page down"); + + // the monthchange event is fired when the displayed month is changed + synthesizeKeyExpectEvent("VK_UP", { }, ktarget, "monthchange", testid + "key up after month change"); + is(dp.value, "2003-02-15", testid + "key up after month change"); + + synthesizeKeyExpectEvent("VK_PAGE_UP", { }, ktarget, "monthchange", testid + "key page up"); + is(dp.value, "2003-02-15", testid + "key page up"); + + // check handling at the start and end of the month + dp.value = "2010-10-01"; + synthesizeKeyExpectEvent("VK_PAGE_UP", { }, ktarget, "monthchange", testid + "key page up 2010-10-01"); + is(dp.displayedMonth, 8, testid + "key page up 2010-10-01 displayedMonth"); + is(dp.displayedYear, 2010, testid + "key page up 2010-10-01 displayedYear"); + + dp.value = "2010-10-01"; + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { }, ktarget, "monthchange", testid + "key page down 2010-10-01"); + is(dp.displayedMonth, 10, testid + "key page down 2010-10-01 displayedMonth"); + is(dp.displayedYear, 2010, testid + "key page down 2010-10-01 displayedYear"); + + dp.value = "2010-10-31"; + synthesizeKeyExpectEvent("VK_PAGE_UP", { }, ktarget, "monthchange", testid + "key page up 2010-10-31"); + is(dp.displayedMonth, 8, testid + "key page up 2010-10-31 displayedMonth"); + is(dp.displayedYear, 2010, testid + "key page up 2010-10-01 displayedYear"); + dp.value = "2010-10-31"; + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { }, ktarget, "monthchange", testid + "key page down 2010-10-31"); + is(dp.displayedMonth, 10, testid + "key page down 2010-10-31 displayedMonth"); + is(dp.displayedYear, 2010, testid + "key page up 2010-10-31 displayedYear"); + + // check handling at the end of february + dp.value = "2010-03-31"; + synthesizeKeyExpectEvent("VK_PAGE_UP", { }, ktarget, "monthchange", testid + "key page up 2010-03-31"); + is(dp.displayedMonth, 1, testid + "key page up 2010-03-31 displayedMonth"); + is(dp.displayedYear, 2010, testid + "key page up 2010-03-31 displayedYear"); + synthesizeKeyExpectEvent("VK_PAGE_UP", { }, ktarget, "monthchange", testid + "key page up 2010-02-28"); + is(dp.displayedMonth, 0, testid + "key page up 2010-02-28 displayedMonth"); + is(dp.displayedYear, 2010, testid + "key page up 2010-02-28 displayedYear"); + + dp.value = "2010-01-31"; + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { }, ktarget, "monthchange", testid + "key page down 2010-01-31"); + is(dp.displayedMonth, 1, testid + "key page down 2010-01-31 displayedMonth"); + is(dp.displayedYear, 2010, testid + "key page up 2010-01-31 displayedYear"); + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { }, ktarget, "monthchange", testid + "key page down 2010-02-28"); + is(dp.displayedMonth, 2, testid + "key page down 2010-02-28 displayedMonth"); + is(dp.displayedYear, 2010, testid + "key page up 2010-02-28 displayedYear"); + + // check handling at the end of february during a leap year + dp.value = "2008-01-31"; + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { }, ktarget, "monthchange", testid + "key page down 2008-01-31"); + is(dp.displayedMonth, 1, testid + "key page down 2008-01-31 displayedMonth"); + is(dp.displayedYear, 2008, testid + "key page up 2008-01-31 displayedYear"); + dp.value = "2008-03-31"; + synthesizeKeyExpectEvent("VK_PAGE_UP", { }, ktarget, "monthchange", testid + "key page up 2008-03-31"); + is(dp.displayedMonth, 1, testid + "key page up 2008-03-31 displayedMonth"); + is(dp.displayedYear, 2008, testid + "key page up 2008-03-31 displayedYear"); + + // the value of a read only datepicker cannot be changed + dp.value = "2003-02-15"; + + dp.readOnly = true; + synthesizeKeyExpectEvent("VK_LEFT", { }, ktarget, "!change", testid + "key left read only"); + is(dp.value, "2003-02-15", testid + "key left read only"); + synthesizeKeyExpectEvent("VK_RIGHT", { }, ktarget, "!change", testid + "key right read only"); + is(dp.value, "2003-02-15", testid + "key right read only"); + synthesizeKeyExpectEvent("VK_DOWN", { }, ktarget, "!change", testid + "key down read only"); + is(dp.value, "2003-02-15", testid + "key down read only"); + synthesizeKeyExpectEvent("VK_UP", { }, ktarget, "!change", testid + "key up read only"); + is(dp.value, "2003-02-15", testid + "key up read only"); + + // month can still be changed even when readonly + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { }, ktarget, "monthchange", + testid + "key page up read only"); + synthesizeKeyExpectEvent("VK_PAGE_UP", { }, ktarget, "monthchange", + testid + "key page down read only"); + + dp.readOnly = false; + synthesizeKeyExpectEvent("VK_LEFT", { }, ktarget, "change", testid + "key left changeable again"); + is(dp.value, "2003-02-14", testid + "key left changeable again"); + + // the value of a disabled datepicker cannot be changed + dp.disabled = true; + synthesizeKeyExpectEvent("VK_LEFT", { }, ktarget, "!change", testid + "key left disabled"); + is(dp.value, "2003-02-14", testid + "key left disabled"); + synthesizeKeyExpectEvent("VK_RIGHT", { }, ktarget, "!change", testid + "key right disabled"); + is(dp.value, "2003-02-14", testid + "key right disabled"); + synthesizeKeyExpectEvent("VK_DOWN", { }, ktarget, "!change", testid + "key down disabled"); + is(dp.value, "2003-02-14", testid + "key down disabled"); + synthesizeKeyExpectEvent("VK_UP", { }, ktarget, "!change", testid + "key up disabled"); + is(dp.value, "2003-02-14", testid + "key up disabled"); + + // month cannot be changed even when disabled + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { }, ktarget, "!monthchange", + testid + "key page down disabled"); + synthesizeKeyExpectEvent("VK_PAGE_UP", { }, ktarget, "!monthchange", + testid + "key page up disabled"); + + dp.disabled = false; + synthesizeKeyExpectEvent("VK_RIGHT", { }, ktarget, "change", testid + "key right enabled again"); + is(dp.value, "2003-02-15", testid + "key right enabled again"); +} + +function testtag_datepicker_UI_popup() +{ + var dppopup = document.getElementById("datepicker-popup"); + is(dppopup.open, true, "datepicker popup after open"); + testtag_datepicker_UI_grid(dppopup, "popup", "datepicker popup "); + dppopup.open = false; +} + +function testtag_datepicker_UI_key(dp, testid, value, field, + uyear, umonth, udate, + dyear, dmonth, ddate) +{ + dp.value = value; + field.focus(); + + synthesizeKey("VK_UP", { }); + testtag_comparedate(dp, testid + " " + value + " key up", uyear, umonth, udate); + + synthesizeKey("VK_DOWN", { }); + testtag_comparedate(dp, testid + " " + value + " key down", dyear, dmonth, ddate); +} + +function testtag_getdatestring(year, month, date) +{ + month = (month < 9) ? ("0" + ++month) : month + 1; + if (date < 10) + date = "0" + date; + return year + "-" + month + "-" + date; +} + +function testtag_comparedate(dp, testid, year, month, date, displayedMonth, displayedYear) +{ + is(dp.value, testtag_getdatestring(year, month, date), testid + " value"); + if (testid.indexOf("initial") == -1) + is(dp.getAttribute("value"), + testtag_getdatestring(year, month, date), + testid + " value attribute"); + + var dateValue = dp.dateValue; + ok(dateValue.getFullYear() == year && + dateValue.getMonth() == month && + dateValue.getDate() == date, + testid + " dateValue"); + + is(dp.year, year, testid + " year"); + is(dp.month, month, testid + " month"); + is(dp.displayedMonth, displayedMonth ? displayedMonth : month, testid + " displayedMonth"); + is(dp.displayedYear, displayedYear ? displayedYear : year, testid + " displayedYear"); + is(dp.date, date, testid + " date"); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_deck.xul b/toolkit/content/tests/chrome/test_deck.xul new file mode 100644 index 0000000000..25c59c38a9 --- /dev/null +++ b/toolkit/content/tests/chrome/test_deck.xul @@ -0,0 +1,133 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for deck + --> +<window title="Deck Test" + onload="setTimeout(run_tests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<deck id="deck1" style="padding-top: 5px; padding-bottom: 12px;"> + <button id="d1b1" label="Button One"/> + <button id="d1b2" label="Button Two is larger" height="80" style="margin: 1px;"/> +</deck> +<deck id="deck2" selectedIndex="1"> + <button id="d2b1" label="Button One"/> + <button id="d2b2" label="Button Two"/> +</deck> +<deck id="deck3" selectedIndex="1"> + <button id="d3b1" label="Remove me"/> + <button id="d3b2" label="Keep me selected"/> +</deck> +<deck id="deck4" selectedIndex="5"> + <button id="d4b1" label="Remove me"/> + <button id="d4b2" label="Remove me"/> + <button id="d4b3" label="Remove me"/> + <button id="d4b4" label="Button 4"/> + <button id="d4b5" label="Button 5"/> + <button id="d4b6" label="Keep me selected"/> + <button id="d4b7" label="Button 7"/> +</deck> +<deck id="deck5" selectedIndex="2"> + <button id="d5b1" label="Button 1"/> + <button id="d5b2" label="Button 2"/> + <button id="d5b3" label="Keep me selected"/> + <button id="d5b4" label="Remove me"/> + <button id="d5b5" label="Remove me"/> + <button id="d5b6" label="Remove me"/> +</deck> + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function run_tests() { + test_deck(); + test_deck_child_removal(); + SimpleTest.finish(); +} + +function test_deck() +{ + var deck = $("deck1"); + ok(deck.selectedIndex === '0', "deck one selectedIndex"); + // this size is the button height, 80, plus the button padding of 1px on each side, + // plus the deck's 5px top padding and the 12px bottom padding. + var rect = deck.getBoundingClientRect(); + is(Math.round(rect.bottom) - Math.round(rect.top), 99, "deck size of largest child"); + synthesizeMouseExpectEvent(deck, 12, 12, { }, $("d1b1"), "click", "mouse on deck one"); + + // change the selected page of the deck and ensure that the mouse click goes + // to the button on that page + deck.selectedIndex = 1; + ok(deck.selectedIndex === '1', "deck one selectedIndex after change"); + synthesizeMouseExpectEvent(deck, 9, 9, { }, $("d1b2"), "click", "mouse on deck one after change"); + + deck = $("deck2"); + ok(deck.selectedIndex === '1', "deck two selectedIndex"); + synthesizeMouseExpectEvent(deck, 9, 9, { }, $("d2b2"), "click", "mouse on deck two"); +} + +function test_deck_child_removal() +{ + // Start with a simple case where we have two child nodes in a deck, with + // the second child (index 1) selected. Removing the first node should + // automatically set the selectedIndex at 0. + let deck = $("deck3"); + let child = $("d3b1"); + is(deck.selectedIndex, "1", "Should have the deck element at index 1 selected"); + + // Remove the child at the 0th index. The deck should automatically + // set the selectedIndex to "0". + child.remove(); + is(deck.selectedIndex, "0", "Should have the deck element at index 0 selected"); + + // Now scale it up by using a deck with 7 child nodes, and remove the + // first three, making sure that the selectedIndex is decremented + // each time. + deck = $("deck4"); + let expectedIndex = 5; + is(deck.selectedIndex, String(expectedIndex), + "Should have the deck element at index " + expectedIndex + " selected"); + + for (let i = 0; i < 3; ++i) { + deck.firstChild.remove(); + expectedIndex--; + is(deck.selectedIndex, String(expectedIndex), + "Should have the deck element at index " + expectedIndex + " selected"); + } + + // Check that removing the currently selected node doesn't change + // behaviour. + deck.childNodes[expectedIndex].remove(); + is(deck.selectedIndex, String(expectedIndex), + "The selectedIndex should not change when removing the node " + + "at the selected index."); + + // Finally, make sure we haven't changed the behaviour when removing + // nodes at indexes greater than the selected node. + deck = $("deck5"); + expectedIndex = 2; + is(deck.selectedIndex, String(expectedIndex), + "Should have the deck element at index " + expectedIndex + " selected"); + + // And then remove all of the nodes, starting from last to first, making + // sure that the selectedIndex does not change. + while (deck.lastChild) { + deck.lastChild.remove(); + is(deck.selectedIndex, String(expectedIndex), + "Should have the deck element at index " + expectedIndex + " selected"); + } +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_dialogfocus.xul b/toolkit/content/tests/chrome/test_dialogfocus.xul new file mode 100644 index 0000000000..80474e2b91 --- /dev/null +++ b/toolkit/content/tests/chrome/test_dialogfocus.xul @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<button id="test" label="Test"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); + +var expected = [ "one", "_extra2", "tab", "one", "tabbutton2", "tabbutton", "two", "textbox-yes", "one" ]; +// non-Mac will always focus the default button if any of the dialog buttons +// would be focused +if (navigator.platform.indexOf("Mac") == -1) + expected[1] = "_accept"; + +var step = 0; +var fullKeyboardAccess = false; + +function startTest() +{ + var testButton = document.getElementById("test"); + synthesizeKey("VK_TAB", { }); + fullKeyboardAccess = (document.activeElement == testButton); + info("We " + (fullKeyboardAccess ? "have" : "don't have") + " full keyboard access"); + runTest(); +} + +function runTest() +{ + step++; + info("runTest(), step = " + step + ", expected = " + expected[step - 1]); + if (step > expected.length || (!fullKeyboardAccess && step == 2)) { + info("finishing"); + SimpleTest.finish(); + return; + } + + var expectedFocus = expected[step - 1]; + var win = window.openDialog("dialog_dialogfocus.xul", "_new", "chrome,dialog", step); + + function checkDialogFocus(event) + { + info("checkDialogFocus()"); + // if full keyboard access is not on, just skip the tests + var match = false; + if (fullKeyboardAccess) { + if (!(event.target instanceof Element)) { + info("target not an Element"); + return; + } + + if (expectedFocus == "textbox-yes") + match = (win.document.activeElement == win.document.getElementById(expectedFocus).inputField); + else if (expectedFocus[0] == "_") + match = (win.document.activeElement.dlgType == expectedFocus.substring(1)); + else + match = (win.document.activeElement.id == expectedFocus); + info("match = " + match); + if (!match) + return; + } + else { + match = (win.document.activeElement == win.document.documentElement); + info("match = " + match); + } + + win.removeEventListener("focus", checkDialogFocus, true); + ok(match, "focus step " + step); + + win.close(); + SimpleTest.waitForFocus(runTest, window); + } + + win.addEventListener("focus", checkDialogFocus, true); +} + +SimpleTest.waitForFocus(startTest, window); + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_findbar.xul b/toolkit/content/tests/chrome/test_findbar.xul new file mode 100644 index 0000000000..9cbe73c475 --- /dev/null +++ b/toolkit/content/tests/chrome/test_findbar.xul @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=257061 +https://bugzilla.mozilla.org/show_bug.cgi?id=288254 +--> +<window title="Mozilla Bug 257061 and Bug 288254" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=257061">Mozilla Bug 257061</a> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=288254">Mozilla Bug 288254</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 257061 and Bug 288254 **/ +SimpleTest.waitForExplicitFinish(); + +// Since bug 978861, this pref is set to `false` on OSX. For this test, we'll +// set it `true` to disable the find clipboard on OSX, which interferes with +// our tests. +SpecialPowers.pushPrefEnv({ + set: [["accessibility.typeaheadfind.prefillwithselection", true]] +}, () => { + window.open("findbar_window.xul", "findbartest", "chrome,width=600,height=600"); +}); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_findbar_entireword.xul b/toolkit/content/tests/chrome/test_findbar_entireword.xul new file mode 100644 index 0000000000..dc39fe09dd --- /dev/null +++ b/toolkit/content/tests/chrome/test_findbar_entireword.xul @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=269442 +--> +<window title="Mozilla Bug 269442" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/MochiKit/packed.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=269442"> + Mozilla Bug 269442 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 269442 **/ + SimpleTest.waitForExplicitFinish(); + window.open("findbar_entireword_window.xul", "269442test", + "chrome,width=600,height=600"); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_findbar_events.xul b/toolkit/content/tests/chrome/test_findbar_events.xul new file mode 100644 index 0000000000..d75e5ccb57 --- /dev/null +++ b/toolkit/content/tests/chrome/test_findbar_events.xul @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=793275 +--> +<window title="Mozilla Bug 793275" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=793275"> + Mozilla Bug 793275 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 793275 **/ + SimpleTest.waitForExplicitFinish(); + window.open("findbar_events_window.xul", "793275test", + "chrome,width=600,height=600"); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_focus_anons.xul b/toolkit/content/tests/chrome/test_focus_anons.xul new file mode 100644 index 0000000000..848590887b --- /dev/null +++ b/toolkit/content/tests/chrome/test_focus_anons.xul @@ -0,0 +1,119 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tests for focus on elements with anonymous focusable children" + onload="SimpleTest.waitForFocus(runTests);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<label accesskey="a" control="menulist"/> +<label accesskey="b" control="textbox"/> +<label accesskey="c" control="scale"/> + +<menulist id="menulist" editable="true"> + <menupopup> + <menuitem label="One"/> + </menupopup> +</menulist> +<textbox id="textbox"/> +<scale id="scale"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gBlurs = 0, gFocuses = 0; +var gExpectedBlur = ""; +var gExpectedFocus = ""; + +function blurOccurred(event) { + gBlurs++; + is(event.originalTarget, gExpectedBlur, "blur " + gBlurs + "," + event.originalTarget.localName); +} + +function focusOccurred(event) { + gFocuses++; + is(event.originalTarget, gExpectedFocus, "focus " + gFocuses + "," + event.originalTarget.localName); +} + +function runTests() +{ + addEventListener("focus", focusOccurred, true); + addEventListener("blur", blurOccurred, true); + + gExpectedBlur = null; + gExpectedFocus = $("menulist").inputField; + $("menulist").focus(); + + gExpectedBlur = gExpectedFocus; + gExpectedFocus = $("textbox").inputField; + $("textbox").focus(); + + gExpectedBlur = gExpectedFocus; + gExpectedFocus = document.getAnonymousNodes($("scale"))[0]; + $("scale").focus(); + + var accessKeyDetails = (navigator.platform.indexOf("Mac") >= 0) ? + { altKey: true, ctrlKey : true } : + { altKey : true, shiftKey: true }; + + gExpectedBlur = document.getAnonymousNodes($("scale"))[0]; + gExpectedFocus = $("menulist").inputField; + synthesizeKey("a", accessKeyDetails); + + gExpectedBlur = gExpectedFocus; + gExpectedFocus = $("textbox").inputField; + synthesizeKey("b", accessKeyDetails); + + gExpectedBlur = gExpectedFocus; + gExpectedFocus = document.getAnonymousNodes($("scale"))[0]; + synthesizeKey("c", accessKeyDetails); + + if (navigator.platform.indexOf("Mac") == -1) { + gExpectedBlur = gExpectedFocus; + gExpectedFocus = $("textbox").inputField; + synthesizeKey("VK_TAB", { shiftKey: true }); + + gExpectedBlur = gExpectedFocus; + gExpectedFocus = $("menulist").inputField; + synthesizeKey("VK_TAB", { shiftKey: true }); + + gExpectedBlur = gExpectedFocus; + gExpectedFocus = $("textbox").inputField; + synthesizeKey("VK_TAB", { }); + + gExpectedBlur = gExpectedFocus; + gExpectedFocus = document.getAnonymousNodes($("scale"))[0]; + synthesizeKey("VK_TAB", { }); + + is(gBlurs, 9, "correct number of blurs"); + is(gFocuses, 10, "correct number of focuses"); + } + else { + is(gBlurs, 5, "correct number of blurs"); + is(gFocuses, 6, "correct number of focuses"); + } + + removeEventListener("focus", focusOccurred, true); + removeEventListener("blur", blurOccurred, true); + + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_hiddenitems.xul b/toolkit/content/tests/chrome/test_hiddenitems.xul new file mode 100644 index 0000000000..7e44852df9 --- /dev/null +++ b/toolkit/content/tests/chrome/test_hiddenitems.xul @@ -0,0 +1,89 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=317422 +--> +<window title="Mozilla Bug 317422" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=317422" + target="_blank">Mozilla Bug 317422</a> + </body> + + <richlistbox id="richlistbox" seltype="multiple"> + <richlistitem id="richlistbox_item1"><label value="Item 1"/></richlistitem> + <richlistitem id="richlistbox_item2"><label value="Item 2"/></richlistitem> + <richlistitem id="richlistbox_item3" hidden="true"><label value="Item 3"/></richlistitem> + <richlistitem id="richlistbox_item4"><label value="Item 4"/></richlistitem> + <richlistitem id="richlistbox_item5" collapsed="true"><label value="Item 5"/></richlistitem> + <richlistitem id="richlistbox_item6"><label value="Item 6"/></richlistitem> + <richlistitem id="richlistbox_item7" hidden="true"><label value="Item 7"/></richlistitem> + </richlistbox> + + <listbox id="listbox" seltype="multiple"> + <listitem id="listbox_item1" label="Item 1"/> + <listitem id="listbox_item2" label="Item 2"/> + <listitem id="listbox_item3" label="Item 3" hidden="true"/> + <listitem id="listbox_item4" label="Item 4"/> + <listitem id="listbox_item5" label="Item 5" collapsed="true"/> + <listitem id="listbox_item6" label="Item 6"/> + <listitem id="listbox_item7" label="Item 7" hidden="true"/> + </listbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 317422 **/ +SimpleTest.waitForExplicitFinish(); + +function testListbox(id) +{ + var listbox = document.getElementById(id); + listbox.focus(); + is(listbox.getRowCount(), 7, id + ": Returned the wrong number of rows"); + is(listbox.getItemAtIndex(2).id, id + "_item3", id + ": Should still return hidden items"); + listbox.selectedIndex = 0; + is(listbox.selectedItem.id, id + "_item1", id + ": First item was not selected"); + sendKey("DOWN"); + is(listbox.selectedItem.id, id + "_item2", id + ": Down didn't move to second item"); + sendKey("DOWN"); + is(listbox.selectedItem.id, id + "_item4", id + ": Down didn't skip hidden item"); + sendKey("DOWN"); + is(listbox.selectedItem.id, id + "_item6", id + ": Down didn't skip collapsed item"); + sendKey("UP"); + is(listbox.selectedItem.id, id + "_item4", id + ": Up didn't skip collapsed item"); + sendKey("UP"); + is(listbox.selectedItem.id, id + "_item2", id + ": Up didn't skip hidden item"); + listbox.selectAll(); + is(listbox.selectedItems.length, 7, id + ": Should have still selected all items"); + listbox.invertSelection(); + is(listbox.selectedItems.length, 0, id + ": Should have unselected all items"); + listbox.selectedIndex = 2; + ok(listbox.selectedItem == listbox.getItemAtIndex(2), id + ": Should have selected the hidden item"); + listbox.selectedIndex = 0; + sendKey("END"); + is(listbox.selectedItem.id, id + "_item6", id + ": Should have moved to the last unhidden item"); + sendMouseEvent({type: 'click'}, id + "_item1"); + ok(listbox.selectedItem == listbox.getItemAtIndex(0), id + ": Should have selected the first item"); + is(listbox.selectedItems.length, 1, id + ": Should only be one selected item"); + sendMouseEvent({type: 'click', shiftKey: true}, id + "_item6"); + is(listbox.selectedItems.length, 4, id + ": Should have selected all visible items"); + listbox.selectedIndex = 0; + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item6", id + ": Page down should go to the last visible item"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item1", id + ": Page up should go to the first visible item"); +} + +window.onload = function runTests() { + testListbox("richlistbox"); + testListbox("listbox"); + SimpleTest.finish(); +}; + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_hiddenpaging.xul b/toolkit/content/tests/chrome/test_hiddenpaging.xul new file mode 100644 index 0000000000..37b1097183 --- /dev/null +++ b/toolkit/content/tests/chrome/test_hiddenpaging.xul @@ -0,0 +1,161 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=317422 +--> +<window title="Mozilla Bug 317422" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <style xmlns="http://www.w3.org/1999/xhtml"> + /* This makes the richlistbox about 4.5 rows high */ + richlistitem { + height: 30px; + } + richlistbox { + height: 135px; + } + </style> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=317422" + target="_blank">Mozilla Bug 317422</a> + </body> + + <richlistbox id="richlistbox" seltype="multiple"> + <richlistitem id="richlistbox_item1"><label value="Item 1"/></richlistitem> + <richlistitem id="richlistbox_item2"><label value="Item 2"/></richlistitem> + <richlistitem id="richlistbox_item3" hidden="true"><label value="Item 3"/></richlistitem> + <richlistitem id="richlistbox_item4"><label value="Item 4"/></richlistitem> + <richlistitem id="richlistbox_item5" collapsed="true"><label value="Item 5"/></richlistitem> + <richlistitem id="richlistbox_item6"><label value="Item 6"/></richlistitem> + <richlistitem id="richlistbox_item7"><label value="Item 7"/></richlistitem> + <richlistitem id="richlistbox_item8"><label value="Item 8"/></richlistitem> + <richlistitem id="richlistbox_item9"><label value="Item 9"/></richlistitem> + <richlistitem id="richlistbox_item10"><label value="Item 10"/></richlistitem> + <richlistitem id="richlistbox_item11"><label value="Item 11"/></richlistitem> + <richlistitem id="richlistbox_item12"><label value="Item 12"/></richlistitem> + <richlistitem id="richlistbox_item13"><label value="Item 13"/></richlistitem> + <richlistitem id="richlistbox_item14"><label value="Item 14"/></richlistitem> + <richlistitem id="richlistbox_item15" hidden="true"><label value="Item 15"/></richlistitem> + </richlistbox> + + <listbox id="listbox" seltype="multiple" rows="5"> + <listitem id="listbox_item1" label="Item 1"/> + <listitem id="listbox_item2" label="Item 2"/> + <listitem id="listbox_item3" label="Item 3" hidden="true"/> + <listitem id="listbox_item4" label="Item 4"/> + <listitem id="listbox_item5" label="Item 5" hidden="true"/> + <listitem id="listbox_item6" label="Item 6"/> + <listitem id="listbox_item7" label="Item 7"/> + <listitem id="listbox_item8" label="Item 8"/> + <listitem id="listbox_item9" label="Item 9"/> + <listitem id="listbox_item10" label="Item 10"/> + <listitem id="listbox_item11" label="Item 11"/> + <listitem id="listbox_item12" label="Item 12"/> + <listitem id="listbox_item13" label="Item 13"/> + <listitem id="listbox_item14" label="Item 14"/> + <listitem id="listbox_item15" label="Item 15" hidden="true"/> + </listbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 317422 **/ +SimpleTest.waitForExplicitFinish(); + +function testRichlistbox() +{ + var id = "richlistbox"; + var listbox = document.getElementById(id); + listbox.focus(); + listbox.selectedIndex = 0; + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item7", id + ": Page down should go to the item one visible page away"); + is(listbox.getIndexOfFirstVisibleRow(), 6, id + ": Page down should have scrolled down a visible page"); + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item11", id + ": Second page down should go to the item two visible pages away"); + is(listbox.getIndexOfFirstVisibleRow(), 9, id + ": Second page down should not scroll beyond the end"); + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item14", id + ": Third page down should go to the last visible item"); + is(listbox.getIndexOfFirstVisibleRow(), 9, id + ": Third page down should not have scrolled at all"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item10", id + ": Page up should go to the item one visible page away"); + is(listbox.getIndexOfFirstVisibleRow(), 5, id + ": Page up should scroll up a visible page"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item6", id + ": Second page up should go to the item two visible pages away"); + is(listbox.getIndexOfFirstVisibleRow(), 0, id + ": Second page up should not scroll beyond the start"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item1", id + ": Third page up should return to the first visible item"); + is(listbox.getIndexOfFirstVisibleRow(), 0, id + ": Third page up should not have scrolled at all"); +} + +function testListbox() +{ + var id = "listbox"; + var listbox = document.getElementById(id); + + if (!window.matchMedia("(-moz-overlay-scrollbars)").matches) { + // Check that a scrollbar is visible by comparing the width of the listitem + // with the width of the listbox. This is a simple way to do this without + // checking the anonymous content. + ok(listbox.firstChild.getBoundingClientRect().width < listbox.getBoundingClientRect().width - 10, + id + ": Scrollbar visible"); + } + + var rowHeight = listbox.firstChild.getBoundingClientRect().height; + + listbox.focus(); + listbox.selectedIndex = 0; + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item8", id + ": Page down should go to the item one visible page away"); + is(listbox.getIndexOfFirstVisibleRow(), 7, id + ": Page down should have scrolled down a visible page"); + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item13", id + ": Second page down should go to the item two visible pages away"); + is(listbox.getIndexOfFirstVisibleRow(), 9, id + ": Second page down should not scroll beyond the end"); + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item14", id + ": Third page down should go to the last visible item"); + is(listbox.getIndexOfFirstVisibleRow(), 9, id + ": Third page down should not have scrolled at all"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item9", id + ": Page up should go to the item one visible page away"); + // the listScrollbox seems to go haywire when scrolling up with hidden listitems + todo_is(listbox.getIndexOfFirstVisibleRow(), 3, id + ": Page up should scroll up a visible page"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item2", id + ": Second page up should go to the item two visible pages away"); + is(listbox.getIndexOfFirstVisibleRow(), 0, id + ": Second page up should not scroll beyond the start"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item1", id + ": Third page up should return to the first visible item"); + is(listbox.getIndexOfFirstVisibleRow(), 0, id + ": Third page up should not have scrolled at all"); + + var scrollHeight = document.getAnonymousNodes(listbox)[1].lastChild.scrollHeight; + is(scrollHeight, rowHeight * 15, id + ": scrollHeight when rows set"); + + listbox.minHeight = 50; + scrollHeight = document.getAnonymousNodes(listbox)[1].lastChild.scrollHeight; + is(scrollHeight, rowHeight * 15, id + ": scrollHeight when rows and minimium height set"); + + listbox.removeAttribute("rows"); + + var availHeight = document.getAnonymousNodes(listbox)[1].lastChild.getBoundingClientRect().height; + // The listbox layout adds this extra height in GetPrefSize. Not sure what it's for though. + var e = (rowHeight * 15 - availHeight) % rowHeight; + var extraHeight = (e == 0) ? 0 : rowHeight - e; + + scrollHeight = document.getAnonymousNodes(listbox)[1].lastChild.scrollHeight; + is(scrollHeight, rowHeight * 15 + extraHeight, id + ": scrollHeight when minimium height set"); + + listbox.removeAttribute("minheight"); + scrollHeight = document.getAnonymousNodes(listbox)[1].lastChild.scrollHeight; + is(scrollHeight, rowHeight * 15 + extraHeight, id + ": scrollHeight"); +} + +window.onload = function runTests() { + testRichlistbox(); + testListbox(); + SimpleTest.finish(); +}; + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_keys.xul b/toolkit/content/tests/chrome/test_keys.xul new file mode 100644 index 0000000000..97cc8a2413 --- /dev/null +++ b/toolkit/content/tests/chrome/test_keys.xul @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Keys Test" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_keys.xul", "_blank", "chrome,width=200,height=200"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_labelcontrol.xul b/toolkit/content/tests/chrome/test_labelcontrol.xul new file mode 100644 index 0000000000..b33667be05 --- /dev/null +++ b/toolkit/content/tests/chrome/test_labelcontrol.xul @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for label control="value" + --> +<window title="tabindex" width="500" height="600" + onload="runTests()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<label id="lab" control="ctl"/> +<textbox id="ctl" value="Test"/> +<checkbox id="chk" value="Checkbox"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + is($("lab").control, "ctl", "control"); + is($("lab").labeledControlElement, $("ctl"), "labeledControlElement"); + is($("ctl").labelElement, $("lab"), "labelElement"); + is($("chk").labelElement.className, "checkbox-label", "labelElement"); + + SimpleTest.finish(); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_largemenu.xul b/toolkit/content/tests/chrome/test_largemenu.xul new file mode 100644 index 0000000000..8841e2b16a --- /dev/null +++ b/toolkit/content/tests/chrome/test_largemenu.xul @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Large Menu Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_largemenu.xul", "_blank", "chrome,width=200,height=200"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menu.xul b/toolkit/content/tests/chrome/test_menu.xul new file mode 100644 index 0000000000..877efadb1a --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu.xul @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Destruction Test" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + + <menubar> + <menu label="top" id="top"> + <menupopup> + <menuitem label="top item"/> + + <menu label="hello" id="nested"> + <menupopup> + <menuitem label="item1"/> + <menuitem label="item2" id="item2"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menubar> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function runTests() + { + var menu = document.getElementById("nested"); + + // nsIDOMXULContainerElement::getIndexOfItem(); + var item = document.getElementById("item2"); + is(menu.getIndexOfItem(item), 1, + "nsIDOMXULContainerElement::getIndexOfItem() failed."); + + // nsIDOMXULContainerElement::getItemAtIndex(); + var itemAtIdx = menu.getItemAtIndex(1); + is(itemAtIdx, item, + "nsIDOMXULContainerElement::getItemAtIndex() failed."); + + // nsIDOMXULContainerElement::itemCount + is(menu.itemCount, 2, "nsIDOMXULContainerElement::itemCount failed."); + + // nsIDOMXULContainerElement::parentContainer + var topmenu = document.getElementById("top"); + is(menu.parentContainer, topmenu, + "nsIDOMXULContainerElement::parentContainer failed."); + + // nsIDOMXULContainerElement::appendItem(); + var item = menu.appendItem("item3"); + is(menu.getIndexOfItem(item), 2, + "nsIDOMXULContainerElement::appendItem() failed."); + + // nsIDOMXULContainerElement::insertItemAt(); + var item = menu.insertItemAt(0, "itemZero"); + is(item, menu.getItemAtIndex(0), + "nsIDOMXULContainerElement::insertItemAt() failed."); + + // nsIDOMXULContainerElement::removeItemAt(); + var item = menu.removeItemAt(0); + is(3, menu.itemCount, + "nsIDOMXULContainerElement::removeItemAt() failed."); + + SimpleTest.finish(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + +</window> + diff --git a/toolkit/content/tests/chrome/test_menu_anchored.xul b/toolkit/content/tests/chrome/test_menu_anchored.xul new file mode 100644 index 0000000000..8fdda3f25f --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu_anchored.xul @@ -0,0 +1,77 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + Test for menus with the anchor attribute set + --> +<window title="Anchored Menus Test" + align="start" + onload="setTimeout(runTest, 0,'tb1');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + <script type="application/javascript" src="xul_selectcontrol.js"/> + +<hbox> + +<toolbarbutton id="tb1" type="menu-button" label="Open" anchor="dropmarker"> + <menupopup id="popup1" + onpopupshown="checkPopup(this, document.getAnonymousElementByAttribute(this.parentNode, 'anonid', 'dropmarker'))" + onpopuphidden="runTest('tb2')"> + <menuitem label="Item"/> + </menupopup> +</toolbarbutton> + +<toolbarbutton id="tb2" type="menu-button" label="Open" anchor="someanchor"> + <menupopup id="popup2" onpopupshown="checkPopup(this, $('someanchor'))" onpopuphidden="runTest('tb3')"> + <menuitem label="Item"/> + </menupopup> +</toolbarbutton> + +<toolbarbutton id="tb3" type="menu-button" label="Open" anchor="noexist"> + <menupopup id="popup3" onpopupshown="checkPopup(this, this.parentNode)" onpopuphidden="SimpleTest.finish()"> + <menuitem label="Item"/> + </menupopup> +</toolbarbutton> + +</hbox> + +<hbox pack="end" width="180"> + <button id="someanchor" label="Anchor"/> +</hbox> + +<!-- test results are displayed in the html:body --> +<body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"><![CDATA[ + +function runTest(menuid) +{ + let menu = $(menuid); + let dropmarker = document.getAnonymousElementByAttribute(menu, "anonid", "dropmarker"); + + synthesizeMouseAtCenter(dropmarker, { }); +} + +function isWithinHalfPixel(a, b) +{ + return Math.abs(a - b) <= 0.5; +} + +function checkPopup(popup, anchor) +{ + let popupRect = popup.getBoundingClientRect(); + let anchorRect = anchor.getBoundingClientRect(); + + ok(isWithinHalfPixel(popupRect.left, anchorRect.left), popup.id + " left"); + ok(isWithinHalfPixel(popupRect.top, anchorRect.bottom), popup.id + " top"); + + popup.hidePopup(); +} + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_menu_hide.xul b/toolkit/content/tests/chrome/test_menu_hide.xul new file mode 100644 index 0000000000..b5ce934db0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu_hide.xul @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Destruction Test" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<menu id="menu"> + <menupopup onpopupshown="this.firstChild.open = true" onpopuphidden="if (event.target == this) done()"> + <menu id="submenu" label="One"> + <menupopup onpopupshown="submenuOpened();"> + <menuitem label="Two"/> + </menupopup> + </menu> + </menupopup> +</menu> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + $("menu").open = true; +} + +function submenuOpened() +{ + var submenu = $("submenu") + is(submenu.getAttribute('_moz-menuactive'), "true", "menu highlighted"); + submenu.hidden = true; + $("menu").open = false; +} + +function done() +{ + ok(!$("submenu").hasAttribute('_moz-menuactive'), "menu unhighlighted"); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menuchecks.xul b/toolkit/content/tests/chrome/test_menuchecks.xul new file mode 100644 index 0000000000..8128b738ca --- /dev/null +++ b/toolkit/content/tests/chrome/test_menuchecks.xul @@ -0,0 +1,147 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Checkbox and Radio Tests" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + + <hbox> + <button id="menu" type="menu" label="View"> + <menupopup id="popup" onpopupshown="popupShown()" onpopuphidden="popupHidden()"> + <menuitem id="toolbar" label="Show Toolbar" type="checkbox"/> + <menuitem id="statusbar" label="Show Status Bar" type="checkbox" checked="true"/> + <menuitem id="bookmarks" label="Show Bookmarks" type="checkbox" autocheck="false"/> + <menuitem id="history" label="Show History" type="checkbox" autocheck="false" checked="true"/> + <menuseparator/> + <menuitem id="byname" label="By Name" type="radio" name="sort"/> + <menuitem id="bydate" label="By Date" type="radio" name="sort" checked="true"/> + <menuseparator/> + <menuitem id="ascending" label="Ascending" type="radio" name="order" checked="true"/> + <menuitem id="descending" label="Descending" type="radio" name="order" autocheck="false"/> + <menuitem id="bysubject" label="By Subject" type="radio" name="sort"/> + </menupopup> + </button> + + </hbox> + + <!-- + This test checks that checkbox and radio menu items work properly + --> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + var gTestIndex = 0; + + // tests to perform + var tests = [ + { + testname: "select unchecked checkbox", + item: "toolbar", + checked: ["toolbar", "statusbar", "history", "bydate", "ascending"] + }, + { + testname: "select checked checkbox", + item: "statusbar", + checked: ["toolbar", "history", "bydate", "ascending"] + }, + { + testname: "select unchecked autocheck checkbox", + item: "bookmarks", + checked: ["toolbar", "history", "bydate", "ascending"] + }, + { + testname: "select checked autocheck checkbox", + item: "history", + checked: ["toolbar", "history", "bydate", "ascending"] + }, + { + testname: "select unchecked radio", + item: "byname", + checked: ["toolbar", "history", "byname", "ascending"] + }, + { + testname: "select checked radio", + item: "byname", + checked: ["toolbar", "history", "byname", "ascending"] + }, + { + testname: "select out of order checked radio", + item: "bysubject", + checked: ["toolbar", "history", "bysubject", "ascending"] + }, + { + testname: "select first radio again", + item: "byname", + checked: ["toolbar", "history", "byname", "ascending"] + }, + { + testname: "select autocheck radio", + item: "descending", + checked: ["toolbar", "history", "byname", "ascending"] + } + ]; + + function runTest() + { + checkMenus(["statusbar", "history", "bydate", "ascending"], "initial"); + document.getElementById("menu").open = true; + } + + function checkMenus(checkedItems, testname) + { + var isok = true; + var children = document.getElementById("popup").childNodes; + for (var c = 0; c < children.length; c++) { + var child = children[c]; + if ((checkedItems.indexOf(child.id) != -1 && child.getAttribute("checked") != "true") || + (checkedItems.indexOf(child.id) == -1 && child.hasAttribute("checked"))) { + isok = false; + break; + } + } + + ok(isok, testname); + } + + function popupShown() + { + var test = tests[gTestIndex]; + synthesizeMouse(document.getElementById(test.item), 4, 4, { }); + } + + function popupHidden() + { + if (gTestIndex < tests.length) { + var test = tests[gTestIndex]; + checkMenus(test.checked, test.testname); + gTestIndex++; + if (gTestIndex < tests.length) { + document.getElementById("menu").open = true; + } + else { + // manually setting the checkbox should also update the radio state + document.getElementById("bydate").setAttribute("checked", "true"); + checkMenus(["toolbar", "history", "bydate", "ascending"], "set checked attribute on radio"); + SimpleTest.finish(); + } + } + } + + ]]> + </script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menuitem_blink.xul b/toolkit/content/tests/chrome/test_menuitem_blink.xul new file mode 100644 index 0000000000..319c284fd7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menuitem_blink.xul @@ -0,0 +1,106 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Blinking Context Menu Item Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + + <menulist id="menulist"> + <menupopup id="menupopup"> + <menuitem label="Menu Item" id="menuitem"/> + </menupopup> + </menulist> +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(startTest); + +function startTest() { + if (!/Mac/.test(navigator.platform)) { + ok(true, "Nothing to test on non-Mac."); + SimpleTest.finish(); + return; + } + // Destroy frame while removing the _moz-menuactive attribute. + test_crash("REMOVAL", test2); +} + +function test2() { + // Destroy frame while adding the _moz-menuactive attribute. + test_crash("ADDITION", test3); +} + +function test3() { + // Don't mess with the frame, just test whether we've blinked. + test_crash("", SimpleTest.finish); +} + +function test_crash(when, andThen) { + var menupopup = document.getElementById("menupopup"); + var menuitem = document.getElementById("menuitem"); + var attrChanges = { "REMOVAL": 0, "ADDITION": 0 }; + var storedEvent = null; + menupopup.addEventListener("popupshown", function () { + menupopup.removeEventListener("popupshown", arguments.callee, false); + menuitem.addEventListener("mouseup", function (e) { + menuitem.removeEventListener("mouseup", arguments.callee, true); + menuitem.addEventListener("DOMAttrModified", function (e) { + if (e.attrName == "_moz-menuactive") { + if (!attrChanges[e.attrChange]) + attrChanges[e.attrChange] = 1; + else + attrChanges[e.attrChange]++; + storedEvent = e; + if (e.attrChange == e[when]) { + menuitem.hidden = true; + menuitem.getBoundingClientRect(); + ok(true, "Didn't crash on _moz-menuactive " + when.toLowerCase() + " during blinking") + menuitem.hidden = false; + menuitem.removeEventListener("DOMAttrModified", arguments.callee, false); + SimpleTest.executeSoon(function () { + menupopup.hidePopup(); + }); + } + } + }, false); + }, true); + menupopup.addEventListener("popuphidden", function() { + menupopup.removeEventListener("popuphidden", arguments.callee, false); + if (!when) { + // Test whether we've blinked at all. + var shouldBlink = navigator.platform.match(/Mac/); + var expectedNumRemoval = shouldBlink ? 2 : 1; + var expectedNumAddition = shouldBlink ? 1 : 0; + ok(storedEvent, "got DOMAttrModified events after clicking menuitem") + is(attrChanges[storedEvent.REMOVAL], expectedNumRemoval, "blinking unset attributes correctly"); + is(attrChanges[storedEvent.ADDITION], expectedNumAddition, "blinking set attributes correctly"); + } + SimpleTest.executeSoon(andThen); + }, false); + synthesizeMouse(menuitem, 10, 5, { type : "mousemove" }); + synthesizeMouse(menuitem, 10, 5, { type : "mousemove" }); + synthesizeMouse(menuitem, 10, 5, { type : "mousedown" }); + SimpleTest.executeSoon(function () { + synthesizeMouse(menuitem, 10, 5, { type : "mouseup" }); + }); + }, false); + document.getElementById("menulist").open = true; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menuitem_commands.xul b/toolkit/content/tests/chrome/test_menuitem_commands.xul new file mode 100644 index 0000000000..e31774ccc3 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menuitem_commands.xul @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menuitem Commands Test" + onload="runOrOpen()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +function checkAttributes(elem, label, accesskey, disabled, hidden, isAfter) +{ + var is = window.opener.wrappedJSObject.SimpleTest.is; + + is(elem.getAttribute("label"), label, elem.id + " label " + (isAfter ? "after" : "before") + " open"); + is(elem.getAttribute("accesskey"), accesskey, elem.id + " accesskey " + (isAfter ? "after" : "before") + " open"); + is(elem.getAttribute("disabled"), disabled, elem.id + " disabled " + (isAfter ? "after" : "before") + " open"); + is(elem.getAttribute("hidden"), hidden, elem.id + " hidden " + (isAfter ? "after" : "before") + " open"); +} + +function runOrOpen() +{ + if (window.opener) { + SimpleTest.waitForFocus(runTest); + } + else { + window.open("test_menuitem_commands.xul", "", "chrome"); + } +} + +function runTest() +{ + runTestSet(""); + runTestSet("bar"); + window.close(); + window.opener.wrappedJSObject.SimpleTest.finish(); +} + +function runTestSet(suffix) +{ + var isMac = (navigator.platform.indexOf("Mac") >= 0); + + var one = $("one" + suffix); + var two = $("two" + suffix); + var three = $("three" + suffix); + var four = $("four" + suffix); + + checkAttributes(one, "One", "", "", "true", false); + checkAttributes(two, "", "", "false", "", false); + checkAttributes(three, "Three", "T", "true", "", false); + checkAttributes(four, "Four", "F", "", "", false); + + if (isMac && suffix) { + var utils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindowUtils); + utils.forceUpdateNativeMenuAt("0"); + } + else { + $("menu" + suffix).open = true; + } + + checkAttributes(one, "One", "", "", "false", true); + checkAttributes(two, "Cat", "C", "", "", true); + checkAttributes(three, "Dog", "D", "false", "true", true); + checkAttributes(four, "Four", "F", "true", "", true); + + $("menu" + suffix).open = false; +} +]]> +</script> + +<command id="cmd_one" hidden="false"/> +<command id="cmd_two" label="Cat" accesskey="C"/> +<command id="cmd_three" label="Dog" accesskey="D" disabled="false" hidden="true"/> +<command id="cmd_four" disabled="true"/> + +<button id="menu" type="menu"> + <menupopup> + <menuitem id="one" label="One" hidden="true" command="cmd_one"/> + <menuitem id="two" disabled="false" command="cmd_two"/> + <menuitem id="three" label="Three" accesskey="T" disabled="true" command="cmd_three"/> + <menuitem id="four" label="Four" accesskey="F" command="cmd_four"/> + </menupopup> +</button> + +<menubar> + <menu id="menubar" label="Sample"> + <menupopup> + <menuitem id="onebar" label="One" hidden="true" command="cmd_one"/> + <menuitem id="twobar" disabled="false" command="cmd_two"/> + <menuitem id="threebar" label="Three" accesskey="T" disabled="true" command="cmd_three"/> + <menuitem id="fourbar" label="Four" accesskey="F" command="cmd_four"/> + </menupopup> + </menu> +</menubar> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist.xul b/toolkit/content/tests/chrome/test_menulist.xul new file mode 100644 index 0000000000..4e3817d89c --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist.xul @@ -0,0 +1,314 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist Tests" + onload="setTimeout(testtag_menulists, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + <script type="application/javascript" src="xul_selectcontrol.js"></script> + +<vbox id="scroller" style="overflow: auto" height="60"> + <menulist id="menulist" onpopupshown="test_menulist_open(this, this.parentNode)" + onpopuphidden="$('menulist-in-listbox').open = true;"> + <menupopup id="menulist-popup"/> + </menulist> + <button label="Two"/> + <button label="Three"/> +</vbox> +<listbox id="scroller-in-listbox" style="overflow: auto" height="60"> + <listitem allowevents="true"> + <menulist id="menulist-in-listbox" onpopupshown="test_menulist_open(this, this.parentNode.parentNode)" + onpopuphidden="SimpleTest.executeSoon(checkScrollAndFinish)"> + <menupopup id="menulist-in-listbox-popup"> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + </menupopup> + </menulist> + </listitem> + <listitem label="Two"/> + <listitem label="Three"/> + <listitem label="Four"/> + <listitem label="Five"/> + <listitem label="Six"/> +</listbox> + +<hbox> + <menulist id="menulist-size"> + <menupopup> + <menuitem label="Menuitem Label" width="200"/> + </menupopup> + </menulist> +</hbox> + +<menulist id="menulist-editable" editable="true"> + <menupopup id="menulist-popup-editable"/> +</menulist> + +<menulist id="menulist-initwithvalue" value="two"> + <menupopup> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three"/> + </menupopup> +</menulist> +<menulist id="menulist-initwithselected" value="two"> + <menupopup> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three" selected="true"/> + </menupopup> +</menulist> +<menulist id="menulist-editable-initwithvalue" editable="true" value="Two"> + <menupopup> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three"/> + </menupopup> +</menulist> +<menulist id="menulist-editable-initwithselected" editable="true" value="two"> + <menupopup> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three" selected="true"/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function testtag_menulists() +{ + testtag_menulist_UI_start($("menulist"), false); +} + +function testtag_menulist_UI_start(element, editable) +{ + var testprefix = editable ? "editable" : ""; + + // check the menupopup property + var popup = element.menupopup; + ok(popup && popup.localName == "menupopup" && + popup.parentNode == element, testprefix + " menupopup"); + + // test the interfaces that menulist implements + test_nsIDOMXULMenuListElement(element, testprefix, editable); +} + +function testtag_menulist_UI_finish(element, editable) +{ + element.value = ""; + + test_nsIDOMXULSelectControlElement(element, "menuitem", + editable ? "editable" : null); + + if (!editable) { + testtag_menulist_UI_start($("menulist-editable"), true); + } + else { + // bug 566154, the menulist width should account for vertical scrollbar + ok(document.getElementById("menulist-size").getBoundingClientRect().width >= 210, + "menulist popup width includes scrollbar width"); + + $("menulist").open = true; + } +} + +function test_nsIDOMXULMenuListElement(element, testprefix, editable) +{ + is(element.open, false, testprefix + " open"); + is(element.editable, editable, testprefix + " editable"); + + if (editable) { + var inputField = element.inputField; + is(inputField && + inputField instanceof Components.interfaces.nsIDOMHTMLInputElement, + true, testprefix + " inputField"); + + // check if the select method works + inputField.select(); + is(inputField.selectionStart, 0, testprefix + " empty select selectionStart"); + is(inputField.selectionEnd, 0, testprefix + " empty select selectionEnd"); + + element.value = "Some Text"; + inputField.select(); + is(inputField.selectionStart, 0, testprefix + " empty select selectionStart"); + is(inputField.selectionEnd, 9, testprefix + " empty select selectionEnd"); + } + else { + is(element.inputField, null , testprefix + " inputField"); + } + + element.appendItem("Item One", "one"); + var seconditem = element.appendItem("Item Two", "two"); + var thirditem = element.appendItem("Item Three", "three"); + element.appendItem("Item Four", "four"); + + seconditem.image = "happy.png"; + seconditem.setAttribute("description", "This is the second description"); + thirditem.image = "happy.png"; + thirditem.setAttribute("description", "This is the third description"); + + // check the image and description properties + // editable menulists don't use the image or description properties currently + if (editable) { + element.selectedIndex = 1; + is(element.image, "", testprefix + " image set to selected"); + is(element.description, "", testprefix + " description set to selected"); + test_nsIDOMXULMenuListElement_finish(element, testprefix, editable); + } + else { + element.selectedIndex = 1; + is(element.image, "happy.png", testprefix + " image set to selected"); + is(element.description, "This is the second description", testprefix + " description set to selected"); + element.selectedIndex = -1; + is(element.image, "", testprefix + " image set when none selected"); + is(element.description, "", testprefix + " description set when none selected"); + element.selectedIndex = 2; + is(element.image, "happy.png", testprefix + " image set to selected again"); + is(element.description, "This is the third description", testprefix + " description set to selected again"); + + // check that changing the properties of the selected item changes the menulist's properties + let properties = [{attr: "label", value: "Item Number Three"}, + {attr: "value", value: "item-three"}, + {attr: "image", value: "smile.png"}, + {attr: "description", value: "Changed description"}]; + test_nsIDOMXULMenuListElement_properties(element, testprefix, editable, thirditem, properties); + } +} + +function test_nsIDOMXULMenuListElement_properties(element, testprefix, editable, thirditem, properties) +{ + let {attr, value} = properties.shift(); + let last = (properties.length == 0); + + let mutObserver = new MutationObserver(() => { + is(element.getAttribute(attr), value, `${testprefix} ${attr} modified`); + done(); + }); + mutObserver.observe(element, { attributeFilter: [attr] }); + + let failureTimeout = setTimeout(() => { + ok(false, `${testprefix} ${attr} should have updated`); + done(); + }, 2000); + + function done() + { + clearTimeout(failureTimeout); + mutObserver.disconnect(); + if (!last) { + test_nsIDOMXULMenuListElement_properties(element, testprefix, editable, thirditem, properties); + } + else { + test_nsIDOMXULMenuListElement_unselected(element, testprefix, editable, thirditem); + } + } + + thirditem.setAttribute(attr, value) +} + +function test_nsIDOMXULMenuListElement_unselected(element, testprefix, editable, thirditem) +{ + let seconditem = thirditem.previousElementSibling; + seconditem.label = "Changed Label 2"; + is(element.label, "Item Number Three", testprefix + " label of another item modified"); + + element.selectedIndex = 0; + is(element.image, "", testprefix + " image set to selected with no image"); + is(element.description, "", testprefix + " description set to selected with no description"); + test_nsIDOMXULMenuListElement_finish(element, testprefix, editable); +} + +function test_nsIDOMXULMenuListElement_finish(element, testprefix, editable) +{ + // check the removeAllItems method + element.appendItem("An Item", "anitem"); + element.appendItem("Another Item", "anotheritem"); + element.removeAllItems(); + is(element.itemCount, 0, testprefix + " removeAllItems"); + + testtag_menulist_UI_finish(element, editable); +} + +function test_menulist_open(element, scroller) +{ + element.appendItem("Scroll Item 1", "scrollitem1"); + element.appendItem("Scroll Item 2", "scrollitem2"); + element.focus(); + element.selectedIndex = 0; + +/* + // bug 530504, mousewheel while menulist is open should not scroll menulist + // items or parent + var scrolled = false; + var mouseScrolled = function (event) { scrolled = true; } + window.addEventListener("DOMMouseScroll", mouseScrolled, false); + synthesizeWheel(element, 2, 2, { deltaY: 10, + deltaMode: WheelEvent.DOM_DELTA_LINE }); + is(scrolled, true, "mousescroll " + element.id); + is(scroller.scrollTop, 0, "scroll position on mousescroll " + element.id); + window.removeEventListener("DOMMouseScroll", mouseScrolled, false); +*/ + + // bug 543065, hovering the mouse over an item should highlight it, not + // scroll the parent, and not change the selected index. + var item = element.menupopup.childNodes[1]; + + synthesizeMouse(element.menupopup.childNodes[1], 2, 2, { type: "mousemove" }); + synthesizeMouse(element.menupopup.childNodes[1], 6, 6, { type: "mousemove" }); + is(element.menuBoxObject.activeChild, item, "activeChild after menu highlight " + element.id); + is(element.selectedIndex, 0, "selectedIndex after menu highlight " + element.id); + is(scroller.scrollTop, 0, "scroll position after menu highlight " + element.id); + + element.open = false; +} + +function checkScrollAndFinish() +{ + is($("scroller").scrollTop, 0, "mousewheel on menulist does not scroll vbox parent"); + is($("scroller-in-listbox").scrollTop, 0, "mousewheel on menulist does not scroll listbox parent"); + + // bug 561243, outline causes the mouse click to be targeted incorrectly + var editableMenulist = $("menulist-editable"); + editableMenulist.className = "outlined"; + + synthesizeMouse(editableMenulist.inputField, 25, 8, { type: "mousedown" }); + synthesizeMouse(editableMenulist.inputField, 25, 8, { type: "mouseup" }); + isnot(editableMenulist.inputField.selectionStart, editableMenulist.inputField.textLength, + "mouse event on editable menulist with outline caret position"); + + let menulist = $("menulist-size"); + menulist.addEventListener("popupshown", function testAltClose() { + menulist.removeEventListener("popupshown", testAltClose); + + sendKey("ALT"); + is(menulist.menupopup.state, "open", "alt doesn't close menulist"); + menulist.open = false; + + SimpleTest.finish(); + }); + + menulist.open = true; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<style> +.outlined > .menulist-editable-box { outline: 1px solid black; } +</style> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_keynav.xul b/toolkit/content/tests/chrome/test_menulist_keynav.xul new file mode 100644 index 0000000000..c2e404c09a --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_keynav.xul @@ -0,0 +1,272 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist Key Navigation Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<button id="button1" label="One"/> +<menulist id="list"> + <menupopup id="popup" onpopupshowing="return gShowPopup;"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> + <menuitem id="i2b" disabled="true" label="Two and a Half"/> + <menuitem id="i3" label="Three"/> + <menuitem id="i4" label="Four"/> + </menupopup> +</menulist> +<button id="button2" label="Two"/> +<menulist id="list2"> + <menupopup id="popup" onpopupshown="checkCursorNavigation();"> + <menuitem id="b1" label="One"/> + <menuitem id="b2" label="Two" selected="true"/> + <menuitem id="b3" label="Three"/> + <menuitem id="b4" label="Four"/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gShowPopup = false; +var gModifiers = 0; +var gOpenPhase = false; + +var list = $("list"); +let expectCommandEvent; + +var iswin = (navigator.platform.indexOf("Win") == 0); +var ismac = (navigator.platform.indexOf("Mac") == 0); + +function runTests() +{ + list.focus(); + + // on Mac, up and cursor keys open the menu, but on other platforms, the + // cursor keys navigate between items without opening the menu + if (navigator.platform.indexOf("Mac") == -1) { + expectCommandEvent = true; + keyCheck(list, "VK_DOWN", 2, "cursor down"); + keyCheck(list, "VK_DOWN", 3, "cursor down skip disabled"); + keyCheck(list, "VK_UP", 2, "cursor up skip disabled"); + keyCheck(list, "VK_UP", 1, "cursor up"); + keyCheck(list, "VK_UP", 4, "cursor up wrap"); + keyCheck(list, "VK_DOWN", 1, "cursor down wrap"); + } + + // check that attempting to open the menulist does not change the selection + synthesizeKey("VK_DOWN", { altKey: navigator.platform.indexOf("Mac") == -1 }); + is(list.selectedItem, $("i1"), "open menulist down selectedItem"); + synthesizeKey("VK_UP", { altKey: navigator.platform.indexOf("Mac") == -1 }); + is(list.selectedItem, $("i1"), "open menulist up selectedItem"); + + list.selectedItem = $("i1"); + + pressLetter(); +} + +function pressLetter() +{ + // A command event should be fired only if the menulist is closed, or on Windows, + // where items are selected immediately. + expectCommandEvent = !gOpenPhase || iswin; + + synthesizeKey("G", { }); + is(list.selectedItem, $("i1"), "letter pressed not found selectedItem"); + + keyCheck(list, "T", 2, "letter pressed"); + + if (!gOpenPhase) { + SpecialPowers.setIntPref("ui.menu.incremental_search.timeout", 0); // prevent to timeout + keyCheck(list, "T", 2, "same letter pressed"); + SpecialPowers.clearUserPref("ui.menu.incremental_search.timeout"); + } + + setTimeout(pressedAgain, 1200); +} + +function pressedAgain() +{ + keyCheck(list, "T", 3, "letter pressed again"); + SpecialPowers.setIntPref("ui.menu.incremental_search.timeout", 0); // prevent to timeout + keyCheck(list, "W", 2, "second letter pressed"); + SpecialPowers.clearUserPref("ui.menu.incremental_search.timeout"); + setTimeout(differentPressed, 1200); +} + +function differentPressed() +{ + keyCheck(list, "O", 1, "different letter pressed"); + + if (gOpenPhase) { + list.open = false; + tabAndScroll(); + } + else { + // Run the letter tests again with the popup open + info("list open phase"); + + list.selectedItem = $("i1"); + + // Hide and show the list to avoid using any existing incremental key state. + list.hidden = true; + list.clientWidth; + list.hidden = false; + + gShowPopup = true; + gOpenPhase = true; + + list.addEventListener("popupshown", function popupShownListener() { + list.removeEventListener("popupshown", popupShownListener, false); + pressLetter(); + }, false); + + list.open = true; + } +} + +function tabAndScroll() +{ + list = $("list"); + + if (navigator.platform.indexOf("Mac") == -1) { + $("button1").focus(); + synthesizeKeyExpectEvent("VK_TAB", { }, list, "focus", "focus to menulist"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("button2"), "focus", "focus to button"); + is(document.activeElement, $("button2"), "tab from menulist focused button"); + } + + // now make sure that using a key scrolls the menu correctly + + for (let i = 0; i < 65; i++) { + list.appendItem("Item" + i, "item" + i); + } + list.open = true; + is(list.getBoundingClientRect().width, list.firstChild.getBoundingClientRect().width, + "menu and popup width match"); + var minScrollbarWidth = window.matchMedia("(-moz-overlay-scrollbars)").matches ? 0 : 3; + ok(list.getBoundingClientRect().width >= list.getItemAtIndex(0).getBoundingClientRect().width + minScrollbarWidth, + "menuitem width accounts for scrollbar"); + list.open = false; + + list.menupopup.maxHeight = 100; + list.open = true; + + var rowdiff = list.getItemAtIndex(1).getBoundingClientRect().top - + list.getItemAtIndex(0).getBoundingClientRect().top; + + var item = list.getItemAtIndex(10); + var originalPosition = item.getBoundingClientRect().top; + + list.menuBoxObject.activeChild = item; + ok(item.getBoundingClientRect().top < originalPosition, + "position of item 1: " + item.getBoundingClientRect().top + " -> " + originalPosition); + + originalPosition = item.getBoundingClientRect().top; + + synthesizeKey("VK_DOWN", { }); + is(item.getBoundingClientRect().top, originalPosition - rowdiff, "position of item 10"); + + list.open = false; + + checkEnter(); +} + +function keyCheck(list, key, index, testname) +{ + var item = $("i" + index); + synthesizeKeyExpectEvent(key, { }, item, expectCommandEvent ? "command" : "!command", testname); + is(list.selectedItem, expectCommandEvent ? item : $("i1"), testname + " selectedItem"); +} + +function checkModifiers(event) +{ + var expectedModifiers = (gModifiers == 1); + is(event.shiftKey, expectedModifiers, "shift key pressed"); + is(event.ctrlKey, expectedModifiers, "ctrl key pressed"); + is(event.altKey, expectedModifiers, "alt key pressed"); + is(event.metaKey, expectedModifiers, "meta key pressed"); + gModifiers++; +} + +function checkEnter() +{ + list.addEventListener("popuphidden", checkEnterWithModifiers, false); + list.addEventListener("command", checkModifiers, false); + list.open = true; + synthesizeKey("VK_RETURN", { }); +} + +function checkEnterWithModifiers() +{ + is(gModifiers, 1, "modifiers checked when not set"); + + ok(!list.open, "list closed on enter press"); + list.removeEventListener("popuphidden", checkEnterWithModifiers, false); + + list.addEventListener("popuphidden", verifyPopupOnClose, false); + list.open = true; + + synthesizeKey("VK_RETURN", { shiftKey: true, ctrlKey: true, altKey: true, metaKey: true }); +} + +function verifyPopupOnClose() +{ + is(gModifiers, 2, "modifiers checked when set"); + + ok(!list.open, "list closed on enter press with modifiers"); + list.removeEventListener("popuphidden", verifyPopupOnClose, false); + + list = $("list2"); + list.focus(); + list.open = true; +} + +function checkCursorNavigation() +{ + var commandEventsCount = 0; + list.addEventListener("command", event => { + is(event.target, list.selectedItem, "command event fired on selected item"); + commandEventsCount++; + }, false); + + is(list.selectedIndex, 1, "selectedIndex before cursor down"); + synthesizeKey("VK_DOWN", { }); + is(list.selectedIndex, iswin ? 2 : 1, "selectedIndex after cursor down"); + is(commandEventsCount, iswin ? 1 : 0, "selectedIndex after cursor down command event"); + is(list.menupopup.state, "open", "cursor down popup state"); + synthesizeKey("VK_PAGE_DOWN", { }); + is(list.selectedIndex, iswin ? 3 : 1, "selectedIndex after page down"); + is(commandEventsCount, iswin ? 2 : 0, "selectedIndex after page down command event"); + is(list.menupopup.state, "open", "page down popup state"); + + synthesizeKey("VK_UP", { altKey: true }); + is(list.open, ismac, "alt+up closes popup"); + + if (ismac) { + list.open = false; + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_null_value.xul b/toolkit/content/tests/chrome/test_menulist_null_value.xul new file mode 100644 index 0000000000..2545b6cdec --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_null_value.xul @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist value property" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<menulist id="list"> + <menupopup> + <menuitem id="i0" label="Zero" value="0"/> + <menuitem id="i1" label="One" value="item1"/> + <menuitem id="i2" label="Two" value="item2"/> + <menuitem id="ifalse" label="False" value="false"/> + <menuitem id="iempty" label="Empty" value=""/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + var list = document.getElementById("list"); + + list.value = "item2"; + is(list.value, "item2", "Check list value after setting value"); + is(list.getAttribute("label"), "Two", "Check list label after setting value"); + + list.selectedItem = null; + is(list.value, "", "Check list value after setting selectedItem to null"); + is(list.getAttribute("label"), "", "Check list label after setting selectedItem to null"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + is(list.value, "item1", "Check list value after setting selectedIndex"); + is(list.getAttribute("label"), "One", "Check list label after setting selectedIndex"); + + // check that an item can have the "false" value + list.value = false; + is(list.value, "false", "Check list value after setting it to false"); + is(list.getAttribute("label"), "False", "Check list labem after setting value to false"); + + // check that an item can have the "0" value + list.value = 0; + is(list.value, "0", "Check list value after setting it to 0"); + is(list.getAttribute("label"), "Zero", "Check list label after setting value to 0"); + + // check that an item can have the empty string value. + list.value = ""; + is(list.value, "", "Check list value after setting it to an empty string"); + is(list.getAttribute("label"), "Empty", "Check list label after setting value to an empty string"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + // set the value to null and test it (bug 408940) + list.value = null; + is(list.value, "", "Check list value after setting value to null"); + is(list.getAttribute("label"), "", "Check list label after setting value to null"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + // set the value to undefined and test it (bug 408940) + list.value = undefined; + is(list.value, "", "Check list value after setting value to undefined"); + is(list.getAttribute("label"), "", "Check list label after setting value to undefined"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + // set the value to something that does not exist in any menuitem of the list + // and make sure the previous label is removed + list.value = "this does not exist"; + is(list.value, "this does not exist", "Check the list value after setting it to something not associated witn an existing menuitem"); + is(list.getAttribute("label"), "", "Check that the list label is empty after selecting a nonexistent item"); + + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_paging.xul b/toolkit/content/tests/chrome/test_menulist_paging.xul new file mode 100644 index 0000000000..c58e0328f1 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_paging.xul @@ -0,0 +1,163 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist Tests" + onload="setTimeout(startTest, 0);" + onpopupshown="menulistShown()" onpopuphidden="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<menulist id="menulist1"> + <menupopup id="menulist-popup1"> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <menuitem label="Ten"/> + </menupopup> +</menulist> + +<menulist id="menulist2"> + <menupopup id="menulist-popup2"> + <menuitem label="One" disabled="true"/> + <menuitem label="Two" selected="true"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <menuitem label="Ten" disabled="true"/> + </menupopup> +</menulist> + +<menulist id="menulist3"> + <menupopup id="menulist-popup3"> + <label value="One"/> + <menuitem label="Two" selected="true"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five" disabled="true"/> + <menuitem label="Six" disabled="true"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <label value="Ten"/> + </menupopup> +</menulist> + +<menulist id="menulist4"> + <menupopup id="menulist-popup4"> + <label value="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six" selected="true"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <label value="Ten"/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +let test; + +// Fields: +// list - menulist id +// initial - initial selected index +// scroll - index of item at top of the visible scrolled area, -1 to skip this test +// downs - array of indicies that will be selected when pressing down in sequence +// ups - array of indicies that will be selected when pressing up in sequence +let tests = [ + { list: "menulist1", initial: 0, scroll: 0, downs: [3, 6, 9, 9], + ups: [6, 3, 0, 0] }, + { list: "menulist2", initial: 1, scroll: 0, downs: [4, 7, 8, 8], + ups: [5, 2, 1] }, + { list: "menulist3", initial: 1, scroll: -1, downs: [6, 8, 8], + ups: [3, 1, 1] }, + { list: "menulist4", initial: 5, scroll: 2, downs: [], ups: [] } +]; + +function startTest() +{ + let popup = document.getElementById("menulist-popup1"); + let menupopupHeight = popup.getBoundingClientRect().height; + let menuitemHeight = popup.firstChild.getBoundingClientRect().height; + + // First, set the height of each popup to the height of four menuitems plus + // any padding and border on the menupopup. + let height = menuitemHeight * 4 + (menupopupHeight - menuitemHeight * 10); + popup.height = height; + document.getElementById("menulist-popup2").height = height; + document.getElementById("menulist-popup3").height = height; + document.getElementById("menulist-popup4").height = height; + + runTest(); +} + +function runTest() +{ + if (!tests.length) { + SimpleTest.finish(); + return; + } + + test = tests.shift(); + document.getElementById(test.list).open = true; +} + +function menulistShown() +{ + let menulist = document.getElementById(test.list); + is(menulist.menuBoxObject.activeChild.label, menulist.getItemAtIndex(test.initial).label, test.list + " initial selection"); + + let cs = window.getComputedStyle(menulist.menupopup); + let bpTop = parseFloat(cs.paddingTop) + parseFloat(cs.borderTopWidth); + + // Skip menulist3 as it has a label that scrolling doesn't need normally deal with. + if (test.scroll >= 0) { + is(menulist.menupopup.childNodes[test.scroll].getBoundingClientRect().top, + menulist.menupopup.getBoundingClientRect().top + bpTop, + "Popup scroll at correct position"); + } + + for (let i = 0; i < test.downs.length; i++) { + sendKey("PAGE_DOWN"); + is(menulist.menuBoxObject.activeChild.label, menulist.getItemAtIndex(test.downs[i]).label, test.list + " page down " + i); + } + + for (let i = 0; i < test.ups.length; i++) { + sendKey("PAGE_UP"); + is(menulist.menuBoxObject.activeChild.label, menulist.getItemAtIndex(test.ups[i]).label, test.list + " page up " + i); + } + + menulist.open = false; +} +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_position.xul b/toolkit/content/tests/chrome/test_menulist_position.xul new file mode 100644 index 0000000000..a146cb85ef --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_position.xul @@ -0,0 +1,97 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist position Test" + onload="setTimeout(init, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<!-- + This test checks the position of a menulist's popup. + --> + +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +var menulist; + +function init() +{ + menulist = document.getElementById("menulist"); + menulist.open = true; +} + +function isWithinHalfPixel(a, b) +{ + return Math.abs(a - b) <= 0.5; +} + +function popupShown() +{ + var menurect = menulist.getBoundingClientRect(); + var popuprect = menulist.menupopup.getBoundingClientRect(); + + let marginLeft = parseFloat(getComputedStyle(menulist.menupopup).marginLeft); + ok(isWithinHalfPixel(menurect.left + marginLeft, popuprect.left), "left position"); + ok(isWithinHalfPixel(menurect.right + marginLeft, popuprect.right), "right position"); + + let index = menulist.selectedIndex; + if (menulist.selectedItem && navigator.platform.indexOf("Mac") >= 0) { + let menulistlabel = document.getAnonymousElementByAttribute(menulist, "class", "menulist-label"); + let mitemlabel = document.getAnonymousElementByAttribute(menulist.selectedItem, "class", "menu-iconic-text"); + + ok(isWithinHalfPixel(menulistlabel.getBoundingClientRect().left, + mitemlabel.getBoundingClientRect().left), + "Labels horizontally aligned for index " + index); + ok(isWithinHalfPixel(menulistlabel.getBoundingClientRect().top, + mitemlabel.getBoundingClientRect().top), + "Labels vertically aligned for index " + index); + } + else { + let marginTop = parseFloat(getComputedStyle(menulist.menupopup).marginTop); + ok(isWithinHalfPixel(menurect.bottom + marginTop, popuprect.top), + "Vertical alignment with no selection for index " + index); + } + + menulist.open = false; +} + +function popupHidden() +{ + if (!menulist.selectedItem) { + SimpleTest.finish(); + } + else { + menulist.selectedItem = menulist.selectedItem.nextSibling; + menulist.open = true; + } +} +]]> +</script> + +<hbox align="center" pack="center" style="margin-top: 100px;"> + <menulist id="menulist" onpopupshown="popupShown();" onpopuphidden="popupHidden();"> + <menupopup> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + </menupopup> + </menulist> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_mousescroll.xul b/toolkit/content/tests/chrome/test_mousescroll.xul new file mode 100644 index 0000000000..91ccf56831 --- /dev/null +++ b/toolkit/content/tests/chrome/test_mousescroll.xul @@ -0,0 +1,274 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=378028 +--> +<window title="Mozilla Bug 378028" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=378028" + target="_blank">Mozilla Bug 378028</a> + </body> + + <!-- richlistbox currently has no way of giving us a defined number of + rows, so we just choose an arbitrary height limit that should give + us plenty of vertical scrollability --> + <richlistbox id="richlistbox" style="height:50px;"> + <richlistitem id="richlistbox_item0" hidden="true"><label value="Item 0"/></richlistitem> + <richlistitem id="richlistbox_item1"><label value="Item 1"/></richlistitem> + <richlistitem id="richlistbox_item2"><label value="Item 2"/></richlistitem> + <richlistitem id="richlistbox_item3"><label value="Item 3"/></richlistitem> + <richlistitem id="richlistbox_item4"><label value="Item 4"/></richlistitem> + <richlistitem id="richlistbox_item5"><label value="Item 5"/></richlistitem> + <richlistitem id="richlistbox_item6"><label value="Item 6"/></richlistitem> + <richlistitem id="richlistbox_item7"><label value="Item 7"/></richlistitem> + <richlistitem id="richlistbox_item8"><label value="Item 8"/></richlistitem> + </richlistbox> + + <listbox id="listbox" rows="2"> + <listitem id="listbox_item0" label="Item 0" hidden="true"/> + <listitem id="listbox_item1" label="Item 1"/> + <listitem id="listbox_item2" label="Item 2"/> + <listitem id="listbox_item3" label="Item 3"/> + <listitem id="listbox_item4" label="Item 4"/> + <listitem id="listbox_item5" label="Item 5"/> + <listitem id="listbox_item6" label="Item 6"/> + <listitem id="listbox_item7" label="Item 7"/> + <listitem id="listbox_item8" label="Item 8"/> + </listbox> + + <box orient="horizontal"> + <arrowscrollbox id="hscrollbox" clicktoscroll="true" orient="horizontal" + smoothscroll="false" style="max-width:80px;" flex="1"> + <hbox style="width:40px; height:20px; background:black;" hidden="true"/> + <hbox style="width:40px; height:20px; background:white;"/> + <hbox style="width:40px; height:20px; background:black;"/> + <hbox style="width:40px; height:20px; background:white;"/> + <hbox style="width:40px; height:20px; background:black;"/> + <hbox style="width:40px; height:20px; background:white;"/> + <hbox style="width:40px; height:20px; background:black;"/> + <hbox style="width:40px; height:20px; background:white;"/> + <hbox style="width:40px; height:20px; background:black;"/> + </arrowscrollbox> + </box> + + <arrowscrollbox id="vscrollbox" clicktoscroll="true" orient="vertical" + smoothscroll="false" style="max-height:80px;" flex="1"> + <vbox style="width:100px; height:40px; background:black;" hidden="true"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + </arrowscrollbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 378028 **/ +/* and for Bug 350471 **/ +var smoothScrollPref = "general.smoothScroll"; +SpecialPowers.setBoolPref(smoothScrollPref, false); +SimpleTest.waitForExplicitFinish(); + +const deltaModes = [ + WheelEvent.DOM_DELTA_PIXEL, // 0 + WheelEvent.DOM_DELTA_LINE, // 1 + WheelEvent.DOM_DELTA_PAGE // 2 +]; + +function testListbox(id) +{ + var listbox = document.getElementById(id); + + function helper(aStart, aDelta, aIntDelta, aDeltaMode) + { + listbox.scrollToIndex(aStart); + synthesizeWheel(listbox, 10, 10, + { deltaMode: aDeltaMode, deltaY: aDelta, + lineOrPageDeltaY: aIntDelta }); + var expectedPos = aStart; + if (aIntDelta) { + if (aDeltaMode == WheelEvent.DOM_DELTA_PAGE) { + expectedPos += aIntDelta > 0 ? listbox.getNumberOfVisibleRows() : + -listbox.getNumberOfVisibleRows(); + } else { + expectedPos += aIntDelta; + } + } + is(listbox.getIndexOfFirstVisibleRow(), expectedPos, + "testListbox(" + id + "): vertical, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + aIntDelta + + " aDeltaMode " + aDeltaMode); + + // Check that horizontal scrolling has no effect + listbox.scrollToIndex(aStart); + synthesizeWheel(listbox, 10, 10, + { deltaMode: aDeltaMode, deltaX: aDelta, + lineOrPageDeltaX: aIntDelta }); + is(listbox.getIndexOfFirstVisibleRow(), aStart, + "testListbox(" + id + "): horizontal, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + aIntDelta + + " aDeltaMode " + aDeltaMode); + } + deltaModes.forEach(function(aDeltaMode) { + let delta = (aDeltaMode == WheelEvent.DOM_DELTA_PIXEL) ? 5.0 : 0.3; + helper(5, -delta, 0, aDeltaMode); + helper(5, -delta, -1, aDeltaMode); + helper(5, delta, 1, aDeltaMode); + helper(5, delta, 0, aDeltaMode); + }); +} + +function testRichListbox(id, andThen) +{ + var listbox = document.getElementById(id); + var tests = []; + + var winUtils = SpecialPowers.getDOMWindowUtils(window); + winUtils.advanceTimeAndRefresh(100); + + function nextTest() { + var [aStart, aDelta, aIntDelta, aDeltaMode] = tests.shift(); + listbox.scrollToIndex(aStart); + + let event = { + deltaMode: aDeltaMode, + deltaY: aDelta, + lineOrPageDeltaY: aIntDelta + }; + sendWheelAndPaint(listbox, 10, 10, event, function() { + var change = listbox.getIndexOfFirstVisibleRow() - aStart; + var direction = (change > 0) - (change < 0); + var expected = (aDelta > 0) - (aDelta < 0); + is(direction, expected, + "testRichListbox(" + id + "): vertical, starting " + aStart + + " delta " + aDelta + " lineOrPageDeltaY " + aIntDelta + + " aDeltaMode " + aDeltaMode); + + // Check that horizontal scrolling has no effect + let event = { + deltaMode: aDeltaMode, + deltaX: aDelta, + lineOrPageDeltaX: aIntDelta + }; + + listbox.scrollToIndex(aStart); + sendWheelAndPaint(listbox, 10, 10, event, function() { + is(listbox.getIndexOfFirstVisibleRow(), aStart, + "testRichListbox(" + id + "): horizontal, starting " + aStart + + " delta " + aDelta + " lineOrPageDeltaX " + aIntDelta + + " aDeltaMode " + aDeltaMode); + + if (!tests.length) { + winUtils.restoreNormalRefresh(); + andThen(); + return; + } + + nextTest(); + }); + }); + } + + // richlistbox currently uses native XUL scrolling, so the "line" + // amounts don't necessarily correspond 1-to-1 with listbox items. So + // we just check that scrolling up/down scrolls in the right direction. + deltaModes.forEach(function(aDeltaMode) { + let delta = (aDeltaMode == WheelEvent.DOM_DELTA_PIXEL) ? 32.0 : 2.0; + tests.push([5, -delta, -1, aDeltaMode]); + tests.push([5, -delta, 0, aDeltaMode]); + tests.push([5, delta, 1, aDeltaMode]); + tests.push([5, delta, 0, aDeltaMode]); + }); + + nextTest(); +} + +function testArrowScrollbox(id) +{ + var scrollbox = document.getElementById(id); + var scrollBoxObject = scrollbox.scrollBoxObject; + var orient = scrollbox.getAttribute("orient"); + + function helper(aStart, aDelta, aDeltaMode, aExpected) + { + var lineOrPageDelta = (aDeltaMode == WheelEvent.DOM_DELTA_PIXEL) ? aDelta / 10 : aDelta; + var orientIsHorizontal = (orient == "horizontal"); + + scrollBoxObject.scrollTo(aStart, aStart); + + for (var i = orientIsHorizontal ? 2 : 0; i >= 0; i--) { + synthesizeWheel(scrollbox, 5, 5, + { deltaMode: aDeltaMode, deltaY: aDelta, + lineOrPageDeltaY: lineOrPageDelta }); + + var pos = orientIsHorizontal ? scrollBoxObject.positionX : + scrollBoxObject.positionY; + + // Note, vertical mouse scrolling is allowed to scroll horizontal + // arrowscrollboxes, because many users have no horizontal mouse scroll + // capability + let expected = !i ? aExpected : aStart; + is(pos, expected, + "testArrowScrollbox(" + id + "): vertical, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + lineOrPageDelta + + " aDeltaMode " + aDeltaMode); + } + + scrollBoxObject.scrollTo(aStart, aStart); + for (var i = orientIsHorizontal ? 2 : 0; i >= 0; i--) { + synthesizeWheel(scrollbox, 5, 5, + { deltaMode: aDeltaMode, deltaX: aDelta, + lineOrPageDeltaX: lineOrPageDelta }); + // horizontal mouse scrolling is never allowed to scroll vertical + // arrowscrollboxes + var pos = orientIsHorizontal ? scrollBoxObject.positionX : + scrollBoxObject.positionY; + let expected = (!i && orientIsHorizontal) ? aExpected : aStart; + is(pos, expected, + "testArrowScrollbox(" + id + "): horizontal, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + lineOrPageDelta + + " aDeltaMode " + aDeltaMode); + } + } + + var scrolledWidth = scrollBoxObject.scrolledWidth; + var scrolledHeight = scrollBoxObject.scrolledHeight; + var scrollMaxX = scrolledWidth - scrollBoxObject.width; + var scrollMaxY = scrolledHeight - scrollBoxObject.height; + var scrollMax = orient == "horizontal" ? scrollMaxX : scrollMaxY; + + deltaModes.forEach(function(aDeltaMode) { + helper(50, -1000, aDeltaMode, 0); + helper(50, 1000, aDeltaMode, scrollMax); + helper(50, 0, aDeltaMode, 50); + helper(50, 0, aDeltaMode, 50); + }); +} + +function runTests() +{ + testRichListbox("richlistbox", function() { + testListbox("listbox"); + testArrowScrollbox("hscrollbox"); + testArrowScrollbox("vscrollbox"); + SpecialPowers.clearUserPref(smoothScrollPref); + SimpleTest.finish(); + }); +} + +window.onload = function() { setTimeout(runTests, 0); }; + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_notificationbox.xul b/toolkit/content/tests/chrome/test_notificationbox.xul new file mode 100644 index 0000000000..a99d0824e8 --- /dev/null +++ b/toolkit/content/tests/chrome/test_notificationbox.xul @@ -0,0 +1,522 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for notificationbox + --> +<window title="Notification Box" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <notificationbox id="nb"/> + <menupopup id="menupopup" onpopupshown="this.hidePopup()" onpopuphidden="checkPopupClosed()"> + <menuitem label="One"/> + </menupopup> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ +SimpleTest.waitForExplicitFinish(); + +var testtag_notificationbox_buttons = [ + { + label: "Button 1", + accesskey: "u", + callback: testtag_notificationbox_buttonpressed, + popup: "menupopup" + } +]; + +var NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +function testtag_notificationbox_buttonpressed(event) +{ +} + +function testtag_notificationbox(nb) +{ + testtag_notificationbox_State(nb, "initial", null, 0); + + SimpleTest.is(nb.notificationsHidden, false, "initial notificationsHidden"); + SimpleTest.is(nb.removeAllNotifications(false), undefined, "initial removeAllNotifications"); + testtag_notificationbox_State(nb, "initial removeAllNotifications", null, 0); + SimpleTest.is(nb.removeAllNotifications(true), undefined, "initial removeAllNotifications immediate"); + testtag_notificationbox_State(nb, "initial removeAllNotifications immediate", null, 0); + + runTimedTests(tests, -1, nb, null); +} + +var notification_last_events = []; +function notification_eventCallback(event) +{ + notification_last_events.push({ actualEvent: event , item: this }); +} + +/** + * For any notifications that have the notification_eventCallback on + * them, we will have recorded instances of those callbacks firing + * and stored them. This checks to see that the expected event types + * are being fired in order, and targeting the right item. + * + * @param {Array<string>} expectedEvents + * The list of event types, in order, that we expect to have been + * fired on the item. + * @param {<xul:notification>} ntf + * The notification we expect the callback to have been fired from. + * @param {string} testName + * The name of the current test, for logging. + */ +function testtag_notification_eventCallback(expectedEvents, ntf, testName) +{ + for (let i = 0; i < expectedEvents; ++i) { + let expected = expectedEvents[i]; + let { actualEvent, item } = notification_last_events[i]; + SimpleTest.is(actualEvent, expected, testName + ": event name"); + SimpleTest.is(item, ntf, testName + ": event item"); + } + notification_last_events = []; +} + +var tests = +[ + { + test: function(nb, ntf) { + // append a new notification + var ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification"); + return ntf; + }, + result: function(nb, ntf) { + testtag_notificationbox_State(nb, "append", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_LOW); + + // check the getNotificationWithValue method + var ntf_found = nb.getNotificationWithValue("note"); + SimpleTest.is(ntf, ntf_found, "getNotificationWithValue note"); + + var none_found = nb.getNotificationWithValue("notenone"); + SimpleTest.is(none_found, null, "getNotificationWithValue null"); + return ntf; + } + }, + { + test: function(nb, ntf) { + // check that notifications can be removed properly + nb.removeNotification(ntf); + return ntf; + }, + result: function(nb, ntf) { + testtag_notificationbox_State(nb, "removeNotification", null, 0); + + // try removing the notification again to make sure an exception occurs + var exh = false; + try { + nb.removeNotification(ntf); + } catch (ex) { exh = true; } + SimpleTest.is(exh, true, "removeNotification again"); + testtag_notificationbox_State(nb, "removeNotification again", null, 0); + + } + }, + { + test: function(nb, ntf) { + // append a new notification, but now with an event callback + var ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, + testtag_notificationbox_buttons, + notification_eventCallback); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification with callback"); + return ntf; + }, + result: function(nb, ntf) { + testtag_notificationbox_State(nb, "append with callback", ntf, 1); + return ntf; + } + }, + { + test: function(nb, ntf) { + nb.removeNotification(ntf); + return ntf; + }, + result: function(nb, ntf) { + testtag_notificationbox_State(nb, "removeNotification with callback", + null, 0); + + testtag_notification_eventCallback(["removed"], ntf, "removeNotification()"); + return ntf; + } + }, + { + test: function(nb, ntf) { + var ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, + testtag_notificationbox_buttons, + notification_eventCallback); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification with callback"); + return ntf; + }, + result: function(nb, ntf) { + testtag_notificationbox_State(nb, "append with callback", ntf, 1); + return ntf; + } + }, + { + test: function(rb, ntf) { + // Dismissing the notification instead of removing it should + // fire a dismissed "event" on the callback, followed by + // a removed "event". + ntf.dismiss(); + return ntf; + }, + result: function(nb, ntf) { + testtag_notificationbox_State(nb, "called dismiss()", null, 0); + testtag_notification_eventCallback(["dismissed", "removed"], ntf, + "dismiss()"); + return ntf; + } + }, + { + test: function(nb, ntf) { + // Create a popup to be used by a menu-button. + var doc = nb.ownerDocument; + var menuPopup = doc.createElementNS(NSXUL, "menupopup"); + var menuItem = menuPopup.appendChild(doc.createElementNS(NSXUL, "menuitem")); + menuItem.setAttribute("label", "Menu Item"); + // Append a notification with a button of type 'menu-button'. + ntf = nb.appendNotification( + "Notification", "note", "happy.png", + nb.PRIORITY_WARNING_LOW, + [{ + label: "Button", + type: "menu-button", + popup: menuPopup + }] + ); + + return ntf; + }, + result: function(nb, ntf) { + testtag_notificationbox_State(nb, "append", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_WARNING_LOW); + var button = ntf.querySelector(".notification-button"); + SimpleTest.is(button.type, "menu-button", "Button type should be set"); + var menuPopup = button.getElementsByTagNameNS(NSXUL, "menupopup"); + SimpleTest.is(menuPopup.length, 1, "There should be a menu attached"); + var menuItem = menuPopup[0].firstChild; + SimpleTest.is(menuItem.localName, "menuitem", "There should be a menu item"); + SimpleTest.is(menuItem.getAttribute("label"), "Menu Item", "Label should match"); + // Clean up. + nb.removeNotification(ntf); + + return [1, null]; + } + }, + { + repeat: true, + test: function(nb, arr) { + var idx = arr[0]; + var ntf = arr[1]; + switch (idx) { + case 1: + // append a new notification + ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification"); + + // Test persistence + ntf.persistence++; + + return [idx, ntf]; + case 2: + case 3: + nb.removeTransientNotifications(); + + return [idx, ntf]; + } + }, + result: function(nb, arr) { + var idx = arr[0]; + var ntf = arr[1]; + switch (idx) { + case 1: + testtag_notificationbox_State(nb, "notification added", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_LOW); + SimpleTest.is(ntf.persistence, 1, "persistence is 1"); + + return [++idx, ntf]; + case 2: + testtag_notificationbox_State(nb, "first removeTransientNotifications", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_LOW); + SimpleTest.is(ntf.persistence, 0, "persistence is now 0"); + + return [++idx, ntf]; + case 3: + testtag_notificationbox_State(nb, "second removeTransientNotifications", null, 0); + + this.repeat = false; + } + } + }, + { + test: function(nb, ntf) { + // append another notification + var ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_MEDIUM, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification again"); + return ntf; + }, + result: function(nb, ntf) { + // check that appending a second notification after removing the first one works + testtag_notificationbox_State(nb, "append again", ntf, 1); + testtag_notification_State(nb, ntf, "append again", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_MEDIUM); + return ntf; + } + }, + { + test: function(nb, ntf) { + // check the removeCurrentNotification method + nb.removeCurrentNotification(); + return ntf; + }, + result: function(nb, ntf) { + testtag_notificationbox_State(nb, "removeCurrentNotification", null, 0); + } + }, + { + test: function(nb, ntf) { + var ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_HIGH, testtag_notificationbox_buttons); + return ntf; + }, + result: function(nb, ntf) { + // test the removeAllNotifications method + testtag_notificationbox_State(nb, "append info_high", ntf, 1); + SimpleTest.is(ntf.priority, nb.PRIORITY_INFO_HIGH, + "notification.priority " + nb.PRIORITY_INFO_HIGH); + SimpleTest.is(nb.removeAllNotifications(false), undefined, "removeAllNotifications"); + } + }, + { + test: function(nb, unused) { + // add a number of notifications and check that they are added in order + nb.appendNotification("Four", "4", null, nb.PRIORITY_INFO_HIGH, testtag_notificationbox_buttons); + nb.appendNotification("Seven", "7", null, nb.PRIORITY_WARNING_HIGH, testtag_notificationbox_buttons); + nb.appendNotification("Two", "2", null, nb.PRIORITY_INFO_LOW, null); + nb.appendNotification("Eight", "8", null, nb.PRIORITY_CRITICAL_LOW, null); + nb.appendNotification("Five", "5", null, nb.PRIORITY_WARNING_LOW, null); + nb.appendNotification("Six", "6", null, nb.PRIORITY_WARNING_HIGH, null); + nb.appendNotification("One", "1", null, nb.PRIORITY_INFO_LOW, null); + nb.appendNotification("Nine", "9", null, nb.PRIORITY_CRITICAL_MEDIUM, null); + var ntf = nb.appendNotification("Ten", "10", null, nb.PRIORITY_CRITICAL_HIGH, null); + nb.appendNotification("Three", "3", null, nb.PRIORITY_INFO_MEDIUM, null); + return ntf; + }, + result: function(nb, ntf) { + SimpleTest.is(nb.currentNotification == ntf ? + nb.currentNotification.value : null, "10", "appendNotification order"); + return 1; + } + }, + { + // test closing notifications to make sure that the current notification is still set properly + repeat: true, + test: function(nb, testidx) { + switch (testidx) { + case 1: + nb.getNotificationWithValue("10").close(); + return [1, 9]; + case 2: + nb.removeNotification(nb.getNotificationWithValue("9")); + return [2, 8]; + case 3: + nb.removeCurrentNotification(); + return [3, 7]; + case 4: + nb.getNotificationWithValue("6").close(); + return [4, 7]; + case 5: + nb.removeNotification(nb.getNotificationWithValue("5")); + return [5, 7]; + case 6: + nb.removeCurrentNotification(); + return [6, 4]; + } + }, + result: function(nb, arr) { + // arr is [testindex, expectedvalue] + SimpleTest.is(nb.currentNotification.value, "" + arr[1], "close order " + arr[0]); + SimpleTest.is(nb.allNotifications.length, 10 - arr[0], "close order " + arr[0] + " count"); + if (arr[0] == 6) + this.repeat = false; + return ++arr[0]; + } + }, + { + test: function(nb, ntf) { + var exh = false; + try { + nb.appendNotification("no", "no", "no", 0, null); + } catch (ex) { exh = true; } + SimpleTest.is(exh, true, "appendNotification priority too low"); + + exh = false; + try { + nb.appendNotification("no", "no", "no", 11, null); + } catch (ex) { exh = true; } + SimpleTest.is(exh, true, "appendNotification priority too high"); + + // check that the other priority types work properly + runTimedTests(appendPriorityTests, -1, nb, nb.PRIORITY_WARNING_LOW); + } + } +]; + +var appendPriorityTests = [ + { + test: function(nb, priority) { + var ntf = nb.appendNotification("Notification", "note", "happy.png", + priority, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification " + priority); + return [ntf, priority]; + }, + result: function(nb, obj) { + SimpleTest.is(obj[0].priority, obj[1], "notification.priority " + obj[1]); + return obj[1]; + } + }, + { + test: function(nb, priority) { + nb.removeCurrentNotification(); + return priority; + }, + result: function(nb, priority) { + if (priority == nb.PRIORITY_CRITICAL_BLOCK) { + let ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, testtag_notificationbox_buttons); + setTimeout(checkPopupTest, 50, nb, ntf); + } + else { + runTimedTests(appendPriorityTests, -1, nb, ++priority); + } + } + } +]; + +function testtag_notificationbox_State(nb, testid, expecteditem, expectedcount) +{ + SimpleTest.is(nb.currentNotification, expecteditem, testid + " currentNotification"); + SimpleTest.is(nb.allNotifications ? nb.allNotifications.length : "no value", + expectedcount, testid + " allNotifications"); +} + +function testtag_notification_State(nb, ntf, testid, label, value, image, priority) +{ + SimpleTest.is(ntf.control, nb, testid + " notification.control"); + SimpleTest.is(ntf.label, label, testid + " notification.label"); + SimpleTest.is(ntf.value, value, testid + " notification.value"); + SimpleTest.is(ntf.image, image, testid + " notification.image"); + SimpleTest.is(ntf.priority, priority, testid + " notification.priority"); + + var type; + switch (priority) { + case nb.PRIORITY_INFO_LOW: + case nb.PRIORITY_INFO_MEDIUM: + case nb.PRIORITY_INFO_HIGH: + type = "info"; + break; + case nb.PRIORITY_WARNING_LOW: + case nb.PRIORITY_WARNING_MEDIUM: + case nb.PRIORITY_WARNING_HIGH: + type = "warning"; + break; + case nb.PRIORITY_CRITICAL_LOW: + case nb.PRIORITY_CRITICAL_MEDIUM: + case nb.PRIORITY_CRITICAL_HIGH: + case nb.PRIORITY_CRITICAL_BLOCK: + type = "critical"; + break; + } + + SimpleTest.is(ntf.type, type, testid + " notification.type"); +} + +function checkPopupTest(nb, ntf) +{ + if (nb._animating) + setTimeout(checkPopupTest, ntf); + else { + var evt = new Event(""); + ntf.dispatchEvent(evt); + evt.target.buttonInfo = testtag_notificationbox_buttons[0]; + ntf._doButtonCommand(evt); + } +} + +function checkPopupClosed() +{ + is(document.popupNode, null, "popupNode null after popup is closed"); + SimpleTest.finish(); +} + +/** + * run one or more tests which perform a test operation, wait for a delay, + * then perform a result operation. + * + * tests - array of objects where each object is : + * { + * test: test function, + * result: result function + * repeat: true to repeat the test + * } + * idx - starting index in tests + * element - element to run tests on + * arg - argument to pass between test functions + * + * If, after executing the result part, the repeat property of the test is + * true, then the test is repeated. If the repeat property is not true, + * continue on to the next test. + * + * The test and result functions take two arguments, the element and the arg. + * The test function may return a value which will passed to the result + * function as its arg. The result function may also return a value which + * will be passed to the next repetition or the next test in the array. + */ +function runTimedTests(tests, idx, element, arg) +{ + if (idx >= 0 && "result" in tests[idx]) + arg = tests[idx].result(element, arg); + + // if not repeating, move on to the next test + if (idx == -1 || !tests[idx].repeat) + idx++; + + if (idx < tests.length) { + var result = tests[idx].test(element, arg); + setTimeout(runTimedTestsWait, 50, tests, idx, element, result); + } +} + +function runTimedTestsWait(tests, idx, element, arg) +{ + // use this secret property to check if the animation is still running. If it + // is, then the notification hasn't fully opened or closed yet + if (element._animating) + setTimeout(runTimedTestsWait, 50, tests, idx, element, arg); + else + runTimedTests(tests, idx, element, arg); +} + +setTimeout(testtag_notificationbox, 0, document.getElementById('nb')); +]]> +</script> + +</window> + diff --git a/toolkit/content/tests/chrome/test_panel.xul b/toolkit/content/tests/chrome/test_panel.xul new file mode 100644 index 0000000000..3b2188d9e7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel.xul @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Panel Tests" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_panel.xul", "_blank", "chrome,left=200,top=200,width=200,height=200"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_panel_focus.xul b/toolkit/content/tests/chrome/test_panel_focus.xul new file mode 100644 index 0000000000..e18f28ca8f --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel_focus.xul @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Panel Focus Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +// use a chrome window for this test as the focus in content windows can be +// adjusted by the current selection position + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + // move the mouse so any tooltips that might be open go away, otherwise this + // test can fail on Mac + synthesizeMouse(document.documentElement, 1, 1, { type: "mousemove" }); + + window.open("window_panel_focus.xul", "_blank", "chrome,width=600,height=600"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_panelfrommenu.xul b/toolkit/content/tests/chrome/test_panelfrommenu.xul new file mode 100644 index 0000000000..72e3965163 --- /dev/null +++ b/toolkit/content/tests/chrome/test_panelfrommenu.xul @@ -0,0 +1,118 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Open panel from menuitem" + onload="setTimeout(runTests, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<!-- + This test does the following: + 1. Opens the menu, causing the popupshown event to fire, which will call menuOpened. + 2. Keyboard events are fired to cause the first item on the menu to be executed. + 3. The command event handler for the first menuitem opens the panel. + 4. As a menuitem was executed, the menu will roll up, hiding it. + 5. The popuphidden event for the menu calls menuClosed which tests the popup states. + 6. The panelOpened function tests the popup states again and hides the popup. + 7. Once the panel's popuphidden event fires, tests are performed to see if + panels inside buttons and toolbarbuttons work. Each is opened and the closed. + --> + +<menu id="menu" onpopupshown="menuOpened()" onpopuphidden="menuClosed();"> + <menupopup> + <menuitem id="i1" label="One" oncommand="$('panel').openPopup($('menu'), 'after_start');"/> + <menuitem id="i2" label="Two"/> + </menupopup> +</menu> + +<panel id="hiddenpanel" hidden="true"/> + +<panel id="panel" onpopupshown="panelOpened()" + onpopuphidden="$('button').focus(); $('button').open = true"> + <textbox/> +</panel> + +<button id="button" type="panel" label="Button"> + <panel onpopupshown="panelOnButtonOpened(this)" + onpopuphidden="$('tbutton').open = true;"> + <button label="OK" oncommand="this.parentNode.parentNode.open = false"/> + </panel> +</button> + +<toolbarbutton id="tbutton" type="panel" label="Toolbarbutton"> + <panel onpopupshown="panelOnToolbarbuttonOpened(this)" + onpopuphidden="SimpleTest.finish()"> + <textbox/> + </panel> +</toolbarbutton> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + is($("hiddenpanel").state, "closed", "hidden popup is closed"); + + var menu = $("menu"); + menu.open = true; +} + +function menuOpened() +{ + synthesizeKey("VK_DOWN", { }); + synthesizeKey("VK_RETURN", { }); +} + +function menuClosed() +{ + // the panel will be open at this point, but the popupshown event + // still needs to fire + is($("panel").state, "showing", "panel is open after menu hide"); + is($("menu").firstChild.state, "closed", "menu is closed after menu hide"); +} + +function panelOpened() +{ + is($("panel").state, "open", "panel is open"); + is($("menu").firstChild.state, "closed", "menu is closed"); + $("panel").hidePopup(); +} + +function panelOnButtonOpened(panel) +{ + is(panel.state, 'open', 'button panel is open'); + is(document.activeElement, document.documentElement, "focus blurred on panel from button open"); + synthesizeKey("VK_DOWN", { }); + is(document.activeElement, document.documentElement, "focus not modified on cursor down from button"); + panel.firstChild.doCommand() +} + +function panelOnToolbarbuttonOpened(panel) +{ + is(panel.state, 'open', 'toolbarbutton panel is open'); + is(document.activeElement, document.documentElement, "focus blurred on panel from toolbarbutton open"); + panel.firstChild.focus(); + synthesizeKey("VK_DOWN", { }); + is(document.activeElement, panel.firstChild.inputField, "focus not modified on cursor down from toolbarbutton"); + panel.parentNode.open = false; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_anchor.xul b/toolkit/content/tests/chrome/test_popup_anchor.xul new file mode 100644 index 0000000000..5839c52a37 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_anchor.xul @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Anchor Tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_popup_anchor.xul", "_blank", "chrome,width=600,height=600"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_anchoratrect.xul b/toolkit/content/tests/chrome/test_popup_anchoratrect.xul new file mode 100644 index 0000000000..c12e225026 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_anchoratrect.xul @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Button Popup Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_popup_anchoratrect.xul", "_blank", "chrome,width=200,height=200"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_attribute.xul b/toolkit/content/tests/chrome/test_popup_attribute.xul new file mode 100644 index 0000000000..2a256078d5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_attribute.xul @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Attribute Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_popup_attribute.xul", "_blank", "width=600,height=700"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_button.xul b/toolkit/content/tests/chrome/test_popup_button.xul new file mode 100644 index 0000000000..3803e465f5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_button.xul @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Button Popup Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_popup_button.xul", "_blank", "width=700,height=700"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_coords.xul b/toolkit/content/tests/chrome/test_popup_coords.xul new file mode 100644 index 0000000000..4597b5cc03 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_coords.xul @@ -0,0 +1,91 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Coordinate Tests" + onload="setTimeout(openThePopup, 0, 'outer');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<deck style="margin-top: 5px; padding-top: 5px;"> + <label id="outer" popup="outerpopup" value="Popup"/> +</deck> + +<panel id="outerpopup" + onpopupshowing="popupShowingEventOccurred(event);" + onpopupshown="eventOccurred(event); openThePopup('inner')" + onpopuphiding="eventOccurred(event);" + onpopuphidden="eventOccurred(event); SimpleTest.finish();"> + <button id="item1" label="First"/> + <label id="inner" value="Second" popup="innerpopup"/> + <button id="item2" label="Third"/> +</panel> + +<menupopup id="innerpopup" + onpopupshowing="popupShowingEventOccurred(event);" + onpopupshown="eventOccurred(event); event.target.hidePopup();" + onpopuphiding="eventOccurred(event);" + onpopuphidden="eventOccurred(event); document.getElementById('outerpopup').hidePopup();"> + <menuitem id="inner1" label="Inner First"/> + <menuitem id="inner2" label="Inner Second"/> +</menupopup> + +<script> +SimpleTest.waitForExplicitFinish(); + +function openThePopup(id) +{ + if (id == "inner") + document.getElementById("item1").focus(); + + var trigger = document.getElementById(id); + synthesizeMouse(trigger, 4, 5, { }); +} + +function eventOccurred(event) +{ + var testname = event.type + " on " + event.target.id + " "; + ok(event instanceof MouseEvent, testname + "is a mouse event"); + is(event.clientX, 0, testname + "clientX"); + is(event.clientY, 0, testname + "clientY"); + is(event.rangeParent, null, testname + "rangeParent"); + is(event.rangeOffset, 0, testname + "rangeOffset"); +} + +function popupShowingEventOccurred(event) +{ + // the popupshowing event should have the event coordinates and + // range position filled in. + var testname = "popupshowing on " + event.target.id + " "; + ok(event instanceof MouseEvent, testname + "is a mouse event"); + + var trigger = document.getElementById(event.target.id == "outerpopup" ? "outer" : "inner"); + var rect = trigger.getBoundingClientRect(); + is(event.clientX, Math.round(rect.left + 4), testname + "clientX"); + is(event.clientY, Math.round(rect.top + 5), testname + "clientY"); + // rangeOffset should be just after the trigger element. As rangeOffset + // considers the zeroth position to be before the first element, the value + // should be one higher than its index within its parent. + is(event.rangeParent, trigger.parentNode, testname + "rangeParent"); + is(event.rangeOffset, Array.indexOf(trigger.parentNode.childNodes, trigger) + 1, testname + "rangeOffset"); + + var popuprect = event.target.getBoundingClientRect(); + is(Math.round(popuprect.left), Math.round(rect.left + 4), "popup left"); + is(Math.round(popuprect.top), Math.round(rect.top + 5), "popup top"); + ok(popuprect.width > 0, "popup width"); + ok(popuprect.height > 0, "popup height"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_keys.xul b/toolkit/content/tests/chrome/test_popup_keys.xul new file mode 100644 index 0000000000..37135af57e --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_keys.xul @@ -0,0 +1,148 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu ignorekeys Test" + onkeydown="keyDown()" onkeypress="gKeyPressCount++; event.stopPropagation(); event.preventDefault();" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<!-- + This test checks that the ignorekeys attribute can be used on a menu to + disable key navigation. The test is performed twice by opening the menu, + simulating a cursor down key, and closing the popup. When keys are enabled, + the first item on the menu should be highlighted, otherwise the first item + should not be highlighted. + --> + +<menupopup id="popup"> + <menuitem id="i1" label="One" onDOMAttrModified="attrModified(event)"/> + <menuitem id="i2" label="Two"/> + <menuitem id="i3" label="Three"/> + <menuitem id="i4" label="Four"/> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gIgnoreKeys = false; +var gIgnoreAttrChange = false; +var gKeyPressCount = 0; + +let {Task} = Components.utils.import("resource://gre/modules/Task.jsm", {}); + +function waitForEvent(target, eventName) { + return new Promise(resolve => { + target.addEventListener(eventName, function eventOccurred(event) { + target.removeEventListener(eventName, eventOccurred, false); + resolve(); + }, false); + }); +} + +function runTests() +{ + Task.async(function* () { + var popup = $("popup"); + popup.enableKeyboardNavigator(false); + is(popup.getAttribute("ignorekeys"), "true", "keys disabled"); + popup.enableKeyboardNavigator(true); + is(popup.hasAttribute("ignorekeys"), false, "keys enabled"); + + let popupShownPromise = waitForEvent(popup, "popupshown"); + popup.openPopup(null, "after_start"); + yield popupShownPromise; + + let popupHiddenPromise = waitForEvent(popup, "popuphidden"); + synthesizeKey("VK_DOWN", { }); + yield popupHiddenPromise; + + is(gKeyPressCount, 0, "keypresses with ignorekeys='false'"); + + gIgnoreKeys = true; + popup.setAttribute("ignorekeys", "true"); + // clear this first to avoid confusion + gIgnoreAttrChange = true; + $("i1").removeAttribute("_moz-menuactive") + gIgnoreAttrChange = false; + + popupShownPromise = waitForEvent(popup, "popupshown"); + popup.openPopup(null, "after_start"); + yield popupShownPromise; + + synthesizeKey("VK_DOWN", { }); + + yield new Promise(resolve => setTimeout(() => resolve(), 1000)); + popupHiddenPromise = waitForEvent(popup, "popuphidden"); + popup.hidePopup(); + yield popupHiddenPromise; + + is(gKeyPressCount, 1, "keypresses with ignorekeys='true'"); + + popup.setAttribute("ignorekeys", "shortcuts"); + // clear this first to avoid confusion + gIgnoreAttrChange = true; + $("i1").removeAttribute("_moz-menuactive") + gIgnoreAttrChange = false; + + popupShownPromise = waitForEvent(popup, "popupshown"); + popup.openPopup(null, "after_start"); + yield popupShownPromise; + + // When ignorekeys="shortcuts", T should be handled but accel+T should propagate. + synthesizeKey("t", { }); + is(gKeyPressCount, 1, "keypresses after t pressed with ignorekeys='shortcuts'"); + + synthesizeKey("t", { accelKey: true }); + is(gKeyPressCount, 2, "keypresses after accel+t pressed with ignorekeys='shortcuts'"); + + popupHiddenPromise = waitForEvent(popup, "popuphidden"); + popup.hidePopup(); + yield popupHiddenPromise; + + SimpleTest.finish(); + })(); +} + +function attrModified(event) +{ + if (gIgnoreAttrChange || event.attrName != "_moz-menuactive") + return; + + // the attribute should not be changed when ignorekeys is enabled + if (gIgnoreKeys) { + ok(false, "move key with keys disabled"); + } + else { + is($("i1").getAttribute("_moz-menuactive"), "true", "move key with keys enabled"); + $("popup").hidePopup(); + } +} + +function keyDown() +{ + // when keys are enabled, the menu should have stopped propagation of the + // event, so a bubbling listener for a keydown event should only occur + // when keys are disabled. + ok(gIgnoreKeys, "key listener fired with keys " + + (gIgnoreKeys ? "disabled" : "enabled")); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_moveToAnchor.xul b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xul new file mode 100644 index 0000000000..344002fdf6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xul @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<vbox align="start"> + <button id="button1" label="Button 1" style="margin-top: 50px;"/> + <button id="button2" label="Button 2" style="margin-top: 60px;"/> +</vbox> + +<menupopup id="popup" onpopupshown="popupshown()" onpopuphidden="SimpleTest.finish()"> + <menuitem label="One"/> + <menuitem label="Two"/> +</menupopup> + +<script> +SimpleTest.waitForExplicitFinish(); + +function runTest(id) +{ + $("popup").openPopup($("button1"), "after_start"); +} + +function popupshown() +{ + var popup = $("popup"); + var popupheight = popup.getBoundingClientRect().height; + var button1rect = $("button1").getBoundingClientRect(); + var button2rect = $("button2").getBoundingClientRect(); + + checkCoords(popup, button1rect.left, button1rect.bottom, "initial"); + + popup.moveToAnchor($("button1"), "after_start", 0, 8); + checkCoords(popup, button1rect.left, button1rect.bottom + 8, "move anchor top + 8"); + + popup.moveToAnchor($("button1"), "after_start", 6, -10); + checkCoords(popup, button1rect.left + 6, button1rect.bottom - 10, "move anchor left + 6, top - 10"); + + popup.moveToAnchor($("button1"), "before_start", -2, 0); + checkCoords(popup, button1rect.left - 2, button1rect.top - popupheight, "move anchor before_start"); + + popup.moveToAnchor($("button2"), "before_start"); + checkCoords(popup, button2rect.left, button2rect.top - popupheight, "move button2"); + + popup.moveToAnchor($("button1"), "end_before"); + checkCoords(popup, button1rect.right, button1rect.top, "move anchor end_before"); + + popup.moveToAnchor($("button2"), "after_start", 5, 4); + checkCoords(popup, button2rect.left + 5, button2rect.bottom + 4, "move button2 left + 5, top + 4"); + + popup.moveTo($("button1").boxObject.screenX + 10, $("button1").boxObject.screenY + 12); + checkCoords(popup, button1rect.left + 10, button1rect.top + 12, "move to button1 screen with offset"); + + popup.moveToAnchor($("button1"), "after_start", 1, 2); + checkCoords(popup, button1rect.left + 1, button1rect.bottom + 2, "move button2 after screen"); + + popup.hidePopup(); +} + +function checkCoords(popup, expectedx, expectedy, testid) +{ + var rect = popup.getBoundingClientRect(); + is(Math.round(rect.left), Math.round(expectedx), testid + " left"); + is(Math.round(rect.top), Math.round(expectedy), testid + " top"); +} + +SimpleTest.waitForFocus(runTest); + +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_preventdefault.xul b/toolkit/content/tests/chrome/test_popup_preventdefault.xul new file mode 100644 index 0000000000..7de5dc3be1 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_preventdefault.xul @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Prevent Default Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<!-- + This tests checks that preventDefault can be called on a popupshowing + event and that preventDefault has no effect for the popuphiding event. + --> + +<script> +SimpleTest.waitForExplicitFinish(); + +var gBlockShowing = true; +var gShownNotAllowed = true; + +function runTest() +{ + document.getElementById("menu").open = true; +} + +function popupShowing(event) +{ + if (gBlockShowing) { + event.preventDefault(); + gBlockShowing = false; + setTimeout(function() { + gShownNotAllowed = false; + document.getElementById("menu").open = true; + }, 3000, true); + } +} + +function popupShown() +{ + ok(!gShownNotAllowed, "popupshowing preventDefault"); + document.getElementById("menu").open = false; +} + +function popupHiding(event) +{ + // since this is a content test, preventDefault should have no effect + event.preventDefault(); +} + +function popupHidden() +{ + ok(true, "popuphiding preventDefault not allowed"); + SimpleTest.finish(); +} +</script> + +<button id="menu" type="menu" label="Menu"> + <menupopup onpopupshowing="popupShowing(event);" + onpopupshown="popupShown();" + onpopuphiding="popupHiding(event);" + onpopuphidden="popupHidden();"> + <menuitem label="Item"/> + </menupopup> +</button> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_preventdefault_chrome.xul b/toolkit/content/tests/chrome/test_popup_preventdefault_chrome.xul new file mode 100644 index 0000000000..46f14cd6a5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_preventdefault_chrome.xul @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Attribute Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_popup_preventdefault_chrome.xul", "_blank", "chrome,width=600,height=600"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_recreate.xul b/toolkit/content/tests/chrome/test_popup_recreate.xul new file mode 100644 index 0000000000..14822acbd0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_recreate.xul @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Recreate Test" + onload="setTimeout(init, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<!-- + This is a test for bug 388361. + + This test checks that a menulist's popup is properly created and sized when + the popup node is removed and another added in its place. + + --> + +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +var gState = "before"; + +function init() +{ + document.getElementById("menulist").open = true; +} + +function isWithinHalfPixel(a, b) +{ + return Math.abs(a - b) <= 0.5; +} + +function recreate() +{ + if (gState == "before") { + var element = document.getElementById("menulist"); + while (element.hasChildNodes()) + element.removeChild(element.firstChild); + element.appendItem("Cat"); + gState = "after"; + document.getElementById("menulist").open = true; + } + else { + SimpleTest.finish(); + } +} + +function checkSize() +{ + var menulist = document.getElementById("menulist"); + var menurect = menulist.getBoundingClientRect(); + var popuprect = menulist.menupopup.getBoundingClientRect(); + + let marginLeft = parseFloat(getComputedStyle(menulist.menupopup).marginLeft); + ok(isWithinHalfPixel(menurect.left + marginLeft, popuprect.left), "left position " + gState); + ok(isWithinHalfPixel(menurect.right + marginLeft, popuprect.right), "right position " + gState); + ok(Math.round(popuprect.right) - Math.round(popuprect.left) > 0, "height " + gState) + document.getElementById("menulist").open = false; +} +]]> +</script> + +<hbox align="center" pack="center"> + <menulist id="menulist" onpopupshown="checkSize();" onpopuphidden="recreate();"> + <menupopup position="after_start"> + <menuitem label="Cat"/> + </menupopup> + </menulist> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_scaled.xul b/toolkit/content/tests/chrome/test_popup_scaled.xul new file mode 100644 index 0000000000..6bbf6c6533 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_scaled.xul @@ -0,0 +1,105 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popups in Scaled Content" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<!-- This test checks that the position is correct in two cases: + - a popup anchored at an element in a scaled document + - a popup opened at a screen coordinate in a scaled window + --> + +<iframe id="frame" width="60" height="140" + src="data:text/html,<html><body><input size='4' id='one'><input size='4' id='two'></body></html>"/> + +<menupopup id="popup" onpopupshown="shown()" onpopuphidden="nextTest()"> + <menuitem label="One"/> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var screenTest = false; +var screenx = -1, screeny = -1; + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + setScale($("frame").contentWindow, 2); + + var anchor = $("frame").contentDocument.getElementById("two"); + anchor.getBoundingClientRect(); // flush to update display after scale change + $("popup").openPopup(anchor, "after_start"); +} + +function setScale(win, scale) +{ + var wn = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation); + var shell = wn.QueryInterface(Components.interfaces.nsIDocShell); + var docViewer = shell.contentViewer; + docViewer.fullZoom = scale; +} + +function shown() +{ + if (screenTest) { + var box = $("popup").boxObject; + is(box.screenX, screenx, "screen left position"); + is(box.screenY, screeny, "screen top position"); + } + else { + var anchor = $("frame").contentDocument.getElementById("two"); + + is(Math.round(anchor.getBoundingClientRect().left * 2), + Math.round($("popup").getBoundingClientRect().left), "anchored left position"); + is(Math.round(anchor.getBoundingClientRect().bottom * 2), + Math.round($("popup").getBoundingClientRect().top), "anchored top position"); + } + + $("popup").hidePopup(); +} + +function nextTest() +{ + if (screenTest) { + setScale(window, 1); + SimpleTest.finish(); + } + else { + screenTest = true; + var box = document.documentElement.boxObject; + + // - the iframe is at 4×, but out here css pixels are only 2× device pixels + // - the popup manager rounds off (or truncates) the coordinates to + // integers, so ensure we pass in even numbers to openPopupAtScreen + screenx = (x = even(box.screenX + 120))/2; + screeny = (y = even(box.screenY + 120))/2; + setScale(window, 2); + $("popup").openPopupAtScreen(x, y); + } +} + +function even(n) +{ + return (n % 2) ? n+1 : n; +} +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_tree.xul b/toolkit/content/tests/chrome/test_popup_tree.xul new file mode 100644 index 0000000000..779f13e684 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_tree.xul @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tree in Popup Test" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<panel id="panel" onpopupshown="treeClick()" onpopuphidden="SimpleTest.finish()"> + <tree id="tree" width="350" rows="5"> + <treecols> + <treecol id="name" label="Name" flex="1"/> + <treecol id="address" label="Street" flex="1"/> + </treecols> + <treechildren id="treechildren"> + <treeitem> + <treerow> + <treecell label="Justin Thyme"/> + <treecell label="800 Bay Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary Goround"/> + <treecell label="47 University Avenue"/> + </treerow> + </treeitem> + </treechildren> + </tree> +</panel> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + $("panel").openPopup(null, "overlap", 2, 2); +} + +function treeClick() +{ + var tree = $("tree"); + is(tree.currentIndex, -1, "selectedIndex before click"); + synthesizeMouseExpectEvent($("treechildren"), 2, 2, { }, $("treechildren"), "click", ""); + is(tree.currentIndex, 0, "selectedIndex after click"); + + var rect = tree.treeBoxObject.getCoordsForCellItem(1, tree.columns.address, ""); + synthesizeMouseExpectEvent($("treechildren"), rect.x, rect.y + 2, + { }, $("treechildren"), "click", ""); + is(tree.currentIndex, 1, "selectedIndex after second click " + rect.x + "," + rect.y); + + $("panel").hidePopup(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popuphidden.xul b/toolkit/content/tests/chrome/test_popuphidden.xul new file mode 100644 index 0000000000..4c344b3d22 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popuphidden.xul @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Hidden Popup Test" + onload="setTimeout(runTests, 0, $('popup'));" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<menupopup id="popup" hidden="true" onpopupshown="ok(true, 'popupshown'); this.hidePopup()" + onpopuphidden="$('popup-hideonshow').openPopup(null, 'after_start')"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> +</menupopup> + +<menupopup id="popup-hideonshow" onpopupshowing="hidePopupWhileShowing(this)" + onpopupshown="ok(false, 'popupshown when hidden')"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> +</menupopup> + +<button id="button" type="menu" label="Menu" onDOMAttrModified="checkEndTest(event)"> + <menupopup id="popupinbutton" hidden="true" + onpopupshown="ok(true, 'popupshown'); ok($('button').open, 'open'); this.hidden = true;"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> + </menupopup> +</button> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests(popup) +{ + popup.hidden = false; + popup.openPopup(null, "after_start"); +} + +function hidePopupWhileShowing(popup) +{ + popup.hidden = true; + popup.clientWidth; // flush layout + is(popup.state, 'closed', 'popupshowing hidden'); + SimpleTest.executeSoon(() => runTests($('popupinbutton'))); +} + +function checkEndTest(event) +{ + var button = $("button"); + if (event.originalTarget != button || event.attrName != 'open' || event.attrChange != event.REMOVAL) + return; + + ok($("popupinbutton").hidden, "popup hidden"); + is($("popupinbutton").state, "closed", "popup state"); + ok(!button.open, "not open after hidden"); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popupincontent.xul b/toolkit/content/tests/chrome/test_popupincontent.xul new file mode 100644 index 0000000000..dafcd09e5a --- /dev/null +++ b/toolkit/content/tests/chrome/test_popupincontent.xul @@ -0,0 +1,131 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup in Content Positioning Tests" + onload="setTimeout(nextTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<!-- + This test checks that popups in content areas don't extend past the content area. + --> + +<hbox> + <spacer width="100"/> + <menu id="menu" label="Menu"> + <menupopup style="margin:10px;" id="popup" onpopupshown="popupShown()" onpopuphidden="nextTest()"> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="A final longer label that is actually quite long. Very long indeed."/> + </menupopup> + </menu> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var step = ""; +var originalHeight = -1; + +function nextTest() +{ + // there are five tests here: + // openPopupAtScreen - checks that opening a popup using openPopupAtScreen + // constrains the popup to the content area + // left and top - check with the left and top attributes set + // open near bottom - open the menu near the bottom of the window + // large menu - try with a menu that is very large and should be scaled + // shorter menu again - try with a menu that is shorter again. It should have + // the same height as the 'left and top' test + var popup = $("popup"); + var menu = $("menu"); + switch (step) { + case "": + step = "openPopupAtScreen"; + popup.openPopupAtScreen(1000, 1200); + break; + case "openPopupAtScreen": + step = "left and top"; + popup.setAttribute("left", "800"); + popup.setAttribute("top", "2900"); + synthesizeMouse(menu, 2, 2, { }); + break; + case "left and top": + step = "open near bottom"; + // request that the menu be opened with a target point near the bottom of the window, + // so that the menu's top margin will push it completely outside the window. + var bo = document.documentElement.boxObject; + popup.setAttribute("top", bo.screenY + window.innerHeight - 5); + synthesizeMouse(menu, 2, 2, { }); + break; + case "open near bottom": + step = "large menu"; + popup.removeAttribute("left"); + popup.removeAttribute("top"); + for (var i = 0; i < 80; i++) + menu.appendItem("Test", ""); + synthesizeMouse(menu, 2, 2, { }); + break; + case "large menu": + step = "shorter menu again"; + for (var i = 0; i < 80; i++) + menu.removeItemAt(menu.itemCount - 1); + synthesizeMouse(menu, 2, 2, { }); + break; + case "shorter menu again": + SimpleTest.finish(); + break; + } +} + +function popupShown() +{ + var windowrect = document.documentElement.getBoundingClientRect(); + var popuprect = $("popup").getBoundingClientRect(); + + // subtract one off the edge due to a rounding issue + ok(popuprect.left >= windowrect.left, step + " left"); + ok(popuprect.right - 1 <= windowrect.right, step + " right"); + + if (step == "left and top") { + originalHeight = popuprect.bottom - popuprect.top; + } + else if (step == "open near bottom") { + // check that the menu flipped up so it's above our requested point + ok(popuprect.bottom - 1 <= windowrect.bottom - 5, step + " bottom"); + } + else if (step == "large menu") { + // add 10 to account for the margin + is(popuprect.top, $("menu").getBoundingClientRect().bottom + 10, step + " top"); + ok(popuprect.bottom == windowrect.bottom || + popuprect.bottom - 1 == windowrect.bottom, step + " bottom"); + } + else { + ok(popuprect.top >= windowrect.top, step + " top"); + ok(popuprect.bottom - 1 <= windowrect.bottom, step + " bottom"); + if (step == "shorter menu again") + is(popuprect.bottom - popuprect.top, originalHeight, step + " height shortened"); + } + + $("menu").open = false; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popupremoving.xul b/toolkit/content/tests/chrome/test_popupremoving.xul new file mode 100644 index 0000000000..f795590f52 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popupremoving.xul @@ -0,0 +1,165 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Removing Tests" + onload="setTimeout(nextTest, 0)" + onDOMAttrModified="modified(event)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<!-- + This test checks that popup elements can be removed in various ways without + crashing. It tests two situations, one with menus that are 'separate', and + one with menus that are 'nested'. In each case, there are four levels of menu. + + The nextTest function starts the process by opening the first menu. A set of + popupshown event listeners are used to open the next menu until all four are + showing. This last one calls removePopup to remove the menu node from the + tree. This should hide the popups as they are no longer in a document. + + A mutation listener is triggered when the fourth menu closes by having its + open attribute cleared. This listener hides the third popup which causes + its frame to be removed. Naturally, we want to ensure that this doesn't + crash when the third menu is removed. + --> + + <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> + +<hbox> + +<menu id="nestedmenu1" label="1"> + <menupopup id="nestedpopup1" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu2" label="2"> + <menupopup id="nestedpopup2" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu3" label="3"> + <menupopup id="nestedpopup3" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu4" label="4" onpopupshown="removePopups()"> + <menupopup id="nestedpopup4"> + <menuitem label="Nested 1"/> + <menuitem label="Nested 2"/> + <menuitem label="Nested 3"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> +</menu> + +<menu id="separatemenu1" label="1"> + <menupopup id="separatepopup1" onpopupshown="$('separatemenu2').open = true"> + <menuitem label="L1 One"/> + <menuitem label="L1 Two"/> + <menuitem label="L1 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu2" label="2"> + <menupopup id="separatepopup2" onpopupshown="$('separatemenu3').open = true" + onpopuphidden="popup2Hidden()"> + <menuitem label="L2 One"/> + <menuitem label="L2 Two"/> + <menuitem label="L2 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu3" label="3" onpopupshown="$('separatemenu4').open = true"> + <menupopup id="separatepopup3"> + <menuitem label="L3 One"/> + <menuitem label="L3 Two"/> + <menuitem label="L3 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu4" label="4" onpopupshown="removePopups()" + onpopuphidden="$('separatemenu2').open = false"> + <menupopup id="separatepopup3"> + <menuitem label="L4 One"/> + <menuitem label="L4 Two"/> + <menuitem label="L4 Three"/> + </menupopup> +</menu> + +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gKey = ""; +gTriggerMutation = null; +gChangeMutation = null; + +function nextTest() +{ + if (gKey == "") { + gKey = "separate"; + } + else if (gKey == "separate") { + gKey = "nested"; + } + else { + SimpleTest.finish(); + return; + } + + $(gKey + "menu1").open = true; +} + +function modified(event) +{ + // use this mutation listener to hide the third popup, destroying its frame. + // It gets triggered when the open attribute is cleared on the fourth menu. + + if (event.target == gTriggerMutation && + event.attrName == "open") { + gChangeMutation.hidden = true; + // force a layout flush + document.documentElement.boxObject.width; + gTriggerMutation = null; + gChangeMutation = null; + } +} + +function removePopups() +{ + var menu2 = $(gKey + "menu2"); + var menu3 = $(gKey + "menu3"); + is(menu2.getAttribute("open"), "true", gKey + " menu 2 open before"); + is(menu3.getAttribute("open"), "true", gKey + " menu 3 open before"); + + gTriggerMutation = menu3; + gChangeMutation = $(gKey + "menu4"); + var menu = $(gKey + "menu1"); + menu.parentNode.removeChild(menu); + + if (gKey == "nested") { + // the 'separate' test checks this during the popup2 hidden event handler + is(menu2.hasAttribute("open"), false, gKey + " menu 2 open after"); + is(menu3.hasAttribute("open"), false, gKey + " menu 3 open after"); + nextTest(); + } +} + +function popup2Hidden() +{ + is($(gKey + "menu2").hasAttribute("open"), false, gKey + " menu 2 open after"); + nextTest(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popupremoving_frame.xul b/toolkit/content/tests/chrome/test_popupremoving_frame.xul new file mode 100644 index 0000000000..dec73c7f70 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popupremoving_frame.xul @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Unload Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<!-- + This test checks that popup elements are removed when the document is changed. + --> + + <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> + +<iframe id="frame" width="300" height="150" src="frame_popupremoving_frame.xul"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gMenus = []; + +function popupsOpened() +{ + var framedoc = $("frame").contentDocument; + framedoc.addEventListener("DOMAttrModified", modified, false); + + // this is the order in which the menus should be hidden (reverse of the + // order they were opened in). The second menu is removed during the + // mutation listener, so gets the event afterwards. + gMenus.push(framedoc.getElementById("nestedmenu4")); + gMenus.push(framedoc.getElementById("nestedmenu2")); + gMenus.push(framedoc.getElementById("nestedmenu3")); + gMenus.push(framedoc.getElementById("nestedmenu1")); + gMenus.push(framedoc.getElementById("separatemenu4")); + gMenus.push(framedoc.getElementById("separatemenu2")); + gMenus.push(framedoc.getElementById("separatemenu3")); + gMenus.push(framedoc.getElementById("separatemenu1")); + + framedoc.location = "about:blank"; +} + +function modified(event) +{ + if (event.attrName != "open") + return; + + var framedoc = $("frame").contentDocument; + + var tohide = null; + if (event.target.id == "separatemenu3") + tohide = framedoc.getElementById("separatemenu2"); + else if (event.target.id == "nestedmenu3") + tohide = framedoc.getElementById("nestedmenu2"); + + if (tohide) { + tohide.hidden = true; + // force a layout flush + $("frame").contentDocument.documentElement.boxObject.width; + } + + is(event.target, gMenus.shift(), event.target.id + " hidden"); + if (gMenus.length == 0) + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_position.xul b/toolkit/content/tests/chrome/test_position.xul new file mode 100644 index 0000000000..695c1bf22c --- /dev/null +++ b/toolkit/content/tests/chrome/test_position.xul @@ -0,0 +1,136 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for positioning + --> +<window title="position" width="500" height="600" + onload="setTimeout(runTest, 0);" + style="margin: 0; border: 0; padding; 0;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + + +<hbox id="box1"> + <button label="0" width="100" height="40" style="margin: 3px;"/> +</hbox> +<scrollbox id="box2" orient="vertical" align="start" width="200" height="50" + style="overflow: hidden; margin-left: 2px; padding: 1px;"> + <deck> + <scrollbox id="box3" orient="vertical" align="start" height="100" + style="overflow: scroll; margin: 1px; padding: 0;"> + <vbox id="innerscroll" width="200" align="start"> + <button id="button1" label="1" width="90" maxwidth="100" + minheight="25" height="35" maxheight="50" + style="min-width: 80px; margin: 5px; border: 4px; padding: 7px; + -moz-appearance: none;"/> + <menu id="menu"> + <menupopup id="popup" style="-moz-appearance: none; margin:0; border: 0; padding: 0;" + onpopupshown="menuOpened()" + onpopuphidden="if (event.target == this) SimpleTest.finish()"> + <menuitem label="One"/> + <menu id="submenu" label="Three"> + <menupopup id="subpopup" style="-moz-appearance: none; margin:0; border: 0; padding: 0;" + onpopupshown="submenuOpened()"> + <menuitem label="Four"/> + </menupopup> + </menu> + </menupopup> + </menu> + <button label="2" maxwidth="100" maxheight="20" style="margin: 5px;"/> + <button label="3" maxwidth="100" maxheight="20" style="margin: 5px;"/> + <button label="4" maxwidth="100" maxheight="20" style="margin: 5px;"/> + </vbox> + <box height="200"/> + </scrollbox> + </deck> +</scrollbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTest() +{ + var winwidth = document.documentElement.boxObject.width; + var innerscroll = $("innerscroll").boxObject.width; + + var box1 = $("box1"); + checkPosition("box1", box1, 0, 0, winwidth, 46); + + var box2 = $("box2"); + checkPosition("box2", box2, 2, 46, winwidth, 96); + + // height is height(box1) = 46 + margin-top(box3) = 1 + margin-top(button1) = 5 + var button1 = $("button1"); + checkPosition("button1", button1, 9, 53, 99, 88); + + var sbo = box2.boxObject; + sbo.scrollTo(7, 16); + + // clientRect height is offset from root so is 16 pixels vertically less + checkPosition("button1 scrolled", button1, 9, 37, 99, 72); + + var box3 = $("box3"); + sbo = box3.boxObject; + sbo.scrollTo(1, 2); + + checkPosition("button1 scrolled", button1, 9, 35, 99, 70); + + $("menu").open = true; +} + +function menuOpened() +{ + $("submenu").open = true; +} + +function submenuOpened() +{ + var menu = $("menu"); + var menuleft = Math.round(menu.getBoundingClientRect().left); + var menubottom = Math.round(menu.getBoundingClientRect().bottom); + + var submenu = $("submenu"); + var submenutop = Math.round(submenu.getBoundingClientRect().top); + var submenuright = Math.round(submenu.getBoundingClientRect().right); + + checkPosition("popup", $("popup"), menuleft, menubottom, -1, -1); + checkPosition("subpopup", $("subpopup"), submenuright, submenutop, -1, -1); + + menu.open = false; +} + +function checkPosition(testid, elem, cleft, ctop, cright, cbottom) +{ + // -1 for right or bottom means that the exact size should not be + // checked, just ensure it is larger then the left or top position + var rect = elem.getBoundingClientRect(); + is(Math.round(rect.left), cleft, testid + " client rect left"); + if (testid != "popup") + is(Math.round(rect.top), ctop, testid + " client rect top"); + if (cright >= 0) + is(Math.round(rect.right), cright, testid + " client rect right"); + else + ok(rect.right - rect.left > 20, testid + " client rect right"); + if (cbottom >= 0) + is(Math.round(rect.bottom), cbottom, testid + " client rect bottom"); + else + ok(rect.bottom - rect.top > 15, testid + " client rect bottom"); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_preferences.xul b/toolkit/content/tests/chrome/test_preferences.xul new file mode 100644 index 0000000000..22e7e2fe96 --- /dev/null +++ b/toolkit/content/tests/chrome/test_preferences.xul @@ -0,0 +1,533 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Preferences Window Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="RunTest();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + + const kPref = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + + // preference values, set 1 + const kPrefValueSet1 = + { + int: 23, + bool: true, + string: "rheeet!", + wstring_data: "日本語", + unichar_data: "äöüßÄÖÜ", + file_data: "/", + + wstring: Components.classes["@mozilla.org/pref-localizedstring;1"] + .createInstance(Components.interfaces.nsIPrefLocalizedString), + unichar: Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString), + file: Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsILocalFile) + }; + kPrefValueSet1.wstring.data = kPrefValueSet1.wstring_data; + kPrefValueSet1.unichar.data = kPrefValueSet1.unichar_data; + SafeFileInit(kPrefValueSet1.file, kPrefValueSet1.file_data); + + // preference values, set 2 + const kPrefValueSet2 = + { + int: 42, + bool: false, + string: "Mozilla", + wstring_data: "헤드라인A", + unichar_data: "áôùšŽ", + file_data: "/home", + + wstring: Components.classes["@mozilla.org/pref-localizedstring;1"] + .createInstance(Components.interfaces.nsIPrefLocalizedString), + unichar: Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString), + file: Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsILocalFile) + }; + kPrefValueSet2.wstring.data = kPrefValueSet2.wstring_data; + kPrefValueSet2.unichar.data = kPrefValueSet2.unichar_data; + SafeFileInit(kPrefValueSet2.file, kPrefValueSet2.file_data); + + + function SafeFileInit(aFile, aPath) + { + // set file path without dying for exceptions + try + { + aFile.initWithPath(aPath); + } + catch (ignored) {} + } + + function CreateEmptyPrefValueSet() + { + var result = + { + int: undefined, + bool: undefined, + string: undefined, + wstring_data: undefined, + unichar_data: undefined, + file_data: undefined, + wstring: undefined, + unichar: undefined, + file: undefined + }; + return result; + } + + function WritePrefsToSystem(aPrefValueSet) + { + // write preference data via XPCOM + kPref.setIntPref ("tests.static_preference_int", aPrefValueSet.int); + kPref.setBoolPref("tests.static_preference_bool", aPrefValueSet.bool); + kPref.setCharPref("tests.static_preference_string", aPrefValueSet.string); + kPref.setComplexValue("tests.static_preference_wstring", + Components.interfaces.nsIPrefLocalizedString, + aPrefValueSet.wstring); + kPref.setComplexValue("tests.static_preference_unichar", + Components.interfaces.nsISupportsString, + aPrefValueSet.unichar); + kPref.setComplexValue("tests.static_preference_file", + Components.interfaces.nsILocalFile, + aPrefValueSet.file); + } + + function ReadPrefsFromSystem() + { + // read preference data via XPCOM + var result = CreateEmptyPrefValueSet(); + try {result.int = kPref.getIntPref ("tests.static_preference_int") } catch (ignored) {}; + try {result.bool = kPref.getBoolPref("tests.static_preference_bool") } catch (ignored) {}; + try {result.string = kPref.getCharPref("tests.static_preference_string")} catch (ignored) {}; + try + { + result.wstring = kPref.getComplexValue("tests.static_preference_wstring", + Components.interfaces.nsIPrefLocalizedString); + result.wstring_data = result.wstring.data; + } + catch (ignored) {}; + try + { + result.unichar = kPref.getComplexValue("tests.static_preference_unichar", + Components.interfaces.nsISupportsString); + result.unichar_data = result.unichar.data; + } + catch (ignored) {}; + try + { + result.file = kPref.getComplexValue("tests.static_preference_file", + Components.interfaces.nsILocalFile); + result.file_data = result.file.data; + } + catch (ignored) {}; + return result; + } + + function GetXULElement(aPrefWindow, aID) + { + return aPrefWindow.document.getElementById(aID); + } + + function WritePrefsToPreferences(aPrefWindow, aPrefValueSet) + { + // write preference data into <preference>s + GetXULElement(aPrefWindow, "tests.static_preference_int" ).value = aPrefValueSet.int; + GetXULElement(aPrefWindow, "tests.static_preference_bool" ).value = aPrefValueSet.bool; + GetXULElement(aPrefWindow, "tests.static_preference_string" ).value = aPrefValueSet.string; + GetXULElement(aPrefWindow, "tests.static_preference_wstring").value = aPrefValueSet.wstring_data; + GetXULElement(aPrefWindow, "tests.static_preference_unichar").value = aPrefValueSet.unichar_data; + GetXULElement(aPrefWindow, "tests.static_preference_file" ).value = aPrefValueSet.file_data; + } + + function ReadPrefsFromPreferences(aPrefWindow) + { + // read preference data from <preference>s + var result = + { + int: GetXULElement(aPrefWindow, "tests.static_preference_int" ).value, + bool: GetXULElement(aPrefWindow, "tests.static_preference_bool" ).value, + string: GetXULElement(aPrefWindow, "tests.static_preference_string" ).value, + wstring_data: GetXULElement(aPrefWindow, "tests.static_preference_wstring").value, + unichar_data: GetXULElement(aPrefWindow, "tests.static_preference_unichar").value, + file_data: GetXULElement(aPrefWindow, "tests.static_preference_file" ).value, + wstring: Components.classes["@mozilla.org/pref-localizedstring;1"] + .createInstance(Components.interfaces.nsIPrefLocalizedString), + unichar: Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString), + file: Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsILocalFile) + } + result.wstring.data = result.wstring_data; + result.unichar.data = result.unichar_data; + SafeFileInit(result.file, result.file_data); + return result; + } + + function WritePrefsToUI(aPrefWindow, aPrefValueSet) + { + // write preference data into UI elements + GetXULElement(aPrefWindow, "static_element_int" ).value = aPrefValueSet.int; + GetXULElement(aPrefWindow, "static_element_bool" ).checked = aPrefValueSet.bool; + GetXULElement(aPrefWindow, "static_element_string" ).value = aPrefValueSet.string; + GetXULElement(aPrefWindow, "static_element_wstring").value = aPrefValueSet.wstring_data; + GetXULElement(aPrefWindow, "static_element_unichar").value = aPrefValueSet.unichar_data; + GetXULElement(aPrefWindow, "static_element_file" ).value = aPrefValueSet.file_data; + } + + function ReadPrefsFromUI(aPrefWindow) + { + // read preference data from <preference>s + var result = + { + int: GetXULElement(aPrefWindow, "static_element_int" ).value, + bool: GetXULElement(aPrefWindow, "static_element_bool" ).checked, + string: GetXULElement(aPrefWindow, "static_element_string" ).value, + wstring_data: GetXULElement(aPrefWindow, "static_element_wstring").value, + unichar_data: GetXULElement(aPrefWindow, "static_element_unichar").value, + file_data: GetXULElement(aPrefWindow, "static_element_file" ).value, + wstring: Components.classes["@mozilla.org/pref-localizedstring;1"] + .createInstance(Components.interfaces.nsIPrefLocalizedString), + unichar: Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString), + file: Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsILocalFile) + } + result.wstring.data = result.wstring_data; + result.unichar.data = result.unichar_data; + SafeFileInit(result.file, result.file_data); + return result; + } + + + function RunInstantPrefTest(aPrefWindow) + { + // remark: there's currently no UI element binding for files + + // were all <preferences> correctly initialized? + var expected = kPrefValueSet1; + var found = ReadPrefsFromPreferences(aPrefWindow); + ok(found.int === expected.int, "instant pref init int" ); + ok(found.bool === expected.bool, "instant pref init bool" ); + ok(found.string === expected.string, "instant pref init string" ); + ok(found.wstring_data === expected.wstring_data, "instant pref init wstring"); + ok(found.unichar_data === expected.unichar_data, "instant pref init unichar"); + todo(found.file_data === expected.file_data, "instant pref init file" ); + + // were all elements correctly initialized? (loose check) + found = ReadPrefsFromUI(aPrefWindow); + ok(found.int == expected.int, "instant element init int" ); + ok(found.bool == expected.bool, "instant element init bool" ); + ok(found.string == expected.string, "instant element init string" ); + ok(found.wstring_data == expected.wstring_data, "instant element init wstring"); + ok(found.unichar_data == expected.unichar_data, "instant element init unichar"); + todo(found.file_data == expected.file_data, "instant element init file" ); + + // do some changes in the UI + expected = kPrefValueSet2; + WritePrefsToUI(aPrefWindow, expected); + + // UI changes should get passed to the <preference>s, + // but currently they aren't if the changes are made programmatically + // (the handlers preference.change/prefpane.input and prefpane.change + // are called for manual changes, though). + found = ReadPrefsFromPreferences(aPrefWindow); + todo(found.int === expected.int, "instant change pref int" ); + todo(found.bool === expected.bool, "instant change pref bool" ); + todo(found.string === expected.string, "instant change pref string" ); + todo(found.wstring_data === expected.wstring_data, "instant change pref wstring"); + todo(found.unichar_data === expected.unichar_data, "instant change pref unichar"); + todo(found.file_data === expected.file_data, "instant change pref file" ); + + // and these changes should get passed to the system instantly + // (which obviously can't pass with the above failing) + found = ReadPrefsFromSystem(); + todo(found.int === expected.int, "instant change element int" ); + todo(found.bool === expected.bool, "instant change element bool" ); + todo(found.string === expected.string, "instant change element string" ); + todo(found.wstring_data === expected.wstring_data, "instant change element wstring"); + todo(found.unichar_data === expected.unichar_data, "instant change element unichar"); + todo(found.file_data === expected.file_data, "instant change element file" ); + + // try resetting the prefs to default values (which should be empty here) + GetXULElement(aPrefWindow, "tests.static_preference_int" ).reset(); + GetXULElement(aPrefWindow, "tests.static_preference_bool" ).reset(); + GetXULElement(aPrefWindow, "tests.static_preference_string" ).reset(); + GetXULElement(aPrefWindow, "tests.static_preference_wstring").reset(); + GetXULElement(aPrefWindow, "tests.static_preference_unichar").reset(); + GetXULElement(aPrefWindow, "tests.static_preference_file" ).reset(); + + // check system + expected = CreateEmptyPrefValueSet(); + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "instant reset system int" ); + ok(found.bool === expected.bool, "instant reset system bool" ); + ok(found.string === expected.string, "instant reset system string" ); + ok(found.wstring_data === expected.wstring_data, "instant reset system wstring"); + ok(found.unichar_data === expected.unichar_data, "instant reset system unichar"); + ok(found.file_data === expected.file_data, "instant reset system file" ); + + // check UI + expected = + { + // alas, we don't have XUL elements with typeof(value) == int :( + // int: 0, + int: "", + bool: false, + string: "", + wstring_data: "", + unichar_data: "", + file_data: "", + wstring: {}, + unichar: {}, + file: {} + }; + found = ReadPrefsFromUI(aPrefWindow); + ok(found.int === expected.int, "instant reset element int" ); + ok(found.bool === expected.bool, "instant reset element bool" ); + ok(found.string === expected.string, "instant reset element string" ); + ok(found.wstring_data === expected.wstring_data, "instant reset element wstring"); + ok(found.unichar_data === expected.unichar_data, "instant reset element unichar"); +// ok(found.file_data === expected.file_data, "instant reset element file" ); + + // check hasUserValue + ok(GetXULElement(aPrefWindow, "tests.static_preference_int" ).hasUserValue === false, "instant reset hasUserValue int" ); + ok(GetXULElement(aPrefWindow, "tests.static_preference_bool" ).hasUserValue === false, "instant reset hasUserValue bool" ); + ok(GetXULElement(aPrefWindow, "tests.static_preference_string" ).hasUserValue === false, "instant reset hasUserValue string" ); + ok(GetXULElement(aPrefWindow, "tests.static_preference_wstring").hasUserValue === false, "instant reset hasUserValue wstring"); + ok(GetXULElement(aPrefWindow, "tests.static_preference_unichar").hasUserValue === false, "instant reset hasUserValue unichar"); + ok(GetXULElement(aPrefWindow, "tests.static_preference_file" ).hasUserValue === false, "instant reset hasUserValue file" ); + + // done with instant apply checks + } + + function RunNonInstantPrefTestGeneral(aPrefWindow) + { + // Non-instant apply tests are harder: not only do we need to check that + // fiddling with the values does *not* change the system settings, but + // also that they *are* (not) set after closing (cancelling) the dialog... + + // remark: there's currently no UI element binding for files + + // were all <preferences> correctly initialized? + var expected = kPrefValueSet1; + var found = ReadPrefsFromPreferences(aPrefWindow); + ok(found.int === expected.int, "non-instant pref init int" ); + ok(found.bool === expected.bool, "non-instant pref init bool" ); + ok(found.string === expected.string, "non-instant pref init string" ); + ok(found.wstring_data === expected.wstring_data, "non-instant pref init wstring"); + ok(found.unichar_data === expected.unichar_data, "non-instant pref init unichar"); + todo(found.file_data === expected.file_data, "non-instant pref init file" ); + + // were all elements correctly initialized? (loose check) + found = ReadPrefsFromUI(aPrefWindow); + ok(found.int == expected.int, "non-instant element init int" ); + ok(found.bool == expected.bool, "non-instant element init bool" ); + ok(found.string == expected.string, "non-instant element init string" ); + ok(found.wstring_data == expected.wstring_data, "non-instant element init wstring"); + ok(found.unichar_data == expected.unichar_data, "non-instant element init unichar"); + todo(found.file_data == expected.file_data, "non-instant element init file" ); + + // do some changes in the UI + expected = kPrefValueSet2; + WritePrefsToUI(aPrefWindow, expected); + + // UI changes should get passed to the <preference>s, + // but currently they aren't if the changes are made programmatically + // (the handlers preference.change/prefpane.input and prefpane.change + // are called for manual changes, though). + found = ReadPrefsFromPreferences(aPrefWindow); + todo(found.int === expected.int, "non-instant change pref int" ); + todo(found.bool === expected.bool, "non-instant change pref bool" ); + todo(found.string === expected.string, "non-instant change pref string" ); + todo(found.wstring_data === expected.wstring_data, "non-instant change pref wstring"); + todo(found.unichar_data === expected.unichar_data, "non-instant change pref unichar"); + todo(found.file_data === expected.file_data, "non-instant change pref file" ); + + // and these changes should *NOT* get passed to the system + // (which obviously always passes with the above failing) + expected = kPrefValueSet1; + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant change element int" ); + ok(found.bool === expected.bool, "non-instant change element bool" ); + ok(found.string === expected.string, "non-instant change element string" ); + ok(found.wstring_data === expected.wstring_data, "non-instant change element wstring"); + ok(found.unichar_data === expected.unichar_data, "non-instant change element unichar"); + todo(found.file_data === expected.file_data, "non-instant change element file" ); + + // try resetting the prefs to default values (which should be empty here) + GetXULElement(aPrefWindow, "tests.static_preference_int" ).reset(); + GetXULElement(aPrefWindow, "tests.static_preference_bool" ).reset(); + GetXULElement(aPrefWindow, "tests.static_preference_string" ).reset(); + GetXULElement(aPrefWindow, "tests.static_preference_wstring").reset(); + GetXULElement(aPrefWindow, "tests.static_preference_unichar").reset(); + GetXULElement(aPrefWindow, "tests.static_preference_file" ).reset(); + + // check system: the current values *MUST NOT* change + expected = kPrefValueSet1; + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant reset system int" ); + ok(found.bool === expected.bool, "non-instant reset system bool" ); + ok(found.string === expected.string, "non-instant reset system string" ); + ok(found.wstring_data === expected.wstring_data, "non-instant reset system wstring"); + ok(found.unichar_data === expected.unichar_data, "non-instant reset system unichar"); + todo(found.file_data === expected.file_data, "non-instant reset system file" ); + + // check UI: these values should be reset + expected = + { + // alas, we don't have XUL elements with typeof(value) == int :( + // int: 0, + int: "", + bool: false, + string: "", + wstring_data: "", + unichar_data: "", + file_data: "", + wstring: {}, + unichar: {}, + file: {} + }; + found = ReadPrefsFromUI(aPrefWindow); + ok(found.int === expected.int, "non-instant reset element int" ); + ok(found.bool === expected.bool, "non-instant reset element bool" ); + ok(found.string === expected.string, "non-instant reset element string" ); + ok(found.wstring_data === expected.wstring_data, "non-instant reset element wstring"); + ok(found.unichar_data === expected.unichar_data, "non-instant reset element unichar"); +// ok(found.file_data === expected.file_data, "non-instant reset element file" ); + + // check hasUserValue + ok(GetXULElement(aPrefWindow, "tests.static_preference_int" ).hasUserValue === false, "non-instant reset hasUserValue int" ); + ok(GetXULElement(aPrefWindow, "tests.static_preference_bool" ).hasUserValue === false, "non-instant reset hasUserValue bool" ); + ok(GetXULElement(aPrefWindow, "tests.static_preference_string" ).hasUserValue === false, "non-instant reset hasUserValue string" ); + ok(GetXULElement(aPrefWindow, "tests.static_preference_wstring").hasUserValue === false, "non-instant reset hasUserValue wstring"); + ok(GetXULElement(aPrefWindow, "tests.static_preference_unichar").hasUserValue === false, "non-instant reset hasUserValue unichar"); + ok(GetXULElement(aPrefWindow, "tests.static_preference_file" ).hasUserValue === false, "non-instant reset hasUserValue file" ); + } + + function RunNonInstantPrefTestClose(aPrefWindow) + { + WritePrefsToPreferences(aPrefWindow, kPrefValueSet2); + } + + function RunCheckCommandRedirect(aPrefWindow) + { + GetXULElement(aPrefWindow, "checkbox").click(); + ok(GetXULElement(aPrefWindow, "tests.static_preference_bool").value, "redirected command bool"); + GetXULElement(aPrefWindow, "checkbox").click(); + ok(!GetXULElement(aPrefWindow, "tests.static_preference_bool").value, "redirected command bool"); + } + + function RunResetPrefTest(aPrefWindow) + { + // try resetting the prefs to default values + GetXULElement(aPrefWindow, "tests.static_preference_int" ).reset(); + GetXULElement(aPrefWindow, "tests.static_preference_bool" ).reset(); + GetXULElement(aPrefWindow, "tests.static_preference_string" ).reset(); + GetXULElement(aPrefWindow, "tests.static_preference_wstring").reset(); + GetXULElement(aPrefWindow, "tests.static_preference_unichar").reset(); + GetXULElement(aPrefWindow, "tests.static_preference_file" ).reset(); + } + + function InitTestPrefs(aInstantApply) + { + // set instant apply mode and init prefs to set 1 + kPref.setBoolPref("browser.preferences.instantApply", aInstantApply); + WritePrefsToSystem(kPrefValueSet1); + } + + function RunTestInstant() + { + // test with instantApply + InitTestPrefs(true); + openDialog("window_preferences.xul", "", "modal", RunInstantPrefTest, false); + + // - test deferred reset in child window + InitTestPrefs(true); + openDialog("window_preferences2.xul", "", "modal", RunResetPrefTest, false); + expected = kPrefValueSet1; + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "instant reset deferred int" ); + ok(found.bool === expected.bool, "instant reset deferred bool" ); + ok(found.string === expected.string, "instant reset deferred string" ); + ok(found.wstring_data === expected.wstring_data, "instant reset deferred wstring"); + ok(found.unichar_data === expected.unichar_data, "instant reset deferred unichar"); + todo(found.file_data === expected.file_data, "instant reset deferred file" ); + } + + function RunTestNonInstant() + { + // test without instantApply + // - general tests, similar to instant apply + InitTestPrefs(false); + openDialog("window_preferences.xul", "", "modal", RunNonInstantPrefTestGeneral, false); + + // - test Cancel + InitTestPrefs(false); + openDialog("window_preferences.xul", "", "modal", RunNonInstantPrefTestClose, false); + var expected = kPrefValueSet1; + var found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant cancel system int" ); + ok(found.bool === expected.bool, "non-instant cancel system bool" ); + ok(found.string === expected.string, "non-instant cancel system string" ); + ok(found.wstring_data === expected.wstring_data, "non-instant cancel system wstring"); + ok(found.unichar_data === expected.unichar_data, "non-instant cancel system unichar"); + todo(found.file_data === expected.file_data, "non-instant cancel system file" ); + + // - test Accept + InitTestPrefs(false); + openDialog("window_preferences.xul", "", "modal", RunNonInstantPrefTestClose, true); + expected = kPrefValueSet2; + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant accept system int" ); + ok(found.bool === expected.bool, "non-instant accept system bool" ); + ok(found.string === expected.string, "non-instant accept system string" ); + ok(found.wstring_data === expected.wstring_data, "non-instant accept system wstring"); + ok(found.unichar_data === expected.unichar_data, "non-instant accept system unichar"); + todo(found.file_data === expected.file_data, "non-instant accept system file" ); + + // - test deferred reset in child window + InitTestPrefs(false); + openDialog("window_preferences2.xul", "", "modal", RunResetPrefTest, true); + expected = CreateEmptyPrefValueSet(); + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant reset deferred int" ); + ok(found.bool === expected.bool, "non-instant reset deferred bool" ); + ok(found.string === expected.string, "non-instant reset deferred string" ); + ok(found.wstring_data === expected.wstring_data, "non-instant reset deferred wstring"); + ok(found.unichar_data === expected.unichar_data, "non-instant reset deferred unichar"); + ok(found.file_data === expected.file_data, "non-instant reset deferred file" ); + } + + function RunTestCommandRedirect() + { + openDialog("window_preferences_commandretarget.xul", "", "modal", RunCheckCommandRedirect, true); + } + + function RunTest() + { + RunTestInstant(); + RunTestNonInstant(); + RunTestCommandRedirect(); + SimpleTest.finish(); + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_preferences_beforeaccept.xul b/toolkit/content/tests/chrome/test_preferences_beforeaccept.xul new file mode 100644 index 0000000000..a1abad3cc9 --- /dev/null +++ b/toolkit/content/tests/chrome/test_preferences_beforeaccept.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Preferences Window beforeaccept Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/MochiKit/packed.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set":[["browser.preferences.instantApply", false]]}, function() { + + // No instant-apply for this test + var prefWindow = openDialog("window_preferences_beforeaccept.xul", "", "", windowOnload); + + function windowOnload() { + var dialogShown = prefWindow.document.getElementById("tests.beforeaccept.dialogShown"); + var called = prefWindow.document.getElementById("tests.beforeaccept.called"); + is(dialogShown.value, true, "dialog opened, shown pref set"); + is(dialogShown.valueFromPreferences, null, "shown pref not committed"); + is(called.value, null, "beforeaccept not yet called"); + is(called.valueFromPreferences, null, "beforeaccept not yet called, pref not committed"); + + // try to accept the dialog, should fail the first time + prefWindow.document.documentElement.acceptDialog(); + is(prefWindow.closed, false, "window not closed"); + is(dialogShown.value, true, "shown pref still set"); + is(dialogShown.valueFromPreferences, null, "shown pref still not committed"); + is(called.value, true, "beforeaccept called"); + is(called.valueFromPreferences, null, "called pref not committed"); + + // try again, this one should succeed + prefWindow.document.documentElement.acceptDialog(); + is(prefWindow.closed, true, "window now closed"); + is(dialogShown.valueFromPreferences, true, "shown pref committed"); + is(called.valueFromPreferences, true, "called pref committed"); + + SimpleTest.finish(); + } +}); + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_preferences_onsyncfrompreference.xul b/toolkit/content/tests/chrome/test_preferences_onsyncfrompreference.xul new file mode 100644 index 0000000000..8a191d97a1 --- /dev/null +++ b/toolkit/content/tests/chrome/test_preferences_onsyncfrompreference.xul @@ -0,0 +1,62 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- This 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/. --> +<window title="Preferences Window beforeaccept Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/MochiKit/packed.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + const PREFS = ['tests.onsyncfrompreference.pref1', + 'tests.onsyncfrompreference.pref2', + 'tests.onsyncfrompreference.pref3']; + + SimpleTest.waitForExplicitFinish(); + + for (let pref of PREFS) { + SpecialPowers.setIntPref(pref, 1); + } + + let counter = 0; + let prefWindow = openDialog("window_preferences_onsyncfrompreference.xul", "", "", onSync); + + SimpleTest.registerCleanupFunction(() => { + for (let pref of PREFS) { + SpecialPowers.clearUserPref(pref); + } + prefWindow.close(); + }); + + // Onsyncfrompreference handler for the prefs + function onSync() { + for (let pref of PREFS) { + // The `value` field of each <preference> element should be initialized by now. + + is(SpecialPowers.getIntPref(pref), prefWindow.document.getElementById(pref).value, + "Pref constructor was called correctly") + } + + counter++; + + if (counter == PREFS.length) { + SimpleTest.finish(); + } + return true; + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_progressmeter.xul b/toolkit/content/tests/chrome/test_progressmeter.xul new file mode 100644 index 0000000000..7810f69915 --- /dev/null +++ b/toolkit/content/tests/chrome/test_progressmeter.xul @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for progressmeter + --> +<window title="Progressmeter" width="500" height="600" + onload="doTests()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <progressmeter id="n1"/> + <progressmeter id="n2" mode="undetermined"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ +SimpleTest.waitForExplicitFinish(); + +function doTests() { + var n1 = document.getElementById("n1"); + var n2 = document.getElementById("n2"); + + SimpleTest.is(n1.mode, "", "mode determined"); + SimpleTest.is(n2.mode, "undetermined", "mode undetermined"); + + SimpleTest.is(n1.value, "0", "determined value"); + SimpleTest.is(n2.value, "0", "undetermined value"); + + // values can only be incremented in multiples of 4 + n1.value = 2; + SimpleTest.is(n1.value, "0", "determined value set 2"); + n1.value = -1; + SimpleTest.is(n1.value, "0", "determined value set -1"); + n1.value = 125; + SimpleTest.is(n1.value, "100", "determined value set 125"); + n1.value = 7; + SimpleTest.is(n1.value, "7", "determined value set 7"); + n1.value = "17"; + SimpleTest.is(n1.value, "17", "determined value set 17 string"); + n1.value = 18; + SimpleTest.is(n1.value, "17", "determined value set 18"); + n1.value = "Cat"; + SimpleTest.is(n1.value, "17", "determined value set invalid"); + + n1.max = 200; + is(n1.max, "200", "max changed"); + n1.value = 150; + n1.max = 120; + is(n1.value, "120", "max lowered below value"); + + n2.value = 2; + SimpleTest.is(n2.value, "0", "undetermined value set 2"); + n2.value = -1; + SimpleTest.is(n2.value, "0", "undetermined value set -1"); + n2.value = 125; + SimpleTest.is(n2.value, "100", "undetermined value set 125"); + n2.value = 7; + SimpleTest.is(n2.value, "7", "undetermined value set 7"); + n2.value = "17"; + SimpleTest.is(n2.value, "17", "undetermined value set 17 string"); + n2.value = 18; + SimpleTest.is(n2.value, "17", "undetermined value set 18"); + n2.value = "Cat"; + SimpleTest.is(n2.value, "17", "determined value set invalid"); + + SimpleTest.finish(); +} + +]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_props.xul b/toolkit/content/tests/chrome/test_props.xul new file mode 100644 index 0000000000..17513df5c7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_props.xul @@ -0,0 +1,91 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for basic properties - this test checks that the basic + properties defined in general.xml and inherited by a number of elements + work properly. + --> +<window title="Basic Properties Test" + onload="setTimeout(test_props, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<command id="cmd_nothing"/> +<command id="cmd_action"/> + +<button id="button" label="Button" accesskey="B" + crop="end" image="happy.png" command="cmd_nothing"/> +<checkbox id="checkbox" label="Checkbox" accesskey="B" + crop="end" image="happy.png" command="cmd_nothing"/> +<radiogroup> + <radio id="radio" label="Radio Button" value="rb1" accesskey="B" + crop="end" image="happy.png" command="cmd_nothing"/> +</radiogroup> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_props() +{ + test_props_forelement($("button"), "Button", null); + test_props_forelement($("checkbox"), "Checkbox", null); + test_props_forelement($("radio"), "Radio Button", "rb1"); + + SimpleTest.finish(); +} + +function test_props_forelement(element, label, value) +{ + // check the initial values + is(element.label, label, "element label"); + if (value) + is(element.value, value, "element value"); + is(element.accessKey, "B", "element accessKey"); + is(element.crop, "end", "element crop"); + is(element.image, "happy.png", "element image"); + is(element.command, "cmd_nothing", "element command"); + ok(element.tabIndex === 0, "element tabIndex"); + + synthesizeMouseExpectEvent(element, 4, 4, { }, $("cmd_nothing"), "command", "element"); + + // make sure that setters return the value + is(element.label = "Label", "Label", "element label setter return"); + if (value) + is(element.value = "lb", "lb", "element value setter return"); + is(element.accessKey = "L", "L", "element accessKey setter return"); + is(element.crop = "start", "start", "element crop setter return"); + is(element.image = "sad.png", "sad.png", "element image setter return"); + is(element.command = "cmd_action", "cmd_action", "element command setter return"); + + // check the value after it was changed + is(element.label, "Label", "element label after set"); + if (value) + is(element.value, "lb", "element value after set"); + is(element.accessKey, "L", "element accessKey after set"); + is(element.crop, "start", "element crop after set"); + is(element.image, "sad.png", "element image after set"); + is(element.command, "cmd_action", "element command after set"); + + synthesizeMouseExpectEvent(element, 4, 4, { }, $("cmd_action"), "command", "element"); + + // check that clicks on disabled items don't fire a command event + ok((element.disabled = true) === true, "element disabled setter return"); + ok(element.disabled === true, "element disabled after set"); + synthesizeMouseExpectEvent(element, 4, 4, { }, $("cmd_action"), "!command", "element"); + + element.disabled = false; +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_radio.xul b/toolkit/content/tests/chrome/test_radio.xul new file mode 100644 index 0000000000..74ab66a34e --- /dev/null +++ b/toolkit/content/tests/chrome/test_radio.xul @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for radio buttons + --> +<window title="Radio Buttons" width="500" height="600" + onload="setTimeout(test_radio, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + <script type="application/javascript" src="xul_selectcontrol.js"/> + +<radiogroup id="radiogroup"/> + +<radiogroup id="radiogroup-initwithvalue" value="two"> + <radio label="One" value="one"/> + <radio label="Two" value="two"/> + <radio label="Three" value="three"/> +</radiogroup> +<radiogroup id="radiogroup-initwithselected" value="two"> + <radio id="one" label="One" value="one" accesskey="o"/> + <radio id="two" label="Two" value="two" accesskey="t"/> + <radio label="Three" value="three" selected="true"/> +</radiogroup> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_radio() +{ + var element = document.getElementById("radiogroup"); + test_nsIDOMXULSelectControlElement(element, "radio", null); + test_nsIDOMXULSelectControlElement_UI(element, null); + + window.blur(); + + var accessKeyDetails = (navigator.platform.indexOf("Mac") >= 0) ? + { altKey : true, ctrlKey : true } : + { altKey : true, shiftKey: true }; + synthesizeKey("t", accessKeyDetails); + + var radiogroup = $("radiogroup-initwithselected"); + is(document.activeElement, radiogroup, "accesskey focuses radiogroup"); + is(radiogroup.selectedItem, $("two"), "accesskey selects radio"); + + $("radiogroup-initwithvalue").focus(); + + $("one").disabled = true; + synthesizeKey("o", accessKeyDetails); + + is(document.activeElement, $("radiogroup-initwithvalue"), "accesskey on disabled radio doesn't focus"); + is(radiogroup.selectedItem, $("two"), "accesskey on disabled radio doesn't change selection"); + + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_richlist_direction.xul b/toolkit/content/tests/chrome/test_richlist_direction.xul new file mode 100644 index 0000000000..f94f1b3ba6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_richlist_direction.xul @@ -0,0 +1,138 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for listbox direction + --> +<window title="Listbox direction test" + onload="test_richlistbox()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + + <richlistbox seltype="multiple" id="richlistbox" flex="1" minheight="80" maxheight="80" height="80" /> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var richListBox = document.getElementById("richlistbox"); + +function getScrollIndexAmount(aDirection) { + return (4 * aDirection + richListBox.currentIndex); +} + +function test_richlistbox() +{ + richListBox.minHeight = richListBox.maxHeight = richListBox.height = + 80 + (80 - richListBox.scrollBoxObject.height); + var height = richListBox.scrollBoxObject.height; + var item; + do { + item = richListBox.appendItem("Test", ""); + item.height = item.minHeight = item.maxHeight = Math.floor(height / 4); + } while (item.getBoundingClientRect().bottom < (height * 2)) + richListBox.appendItem("Test", ""); + richListBox.firstChild.nextSibling.id = "list-box-first"; + richListBox.lastChild.previousSibling.id = "list-box-last"; + + // direction = "reverse", the values here are backwards due to the fact that + // richlistboxes respond differently when a user initiates a selection + richListBox.dir = "reverse"; + var count = richListBox.itemCount; + richListBox.focus(); + richListBox.selectedIndex = count - 1; + sendKey("DOWN"); + is(richListBox.currentIndex, count - 2, "Selection should move to the next item"); + sendKey("UP"); + is(richListBox.currentIndex, count - 1, "Selection should move to the previous item"); + sendKey("END"); + is(richListBox.currentIndex, 0, "Selection should move to the last item"); + sendKey("HOME"); + is(richListBox.currentIndex, count - 1, "Selection should move to the first item"); + var currentIndex = richListBox.currentIndex; + var index = getScrollIndexAmount(-1); + sendKey("PAGE_DOWN"); + is(richListBox.currentIndex, index, "Selection should move to one page down"); + ok(richListBox.currentIndex < currentIndex, "Selection should move downwards"); + sendKey("END"); + currentIndex = richListBox.currentIndex; + index = getScrollIndexAmount(1); + sendKey("PAGE_UP"); + is(richListBox.currentIndex, index, "Selection should move to one page up"); + ok(richListBox.currentIndex > currentIndex, "Selection should move upwards"); + richListBox.selectedItem = richListBox.lastChild; + richListBox.focus(); + synthesizeKey("KEY_ArrowDown", { shiftKey: true, code: "ArrowDown" }, window); + let items = [richListBox.selectedItems[0], + richListBox.selectedItems[1]]; + is(items[0], richListBox.lastChild, "The last element should still be selected"); + is(items[1], richListBox.lastChild.previousSibling, "Both elements should now be selected"); + richListBox.clearSelection(); + richListBox.selectedItem = richListBox.lastChild; + sendMouseEvent({type: "click", shiftKey: true, clickCount: 1}, + "list-box-last", + window); + items = [richListBox.selectedItems[0], + richListBox.selectedItems[1]]; + is(items[0], richListBox.lastChild, "The last element should still be selected"); + is(items[1], richListBox.lastChild.previousSibling, "Both elements should now be selected"); + richListBox.addEventListener("keypress", function(aEvent) { + richListBox.removeEventListener("keypress", arguments.callee, true); + aEvent.preventDefault(); + }, true); + richListBox.selectedIndex = 1; + sendKey("HOME"); + is(richListBox.selectedIndex, 1, "A stopped event should return indexing to normal"); + + // direction = "normal" + richListBox.dir = "normal"; + richListBox.selectedIndex = 0; + sendKey("DOWN"); + is(richListBox.currentIndex, 1, "Selection should move to the next item"); + sendKey("UP"); + is(richListBox.currentIndex, 0, "Selection should move to the previous item"); + sendKey("END"); + is(richListBox.currentIndex, count - 1, "Selection should move to the last item"); + sendKey("HOME"); + is(richListBox.currentIndex, 0, "Selection should move to the first item"); + var currentIndex = richListBox.currentIndex; + var index = richListBox.scrollOnePage(1); + sendKey("PAGE_DOWN"); + is(richListBox.currentIndex, index, "Selection should move to one page down"); + ok(richListBox.currentIndex > currentIndex, "Selection should move downwards"); + sendKey("END"); + currentIndex = richListBox.currentIndex; + index = richListBox.scrollOnePage(-1) + richListBox.currentIndex; + sendKey("PAGE_UP"); + is(richListBox.currentIndex, index, "Selection should move to one page up"); + ok(richListBox.currentIndex < currentIndex, "Selection should move upwards"); + richListBox.selectedItem = richListBox.firstChild; + richListBox.focus(); + synthesizeKey("KEY_ArrowDown", { shiftKey: true, code: "ArrowDown" }, window); + items = [richListBox.selectedItems[0], + richListBox.selectedItems[1]]; + is(items[0], richListBox.firstChild, "The last element should still be selected"); + is(items[1], richListBox.firstChild.nextSibling, "Both elements should now be selected"); + richListBox.clearSelection(); + richListBox.selectedItem = richListBox.firstChild; + sendMouseEvent({type: "click", shiftKey: true, clickCount: 1}, + "list-box-first", + window); + items = [richListBox.selectedItems[0], + richListBox.selectedItems[1]]; + is(items[0], richListBox.firstChild, "The last element should still be selected"); + is(items[1], richListBox.firstChild.nextSibling, "Both elements should now be selected"); + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_righttoleft.xul b/toolkit/content/tests/chrome/test_righttoleft.xul new file mode 100644 index 0000000000..64b1419daa --- /dev/null +++ b/toolkit/content/tests/chrome/test_righttoleft.xul @@ -0,0 +1,126 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window title="Right to Left UI Test" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="chrome://mochikit/content/chrome-harness.js"></script> + <script type="application/javascript" + src="RegisterUnregisterChrome.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <iframe id="subframe" width="100" height="100" onload="frameLoaded();"/> + + <script type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + let prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + const UI_DIRECTION_PREF = "intl.uidirection.ar"; + prefs.setCharPref(UI_DIRECTION_PREF, "rtl"); + + let rootDir = getRootDirectory(window.location.href); + registerManifestPermanently(rootDir + "rtltest/righttoleft.manifest"); + + function runTest() + { + var subframe = document.getElementById("subframe"); + subframe.setAttribute("src", "chrome://ltrtest/content/dirtest.xul"); + } + + function frameLoaded() + { + var subframe = document.getElementById("subframe"); + var subwin = subframe.contentWindow; + var subdoc = subframe.contentDocument; + var url = String(subwin.location); + if (url.indexOf("chrome://ltrtest") == 0) { + is(subwin.getComputedStyle(subdoc.getElementById("hbox"), "").backgroundColor, + "rgb(255, 255, 0)", "left to right with :-moz-locale-dir(ltr)"); + is(subwin.getComputedStyle(subdoc.getElementById("vbox"), "").backgroundColor, + "rgb(255, 255, 255)", "left to right with :-moz-locale-dir(rtl)"); + + is(subwin.getComputedStyle(subdoc.documentElement, "").direction, "ltr", + "left to right direction"); + + subdoc.documentElement.setAttribute("localedir", "rtl"); + + is(subwin.getComputedStyle(subdoc.getElementById("hbox"), "").backgroundColor, + "rgb(255, 255, 255)", "left to right with :-moz-locale-dir(ltr) and localedir='rtl'"); + is(subwin.getComputedStyle(subdoc.getElementById("vbox"), "").backgroundColor, + "rgb(0, 128, 0)", "left to right with :-moz-locale-dir(rtl) and localedir='rtl'"); + is(subwin.getComputedStyle(subdoc.documentElement, "").direction, "rtl", + "left to right direction with localedir='rtl'"); + + subdoc.documentElement.removeAttribute("localedir"); + + is(subwin.getComputedStyle(subdoc.getElementById("hbox"), "").backgroundColor, + "rgb(255, 255, 0)", "left to right with :-moz-locale-dir(ltr) and localedir removed"); + is(subwin.getComputedStyle(subdoc.getElementById("vbox"), "").backgroundColor, + "rgb(255, 255, 255)", "left to right with :-moz-locale-dir(rtl) and localedir removed"); + is(subwin.getComputedStyle(subdoc.documentElement, "").direction, "ltr", + "left to right direction with localedir removed"); + + subframe.setAttribute("src", "chrome://rtltest/content/dirtest.xul"); + } + else if (url.indexOf("chrome://rtltest") == 0) { + is(subwin.getComputedStyle(subdoc.getElementById("hbox"), "").backgroundColor, + "rgb(255, 255, 255)", "right to left with :-moz-locale-dir(ltr)"); + is(subwin.getComputedStyle(subdoc.getElementById("vbox"), "").backgroundColor, + "rgb(0, 128, 0)", "right to left with :-moz-locale-dir(rtl)"); + is(subwin.getComputedStyle(subdoc.documentElement, "").direction, "rtl", + "right to left direction"); + + subdoc.documentElement.setAttribute("localedir", "ltr"); + + is(subwin.getComputedStyle(subdoc.getElementById("hbox"), "").backgroundColor, + "rgb(255, 255, 0)", "right to left with :-moz-locale-dir(ltr) and localedir='ltr'"); + is(subwin.getComputedStyle(subdoc.getElementById("vbox"), "").backgroundColor, + "rgb(255, 255, 255)", "right to left with :-moz-locale-dir(rtl) and localedir='ltr'"); + is(subwin.getComputedStyle(subdoc.documentElement, "").direction, "ltr", + "right to left direction with localedir='ltr'"); + + subdoc.documentElement.removeAttribute("localedir"); + + prefs.setCharPref(UI_DIRECTION_PREF, ""); + is(subwin.getComputedStyle(subdoc.documentElement, "").direction, "ltr", + "left to right direction with no preference set"); + prefs.setCharPref(UI_DIRECTION_PREF + "-QA", "rtl"); + is(subwin.getComputedStyle(subdoc.documentElement, "").direction, "rtl", + "right to left direction with more specific preference set"); + prefs.setCharPref(UI_DIRECTION_PREF, "ltr"); + is(subwin.getComputedStyle(subdoc.documentElement, "").direction, "rtl", + "right to left direction with less specific and more specific preference set"); + prefs.setCharPref(UI_DIRECTION_PREF, "rtl"); + prefs.setCharPref(UI_DIRECTION_PREF + "-QA", "ltr"); + is(subwin.getComputedStyle(subdoc.documentElement, "").direction, "ltr", + "left to right direction specific preference overrides"); + if (prefs.prefHasUserValue(UI_DIRECTION_PREF + "-QA")) + prefs.clearUserPref(UI_DIRECTION_PREF + "-QA"); + + if (prefs.prefHasUserValue(UI_DIRECTION_PREF)) + prefs.clearUserPref(UI_DIRECTION_PREF); + + SimpleTest.finish(); + } + } + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_scale.xul b/toolkit/content/tests/chrome/test_scale.xul new file mode 100644 index 0000000000..c1adea7ffc --- /dev/null +++ b/toolkit/content/tests/chrome/test_scale.xul @@ -0,0 +1,277 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for scale + --> +<window title="scale" width="500" height="600" + onload="setTimeout(testtag_scale, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<hbox> + <vbox> + <scale id="scale-horizontal-normal"/> + <scale id="scale-horizontal-reverse" dir="reverse"/> + </vbox> + <scale id="scale-vertical-normal" orient="vertical"/> + <scale id="scale-vertical-reverse" orient="vertical" dir="reverse"/> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function testtag_scale() +{ + testtag_scale_inner("scale-horizontal-normal", true, false); + testtag_scale_inner("scale-horizontal-reverse", true, true); + testtag_scale_inner("scale-vertical-normal", false, false); + testtag_scale_inner("scale-vertical-reverse", false, true); + + SimpleTest.finish(); +} + +function testtag_scale_inner(elementid, horiz, reverse) +{ + var testid = (horiz ? "horizontal " : "vertical ") + + (reverse ? "reverse " : "normal "); + + var element = document.getElementById(elementid); + + testtag_scale_States(element, 0, "", 0, 100, testid + "initial"); + + element.min = 0; + element.max = 10; + testtag_scale_States(element, 0, "", 0, 10, testid + "first set"); + + element.decrease(); + is(element.value, 0, testid + "decrease"); + element.increase(); + is(element.value, 1, testid + "increase"); + element.value = 0; + is(element.value, 0, testid + "set value"); + + testtag_scale_Increments(element, 0, 10, 6, 7, testid + "increase decrease"); + + // check if changing the min and max adjusts the value to fit in range + element.min = 5; + testtag_scale_States(element, 5, "5", 5, 10, testid + "change min"); + element.value = 15; + is(element.value, 10, testid + "change minmax value set too high"); + element.max = 8; + is(element.value, 8, testid + "change max"); + element.value = 2; + is(element.value, 5, testid + "change minmax set too low"); + + // check negative values + element.min = -15; + element.max = -5; + testtag_scale_States(element, -5, "-5", -15, -5, testid + "minmax negative"); + element.value = -15; + is(element.value, -15, testid + "change max negative"); + testtag_scale_Increments(element, -15, -5, 7, 8, testid + "increase decrease negative"); + + // check case when min is negative and max is positive + element.min = -10; + element.max = 35; + testtag_scale_States(element, -10, "-10", -10, 35, testid + "minmax mixed sign"); + testtag_scale_Increments(element, -10, 35, 25, 30, testid + "increase decrease mixed sign"); + + testtag_scale_UI(element, testid, horiz, reverse); +} + +function testtag_scale_UI(element, testid, horiz, reverse) +{ + element.min = 0; + element.max = 20; + element.value = 7; + element.increment = 2; + element.pageIncrement = 4; + + element.focus(); + + var leftIncrements = horiz && reverse; + var upDecrements = !horiz && !reverse; + synthesizeKeyExpectEvent("VK_LEFT", { }, element, "change", testid + "key left"); + is(element.value, leftIncrements ? 9 : 5, testid + " key left"); + synthesizeKeyExpectEvent("VK_RIGHT", { }, element, "change", testid + "key right"); + is(element.value, 7, testid + " key right"); + synthesizeKeyExpectEvent("VK_UP", { }, element, "change", testid + "key up"); + is(element.value, upDecrements ? 5 : 9, testid + " key up"); + synthesizeKeyExpectEvent("VK_DOWN", { }, element, "change", testid + "key down"); + is(element.value, 7, testid + " key down"); + + synthesizeKeyExpectEvent("VK_PAGE_UP", { }, element, "change", testid + "key page up"); + is(element.value, upDecrements ? 3 : 11, testid + " key page up"); + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { }, element, "change", testid + "key page down"); + is(element.value, 7, testid + " key page down"); + + synthesizeKeyExpectEvent("VK_HOME", { }, element, "change", testid + "key home"); + is(element.value, reverse ? 20 : 0, testid + " key home"); + synthesizeKeyExpectEvent("VK_END", { }, element, "change", testid + "key end"); + is(element.value, reverse ? 0 : 20, testid + " key end"); + + testtag_scale_UI_Mouse(element, testid, horiz, reverse, 4); + + element.min = 4; + element.pageIncrement = 3; + testtag_scale_UI_Mouse(element, testid + " with min", horiz, reverse, 3); + + element.pageIncrement = 30; + testtag_scale_UI_Mouse(element, testid + " with min past", horiz, reverse, 30); +} + +function testtag_scale_UI_Mouse(element, testid, horiz, reverse, pinc) +{ + var initial = reverse ? 8 : 12; + var newval = initial + (reverse ? pinc : -pinc); + var endval = initial; + // in the pinc == 30 case, the page increment is large enough that it would + // just cause the thumb to reach the end of the scale. Make sure that the + // mouse click does not go past the end. + if (pinc == 30) { + newval = reverse ? 20 : 4; + endval = reverse ? 4 : 20; + } + element.value = initial; + + var hmove = horiz ? 25 : 10; + var vmove = horiz ? 10 : 25; + + var leftFn = function () { return reverse ? element.value < newval + 3 : element.value > newval - 3; } + var rightFn = function () { return reverse ? element.value < endval - 3 : element.value < endval + 3; } + + // check that clicking the mouse on the trough moves the thumb properly + synthesizeMouseExpectEvent(element, hmove, vmove, { }, element, "change", testid + "mouse on left movetoclick=default"); + + if (navigator.platform.indexOf("Mac") >= 0) { + if (pinc == 30) + ok(element.value > 4, testid + " mouse on left movetoclick=default"); + else + ok(leftFn, testid + " mouse on left movetoclick=default"); + } + else { + is(element.value, newval, testid + " mouse on left movetoclick=default"); + } + + var rect = element.getBoundingClientRect(); + synthesizeMouseExpectEvent(element, rect.right - rect.left - hmove, + rect.bottom - rect.top - vmove, { }, + element, "change", testid + " mouse on right movetoclick=default"); + + if (navigator.platform.indexOf("Mac") >= 0) { + if (pinc == 30) + ok(element.value < 20, testid + " mouse on right movetoclick=default"); + else + ok(rightFn, testid + " mouse on right movetoclick=default"); + } + else { + is(element.value, endval, testid + " mouse on right movetoclick=default"); + } + + element.setAttribute("movetoclick", "true"); + element.value = initial; + + synthesizeMouseExpectEvent(element, hmove, vmove, { }, element, "change", testid + "mouse on left movetoclick=true"); + if (pinc == 30) + ok(element.value > 4, testid + " mouse on left movetoclick=true"); + else + ok(leftFn, testid + " mouse on left movetoclick=true"); + + var rect = element.getBoundingClientRect(); + synthesizeMouseExpectEvent(element, rect.right - rect.left - hmove, + rect.bottom - rect.top - vmove, { }, + element, "change", testid + " mouse on right movetoclick=true"); + if (pinc == 30) + ok(element.value < 20, testid + " mouse on right movetoclick=true"); + else + ok(rightFn, testid + " mouse on right movetoclick=true"); + + element.setAttribute("movetoclick", "false"); + element.value = initial; + + synthesizeMouseExpectEvent(element, hmove, vmove, { }, element, "change", testid + "mouse on left movetoclick=false"); + is(element.value, newval, testid + " mouse on left movetoclick=false"); + + var rect = element.getBoundingClientRect(); + synthesizeMouseExpectEvent(element, rect.right - rect.left - hmove, + rect.bottom - rect.top - vmove, { }, + element, "change", testid + " mouse on right movetoclick=false"); + is(element.value, endval, testid + " mouse on right movetoclick=false"); + + element.removeAttribute("movetoclick"); + + element.value = reverse ? element.max : element.min; + + synthesizeMouse(element, 8, 8, { type: "mousedown" }); + synthesizeMouse(element, horiz ? 2000 : 8, horiz ? 8 : 2000, { type: "mousemove" }); + is(element.value, reverse ? element.min : element.max, testid + " move mouse too far after end"); + synthesizeMouse(element, 2, 2, { type: "mouseup" }); + + synthesizeMouse(element, rect.width - 8, rect.height - 8, { type: "mousedown" }); + synthesizeMouse(element, horiz ? -2000 : rect.width - 8, horiz ? rect.height - 8 : -2000, { type: "mousemove" }); + is(element.value, reverse ? element.max : element.min, testid + " move mouse too far before start"); + + synthesizeMouse(element, 2, 2, { type: "mouseup" }); + + // now check if moving outside in both directions works. On Windows, + // it should snap back to the original location. + element.value = reverse ? element.max : element.min; + + var expected = (navigator.platform.indexOf("Win") >= 0) ? element.value : + (reverse ? element.min : element.max); + synthesizeMouse(element, 7, 7, { type: "mousedown" }); + synthesizeMouse(element, 2000, 2000, { type: "mousemove" }); + is(element.value, expected, testid + " move mouse ouside in both directions"); + synthesizeMouse(element, 2, 2, { type: "mouseup" }); +} + +function testtag_scale_States(element, evalue, evalueattr, emin, emax, testid) +{ + is(element.getAttribute("value"), evalueattr, testid + " value attribute"); + is(element.value, evalue, testid + " value"); + is(element.min, emin, testid + " min"); + is(element.max, emax, testid + " max"); +} + +function testtag_scale_Increments(element, min, max, increment, pageIncrement, testid) +{ + // check the increase and decrease methods + element.increment = increment; + element.increase(); + is(element.value, min + increment, testid + " increase 1"); + element.increase(); + is(element.value, max, testid + " increase 2"); + element.decrease(); + is(element.value, max - increment, testid + " decrease 1"); + element.decrease(); + is(element.value, min, testid + " decrease 2"); + + // check the increasePage and decreasePage methods + element.pageIncrement = pageIncrement; + element.increasePage(); + is(element.value, min + pageIncrement, testid + " increasePage 1"); + element.increasePage(); + is(element.value, max, testid + " increasePage 2"); + element.decreasePage(); + is(element.value, max - pageIncrement, testid + " decreasePage 1"); + element.decreasePage(); + is(element.value, min, testid + " decreasePage 2"); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_scaledrag.xul b/toolkit/content/tests/chrome/test_scaledrag.xul new file mode 100644 index 0000000000..82356ca1ec --- /dev/null +++ b/toolkit/content/tests/chrome/test_scaledrag.xul @@ -0,0 +1,197 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +XUL <scale> dragging tests +--> +<window title="Dragging XUL scale tests" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + + <hbox flex="1"> + <scale id="scale1" orient="horizontal" flex="1" min="0" max="4" value="2"/> + <scale id="scale2" orient="vertical" flex="1" min="0" max="4" value="2"/> + <scale id="scale3" orient="horizontal" flex="1" movetoclick="true" min="0" max="4" value="2"/> + </hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +function getThumb(aScale) { + return document.getAnonymousElementByAttribute(aScale, "class", "scale-thumb"); +} + +function sendTouch(aType, aRect, aDX, aDY, aMods) { + var cwu = SpecialPowers.getDOMWindowUtils(window); + var x = aRect.left + aRect.width/2 + aDX; + var y = aRect.top + aRect.height/2 + aDY; + if (/mouse/.test(aType)) + cwu.sendMouseEvent(aType, x, y, 0, 1, aMods || 0, false); + else + cwu.sendTouchEvent(aType, [0], [x], [y], [1], [1], [0], [1], 1, aMods || 0, true); +} + +function getOffset(aScale, aDir) { + var rect = aScale.getBoundingClientRect(); + var d = aScale.orient == "horizontal" ? rect.width/4 : rect.height/4; + switch (aDir) { + case "right": return [ d, 0]; + case "left": return [-1*d, 0]; + case "up": return [ 0,-1*d]; + case "down": return [ 0, d]; + case "downleft": return [ -1*d, d]; + case "upleft": return [ -1*d,-1*d]; + case "downright": return [d, d]; + case "upright": return [d,-1*d]; + } + return [0,0]; +} + +function testTouchDragThumb(aDesc, aId, aDir, aVal1, aVal2, aMods) { + info(aDesc); + var scale = document.getElementById(aId); + var [x,y] = getOffset(scale, aDir); + + sendTouch("touchstart", getThumb(scale).getBoundingClientRect(), 0, 0, aMods); + is(scale.value, aVal1, "Touchstart on thumb has correct value"); + sendTouch("touchmove", getThumb(scale).getBoundingClientRect(), x, y, aMods); + sendTouch("touchend", getThumb(scale).getBoundingClientRect(), 0, 0, aMods); + is(scale.value, aVal2, "After touch " + (aDir ? ("and drag " + aDir + " ") : "") + "on thumb, scale has correct value"); + + scale.value = 2; +} + +function testMouseDragThumb(aDesc, aId, aDir, aVal1, aVal2, aMods) { + info(aDesc); + var scale = document.getElementById(aId); + var [x,y] = getOffset(scale, aDir); + + sendTouch("mousedown", getThumb(scale).getBoundingClientRect(), 0, 0, aMods); + is(scale.value, aVal1, "Mousedown on thumb has correct value"); + sendTouch("mousemove", getThumb(scale).getBoundingClientRect(), x, y, aMods); + sendTouch("mouseup", getThumb(scale).getBoundingClientRect(), 0, 0, aMods); + is(scale.value, aVal2, "After mouseup " + (aDir ? ("and drag " + aDir + " ") : "") + "on thumb, scale has correct value"); + + scale.value = 2; +} + +function testTouchDragSlider(aDesc, aId, aDir, aVal1, aVal2, aMods) { + info(aDesc); + var scale = document.getElementById(aId); + var [x,y] = getOffset(scale, aDir); + + sendTouch("touchstart", getThumb(scale).getBoundingClientRect(), x, y, aMods); + is(scale.value, aVal1, "Touchstart on slider has correct value"); + sendTouch("touchmove", getThumb(scale).getBoundingClientRect(), -x, -y, aMods); + sendTouch("touchend", getThumb(scale).getBoundingClientRect(), 0, 0, aMods); + is(scale.value, aVal2, "After touch " + (aDir ? ("and drag " + aDir + " ") : "") + "on slider, scale has correct value"); + + scale.value = 2; +} + +function testMouseDragSlider(aDesc, aId, aDir, aVal1, aVal2, aMods) { + info(aDesc); + var scale = document.getElementById(aId); + var [x,y] = getOffset(scale, aDir); + + sendTouch("mousedown", getThumb(scale).getBoundingClientRect(), x, y, aMods); + is(scale.value, aVal1, "Mousedown on slider has correct value"); + sendTouch("mousemove", getThumb(scale).getBoundingClientRect(), -x, -y, aMods); + sendTouch("mouseup", getThumb(scale).getBoundingClientRect(), 0, 0, aMods); + is(scale.value, aVal2, "After mouseup " + (aDir ? ("and drag " + aDir + " ") : "") + "on slider, scale has correct value"); + + scale.value = 2; +} + +function runTests() { + // test dragging a horizontal slider with touch events by tapping on the thumb + testTouchDragThumb("Touch Horizontal Thumb", "scale1", "", 2, 2); + testTouchDragThumb("TouchDrag Horizontal Thumb Left", "scale1", "left", 2, 1); + testTouchDragThumb("TouchDrag Horizontal Thumb Right", "scale1", "right", 2, 3); + testTouchDragThumb("TouchDrag Horizontal Thumb Up", "scale1", "up", 2, 2); + testTouchDragThumb("TouchDrag Horizontal Thumb Down", "scale1", "down", 2, 2); + testTouchDragThumb("TouchDrag Horizontal Thumb Downleft", "scale1", "downleft", 2, 1); + testTouchDragThumb("TouchDrag Horizontal Thumb Upleft", "scale1", "upleft", 2, 1); + testTouchDragThumb("TouchDrag Horizontal Thumb Upright", "scale1", "upright", 2, 3); + testTouchDragThumb("TouchDrag Horizontal Thumb Downright", "scale1", "downright", 2, 3); + + // test dragging a horizontal slider with mouse events by clicking on the thumb + testMouseDragThumb("Click Horizontal Thumb", "scale1", "", 2, 2); + testMouseDragThumb("MouseDrag Horizontal Thumb Left", "scale1", "left", 2, 1); + testMouseDragThumb("MouseDrag Horizontal Thumb Right", "scale1", "right", 2, 3); + testMouseDragThumb("MouseDrag Horizontal Thumb Up", "scale1", "up", 2, 2); + testMouseDragThumb("MouseDrag Horizontal Thumb Down", "scale1", "down", 2, 2); + testMouseDragThumb("MouseDrag Horizontal Thumb Downleft", "scale1", "downleft", 2, 1); + testMouseDragThumb("MouseDrag Horizontal Thumb Upleft", "scale1", "upleft", 2, 1); + testMouseDragThumb("MouseDrag Horizontal Thumb Upright", "scale1", "upright", 2, 3); + testMouseDragThumb("MouseDrag Horizontal Thumb Downright", "scale1", "downright", 2, 3); + + // test dragging a vertical slider with touch events by tapping on the thumb + testTouchDragThumb("Touch Vertical Thumb", "scale2", "", 2, 2); + testTouchDragThumb("TouchDrag Vertical Thumb Left", "scale2", "left", 2, 2); + testTouchDragThumb("TouchDrag Vertical Thumb Right", "scale2", "right", 2, 2); + testTouchDragThumb("TouchDrag Vertical Thumb Up", "scale2", "up", 2, 1); + testTouchDragThumb("TouchDrag Vertical Thumb Down", "scale2", "down", 2, 3); + testTouchDragThumb("TouchDrag Vertical Thumb Downleft", "scale2", "downleft", 2, 3); + testTouchDragThumb("TouchDrag Vertical Thumb Upleft", "scale2", "upleft", 2, 1); + testTouchDragThumb("TouchDrag Vertical Thumb Upright", "scale2", "upright", 2, 1); + testTouchDragThumb("TouchDrag Vertical Thumb Downright", "scale2", "downright", 2, 3); + + // test dragging a vertical slider with mouse events by clicking on the thumb + testMouseDragThumb("Click Vertical Thumb", "scale2", "", 2, 2); + testMouseDragThumb("MouseDrag Vertical Thumb Left", "scale2", "left", 2, 2); + testMouseDragThumb("MouseDrag Vertical Thumb Right", "scale2", "right", 2, 2); + testMouseDragThumb("MouseDrag Vertical Thumb Up", "scale2", "up", 2, 1); + testMouseDragThumb("MouseDrag Vertical Thumb Down", "scale2", "down", 2, 3); + testMouseDragThumb("MouseDrag Vertical Thumb Downleft", "scale2", "downleft", 2, 3); + testMouseDragThumb("MouseDrag Vertical Thumb Upleft", "scale2", "upleft", 2, 1); + testMouseDragThumb("MouseDrag Vertical Thumb Upright", "scale2", "upright", 2, 1); + testMouseDragThumb("MouseDrag Vertical Thumb Downright", "scale2", "downright", 2, 3); + + var isMac = /Mac/.test(navigator.platform); + + // test dragging a slider by tapping off the thumb + testTouchDragSlider("TouchDrag Slider Left", "scale1", "left", isMac ? 1 : 0, isMac ? 2 : 0); + testTouchDragSlider("TouchDrag Slider Right", "scale1", "right", isMac ? 3 : 4, isMac ? 2 : 4); + testMouseDragSlider("MouseDrag Slider Left", "scale1", "left", isMac ? 1 : 0, isMac ? 2 : 0); + testMouseDragSlider("MouseDrag Slider Right", "scale1", "right", isMac ? 3 : 4, isMac ? 2 : 4); + + // test dragging a slider by tapping off the thumb and holding shift + // modifiers don't affect touch events + var mods = /Mac/.test(navigator.platform) ? Components.interfaces.nsIDOMNSEvent.ALT_MASK : + Components.interfaces.nsIDOMNSEvent.SHIFT_MASK; + testTouchDragSlider("TouchDrag Slider Left+Shift", "scale1", "left", isMac ? 1 : 0, isMac ? 2 : 0, mods); + testTouchDragSlider("TouchDrag Slider Right+Shift", "scale1", "right", isMac ? 3 : 4, isMac ? 2 : 4, mods); + testMouseDragSlider("MouseDrag Slider Left+Shift", "scale1", "left", isMac ? 0 : 1, isMac ? 0 : 2, mods); + testMouseDragSlider("MouseDrag Slider Right+Shift", "scale1", "right", isMac ? 4 : 3, isMac ? 4 : 2, mods); + + // test dragging a slider with movetoclick="true" by tapping off the thumb + testTouchDragSlider("TouchDrag Slider Left+MoveToClick", "scale3", "left", 1, 2); + testTouchDragSlider("TouchDrag Slider Right+MoveToClick", "scale3", "right", 3, 2); + testMouseDragSlider("MouseDrag Slider Left+MoveToClick", "scale3", "left", 1, 2); + testMouseDragSlider("MouseDrag Slider Right+MoveToClick", "scale3", "right", 3, 2); + + // test dragging a slider by tapping off the thumb and holding shift + // modifiers don't affect touch events + testTouchDragSlider("MouseDrag Slider Left+MoveToClick+Shift", "scale3", "left", 1, 2, mods); + testTouchDragSlider("MouseDrag Slider Right+MoveToClick+Shift", "scale3", "right", 3, 2, mods); + testMouseDragSlider("MouseDrag Slider Left+MoveToClick+Shift", "scale3", "left", 0, 0, mods); + testMouseDragSlider("MouseDrag Slider Right+MoveToClick+Shift", "scale3", "right", 4, 4, mods); + + SimpleTest.finish(); +} + +addLoadEvent(function() { SimpleTest.executeSoon(runTests); }); +]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_screenPersistence.xul b/toolkit/content/tests/chrome/test_screenPersistence.xul new file mode 100644 index 0000000000..7d63252939 --- /dev/null +++ b/toolkit/content/tests/chrome/test_screenPersistence.xul @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Window Open Test" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script class="testbody" type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + let win; + var left = 60 + screen.availLeft; + var upper = 60 + screen.availTop; + + function runTest() { + win = window.openDialog("window_screenPosSize.xul", + null, + "chrome,dialog=no,all,screenX=" + left + ",screenY=" + upper + ",outerHeight=200,outerWidth=200"); + SimpleTest.waitForFocus(checkTest, win); + } + function checkTest() { + is(win.screenX, left, "The window should be placed now at x=" + left + "px"); + is(win.screenY, upper, "The window should be placed now at y=" + upper + "px"); + is(win.outerHeight, 200, "The window size should be height=200px"); + is(win.outerWidth, 200, "The window size should be width=200px"); + runTest2(); + } + function runTest2() { + win.close(); + win = window.openDialog("window_screenPosSize.xul", + null, + "chrome,dialog=no,all"); + SimpleTest.waitForFocus(checkTest2, win); + } + function checkTest2() { + let runTime = Components.classes["@mozilla.org/xre/app-info;1"] + .getService(Components.interfaces.nsIXULRuntime); + if (runTime.OS != "Linux") { + is(win.screenX, 80, "The window should be placed now at x=80px"); + is(win.screenY, 80, "The window should be placed now at y=80px"); + } + is(win.outerHeight, 300, "The window size should be height=300px"); + is(win.outerWidth, 300, "The window size should be width=300px"); + win.close(); + SimpleTest.finish(); + } +]]></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_scrollbar.xul b/toolkit/content/tests/chrome/test_scrollbar.xul new file mode 100644 index 0000000000..11dccbee94 --- /dev/null +++ b/toolkit/content/tests/chrome/test_scrollbar.xul @@ -0,0 +1,137 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for scrollbars + --> +<window title="Scrollbar" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"/> + + <hbox> + <scrollbar orient="horizontal" + id="scroller" + curpos="0" + maxpos="600" + pageincrement="400" + width="500" + style="margin:0"/> + </hbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Scrollbar **/ +var scrollbarTester = { + scrollbar: null, + middlePref: false, + startTest: function() { + this.scrollbar = $("scroller"); + this.middlePref = this.getMiddlePref(); + var self = this; + [0, 1, 2].map(function(button) { + [false, true].map(function(alt) { + [false, true].map(function(shift) { + self.testThumbDragging(button, alt, shift); + }) + }) + }); + SimpleTest.finish(); + }, + testThumbDragging: function(button, withAlt, withShift) { + this.reset(); + var x = 160; // on the right half of the thumb + var y = 5; + + var isMac = navigator.platform.indexOf("Mac") != -1; + var runtime = Components.classes["@mozilla.org/xre/app-info;1"] + .getService(Components.interfaces.nsIXULRuntime); + var isGtk = runtime.widgetToolkit.indexOf("gtk") != -1; + + // Start the drag. + this.mousedown(x, y, button, withAlt, withShift); + var newPos = this.getPos(); + var scrollToClick = (newPos != 0); + if (isMac || isGtk) { + ok(!scrollToClick, "On Linux and Mac OS X, clicking the scrollbar thumb "+ + "should never move it."); + } else if (button == 0 && withShift) { + ok(scrollToClick, "On platforms other than Linux and Mac OS X, holding "+ + "shift should enable scroll-to-click on the scrollbar thumb."); + } else if (button == 1 && this.middlePref) { + ok(scrollToClick, "When middlemouse.scrollbarPosition is on, clicking the "+ + "thumb with the middle mouse button should center it "+ + "around the cursor.") + } + + // Move one pixel to the right. + this.mousemove(x+1, y, button, withAlt, withShift); + var newPos2 = this.getPos(); + if (newPos2 != newPos) { + ok(newPos2 > newPos, "Scrollbar thumb should follow the mouse when dragged."); + ok(newPos2 - newPos < 3, "Scrollbar shouldn't move further than the mouse when dragged."); + ok(button == 0 || (button == 1 && this.middlePref) || (button == 2 && isGtk), + "Dragging the scrollbar should only be possible with the left mouse button."); + } else { + // Dragging had no effect. + if (button == 0) { + ok(false, "Dragging the scrollbar thumb should work."); + } else if (button == 1 && this.middlePref && (!isGtk && !isMac)) { + ok(false, "When middlemouse.scrollbarPosition is on, dragging the "+ + "scrollbar thumb should be possible using the middle mouse button."); + } else { + ok(true, "Dragging works correctly."); + } + } + + // Release the mouse button. + this.mouseup(x+1, y, button, withAlt, withShift); + var newPos3 = this.getPos(); + ok(newPos3 == newPos2, + "Releasing the mouse button after dragging the thumb shouldn't move it."); + }, + getMiddlePref: function() { + // It would be better to test with different middlePref settings, + // but the setting is only queried once, at browser startup, so + // changing it here wouldn't have any effect + var prefService = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + var mouseBranch = prefService.getBranch("middlemouse."); + return mouseBranch.getBoolPref("scrollbarPosition"); + }, + setPos: function(pos) { + this.scrollbar.setAttribute("curpos", pos); + }, + getPos: function() { + return this.scrollbar.getAttribute("curpos"); + }, + reset: function() { + this.setPos(0); + }, + mousedown: function(x, y, button, alt, shift) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousedown", 'button': button, + altKey: alt, shiftKey: shift }); + }, + mousemove: function(x, y, button, alt, shift) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousemove", 'button': button, + altKey: alt, shiftKey: shift }); + }, + mouseup: function(x, y, button, alt, shift) { + synthesizeMouse(this.scrollbar, x, y, { type: "mouseup", 'button': button, + altKey: alt, shiftKey: shift }); + } +} + +function doTest() { + setTimeout(function() { scrollbarTester.startTest(); }, 0); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_showcaret.xul b/toolkit/content/tests/chrome/test_showcaret.xul new file mode 100644 index 0000000000..75a8cf6a2b --- /dev/null +++ b/toolkit/content/tests/chrome/test_showcaret.xul @@ -0,0 +1,101 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Show Caret Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<iframe id="f1" width="100" height="100" onload="frameLoaded()" + src="data:text/html,%3Cbody%20style='height:%208000px'%3E%3Cp%3EHello%3C/p%3EGoodbye%3C/body%3E"/> +<!-- <body style='height: 8000px'><p>Hello</p><span id='s'>Goodbye<span></body> --> +<iframe id="f2" type="content" showcaret="true" width="100" height="100" onload="frameLoaded()" + src="data:text/html,%3Cbody%20style%3D%27height%3A%208000px%27%3E%3Cp%3EHello%3C%2Fp%3E%3Cspan%20id%3D%27s%27%3EGoodbye%3Cspan%3E%3C%2Fbody%3E"/> + +<script> +<![CDATA[ + +var framesLoaded = 0; +var otherWindow = null; + +function frameLoaded() { if (++framesLoaded == 2) SimpleTest.waitForFocus(runTest); } + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + var sel1 = frames[0].getSelection(); + sel1.collapse(frames[0].document.body, 0); + + var sel2 = frames[1].getSelection(); + sel2.collapse(frames[1].document.body, 0); + window.frames[0].focus(); + document.commandDispatcher.getControllerForCommand("cmd_moveBottom").doCommand("cmd_moveBottom"); + + var listener = function() { + if (!(frames[0].scrollY > 0)) { + window.content.removeEventListener("scroll", listener, false); + } + } + window.frames[0].addEventListener("scroll", listener, false); + + var sel1 = frames[0].getSelection(); + sel1.collapse(frames[0].document.body, 0); + + var sel2 = frames[1].getSelection(); + sel2.collapse(frames[1].document.body, 0); + + window.frames[0].focus(); + document.commandDispatcher.getControllerForCommand("cmd_moveBottom").doCommand("cmd_moveBottom"); + is(sel1.focusNode, frames[0].document.body, "focusNode for non-showcaret"); + is(sel1.focusOffset, 0, "focusOffset for non-showcaret"); + + window.frames[1].focus(); + document.commandDispatcher.getControllerForCommand("cmd_moveBottom").doCommand("cmd_moveBottom"); + + ok(frames[1].scrollY < + frames[1].document.getElementById('s').getBoundingClientRect().top, + "scrollY for showcaret"); + isnot(sel2.focusNode, frames[1].document.body, "focusNode for showcaret"); + ok(sel2.anchorOffset > 0, "focusOffset for showcaret"); + + otherWindow = window.open("window_showcaret.xul", "_blank", "chrome,width=400,height=200"); + otherWindow.addEventListener("focus", otherWindowFocused, false); +} + +function otherWindowFocused() +{ + otherWindow.removeEventListener("focus", otherWindowFocused, false); + + // enable caret browsing temporarily to test caret movement + var prefs = Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch); + prefs.setBoolPref("accessibility.browsewithcaret", true); + + var hbox = otherWindow.document.documentElement.firstChild; + hbox.focus(); + is(otherWindow.document.activeElement, hbox, "hbox in other window is focused"); + + document.commandDispatcher.getControllerForCommand("cmd_lineNext").doCommand("cmd_lineNext"); + is(otherWindow.document.activeElement, hbox, "hbox still focused in other window after down movement"); + + prefs.setBoolPref("accessibility.browsewithcaret", false); + + otherWindow.close(); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_sorttemplate.xul b/toolkit/content/tests/chrome/test_sorttemplate.xul new file mode 100644 index 0000000000..a08e3b6a8a --- /dev/null +++ b/toolkit/content/tests/chrome/test_sorttemplate.xul @@ -0,0 +1,89 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<?xml-stylesheet href="data:text/css,window > |people { display: none }" type="text/css"?> +<!-- + XUL Widget Test for tabindex + --> +<window title="tabindex" width="500" height="600" + onfocus="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<people id="famouspeople" xmlns=""> + <person name="Napoleon Bonaparte" gender="male"/> + <person name="Cleopatra" gender="female"/> + <person name="Julius Caesar" gender="male"/> + <person name="Ferdinand Magellan" gender="male"/> + <person name="Laura Secord" gender="female"/> +</people> + +<tree id="tree" datasources="#famouspeople" ref="*" querytype="xml" flex="1"> + <treecols> + <treecol label="Name" flex="1" sort="?name"/> + <treecol label="Gender" flex="1" sort="?gender"/> + </treecols> + <template> + <query/> + <rule> + <action> + <treechildren id="treechildren-strings"> + <treeitem uri="?"> + <treerow> + <treecell label="?name"/> + <treecell label="?gender"/> + </treerow> + </treeitem> + </treechildren> + </action> + </rule> + </template> +</tree> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTest() +{ + var tree = $("tree"); + var col = tree.columns[0].element; + synthesizeMouse(col, 12, 2, { }); + checkRowOrder(tree, ["Cleopatra", "Ferdinand Magellan", "Julius Caesar", "Laura Secord", "Napoleon Bonaparte"], "acsending"); + + synthesizeMouse(col, 12, 2, { }); + checkRowOrder(tree, ["Napoleon Bonaparte", "Laura Secord", "Julius Caesar", "Ferdinand Magellan", "Cleopatra"], "descending"); + + synthesizeMouse(col, 12, 2, { }); + checkRowOrder(tree, ["Napoleon Bonaparte", "Laura Secord", "Julius Caesar", "Ferdinand Magellan", "Cleopatra"], "natural"); + + SimpleTest.finish(); +} + +function checkRowOrder(tree, expected, testid) +{ + var index = 0; + var item = tree.firstChild.nextSibling.nextSibling.firstChild; + while (item && index < expected.length) { + if (item.firstChild.firstChild.getAttribute("label") != expected[index++]) + break; + item = item.nextSibling; + } + ok(index == expected.length && !item, testid + " row order"); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_statusbar.xul b/toolkit/content/tests/chrome/test_statusbar.xul new file mode 100644 index 0000000000..160cc25d04 --- /dev/null +++ b/toolkit/content/tests/chrome/test_statusbar.xul @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for statusbar + --> +<window title="Statusbar Test" + onload="setTimeout(test_statusbar, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<statusbar> + <statusbarpanel id="panel" label="OK" image="happy.png"/> +</statusbar> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_statusbar() +{ + var panel = $("panel"); + ok(panel.label, "OK", "statusbarpanel label"); + ok(panel.image, "happy.png", "statusbarpanel image"); + panel.src = "sad.png"; + ok(panel.src, "sad.png", "statusbarpanel set src"); + ok(panel.getAttribute("src"), "sad.png", "statusbarpanel set src attribute"); + + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_subframe_origin.xul b/toolkit/content/tests/chrome/test_subframe_origin.xul new file mode 100644 index 0000000000..bde1e49459 --- /dev/null +++ b/toolkit/content/tests/chrome/test_subframe_origin.xul @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Subframe Event Tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> + +// Added after content child widgets were removed from ui windows. Tests sub frame +// event client coordinate offsets. + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_subframe_origin.xul", "_blank", "chrome,width=600,height=600"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_tabbox.xul b/toolkit/content/tests/chrome/test_tabbox.xul new file mode 100644 index 0000000000..3cbacb15ad --- /dev/null +++ b/toolkit/content/tests/chrome/test_tabbox.xul @@ -0,0 +1,224 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tabboxes + --> +<window title="Tabbox Test" width="500" height="600" + onload="setTimeout(test_tabbox, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + <script type="application/javascript" src="xul_selectcontrol.js"/> + +<vbox id="tabboxes"> + +<tabbox id="tabbox"> + <tabs id="tabs"> + <tab id="tab1" label="Tab 1"/> + <tab id="tab2" label="Tab 2"/> + </tabs> + <tabpanels id="tabpanels"> + <button id="panel1" label="Panel 1"/> + <button id="panel2" label="Panel 2"/> + </tabpanels> +</tabbox> + +<tabbox id="tabbox-initwithvalue"> + <tabs id="tabs-initwithvalue" value="two"> + <tab label="Tab 1" value="one"/> + <tab label="Tab 2" value="two"/> + <tab label="Tab 3" value="three"/> + </tabs> + <tabpanels id="tabpanels-initwithvalue"> + <button label="Panel 1"/> + <button label="Panel 2"/> + <button label="Panel 3"/> + </tabpanels> +</tabbox> + +<tabbox id="tabbox-initwithselected"> + <tabs id="tabs-initwithselected" value="two"> + <tab label="Tab 1" value="one"/> + <tab label="Tab 2" value="two"/> + <tab label="Tab 3" value="three" selected="true"/> + </tabs> + <tabpanels id="tabpanels-initwithselected"> + <button label="Panel 1"/> + <button label="Panel 2"/> + <button label="Panel 3"/> + </tabpanels> +</tabbox> + +</vbox> + +<tabbox id="tabbox-nofocus"> + <textbox id="textbox-extra" hidden="true"/> + <tabs> + <tab label="Tab 1" value="one"/> + <tab id="tab-nofocus" label="Tab 2" value="two"/> + </tabs> + <tabpanels> + <tabpanel> + <button id="tab-nofocus-button" label="Label"/> + </tabpanel> + <tabpanel id="tabpanel-nofocusinpaneltab"> + <label id="tablabel" value="Label"/> + </tabpanel> + </tabpanels> +</tabbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_tabbox() +{ + var tabbox = document.getElementById("tabbox"); + var tabs = document.getElementById("tabs"); + var tabpanels = document.getElementById("tabpanels"); + + test_tabbox_State(tabbox, "tabbox initial", 0, tabs.firstChild, tabpanels.firstChild); + + // check the selectedIndex property + tabbox.selectedIndex = 1; + test_tabbox_State(tabbox, "tabbox selectedIndex 1", 1, tabs.lastChild, tabpanels.lastChild); + + tabbox.selectedIndex = 2; + test_tabbox_State(tabbox, "tabbox selectedIndex 2", 1, tabs.lastChild, tabpanels.lastChild); + + // tabbox must have a selection, so setting to -1 should do nothing + tabbox.selectedIndex = -1; + test_tabbox_State(tabbox, "tabbox selectedIndex -1", 1, tabs.lastChild, tabpanels.lastChild); + + // check the selectedTab property + tabbox.selectedTab = tabs.firstChild; + test_tabbox_State(tabbox, "tabbox selected", 0, tabs.firstChild, tabpanels.firstChild); + + // setting selectedTab to null should not do anything + tabbox.selectedTab = null; + test_tabbox_State(tabbox, "tabbox selectedTab null", 0, tabs.firstChild, tabpanels.firstChild); + + // check the selectedPanel property + tabbox.selectedPanel = tabpanels.lastChild; + test_tabbox_State(tabbox, "tabbox selectedPanel", 0, tabs.firstChild, tabpanels.lastChild); + + // setting selectedPanel to null should not do anything + tabbox.selectedPanel = null; + test_tabbox_State(tabbox, "tabbox selectedPanel null", 0, tabs.firstChild, tabpanels.lastChild); + + tabbox.selectedIndex = 0; + test_tabpanels(tabpanels, tabbox); + + tabs.removeChild(tabs.firstChild); + tabs.removeChild(tabs.firstChild); + + test_tabs(tabs); + + test_tabbox_focus(); +} + +function test_tabpanels(tabpanels, tabbox) +{ + var tab = tabbox.selectedTab; + + // changing the selection on the tabpanels should not affect the tabbox + // or tabs within + // check the selectedIndex property + tabpanels.selectedIndex = 1; + test_tabbox_State(tabbox, "tabpanels tabbox selectedIndex 1", 0, tab, tabpanels.lastChild); + test_tabpanels_State(tabpanels, "tabpanels selectedIndex 1", 1, tabpanels.lastChild); + + tabpanels.selectedIndex = 0; + test_tabbox_State(tabbox, "tabpanels tabbox selectedIndex 2", 0, tab, tabpanels.firstChild); + test_tabpanels_State(tabpanels, "tabpanels selectedIndex 2", 0, tabpanels.firstChild); + + // setting selectedIndex to -1 should do nothing + tabpanels.selectedIndex = 1; + tabpanels.selectedIndex = -1; + test_tabbox_State(tabbox, "tabpanels tabbox selectedIndex -1", 0, tab, tabpanels.lastChild); + test_tabpanels_State(tabpanels, "tabpanels selectedIndex -1", 1, tabpanels.lastChild); + + // check the tabpanels.selectedPanel property + tabpanels.selectedPanel = tabpanels.lastChild; + test_tabbox_State(tabbox, "tabpanels tabbox selectedPanel", 0, tab, tabpanels.lastChild); + test_tabpanels_State(tabpanels, "tabpanels selectedPanel", 1, tabpanels.lastChild); + + // check setting the tabpanels.selectedPanel property to null + tabpanels.selectedPanel = null; + test_tabbox_State(tabbox, "tabpanels selectedPanel null", 0, tab, tabpanels.lastChild); +} + +function test_tabs(tabs) +{ + test_nsIDOMXULSelectControlElement(tabs, "tab", "tabs"); + // XXXndeakin would test the UI aspect of tabs, but the mouse + // events on tabs are fired in a timeout causing the generic + // test_nsIDOMXULSelectControlElement_UI method not to work + // test_nsIDOMXULSelectControlElement_UI(tabs, null); +} + +function test_tabbox_State(tabbox, testid, index, tab, panel) +{ + is(tabbox.selectedIndex, index, testid + " selectedIndex"); + is(tabbox.selectedTab, tab, testid + " selectedTab"); + is(tabbox.selectedPanel, panel, testid + " selectedPanel"); +} + +function test_tabpanels_State(tabpanels, testid, index, panel) +{ + is(tabpanels.selectedIndex, index, testid + " selectedIndex"); + is(tabpanels.selectedPanel, panel, testid + " selectedPanel"); +} + +function test_tabbox_focus() +{ + $("tabboxes").hidden = true; + $(document.activeElement).blur(); + + var tabbox = $("tabbox-nofocus"); + var tab = $("tab-nofocus"); + + when_tab_focused(tab, function () { + ok(document.activeElement, tab, "focus in tab with no focusable elements"); + + tabbox.selectedIndex = 0; + $("tab-nofocus-button").focus(); + + when_tab_focused(tab, function () { + ok(document.activeElement, tab, "focus in tab with no focusable elements, but with something in another tab focused"); + + var textboxExtra = $("textbox-extra"); + textboxExtra.addEventListener("focus", function () { + textboxExtra.removeEventListener("focus", arguments.callee, true); + ok(document.activeElement, textboxExtra, "focus in tab with focus currently in textbox that is sibling of tabs"); + + SimpleTest.finish(); + }, true); + + tabbox.selectedIndex = 0; + textboxExtra.hidden = false; + synthesizeMouseAtCenter(tab, { }); + }); + + synthesizeMouseAtCenter(tab, { }); + }); + + synthesizeMouseAtCenter(tab, { }); +} + +function when_tab_focused(tab, callback) { + tab.addEventListener("focus", function onFocused() { + tab.removeEventListener("focus", onFocused, true); + SimpleTest.executeSoon(callback); + }, true); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tabindex.xul b/toolkit/content/tests/chrome/test_tabindex.xul new file mode 100644 index 0000000000..425cb7b9ed --- /dev/null +++ b/toolkit/content/tests/chrome/test_tabindex.xul @@ -0,0 +1,120 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tabindex + --> +<window title="tabindex" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<!-- + Elements are navigated in the following order: + 1. tabindex > 0 in tree order + 2. tabindex = 0 in tree order + Elements with tabindex = -1 are not in the tab order + --> +<hbox> + <button id="t5" label="One"/> + <checkbox id="no1" label="Two" tabindex="-1"/> + <button id="t6" label="Three" tabindex="0"/> + <checkbox id="t1" label="Four" tabindex="1"/> +</hbox> +<hbox> + <textbox id="t7" idmod="t3" size="3"/> + <textbox id="no2" size="3" tabindex="-1"/> + <textbox id="t8" idmod="t4" size="3" tabindex="0"/> + <textbox id="t2" idmod="t1" size="3" tabindex="1"/> +</hbox> +<hbox> + <button id="no3" style="-moz-user-focus: ignore;" label="One"/> + <checkbox id="no4" style="-moz-user-focus: ignore;" label="Two" tabindex="-1"/> + <button id="t9" style="-moz-user-focus: ignore;" label="Three" tabindex="0"/> + <checkbox id="t3" style="-moz-user-focus: ignore;" label="Four" tabindex="1"/> +</hbox> +<hbox> + <textbox id="t10" idmod="t5" style="-moz-user-focus: ignore;" size="3"/> + <textbox id="no5" style="-moz-user-focus: ignore;" size="3" tabindex="-1"/> + <textbox id="t11" idmod="t6" style="-moz-user-focus: ignore;" size="3" tabindex="0"/> + <textbox id="t4" idmod="t2" style="-moz-user-focus: ignore;" size="3" tabindex="1"/> +</hbox> +<listbox id="t12" idmod="t7"> + <listitem label="Item One"/> +</listbox> + +<hbox> + <!-- the tabindex attribute does not apply to non-controls, so it + should be treated as -1 for non-focusable dropmarkers, and 0 + for focusable dropmarkers. Thus, the first four dropmarkers + are not in the tab order, and the last four dropmarkers should + be in the tab order just after the listbox above. + --> + <dropmarker id="no6"/> + <dropmarker id="no7" tabindex="-1"/> + <dropmarker id="no8" tabindex="0"/> + <dropmarker id="no9" tabindex="1"/> + <dropmarker id="t13" style="-moz-user-focus: normal;"/> + <dropmarker id="t14" style="-moz-user-focus: normal;" tabindex="-1"/> + <dropmarker id="t15" style="-moz-user-focus: normal;" tabindex="0"/> + <dropmarker id="t16" style="-moz-user-focus: normal;" tabindex="1"/> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gAdjustedTabFocusModel = false; +var gTestCount = 16; +var gTestsOccurred = 0; + +function runTests() +{ + var t; + window.addEventListener("focus", function (event) { + if (t == 1 && event.target.id == "t2") { + // looks to be using the MacOSX Full Keyboard Access set to Textboxes + // and lists only so use the idmod attribute instead + gAdjustedTabFocusModel = true; + gTestCount = 7; + } + + var attrcompare = gAdjustedTabFocusModel ? "idmod" : "id"; + + // check for the last test which should wrap aorund to the first item + // consider the focus event on the inner input of textboxes instead + if (event.originalTarget.localName == "input") { + is(document.getBindingParent(event.originalTarget).getAttribute(attrcompare), + "t" + t, "tab " + t + " to inner input"); + gTestsOccurred++; + } + else { + is(event.target.getAttribute(attrcompare), "t" + t, "tab " + t + " to " + event.target.localName) + if (event.target.localName != "textbox") + gTestsOccurred++; + } + }, true); + + for (t = 1; t <= gTestCount; t++) + synthesizeKey("VK_TAB", { }); + + is(gTestsOccurred, gTestCount, "test count"); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_textbox_dictionary.xul b/toolkit/content/tests/chrome/test_textbox_dictionary.xul new file mode 100644 index 0000000000..8e69dd15e3 --- /dev/null +++ b/toolkit/content/tests/chrome/test_textbox_dictionary.xul @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for textbox with Add and Undo Add to Dictionary + --> +<window title="Textbox Add and Undo Add to Dictionary Test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + + <hbox> + <textbox id="t1" value="Hellop" oncontextmenu="runContextMenuTest()" spellcheck="true"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var textbox; +var testNum; + +function bringUpContextMenu(element) +{ + synthesizeMouseAtCenter(element, { type: "contextmenu", button: 2}); +} + +function leftClickElement(element) +{ + synthesizeMouseAtCenter(element, { button: 0 }); +} + +function startTests() +{ + textbox = document.getElementById("t1"); + textbox.focus(); + testNum = 0; + + Components.utils.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm"); + onSpellCheck(textbox, function () { + bringUpContextMenu(textbox); + }); +} + +function runContextMenuTest() +{ + SimpleTest.executeSoon( function() { + // The textbox has its children in an hbox XUL element, so get that first + var hbox = document.getAnonymousNodes(textbox).item(0); + + var contextMenu = document.getAnonymousElementByAttribute(hbox, "anonid", "input-box-contextmenu"); + + switch(testNum) + { + case 0: // "Add to Dictionary" button + var addToDict = contextMenu.querySelector("[anonid=spell-add-to-dictionary]"); + ok(!addToDict.hidden, "Is Add to Dictionary visible?"); + + var separator = contextMenu.querySelector("[anonid=spell-suggestions-separator]"); + ok(!separator.hidden, "Is separator visible?"); + + addToDict.doCommand(); + + contextMenu.hidePopup(); + testNum++; + + onSpellCheck(textbox, function () { + bringUpContextMenu(textbox); + }); + break; + + case 1: // "Undo Add to Dictionary" button + var undoAddDict = contextMenu.querySelector("[anonid=spell-undo-add-to-dictionary]"); + ok(!undoAddDict.hidden, "Is Undo Add to Dictioanry visible?"); + + var separator = contextMenu.querySelector("[anonid=spell-suggestions-separator]"); + ok(!separator.hidden, "Is separator hidden?"); + + undoAddDict.doCommand(); + + contextMenu.hidePopup(); + onSpellCheck(textbox, function () { + SimpleTest.finish(); + }); + break; + } + }); +} + +SimpleTest.waitForFocus(startTests); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_textbox_emptytext.xul b/toolkit/content/tests/chrome/test_textbox_emptytext.xul new file mode 100644 index 0000000000..41c702a90e --- /dev/null +++ b/toolkit/content/tests/chrome/test_textbox_emptytext.xul @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for textbox with placeholder + --> +<window title="Textbox with placeholder test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <hbox> + <textbox id="t1"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function doTests() { + var t1 = $("t1"); + + t1.placeholder = 1; + ok("1" === t1.label, "placeholder exposed as label"); + ok("" === t1.value, "placeholder not exposed as value"); + + t1.label = 2; + ok("2" === t1.label, "label can be set explicitly"); + ok("1" === t1.placeholder, "placeholder persists after setting label"); + + t1.value = 3; + ok("3" === t1.value, "value setter/getter works while placeholder is present"); + ok("1" === t1.placeholder, "placeholder persists after setting value"); + + t1.value = ""; + is(t1.textLength, 0, "textLength while placeholder is displayed"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTests); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_textbox_number.xul b/toolkit/content/tests/chrome/test_textbox_number.xul new file mode 100644 index 0000000000..369e927851 --- /dev/null +++ b/toolkit/content/tests/chrome/test_textbox_number.xul @@ -0,0 +1,353 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for textbox type="number" + --> +<window title="Textbox type='number' test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<hbox> + <textbox id="n1" type="number" size="4"/> + <textbox id="n2" type="number" value="10" min="5" max="15" wraparound="true"/> +</hbox> +<hbox> + <textbox id="n3" type="number" size="4" value="25" min="1" max="12" increment="3"/> +</hbox> +<hbox> + <textbox id="n4" type="number" size="4" value="-2" min="-8" max="18"/> + <textbox id="n5" type="number" value="-17" min="-10" max="-3"/> +</hbox> +<hbox> + <textbox id="n6" type="number" size="4" value="9" min="12" max="8"/> +</hbox> +<hbox> + <textbox id="n7" type="number" size="4" value="4.678" min="2" max="10.5" decimalplaces="2"/> + <textbox id="n8" type="number" hidespinbuttons="true"/> +</hbox> +<hbox> + <textbox id="n9" type="number" size="4" oninput="updateInputEventCount();"/> +</hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ +SimpleTest.waitForExplicitFinish(); + +// ---- NOTE: the numbers used in these tests are carefully chosen to avoid +// ---- floating point rounding issues + +function doTests() { + var n1 = $("n1"); + var n2 = $("n2"); + var n3 = $("n3"); + var n4 = $("n4"); + var n5 = $("n5"); + var n6 = $("n6"); + var n7 = $("n7"); + + testValsMinMax(n1, "initial n1", 0, 0, Infinity); + testValsMinMax(n2, "initial n2", 10, 5, 15); + testValsMinMax(n3, "initial n3", 12, 1, 12); + testValsMinMax(n4, "initial n4", -2, -8, 18); + testValsMinMax(n5, "initial n5", -10, -10, -3); + testValsMinMax(n6, "initial n6", 12, 12, 12); + testValsMinMax(n7, "initial n7", 4.68, 2, 10.5); // value should be rounded + + ok(n1.spinButtons != null && n1.spinButtons.localName == "spinbuttons", "spinButtons set"); + isnot(n1.decimalSymbol, "", "n1.decimalSymbol is set to something"); + n1.decimalSymbol = "."; + SimpleTest.is(n1.decimalSymbol, ".", "n1.decimalSymbol set to '.'"); + SimpleTest.is(n1.wrapAround, false, "wrapAround defaults to false"); + SimpleTest.is(n1.increment, 1, "increment defaults to 1"); + SimpleTest.is(n1.decimalPlaces, 0, "decimalPlaces defaults to 0"); + + SimpleTest.is(n2.wrapAround, true, "wrapAround when set to true"); + SimpleTest.is(n3.increment, 3, "increment when set to 1"); + SimpleTest.is(n7.decimalPlaces, 2, "decimalPlaces when set to 2"); + + // test changing the value + n1.value = "1700"; + testVals(n1, "set value,", 1700); + n1.value = 1600; + testVals(n1, "set value int,", 1600); + n2.value = "2"; + testVals(n2, "set value below min,", 5); + n2.value = 2; + testVals(n2, "set value below min int,", 5); + n2.value = 18; + testVals(n2, "set value above max,", 15); + n2.value = -6; + testVals(n2, "set value below min negative,", 5); + n5.value = -2; + testVals(n5, "set value above max positive,", -3); + n7.value = 5.999; + testVals(n7, "set value to decimal,", 6, "6.00"); + n7.value = "1.42"; + testVals(n7, "set value to decimal below min,", 2.00, "2.00"); + n7.value = 24.1; + testVals(n7, "set value to decimal above max,", 10.5, "10.50"); + n1.value = 4.75; + testVals(n1, "set value to decimal round,", 5); + + // test changing the valueNumber + n1.valueNumber = 27; + testVals(n1, "set valueNumber,", 27); + n2.valueNumber = 1; + testVals(n2, "set valueNumber below min,", 5); + n2.valueNumber = 77; + testVals(n2, "set valueNumber above max,", 15); + n2.valueNumber = -5; + testVals(n2, "set valueNumber below min negative,", 5); + n5.valueNumber = -8; + n5.valueNumber = -1; + testVals(n5, "set valueNumber above max positive,", -3); + n7.valueNumber = 8.23; + testVals(n7, "set valueNumber to decimal,", 8.23); + n7.valueNumber = 0.77; + testVals(n7, "set valueNumber to decimal below min,", 2.00, "2.00"); + n7.valueNumber = 29.157; + testVals(n7, "set valueNumber to decimal above max,", 10.5, "10.50"); + n1.value = 8.9; + testVals(n1, "set valueNumber to decimal round,", 9); + + // test changing the min + n1.value = 6; + n1.min = 8; + testValsMinMax(n1, "set integer min,", 8, 8, Infinity); + n7.value = 5.5; + n7.min = 6.7; + testValsMinMax(n7, "set decimal min,", 6.7, 6.7, 10.5, "6.70"); + + // test changing the max + n1.value = 25; + n1.max = 22; + testValsMinMax(n1, "set integer max,", 22, 8, 22); + n7.value = 10.2; + n7.max = 10.1; + testValsMinMax(n7, "set decimal max,", 10.1, 6.7, 10.1, "10.10"); + + // test decrease() and increase() methods + testIncreaseDecrease(n1, "integer", 1, 0, 8, 22); + testIncreaseDecrease(n7, "decimal", 1, 2, 6.7, 10.1); + testIncreaseDecrease(n3, "integer with increment", 3, 0, 1, 12); + + n7.min = 2.7; + n7.value = 10.1; + n7.increment = 4.3; + SimpleTest.is(n7.increment, 4.3, "increment changed"); + testIncreaseDecrease(n7, "integer with increment", 4.3, 2, 2.7, 10.1); + + n2.value = n2.min; + n2.decrease(); + testVals(n2, "integer wraparound decrease method", n2.max); + n2.increase(); + testVals(n2, "integer wraparound decrease method", n2.min); + + n7.wrapAround = true; + SimpleTest.is(n7.wrapAround, true, "change wrapAround"); + n7.value = n7.min + 0.01; + n7.decrease(); + testVals(n7, "decimal wraparound decrease method", n7.max, n7.max.toFixed(2)); + n7.increase(); + testVals(n7, "decimal wraparound decrease method", n7.min, n7.min.toFixed(2)); + + n1.value = 22; + n1.decimalPlaces = 3; + testVals(n1, "set decimalPlaces 3", 22, "22.000"); + n1.value = 10.624; + testVals(n1, "set decimalPlaces 3 set value,", 10.624); + n1.decimalPlaces = 0; + testVals(n1, "set decimalPlaces 0 set value,", 11); + n1.decimalPlaces = Infinity; + n1.value = 10.678123; + testVals(n1, "set decimalPlaces Infinity set value,", 10.678123); + + n1.decimalSymbol = ","; + SimpleTest.is(n1.decimalSymbol, ",", "n1.decimalSymbol set to ','"); + n1.value = "9.67"; + testVals(n1, "set decimalPlaces set value,", 9.67); + + n1.decimalSymbol = "."; + SimpleTest.is(n1.decimalSymbol, ".", "n1.decimalSymbol set back to '.'"); + n1.decimalPlaces = 0; + + // UI tests + n1.min = 5; + n1.max = 15; + n1.value = 5; + n1.focus(); + + var sb = n1.spinButtons; + var sbbottom = sb.getBoundingClientRect().bottom - sb.getBoundingClientRect().top - 2; + + synthesizeKey("VK_UP", {}); + testVals(n1, "key up", 6); + + synthesizeKey("VK_DOWN", {}); + testVals(n1, "key down", 5); + + synthesizeMouse(sb, 2, 2, {}); + testVals(n1, "spinbuttons up", 6); + synthesizeMouse(sb, 2, sbbottom, {}); + testVals(n1, "spinbuttons down", 5); + + n1.value = 15; + synthesizeKey("VK_UP", {}); + testVals(n1, "key up at max", 15); + synthesizeMouse(sb, 2, 2, {}); + testVals(n1, "spinbuttons up at max", 15); + + n1.value = 5; + synthesizeKey("VK_DOWN", {}); + testVals(n1, "key down at min", 5); + synthesizeMouse(sb, 2, sbbottom, {}); + testVals(n1, "spinbuttons down at min", 5); + + n1.wrapAround = true; + n1.value = 15; + synthesizeKey("VK_UP", {}); + testVals(n1, "key up wraparound at max", 5); + n1.value = 5; + synthesizeKey("VK_DOWN", {}); + testVals(n1, "key down wraparound at min", 15); + + n1.value = 15; + synthesizeMouse(sb, 2, 2, {}); + testVals(n1, "spinbuttons up wraparound at max", 5); + n1.value = 5; + synthesizeMouse(sb, 2, sbbottom, {}); + testVals(n1, "spinbuttons down wraparound at min", 15); + + // check read only state + n1.readOnly = true; + n1.min = -10; + n1.max = 15; + n1.value = 12; + // no events should fire and no changes should occur when the field is read only + synthesizeKeyExpectEvent("VK_UP", { }, n1, "!change", "key up read only"); + is(n1.value, "12", "key up read only value"); + synthesizeKeyExpectEvent("VK_DOWN", { }, n1, "!change", "key down read only"); + is(n1.value, "12", "key down read only value"); + + synthesizeMouseExpectEvent(sb, 2, 2, { }, n1, "!change", "mouse up read only"); + is(n1.value, "12", "mouse up read only value"); + synthesizeMouseExpectEvent(sb, 2, sbbottom, { }, n1, "!change", "mouse down read only"); + is(n1.value, "12", "mouse down read only value"); + + n1.readOnly = false; + n1.disabled = true; + synthesizeMouseExpectEvent(sb, 2, 2, { }, n1, "!change", "mouse up disabled"); + is(n1.value, "12", "mouse up disabled value"); + synthesizeMouseExpectEvent(sb, 2, sbbottom, { }, n1, "!change", "mouse down disabled"); + is(n1.value, "12", "mouse down disabled value"); + + var nsbrect = $("n8").spinButtons.getBoundingClientRect(); + ok(nsbrect.left == 0 && nsbrect.top == 0 && nsbrect.right == 0, nsbrect.bottom == 0, + "hidespinbuttons"); + + var n9 = $("n9"); + is(n9.value, "0", "initial value"); + n9.select(); + synthesizeKey("4", {}); + is(inputEventCount, 1, "input event count"); + is(inputEventValue, "4", "input value"); + is(n9.value, "4", "updated value"); + synthesizeKey("2", {}); + is(inputEventCount, 2, "input event count"); + is(inputEventValue, "42", "input value"); + is(n9.value, "42", "updated value"); + synthesizeKey("VK_BACK_SPACE", {}); + is(inputEventCount, 3, "input event count"); + is(inputEventValue, "4", "input value"); + is(n9.value, "4", "updated value"); + synthesizeKey("A", {accelKey: true}); + synthesizeKey("VK_DELETE", {}); + is(inputEventCount, 4, "input event count"); + is(inputEventValue, "0", "input value"); + is(n9.value, "0", "updated value"); + + SimpleTest.finish(); +} + +var inputEventCount = 0; +var inputEventValue = null; +function updateInputEventCount() { + inputEventValue = $("n9").value; + inputEventCount++; +}; + +function testVals(nb, name, valueNumber, valueFieldNumber) { + if (valueFieldNumber === undefined) + valueFieldNumber = "" + valueNumber; + + SimpleTest.is(nb.value, "" + valueNumber, name + " value is '" + valueNumber + "'"); + SimpleTest.is(nb.valueNumber, valueNumber, name + " valueNumber is " + valueNumber); + + // This value format depends on the localized decimal symbol. + var localizedValue = valueFieldNumber.replace(/\./, nb.decimalSymbol); + SimpleTest.is(nb.inputField.value, localizedValue, + name + " inputField value is '" + localizedValue + "'"); +} + +function testValsMinMax(nb, name, valueNumber, min, max, valueFieldNumber) { + testVals(nb, name, valueNumber, valueFieldNumber); + SimpleTest.is(nb.min, min, name + " min is " + min); + SimpleTest.is(nb.max, max, name + " max is " + max); +} + +function testIncreaseDecrease(nb, testid, increment, fixedCount, min, max) +{ + testid += " "; + + nb.value = max; + nb.decrease(); + testVals(nb, testid + "decrease method", max - increment, + (max - increment).toFixed(fixedCount)); + nb.increase(); + testVals(nb, testid + "increase method", max, max.toFixed(fixedCount)); + nb.value = min; + nb.decrease(); + testVals(nb, testid + "decrease method at min", min, min.toFixed(fixedCount)); + nb.value = max; + nb.increase(); + testVals(nb, testid + "increase method at max", max, max.toFixed(fixedCount)); + + nb.focus(); + nb.value = min; + + // pressing the cursor up and down keys should adjust the value + synthesizeKeyExpectEvent("VK_UP", { }, nb, "change", testid + "key up"); + is(nb.value, String(min + increment), testid + "key up"); + nb.value = max; + synthesizeKeyExpectEvent("VK_UP", { }, nb, "!change", testid + "key up at max"); + is(nb.value, String(max), testid + "key up at max"); + synthesizeKeyExpectEvent("VK_DOWN", { }, nb, "change", testid + "key down"); + is(nb.value, String(max - increment), testid + "key down"); + nb.value = min; + synthesizeKeyExpectEvent("VK_DOWN", { }, nb, "!change", testid + "key down at min"); + is(nb.value, String(min), testid + "key down at min"); + + // check pressing the spinbutton arrows + var sb = nb.spinButtons; + var sbbottom = sb.getBoundingClientRect().bottom - sb.getBoundingClientRect().top - 2; + nb.value = min; + synthesizeMouseExpectEvent(sb, 2, 2, { }, nb, "change", testid + "mouse up"); + is(nb.value, String(min + increment), testid + "mouse up"); + nb.value = max; + synthesizeMouseExpectEvent(sb, 2, 2, { }, nb, "!change", testid + "mouse up at max"); + synthesizeMouseExpectEvent(sb, 2, sbbottom, { }, nb, "change", testid + "mouse down"); + is(nb.value, String(max - increment), testid + "mouse down"); + nb.value = min; + synthesizeMouseExpectEvent(sb, 2, sbbottom, { }, nb, "!change", testid + "mouse down at min"); +} + +SimpleTest.waitForFocus(doTests); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_textbox_search.xul b/toolkit/content/tests/chrome/test_textbox_search.xul new file mode 100644 index 0000000000..ae61533619 --- /dev/null +++ b/toolkit/content/tests/chrome/test_textbox_search.xul @@ -0,0 +1,170 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for search textbox + --> +<window title="Search textbox test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <hbox> + <textbox id="searchbox" + type="search" + oncommand="doSearch(this.value);" + placeholder="random placeholder" + timeout="1"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gExpectedValue; +var gLastTest; + +function doTests() { + var textbox = $("searchbox"); + var icons = document.getAnonymousElementByAttribute(textbox, "anonid", "search-icons"); + var searchIcon = document.getAnonymousElementByAttribute(textbox, "class", "textbox-search-icon"); + var clearIcon = document.getAnonymousElementByAttribute(textbox, "class", "textbox-search-clear"); + + ok(icons, "icon deck found"); + ok(searchIcon, "search icon found"); + ok(clearIcon, "clear icon found"); + is(icons.selectedPanel, searchIcon, "search icon is displayed"); + + is(textbox.placeholder, "random placeholder", "search textbox supports placeholder"); + is(textbox.value, "", "placeholder doesn't interfere with the real value"); + + function iconClick(aIcon) { + is(icons.selectedPanel, aIcon, aIcon.className + " icon must be displayed in order to be clickable"); + + //XXX synthesizeMouse worked on Linux but failed on Windows an Mac + // for unknown reasons. Manually dispatch the event for now. + //synthesizeMouse(aIcon, 0, 0, {}); + + var event = document.createEvent("MouseEvent"); + event.initMouseEvent("click", true, true, window, 1, + 0, 0, 0, 0, + false, false, false, false, + 0, null); + aIcon.dispatchEvent(event); + } + + iconClick(searchIcon); + is(textbox.getAttribute("focused"), "true", "clicking the search icon focuses the textbox"); + + textbox.value = "foo"; + is(icons.selectedPanel, clearIcon, "clear icon is displayed when setting a value"); + + textbox.reset(); + is(textbox.defaultValue, "", "defaultValue is empty"); + is(textbox.value, "", "reset method clears the textbox"); + is(icons.selectedPanel, searchIcon, "search icon is displayed after textbox.reset()"); + + textbox.value = "foo"; + gExpectedValue = ""; + iconClick(clearIcon); + is(textbox.value, "", "clicking the clear icon clears the textbox"); + ok(gExpectedValue == null, "search triggered when clearing the textbox with the clear icon"); + + textbox.value = "foo"; + gExpectedValue = ""; + synthesizeKey("VK_ESCAPE", {}); + is(textbox.value, "", "escape key clears the textbox"); + ok(gExpectedValue == null, "search triggered when clearing the textbox with the escape key"); + + textbox.value = "bar"; + gExpectedValue = "bar"; + textbox.doCommand(); + ok(gExpectedValue == null, "search triggered with doCommand"); + + gExpectedValue = "bar"; + synthesizeKey("VK_RETURN", {}); + ok(gExpectedValue == null, "search triggered with enter key"); + + textbox.value = ""; + textbox.searchButton = true; + is(textbox.getAttribute("searchbutton"), "true", "searchbutton attribute set on the textbox"); + is(searchIcon.getAttribute("searchbutton"), "true", "searchbutton attribute inherited to the search icon"); + + textbox.value = "foo"; + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode if there's a value"); + + gExpectedValue = "foo"; + iconClick(searchIcon); + ok(gExpectedValue == null, "search triggered when clicking the search icon in search button mode"); + is(icons.selectedPanel, clearIcon, "clear icon displayed in search button mode after submitting"); + + synthesizeKey("o", {}); + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode when typing a key"); + + gExpectedValue = "fooo"; + iconClick(searchIcon); // display the clear icon (tested above) + + textbox.value = "foo"; + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode when the value is changed"); + + gExpectedValue = "foo"; + synthesizeKey("VK_RETURN", {}); + ok(gExpectedValue == null, "search triggered with enter key in search button mode"); + is(icons.selectedPanel, clearIcon, "clear icon displayed in search button mode after submitting with enter key"); + + textbox.value = "x"; + synthesizeKey("VK_BACK_SPACE", {}); + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode when deleting the value with the backspace key"); + + gExpectedValue = ""; + synthesizeKey("VK_RETURN", {}); + ok(gExpectedValue == null, "search triggered with enter key in search button mode"); + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode after submitting an empty string"); + + textbox.readOnly = true; + gExpectedValue = "foo"; + textbox.value = "foo"; + iconClick(searchIcon); + ok(gExpectedValue == null, "search triggered when clicking the search icon in search button mode while the textbox is read-only"); + is(icons.selectedPanel, searchIcon, "search icon persists in search button mode after submitting while the textbox is read-only"); + textbox.readOnly = false; + + textbox.disabled = true; + is(searchIcon.getAttribute("disabled"), "true", "disabled attribute inherited to the search icon"); + is(clearIcon.getAttribute("disabled"), "true", "disabled attribute inherited to the clear icon"); + gExpectedValue = false; + textbox.value = "foo"; + iconClick(searchIcon); + ok(gExpectedValue == false, "search *not* triggered when clicking the search icon in search button mode while the textbox is disabled"); + is(icons.selectedPanel, searchIcon, "search icon persists in search button mode when trying to submit while the textbox is disabled"); + textbox.disabled = false; + ok(!searchIcon.hasAttribute("disabled"), "disabled attribute removed from the search icon"); + ok(!clearIcon.hasAttribute("disabled"), "disabled attribute removed from the clear icon"); + + textbox.searchButton = false; + ok(!textbox.hasAttribute("searchbutton"), "searchbutton attribute removed from the textbox"); + ok(!searchIcon.hasAttribute("searchbutton"), "searchbutton attribute removed from the search icon"); + + gLastTest = true; + gExpectedValue = "123"; + textbox.value = "1"; + synthesizeKey("2", {}); + synthesizeKey("3", {}); +} + +function doSearch(aValue) { + is(aValue, gExpectedValue, "search triggered with expected value"); + gExpectedValue = null; + if (gLastTest) + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTests); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_timepicker.xul b/toolkit/content/tests/chrome/test_timepicker.xul new file mode 100644 index 0000000000..98e370137f --- /dev/null +++ b/toolkit/content/tests/chrome/test_timepicker.xul @@ -0,0 +1,207 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for timepicker + --> +<window title="timepicker" width="500" height="600" + onload="setTimeout(testtag_timepicker, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<timepicker id="timepicker"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function testtag_timepicker() +{ + var tp = document.getElementById("timepicker"); + + var testid = "timepicker "; + + var today = new Date(); + var thour = today.getHours(); + var tminute = today.getMinutes(); + var tsecond = today.getSeconds(); + + // testtag_comparetime(tp, testid + "initial", thour, tminute, tsecond); + + // check that setting the value property works + tp.value = testtag_gettimestring(thour, tminute, tsecond); + testtag_comparetime(tp, testid + "set value", thour, tminute, tsecond); + + var numberOrder = /^(\D*)\s*(\d+)(\D*)(\d+)(\D*)(\d+)\s*(\D*)$/; + var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory-nu-latn"; + var fdt = new Date(2000,0,1,16,7,9).toLocaleTimeString(locale); + is(tp.is24HourClock, Number(fdt.match(numberOrder)[2]) > 12, "is24HourClock"); + + // check that setting the dateValue property works + tp.dateValue = today; + testtag_comparetime(tp, testid + "set dateValue", thour, tminute, tsecond); + ok(tp.value !== today, testid + " set dateValue different time"); + + ok(!tp.readOnly, testid + "readOnly"); + tp.readOnly = true; + ok(tp.readOnly, testid + "set readOnly"); + tp.readOnly = false; + ok(!tp.readOnly, testid + "clear readOnly"); + + function setTimeField(field, value, expectException, + expectedHour, expectedMinute, expectedSecond) + { + var exh = false; + try { + tp[field] = value; + } catch (ex) { exh = true; } + is(exh, expectException, testid + "set " + field + " " + value); + testtag_comparetime(tp, testid + "set " + field + " " + value, + expectedHour, expectedMinute, expectedSecond); + } + + // check the value property + setTimeField("value", "0:0:0", false, 0, 0, 0); + setTimeField("value", "21:1:40", false, 21, 1, 40); + setTimeField("value", "7:11:8", false, 7, 11, 8); + setTimeField("value", "04:07:02", false, 4, 7, 2); + setTimeField("value", "10:42:20", false, 10, 42, 20); + + // check that the hour, minute and second fields can be set properly + setTimeField("hour", 7, false, 7, 42, 20); + setTimeField("hour", 0, false, 0, 42, 20); + setTimeField("hour", 21, false, 21, 42, 20); + setTimeField("hour", -1, true, 21, 42, 20); + setTimeField("hour", 24, true, 21, 42, 20); + + setTimeField("minute", 0, false, 21, 0, 20); + setTimeField("minute", 9, false, 21, 9, 20); + setTimeField("minute", 10, false, 21, 10, 20); + setTimeField("minute", 35, false, 21, 35, 20); + setTimeField("minute", -1, true, 21, 35, 20); + setTimeField("minute", 60, true, 21, 35, 20); + + setTimeField("second", 0, false, 21, 35, 0); + setTimeField("second", 9, false, 21, 35, 9); + setTimeField("second", 10, false, 21, 35, 10); + setTimeField("second", 51, false, 21, 35, 51); + setTimeField("second", -1, true, 21, 35, 51); + setTimeField("second", 60, true, 21, 35, 51); + + // check when seconds is not specified + setTimeField("value", "06:05", false, 6, 5, 0); + setTimeField("value", "06:15", false, 6, 15, 0); + setTimeField("value", "16:15", false, 16, 15, 0); + + // check that times overflow properly + setTimeField("value", "5:65:21", false, 6, 5, 21); + setTimeField("value", "5:25:72", false, 5, 26, 12); + + // check invalid values for the value and dateValue properties + tp.value = "14:25:48"; + setTimeField("value", "", true, 14, 25, 48); + setTimeField("value", "1:5:6:6", true, 14, 25, 48); + setTimeField("value", "2:a:19", true, 14, 25, 48); + setTimeField("dateValue", "none", true, 14, 25, 48); + + // check the fields + ok(tp.hourField instanceof HTMLInputElement, testid + "hourField"); + ok(tp.minuteField instanceof HTMLInputElement, testid + "minuteField"); + ok(tp.secondField instanceof HTMLInputElement, testid + "secondField"); + + testtag_timepicker_UI(tp, testid); + + tp.readOnly = true; + + // check that keyboard usage doesn't change the value when the timepicker + // is read only + testtag_timepicker_UI_key(tp, testid + "readonly ", "14:25:48", + tp.hourField, 14, 25, 48, 14, 25, 48); + testtag_timepicker_UI_key(tp, testid + "readonly ", "14:25:48", + tp.minuteField, 14, 25, 48, 14, 25, 48); + testtag_timepicker_UI_key(tp, testid + "readonly ", "14:25:48", + tp.secondField, 14, 25, 48, 14, 25, 48); + + SimpleTest.finish(); +} + +function testtag_timepicker_UI(tp, testid) +{ + testid += "UI"; + + // test adjusting the time with the up and down keys + testtag_timepicker_UI_key(tp, testid, "0:12:25", tp.hourField, 1, 12, 25, 0, 12, 25); + testtag_timepicker_UI_key(tp, testid, "11:12:25", tp.hourField, 12, 12, 25, 11, 12, 25); + testtag_timepicker_UI_key(tp, testid, "7:12:25", tp.hourField, 8, 12, 25, 7, 12, 25); + testtag_timepicker_UI_key(tp, testid, "16:12:25", tp.hourField, 17, 12, 25, 16, 12, 25); + testtag_timepicker_UI_key(tp, testid, "23:12:25", tp.hourField, 0, 12, 25, 23, 12, 25); + + testtag_timepicker_UI_key(tp, testid, "15:23:46", tp.minuteField, 15, 24, 46, 15, 23, 46); + testtag_timepicker_UI_key(tp, testid, "15:0:46", tp.minuteField, 15, 1, 46, 15, 0, 46); + testtag_timepicker_UI_key(tp, testid, "15:59:46", tp.minuteField, 15, 0, 46, 15, 59, 46); + + testtag_timepicker_UI_key(tp, testid, "11:50:46", tp.secondField, 11, 50, 47, 11, 50, 46); + testtag_timepicker_UI_key(tp, testid, "11:50:0", tp.secondField, 11, 50, 1, 11, 50, 0); + testtag_timepicker_UI_key(tp, testid, "11:50:59", tp.secondField, 11, 50, 0, 11, 50, 59); +} + +function testtag_timepicker_UI_key(tp, testid, value, field, + uhour, uminute, usecond, + dhour, dminute, dsecond) +{ + tp.value = value; + field.focus(); + + var eventTarget = tp.readOnly ? null : tp; + + var testname = testid + " " + value + " key up"; + synthesizeKeyExpectEvent("VK_UP", { }, eventTarget, "change", testname); + testtag_comparetime(tp, testname, uhour, uminute, usecond); + + testname = testid + " " + value + " key down"; + synthesizeKeyExpectEvent("VK_DOWN", { }, eventTarget, "change", testname); + testtag_comparetime(tp, testname, dhour, dminute, dsecond); +} + +function testtag_gettimestring(hour, minute, second) +{ + if (minute < 10) + minute = "0" + minute; + if (second < 10) + second = "0" + second; + return hour + ":" + minute + ":" + second; +} + +function testtag_comparetime(tp, testid, hour, minute, second) +{ + is(tp.value, testtag_gettimestring(hour, minute, second), testid + " value"); + is(tp.getAttribute("value"), + testtag_gettimestring(hour, minute, second), + testid + " value attribute"); + + var dateValue = tp.dateValue; + ok(dateValue.getHours() == hour && + dateValue.getMinutes() == minute && + dateValue.getSeconds() == second, + testid + " dateValue"); + + is(tp.hour, hour, testid + " hour"); + is(tp.minute, minute, testid + " minute"); + is(tp.second, second, testid + " second"); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_titlebar.xul b/toolkit/content/tests/chrome/test_titlebar.xul new file mode 100644 index 0000000000..d48e04a129 --- /dev/null +++ b/toolkit/content/tests/chrome/test_titlebar.xul @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for the titlebar element and window dragging + --> +<window title="Titlebar" width="200" height="200" + onload="setTimeout(test_titlebar, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_titlebar() +{ + window.open("window_titlebar.xul", "_blank", "chrome,left=200,top=200"); +} + +function done(testWindow) +{ + testWindow.close(); + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_toolbar.xul b/toolkit/content/tests/chrome/test_toolbar.xul new file mode 100644 index 0000000000..7adaa6aa33 --- /dev/null +++ b/toolkit/content/tests/chrome/test_toolbar.xul @@ -0,0 +1,227 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Toolbar" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="startTest();"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <toolbox id="toolbox"> + <toolbarpalette> + <toolbarbutton id="p1" label="p1"/> + <toolbarbutton id="p2" label="p2"/> + <toolbarbutton id="p3" label="p3"/> + <toolbarbutton id="p4" label="p4"/> + <toolbarbutton id="p5" label="p5"/> + <toolbarbutton id="p6" label="p6"/> + <toolbarbutton id="p7" label="p7"/> + <toolbarbutton id="p8" label="p8"/> + <toolbarbutton id="p9" label="p9"/> + <toolbarbutton id="p10" label="p10"/> + <toolbarbutton id="p11" label="p11"/> + <toolbarbutton id="p12" label="p12"/> + </toolbarpalette> + + <toolbar id="tb1" defaultset="p1,p2"/> + <toolbar id="tb2" defaultset="p4,p3"/> + <toolbar id="tb3" defaultset="p5,p6,t31"> + <toolbarbutton id="t31" label="t31" removable="true"/> + </toolbar> + <toolbar id="tb4" defaultset="t41,p7,p8"> + <toolbarbutton id="t41" label="t41" removable="true"/> + </toolbar> + <toolbar id="tb5" defaultset="p9,t51,p10"> + <toolbarbutton id="t51" label="t51" removable="true"/> + </toolbar> + + <toolbar id="tb-test" defaultset="p11,p12"/> + <toolbar id="tb-test2" defaultset=""/> + <!-- fixed toolbarbuttons always have 'fixed' in their id --> + <toolbar id="tb-test3" defaultset=""> + <toolbarbutton id="tb-fixed-1" label="tb-test3-1"/> + <toolbarbutton id="tb-fixed-2" label="tb-test3-2" removable="false"/> + <toolbarbutton id="tb-fixed-3" label="tb-test3-3"/> + </toolbar> + </toolbox> + + <toolbar id="notoolbox"/> + + <!-- test resuls are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" + style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="text/javascript"><![CDATA[ + const SPACER = /^spacer\d+/; + const SEPARATOR = /^separator\d+/; + const SPRING = /^spring\d+/; + + function testSet(aTb, aIDs, aResultIDs, aUseFixed) { + // build a list of the fixed items in the order they appear on the toolbar + var fixedSet = []; + if (aUseFixed) { + for (let i = 0; i < aTb.childNodes.length; i++) { + var id = aTb.childNodes[i].id; + if (id.indexOf("fixed") >= 0) + fixedSet.push(id); + } + } + + var currentSet = aIDs.join(","); + ok(currentSet, "setting currentSet: " + currentSet); + aTb.currentSet = currentSet; + var resultIDs = aResultIDs || aIDs; + checkSet(aTb, resultIDs, fixedSet); + } + + var checkSetCount = 0; + function checkSet(aTb, aResultIDs, aFixedSet) { + checkSetCount++; + var testID = "checkSet(" + checkSetCount + ") "; + + for (let i = 0; i < aTb.childNodes.length; i++) { + let id = aTb.childNodes[i].id; + if (aResultIDs[i] instanceof RegExp) { + ok(aResultIDs[i].test(id), + testID + "correct ID " + aResultIDs[i] + " for toolbar " + aTb.id + "; got: " + id); + } + else if (aResultIDs[i] == "*") { + is(id, aFixedSet.shift(), testID + "is fixed with ID " + id + " for toolbar " + aTb.id); + } + else { + is(id, aResultIDs[i], + testID + "correct ID " + aResultIDs[i] + " for toolbar " + aTb.id + + "****" + aResultIDs + "," + i + ","); + // remove the item from the fixed set once found + if (aFixedSet && id.indexOf("fixed") >= 0) + aFixedSet.splice(aFixedSet.indexOf(id), 1); + } + } + + if (aFixedSet) + is(aFixedSet.length, 0, testID + "extra fixed items for " + aTb.id); + is(aTb.childNodes.length, aResultIDs.length, + testID + "correct number of children for " + aTb.id); + } + + function test_defaultSet() { + checkSet($("tb1"), ["p1", "p2"]); + checkSet($("tb2"), ["p4", "p3"]); + checkSet($("tb3"), ["p5", "p6", "t31"]); + checkSet($("tb4"), ["t41", "p7", "p8"]); + checkSet($("tb5"), ["p9", "t51", "p10"]); + } + + function test_currentSet(aTb) { + ok(aTb, "have toolbar"); + var defaultSet = aTb.getAttribute("defaultset"); + var setLength = (defaultSet && defaultSet.split(",").length) || 0; + is(setLength, aTb.childNodes.length, "correct # of children initially"); + + var emptySet = [["__empty"], []]; + var testSets = [ + emptySet, + [["p11"]], + [["p11","p12"]], + [["p11","p12","bogus"], ["p11","p12"]], + [["p11"]], + emptySet, + [["spacer"], [SPACER]], + [["spring"], [SPRING]], + [["separator"], [SEPARATOR]], + [["p11", "p11", "p12", "spacer", "p11"], ["p11", "p12", SPACER]], + [["separator", "separator", "p11", "spring", "spacer"], + [SEPARATOR, SEPARATOR, "p11", SPRING, SPACER]], + [["separator", "spacer", "separator", "p11", "spring", "spacer", "p12", "spring"], + [SEPARATOR, SPACER, SEPARATOR, "p11", SPRING, SPACER, "p12", SPRING]], + emptySet + ]; + + cycleSets(aTb, testSets, emptySet, false); + } + + function test_currentSet_nonremovable() { + var tb = $("tb-test3"); + ok(tb, "have tb-test-3"); + + // the * used in the tests below means that any fixed item can appear in that position + var emptySet = [["__empty"], ["*", "*", "*"]]; + var testSets = [ + [["p1", "tb-fixed-1", "p2"], + ["p1", "tb-fixed-1", "p2", "*", "*"]], + [["p1", "tb-fixed-2", "p2"], + ["p1", "tb-fixed-2", "p2", "*", "*"]], + [["p1", "tb-fixed-3", "p2"], + ["p1", "tb-fixed-3", "p2", "*", "*"]], + emptySet, + + [["tb-fixed-1", "tb-fixed-2", "tb-fixed-3"], + ["tb-fixed-1", "tb-fixed-2", "tb-fixed-3"]], + [["tb-fixed-3", "tb-fixed-2", "tb-fixed-1"], + ["tb-fixed-3", "tb-fixed-2", "tb-fixed-1"]], + + [["tb-fixed-1", "tb-fixed-2", "tb-fixed-3", "p1", "p2"], + ["tb-fixed-1", "tb-fixed-2", "tb-fixed-3", "p1", "p2"]], + + [["tb-fixed-1", "p2", "p1"], + ["tb-fixed-1", "p2", "p1", "*", "*"]], + + [["tb-fixed-1", "p2"], + ["tb-fixed-1", "p2", "*", "*"]], + + [["p1", "p2"], ["p1", "p2", "*", "*", "*"]], + [["p2", "p1"], ["p2", "p1", "*", "*", "*"]], + + [["tb-fixed-3", "spacer", "p1"], + ["tb-fixed-3", SPACER, "p1", "*", "*"]] + ]; + + cycleSets(tb, testSets, emptySet, true); + } + + function cycleSets(aTb, aSets, aEmptySet, aUseFixed) { + // Since a lot of the tricky cases handled in the currentSet setter + // depend on going from one state to another, run through the test set + // multiple times in different orders. + var length = aSets.length; + + for (var i = 0; i < length; i++) { + testSet(aTb, aSets[i][0], aSets[i][1], aUseFixed); + } + for (var i = length - 1; i >= 0; i--) { + testSet(aTb, aSets[i][0], aSets[i][1], aUseFixed); + } + for (var i = 0; i < length; i++) { + testSet(aTb, aSets[i][0], aSets[i][1], aUseFixed); + testSet(aTb, aSets[length - i - 1][0], aSets[length - i - 1][1], aUseFixed); + testSet(aTb, aSets[i][0], aSets[i][1], aUseFixed); + testSet(aTb, aSets[i][0], aSets[i][1], aUseFixed); + } + for (var i = 0; i < length; i++) { + testSet(aTb, aEmptySet[0], aEmptySet[1], aUseFixed); + testSet(aTb, aSets[i][0], aSets[i][1], aUseFixed); + } + } + + SimpleTest.waitForExplicitFinish(); + function startTest() { + test_defaultSet(); + test_currentSet($("tb-test")); + test_currentSet($("tb-test2")); + test_currentSet_nonremovable(); + + var toolbox = $("toolbox"); + var toolbars = document.getElementsByTagName("toolbar"); + for (var t = 0; t < toolbars.length; t++) { + var toolbar = toolbars[t]; + is(toolbar.toolbox, toolbar.id == "notoolbox" ? null : toolbox, + "toolbar " + toolbar.id + " has correct toolbox"); + } + + $("tb1").toolbox = document.documentElement; + is($("tb1").toolbox, toolbox, "toolbox still correct after set"); + SimpleTest.finish(); + } + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_tooltip.xul b/toolkit/content/tests/chrome/test_tooltip.xul new file mode 100644 index 0000000000..b5650352da --- /dev/null +++ b/toolkit/content/tests/chrome/test_tooltip.xul @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tooltip Tests" + onload="setTimeout(runTest, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_tooltip.xul", "_blank", "chrome,width=700,height=700"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_tooltip_noautohide.xul b/toolkit/content/tests/chrome/test_tooltip_noautohide.xul new file mode 100644 index 0000000000..979e424774 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tooltip_noautohide.xul @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tooltip Noautohide Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + +<tooltip id="thetooltip" noautohide="true" + onpopupshown="setTimeout(tooltipStillShown, 6000)" + onpopuphidden="ok(gChecked, 'tooltip did not hide'); SimpleTest.finish()"> + <label id="label" value="This is a tooltip"/> +</tooltip> + +<button id="button" label="Tooltip Text" tooltip="thetooltip"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var gChecked = false; + +function runTests() +{ + var button = document.getElementById("button"); + var windowUtils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils); + windowUtils.disableNonTestMouseEvents(true); + synthesizeMouse(button, 2, 2, { type: "mouseover" }); + synthesizeMouse(button, 4, 4, { type: "mousemove" }); + synthesizeMouse(button, 6, 6, { type: "mousemove" }); + windowUtils.disableNonTestMouseEvents(false); +} + +function tooltipStillShown() +{ + gChecked = true; + document.getElementById("thetooltip").hidePopup(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree.xul b/toolkit/content/tests/chrome/test_tree.xul new file mode 100644 index 0000000000..52de74e462 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree.xul @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tree using multiple row selection + --> +<window title="Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-simple', 'treechildren-simple', 'multiple', 'simple', 'tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<script src="tree_shared.js"/> + +<tree id="tree-simple" rows="4"> + <treecols> + <treecol id="name" label="Name" sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" flex="1"/> + </treecols> + <treechildren id="treechildren-simple"> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> + diff --git a/toolkit/content/tests/chrome/test_tree_hier.xul b/toolkit/content/tests/chrome/test_tree_hier.xul new file mode 100644 index 0000000000..d1a599eaa1 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_hier.xul @@ -0,0 +1,136 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for hierarchical tree + --> +<window title="Hierarchical Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-hier', 'treechildren-hier', 'multiple', '', 'hierarchical tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<script src="tree_shared.js"/> + +<tree id="tree-hier" rows="4"> + <treecols> + <treecol id="name" label="Name" primary="true" + sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" flex="2"/> + <treecol id="planet" label="Planet" flex="1"/> + <treecol id="gender" label="Gender" flex="1" cycler="true"/> + </treecols> + <treechildren id="treechildren-hier"> + <treeitem> + <treerow properties="firstrow"> + <treecell label="Mary" value="mary" properties="firstname"/> + <treecell label="206 Garden Avenue" value="206ga"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell/> + <treecell value="19ms"/> + <treecell label="Earth"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem container="true"> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + <treecell label="Saturn"/> + <treecell label="Female" value="f"/> + </treerow> + <treechildren> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + <treecell label="Female" value="f"/> + <treecell label="Neptune"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + <treecell label="Omicron Persei 8"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell label="Neptune"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue" selectable="false"/> + <treecell label=""/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + <treecell label="Neptune"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell label="Mars"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree_hier_cell.xul b/toolkit/content/tests/chrome/test_tree_hier_cell.xul new file mode 100644 index 0000000000..29e92ba3b6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_hier_cell.xul @@ -0,0 +1,136 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for cell selection tree + --> +<window title="Cell Selection Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-cell', 'treechildren-cell', 'cell', '', 'cell selection tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<script src="tree_shared.js"/> + +<tree id="tree-cell" rows="4" seltype="cell"> + <treecols> + <treecol id="name" label="Name" primary="true" + sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" flex="2"/> + <treecol id="planet" label="Planet" flex="1"/> + <treecol id="gender" label="Gender" flex="1" cycler="true"/> + </treecols> + <treechildren id="treechildren-cell"> + <treeitem> + <treerow properties="firstrow"> + <treecell label="Mary" value="mary" properties="firstname"/> + <treecell label="206 Garden Avenue" value="206ga"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell/> + <treecell value="19ms"/> + <treecell label="Earth"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem container="true"> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + <treecell label="Saturn"/> + <treecell label="Female" value="f"/> + </treerow> + <treechildren> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + <treecell label="Female" value="f"/> + <treecell label="Neptune"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + <treecell label="Omicron Persei 8"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell label="Neptune"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue" selectable="false"/> + <treecell label=""/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + <treecell label="Neptune"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell label="Mars"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree_single.xul b/toolkit/content/tests/chrome/test_tree_single.xul new file mode 100644 index 0000000000..9b64cb488a --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_single.xul @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for single selection tree + --> +<window title="Single Selection Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-single', 'treechildren-single', + 'single', 'simple', 'single selection tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<script src="tree_shared.js"/> + +<tree id="tree-single" rows="4" seltype="single"> + <treecols> + <treecol id="name" label="Name" sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" flex="1"/> + </treecols> + <treechildren id="treechildren-single"> + <treeitem> + <treerow properties="firstrow"> + <treecell label="Mary" value="mary" properties="firstname"/> + <treecell label="206 Garden Avenue" value="206ga"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell/> + <treecell value="19ms"/> + </treerow> + </treeitem> + <treeitem container="true"> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + </treerow> + <treechildren> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue" selectable="false"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree_view.xul b/toolkit/content/tests/chrome/test_tree_view.xul new file mode 100644 index 0000000000..235e3a5948 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_view.xul @@ -0,0 +1,118 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tree using a custom nsITreeView + --> +<window title="Tree" onload="init()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <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> + +<script src="tree_shared.js"/> + +<script> +<![CDATA[ + +// This is our custom view, based on the treeview interface +var view = +{ + treeData: [["Mary", "206 Garden Avenue"], + ["Chris", "19 Marion Street"], + ["Sarah", "702 Fern Avenue"], + ["John", "99 Westminster Avenue"]], + value: "", + rowCount: 8, + getCellText: function(row, column) { return this.treeData[row % 4][column.index]; }, + getCellValue: function(row, column) { return this.value; }, + setCellText: function(row, column, val) { this.treeData[row % 4][column.index] = val; }, + setCellValue: function(row, column, val) { this.value = val; }, + setTree: function(tree) { this.tree = tree; }, + isContainer: function(row) { return false; }, + isContainerOpen: function(row) { return false; }, + isContainerEmpty: function(row) { return false; }, + isSeparator: function(row) { return false; }, + isSorted: function(row) { return false; }, + isSelectable: function(row, column) { return true; }, + isEditable: function(row, column) { return row != 2 || column.index != 1; }, + getProgressMode: function(row, column) { return Components.interfaces.nsITreeView.PROGRESS_NORMAL; }, + getParentIndex: function(row, column) { return -1; }, + getLevel: function(row) { return 0; }, + hasNextSibling: function(row, column) { return row != this.rowCount - 1; }, + getImageSrc: function(row, column) { return ""; }, + cycleHeader: function(column) { }, + getRowProperties: function(row) { return ""; }, + getCellProperties: function(row, column) { return ""; }, + getColumnProperties: function(column) + { + if (!column.index) { + return "one two"; + } + + return ""; + } +} + +function getCustomTreeViewCellInfo() +{ + var obj = { rows: [] }; + + for (var row = 0; row < view.rowCount; row++) { + var cellInfo = [ ]; + for (var column = 0; column < 1; column++) { + cellInfo.push({ label: "" + view.treeData[row % 4][column], + value: "", + properties: "", + editable: row != 2 || column.index != 1, + selectable: true, + image: "", + mode: Components.interfaces.nsITreeView.PROGRESS_NORMAL }); + } + + obj.rows.push({ cells: cellInfo, + properties: "", + container: false, + separator: false, + children: null, + level: 0, + parent: -1 }); + } + + return obj; +} + +function init() +{ + var tree = document.getElementById("tree-view"); + tree.view = view; + tree.treeBoxObject.ensureRowIsVisible(0); + is(tree.treeBoxObject.getFirstVisibleRow(), 0, "first visible after ensureRowIsVisible on load"); + tree.setAttribute("rows", "4"); + + setTimeout(testtag_tree, 0, "tree-view", "treechildren-view", "multiple", "simple", "tree view"); +} + +]]> +</script> + +<tree id="tree-view"> + <treecols> + <treecol id="name" label="Name" sort="label" flex="1"/> + <treecol id="address" label="Address" flex="1"/> + </treecols> + <treechildren id="treechildren-view"/> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> + diff --git a/toolkit/content/tests/chrome/window_browser_drop.xul b/toolkit/content/tests/chrome/window_browser_drop.xul new file mode 100644 index 0000000000..8a22ccce90 --- /dev/null +++ b/toolkit/content/tests/chrome/window_browser_drop.xul @@ -0,0 +1,254 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Browser Drop Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +<![CDATA[ + +Components.utils.import("resource://testing-common/ContentTask.jsm"); + +function dropOnRemoteBrowserAsync(browser, data, shouldExpectStateChange) { + ContentTask.setTestScope(window); // Need this so is/isnot/ok are available inside the contenttask + return ContentTask.spawn(browser, {data, shouldExpectStateChange}, function*({data, shouldExpectStateChange}) { + let { interfaces: Ci, utils: Cu } = Components; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + + if (!content.document.documentElement) { + // Wait until the testing document gets loaded. + yield new Promise(resolve => { + let onload = function() { + content.window.removeEventListener("load", onload); + resolve(); + }; + content.window.addEventListener("load", onload); + }); + } + + let dataTransfer = new content.DataTransfer("dragstart", false); + for (let i = 0; i < data.length; i++) { + let types = data[i]; + for (let j = 0; j < types.length; j++) { + dataTransfer.mozSetDataAt(types[j].type, types[j].data, i); + } + } + let event = content.document.createEvent("DragEvent"); + event.initDragEvent("drop", true, true, content, 0, 0, 0, 0, 0, + false, false, false, false, 0, null, dataTransfer); + content.document.body.dispatchEvent(event); + + let links = []; + try { + links = Services.droppedLinkHandler.dropLinks(event, true); + } catch (ex) { + if (shouldExpectStateChange) { + ok(false, "Should not have gotten an exception from the dropped link handler, but got: " + ex); + Cu.reportError(ex); + } + } + + return links; + }); +} + +function* expectLink(browser, expectedLinks, data, testid, onbody=false) { + let lastLinks = []; + let lastLinksPromise = new Promise(resolve => { + browser.droppedLinkHandler = function(event, links) { + info(`droppedLinkHandler called, received links ${JSON.stringify(links)}`); + if (expectedLinks.length == 0) { + ok(false, `droppedLinkHandler called for ${JSON.stringify(links)} which we didn't expect.`); + } + lastLinks = links; + resolve(links); + }; + }); + + function dropOnBrowserSync() { + let dropEl = onbody ? browser.contentDocument.body : browser; + synthesizeDrop(dropEl, dropEl, data, "", dropEl.ownerDocument.defaultView); + } + let links; + if (browser.isRemoteBrowser) { + let remoteLinks = yield dropOnRemoteBrowserAsync(browser, data, expectedLinks.length != 0); + is(remoteLinks.length, expectedLinks.length, testid + " remote links length"); + for (let i = 0, length = remoteLinks.length; i < length; i++) { + is(remoteLinks[i].url, expectedLinks[i].url, testid + "[" + i + "] remote link"); + is(remoteLinks[i].name, expectedLinks[i].name, testid + "[" + i + "] remote name"); + } + + if (expectedLinks.length == 0) { + // There is no way to check if nothing happens asynchronously. + return; + } + + links = yield lastLinksPromise; + } else { + dropOnBrowserSync(); + links = lastLinks; + } + + is(links.length, expectedLinks.length, testid + " links length"); + for (let i = 0, length = links.length; i < length; i++) { + is(links[i].url, expectedLinks[i].url, testid + "[" + i + "] link"); + is(links[i].name, expectedLinks[i].name, testid + "[" + i + "] name"); + } +}; + +function* dropLinksOnBrowser(browser, type) { + // Dropping single text/plain item with single link should open single + // page. + yield* expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" } ], + [ [ { type: "text/plain", + data: "http://www.mozilla.org/" } ] ], + "text/plain drop on browser " + type); + + // Dropping single text/plain item with multiple links should open + // multiple pages. + yield* expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" }, + { url: "http://www.example.com/", + name: "http://www.example.com/" } ], + [ [ { type: "text/plain", + data: "http://www.mozilla.org/\nhttp://www.example.com/" } ] ], + "text/plain with 2 URLs drop on browser " + type); + + // Dropping sinlge unsupported type item should not open anything. + yield* expectLink(browser, + [], + [ [ { type: "text/link", + data: "http://www.mozilla.org/" } ] ], + "text/link drop on browser " + type); + + // Dropping single text/uri-list item with single link should open single + // page. + yield* expectLink(browser, + [ { url: "http://www.example.com/", + name: "http://www.example.com/" } ], + [ [ { type: "text/uri-list", + data: "http://www.example.com/" } ] ], + "text/uri-list drop on browser " + type); + + // Dropping single text/uri-list item with multiple links should open + // multiple pages. + yield* expectLink(browser, + [ { url: "http://www.example.com/", + name: "http://www.example.com/" }, + { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" }], + [ [ { type: "text/uri-list", + data: "http://www.example.com/\nhttp://www.mozilla.org/" } ] ], + "text/uri-list with 2 URLs drop on browser " + type); + + // Name in text/x-moz-url should be handled. + yield* expectLink(browser, + [ { url: "http://www.example.com/", + name: "Example.com" } ], + [ [ { type: "text/x-moz-url", + data: "http://www.example.com/\nExample.com" } ] ], + "text/x-moz-url drop on browser " + type); + + yield* expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "Mozilla.org" }, + { url: "http://www.example.com/", + name: "Example.com" } ], + [ [ { type: "text/x-moz-url", + data: "http://www.mozilla.org/\nMozilla.org\nhttp://www.example.com/\nExample.com" } ] ], + "text/x-moz-url with 2 URLs drop on browser " + type); + + // Dropping multiple items should open multiple pages. + yield* expectLink(browser, + [ { url: "http://www.example.com/", + name: "Example.com" }, + { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" }], + [ [ { type: "text/x-moz-url", + data: "http://www.example.com/\nExample.com" } ], + [ { type: "text/plain", + data: "http://www.mozilla.org/" } ] ], + "text/x-moz-url and text/plain drop on browser " + type); + + // Dropping single item with multiple types should open single page. + yield* expectLink(browser, + [ { url: "http://www.example.org/", + name: "Example.com" } ], + [ [ { type: "text/plain", + data: "http://www.mozilla.org/" }, + { type: "text/x-moz-url", + data: "http://www.example.org/\nExample.com" } ] ], + "text/plain and text/x-moz-url drop on browser " + type); + + // Dropping javascript or data: URLs should fail: + yield* expectLink(browser, + [], + [ [ { type: "text/plain", + data: "javascript:'bad'" } ] ], + "text/plain javascript url drop on browser " + type); + yield* expectLink(browser, + [], + [ [ { type: "text/plain", + data: "jAvascript:'also bad'" } ] ], + "text/plain mixed-case javascript url drop on browser " + type); + yield* expectLink(browser, + [], + [ [ { type: "text/plain", + data: "data:text/html,bad" } ] ], + "text/plain data url drop on browser " + type); + + // Dropping a chrome url should fail as we don't have a source node set, + // defaulting to a source of file:/// + yield* expectLink(browser, + [], + [ [ { type: "text/x-moz-url", + data: "chrome://browser/content/browser.xul" } ] ], + "text/x-moz-url chrome url drop on browser " + type); + + if (browser.type == "content") { + yield ContentTask.spawn(browser, null, function() { + content.window.stopMode = true; + }); + + // stopPropagation should not prevent the browser link handling from occuring + yield* expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" } ], + [ [ { type: "text/uri-list", + data: "http://www.mozilla.org/" } ] ], + "text/x-moz-url drop on browser with stopPropagation drop event", true); + + yield ContentTask.spawn(browser, null, function() { + content.window.cancelMode = true; + }); + + // Canceling the event, however, should prevent the link from being handled. + yield* expectLink(browser, + [], + [ [ { type: "text/uri-list", data: "http://www.mozilla.org/" } ] ], + "text/x-moz-url drop on browser with cancelled drop event", true); + } +} + +function info(msg) { window.opener.wrappedJSObject.SimpleTest.info(msg); } +function is(l, r, n) { window.opener.wrappedJSObject.SimpleTest.is(l,r,n); } +function ok(v, n) { window.opener.wrappedJSObject.SimpleTest.ok(v,n); } + +]]> +</script> + +<browser id="chromechild" src="about:blank"/> +<browser id="contentchild" type="content" width="100" height="100" + src="data:text/html,<html draggable='true'><body draggable='true' style='width: 100px; height: 100px;' ondragover='event.preventDefault()' ondrop='if (window.stopMode) event.stopPropagation(); if (window.cancelMode) event.preventDefault();'></body></html>"/> + +<browser id="remote-contentchild" type="content" width="100" height="100" remote="true" + src="data:text/html,<html draggable='true'><body draggable='true' style='width: 100px; height: 100px;' ondragover='event.preventDefault()' ondrop='if (window.stopMode) event.stopPropagation(); if (window.cancelMode) event.preventDefault();'></body></html>"/> +</window> diff --git a/toolkit/content/tests/chrome/window_chromemargin.xul b/toolkit/content/tests/chrome/window_chromemargin.xul new file mode 100644 index 0000000000..3dec6d1379 --- /dev/null +++ b/toolkit/content/tests/chrome/window_chromemargin.xul @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="window" title="Subframe Origin Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +chrome margins rock! +<script> + +// Tests parsing of the chrome margin attrib on a window. + +function ok(condition, message) { + window.opener.wrappedJSObject.SimpleTest.ok(condition, message); +} + +function doSingleTest(param, shouldSucceed) +{ + var exception = null; + try { + document.documentElement.removeAttribute("chromemargin"); + document.documentElement.setAttribute("chromemargin", param); + ok(document. + documentElement. + getAttribute("chromemargin") == param, "couldn't set/get chromemargin?"); + } catch (ex) { + exception = ex; + } + if (shouldSucceed) + ok(!exception, "failed for param:'" + param + "'"); + else + ok(exception, "did not fail for invalid param:'" + param + "'"); + return true; +} + +function runTests() +{ + var doc = document.documentElement; + + // make sure we can set and get + doc.setAttribute("chromemargin", "0,0,0,0"); + ok(doc.getAttribute("chromemargin") == "0,0,0,0", "couldn't set/get chromemargin?"); + doc.setAttribute("chromemargin", "-1,-1,-1,-1"); + ok(doc.getAttribute("chromemargin") == "-1,-1,-1,-1", "couldn't set/get chromemargin?"); + + // test remove + doc.removeAttribute("chromemargin"); + ok(doc.getAttribute("chromemargin") == "", "couldn't remove chromemargin?"); + + // we already test these really well in a c++ test in widget + doSingleTest("1,2,3,4", true); + doSingleTest("-2,-2,-2,-2", true); + doSingleTest("1,1,1,1", true); + doSingleTest("", false); + doSingleTest("12123123", false); + doSingleTest("0,-1,-1,-1", true); + doSingleTest("-1,0,-1,-1", true); + doSingleTest("-1,-1,0,-1", true); + doSingleTest("-1,-1,-1,0", true); + doSingleTest("1234567890,1234567890,1234567890,1234567890", true); + doSingleTest("-1,-1,-1,-1", true); + + window.opener.wrappedJSObject.SimpleTest.finish(); + window.close(); +} + +window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTests, window); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/window_cursorsnap_dialog.xul b/toolkit/content/tests/chrome/window_cursorsnap_dialog.xul new file mode 100644 index 0000000000..df6f0bf026 --- /dev/null +++ b/toolkit/content/tests/chrome/window_cursorsnap_dialog.xul @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<dialog title="Cursor snapping test" id="dialog" + width="600" height="600" + onload="onload();" + onunload="onunload();" + buttons="accept,cancel" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function canRetryTest() +{ + return window.opener.wrappedJSObject.canRetryTest(); +} + +function getTimeoutTime() +{ + return window.opener.wrappedJSObject.getTimeoutTime(); +} + +var gTimer; +var gRetry; + +function finishByTimeout() +{ + var button = document.getElementById("dialog").getButton("accept"); + if (button.disabled) + ok(true, "cursor is NOT snapped to the disabled button (dialog)"); + else if (button.hidden) + ok(true, "cursor is NOT snapped to the hidden button (dialog)"); + else { + if (!canRetryTest()) { + ok(false, "cursor is NOT snapped to the default button (dialog)"); + } else { + // otherwise, this may be unexpected timeout, we should retry the test. + gRetry = true; + } + } + finish(); +} + +function finish() +{ + window.close(); +} + +function onMouseMove(aEvent) +{ + var button = document.getElementById("dialog").getButton("accept"); + if (button.disabled) + ok(false, "cursor IS snapped to the disabled button (dialog)"); + else if (button.hidden) + ok(false, "cursor IS snapped to the hidden button (dialog)"); + else + ok(true, "cursor IS snapped to the default button (dialog)"); + clearTimeout(gTimer); + finish(); +} + +function onload() +{ + var button = document.getElementById("dialog").getButton("accept"); + button.addEventListener("mousemove", onMouseMove, false); + + if (window.opener.wrappedJSObject.gDisable) { + button.disabled = true; + } + if (window.opener.wrappedJSObject.gHidden) { + button.hidden = true; + } + gRetry = false; + gTimer = setTimeout(finishByTimeout, getTimeoutTime()); +} + +function onunload() +{ + if (gRetry) { + window.opener.wrappedJSObject.retryCurrentTest(); + } else { + window.opener.wrappedJSObject.runNextTest(); + } +} + +]]> +</script> + +</dialog> diff --git a/toolkit/content/tests/chrome/window_cursorsnap_wizard.xul b/toolkit/content/tests/chrome/window_cursorsnap_wizard.xul new file mode 100644 index 0000000000..a226d02b74 --- /dev/null +++ b/toolkit/content/tests/chrome/window_cursorsnap_wizard.xul @@ -0,0 +1,111 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<wizard title="Cursor snapping test" id="wizard" + width="600" height="600" + onload="onload();" + onunload="onunload();" + buttons="accept,cancel" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <wizardpage> + <label value="first page"/> + </wizardpage> + + <wizardpage> + <label value="second page"/> + </wizardpage> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function canRetryTest() +{ + return window.opener.wrappedJSObject.canRetryTest(); +} + +function getTimeoutTime() +{ + return window.opener.wrappedJSObject.getTimeoutTime(); +} + +var gTimer; +var gRetry = false; + +function finishByTimeout() +{ + var button = document.getElementById("wizard").getButton("next"); + if (button.disabled) + ok(true, "cursor is NOT snapped to the disabled button (wizard)"); + else if (button.hidden) + ok(true, "cursor is NOT snapped to the hidden button (wizard)"); + else { + if (!canRetryTest()) { + ok(false, "cursor is NOT snapped to the default button (wizard)"); + } else { + // otherwise, this may be unexpected timeout, we should retry the test. + gRetry = true; + } + } + finish(); +} + +function finish() +{ + window.close(); +} + +function onMouseMove() +{ + var button = document.getElementById("wizard").getButton("next"); + if (button.disabled) + ok(false, "cursor IS snapped to the disabled button (wizard)"); + else if (button.hidden) + ok(false, "cursor IS snapped to the hidden button (wizard)"); + else + ok(true, "cursor IS snapped to the default button (wizard)"); + clearTimeout(gTimer); + finish(); +} + +function onload() +{ + var button = document.getElementById("wizard").getButton("next"); + button.addEventListener("mousemove", onMouseMove, false); + + if (window.opener.wrappedJSObject.gDisable) { + button.disabled = true; + } + if (window.opener.wrappedJSObject.gHidden) { + button.hidden = true; + } + gTimer = setTimeout(finishByTimeout, getTimeoutTime()); +} + +function onunload() +{ + if (gRetry) { + window.opener.wrappedJSObject.retryCurrentTest(); + } else { + window.opener.wrappedJSObject.runNextTest(); + } +} + +]]> +</script> + +</wizard> diff --git a/toolkit/content/tests/chrome/window_keys.xul b/toolkit/content/tests/chrome/window_keys.xul new file mode 100644 index 0000000000..79de9ac45e --- /dev/null +++ b/toolkit/content/tests/chrome/window_keys.xul @@ -0,0 +1,202 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Key Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gExpected = null; + +const kIsWin = navigator.platform.indexOf("Win") >= 0; + +// Only on Windows, osKey state is ignored when there is no shortcut key handler +// which exactly matches with osKey state. +var keysToTest = [ + ["k-v", "V", { } ], + ["", "V", { shiftKey: true } ], + ["k-v-scy", "V", { ctrlKey: true } ], + ["", "V", { altKey: true } ], + ["", "V", { metaKey: true } ], + [kIsWin ? "k-v" : "", "V", { osKey: true } ], + ["k-v-scy", "V", { shiftKey: true, ctrlKey: true } ], + ["", "V", { shiftKey: true, ctrlKey: true, altKey: true } ], + ["k-e-y", "E", { } ], + ["", "E", { shiftKey: true } ], + ["", "E", { ctrlKey: true } ], + ["", "E", { altKey: true } ], + ["", "E", { metaKey: true } ], + [kIsWin ? "k-e-y" : "", "E", { osKey: true } ], + ["k-d-a", "D", { altKey: true } ], + ["k-8-m", "8", { metaKey: true } ], + [kIsWin ? "k-8-m" : "", "8", { metaKey: true, osKey: true } ], + ["k-a-o", "A", { osKey: true } ], + ["", "A", { osKey: true, metaKey: true } ], + ["", "B", {} ], + ["k-b-myo", "B", { osKey: true } ], + ["k-b-myo", "B", { osKey: true, metaKey: true } ], + ["k-f-oym", "F", { metaKey: true } ], + ["k-f-oym", "F", { metaKey: true, osKey: true } ], + ["k-c-scaym", "C", { metaKey: true } ], + ["k-c-scaym", "C", { shiftKey: true, ctrlKey: true, altKey: true, metaKey: true } ], + [kIsWin ? "k-c-scaym" : "", "C", { shiftKey: true, ctrlKey: true, altKey: true, metaKey: true, osKey: true } ], + ["", "V", { shiftKey: true, ctrlKey: true, altKey: true } ], + ["k-h-l", "H", { accelKey: true } ], +// ["k-j-s", "J", { accessKey: true } ], + ["", "T", { } ], + ["k-g-c", "G", { ctrlKey: true } ], + ["k-g-co", "G", { ctrlKey: true, osKey: true } ], + ["scommand", "Y", { } ], + ["", "U", { } ], +]; + +function runTest() +{ + iterateKeys(true, "normal"); + + var keyset = document.getElementById("keyset"); + keyset.setAttribute("disabled", "true"); + iterateKeys(false, "disabled"); + + var keyset = document.getElementById("keyset"); + keyset.removeAttribute("disabled"); + iterateKeys(true, "reenabled"); + + keyset.parentNode.removeChild(keyset); + iterateKeys(false, "removed"); + + document.documentElement.appendChild(keyset); + iterateKeys(true, "appended"); + + var accelText = menuitem => menuitem.getAttribute("acceltext").toLowerCase(); + + $("menubutton").open = true; + + // now check if a menu updates its accelerator text when a key attribute is changed + var menuitem1 = $("menuitem1"); + ok(accelText(menuitem1).indexOf("d") >= 0, "menuitem1 accelText before"); + if (kIsWin) { + ok(accelText(menuitem1).indexOf("alt") >= 0, "menuitem1 accelText modifier before"); + } + + menuitem1.setAttribute("key", "k-s-c"); + ok(accelText(menuitem1).indexOf("s") >= 0, "menuitem1 accelText after"); + if (kIsWin) { + ok(accelText(menuitem1).indexOf("ctrl") >= 0, "menuitem1 accelText modifier after"); + } + + menuitem1.setAttribute("acceltext", "custom"); + is(accelText(menuitem1), "custom", "menuitem1 accelText set custom"); + menuitem1.removeAttribute("acceltext"); + ok(accelText(menuitem1).indexOf("s") >= 0, "menuitem1 accelText remove"); + if (kIsWin) { + ok(accelText(menuitem1).indexOf("ctrl") >= 0, "menuitem1 accelText modifier remove"); + } + + var menuitem2 = $("menuitem2"); + is(accelText(menuitem2), "", "menuitem2 accelText before"); + menuitem2.setAttribute("key", "k-s-c"); + ok(accelText(menuitem2).indexOf("s") >= 0, "menuitem2 accelText before"); + if (kIsWin) { + ok(accelText(menuitem2).indexOf("ctrl") >= 0, "menuitem2 accelText modifier before"); + } + + menuitem2.setAttribute("key", "k-h-l"); + ok(accelText(menuitem2).indexOf("h") >= 0, "menuitem2 accelText after"); + if (kIsWin) { + ok(accelText(menuitem2).indexOf("ctrl") >= 0, "menuitem2 accelText modifier after"); + } + + menuitem2.removeAttribute("key"); + is(accelText(menuitem2), "", "menuitem2 accelText after remove"); + + $("menubutton").open = false; + + window.close(); + window.opener.wrappedJSObject.SimpleTest.finish(); +} + +function iterateKeys(enabled, testid) +{ + for (var k = 0; k < keysToTest.length; k++) { + gExpected = keysToTest[k]; + var expectedKey = gExpected[0]; + if (!gExpected[2].accessKey || navigator.platform.indexOf("Mac") == -1) { + synthesizeKey(gExpected[1], gExpected[2]); + ok((enabled && expectedKey) || expectedKey == "k-d-a" ? + !gExpected : gExpected, testid + " key step " + (k + 1)); + } + } +} + +function checkKey(event) +{ + // the first element of the gExpected array holds the id of the <key> element + // that was expected. If this is empty, a handler wasn't expected to be called + if (gExpected[0]) + is(event.originalTarget.id, gExpected[0], "key " + gExpected[1]); + else + is("key " + event.originalTarget.id + " was activated", "", "key " + gExpected[1]); + gExpected = null; +} + +function is(l, r, n) { window.opener.wrappedJSObject.SimpleTest.is(l,r,n); } +function ok(v, n) { window.opener.wrappedJSObject.SimpleTest.ok(v,n); } + +SimpleTest.waitForFocus(runTest); + +]]> +</script> + +<command id="scommand" oncommand="checkKey(event)"/> +<command id="scommand-disabled" disabled="true"/> + +<keyset id="keyset"> + <key id="k-v" key="v" oncommand="checkKey(event)"/> + <key id="k-v-scy" key="v" modifiers="shift any control" oncommand="checkKey(event)"/> + <key id="k-e-y" key="e" modifiers="any" oncommand="checkKey(event)"/> + <key id="k-8-m" key="8" modifiers="meta" oncommand="checkKey(event)"/> + <key id="k-a-o" key="a" modifiers="os" oncommand="checkKey(event)"/> + <key id="k-b-myo" key="b" modifiers="meta any os" oncommand="checkKey(event)"/> + <key id="k-f-oym" key="f" modifiers="os any meta" oncommand="checkKey(event)"/> + <key id="k-c-scaym" key="c" modifiers="shift control alt any meta" oncommand="checkKey(event)"/> + <key id="k-h-l" key="h" modifiers="accel" oncommand="checkKey(event)"/> + <key id="k-j-s" key="j" modifiers="access" oncommand="checkKey(event)"/> + <key id="k-t-y" disabled="true" key="t" oncommand="checkKey(event)"/> + <key id="k-g-c" key="g" modifiers="control" oncommand="checkKey(event)"/> + <key id="k-g-co" key="g" modifiers="control os" oncommand="checkKey(event)"/> + <key id="k-y" key="y" command="scommand"/> + <key id="k-u" key="u" command="scommand-disabled"/> +</keyset> + +<keyset id="keyset2"> + <key id="k-d-a" key="d" modifiers="alt" oncommand="checkKey(event)"/> + <key id="k-s-c" key="s" modifiers="control" oncommand="checkKey(event)"/> +</keyset> + +<button id="menubutton" label="Menu" type="menu"> + <menupopup> + <menuitem id="menuitem1" label="Item 1" key="k-d-a"/> + <menuitem id="menuitem2" label="Item 2"/> + </menupopup> +</button> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_largemenu.xul b/toolkit/content/tests/chrome/window_largemenu.xul new file mode 100644 index 0000000000..72e1c077d8 --- /dev/null +++ b/toolkit/content/tests/chrome/window_largemenu.xul @@ -0,0 +1,425 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Large Menu Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<!-- + This test checks that a large menu is displayed with arrow buttons + and is on the screen. + --> + +<script> +<![CDATA[ + +var gOverflowed = false, gUnderflowed = false; +var gContextMenuTests = false; +var gScreenY = -1; +var gTestIndex = 0; +var gTests = ["open normal", "open when bottom would overlap", "open with scrolling", + "open after scrolling", "open small again", + "menu movement", "panel movement", + "context menu enough space below", + "context menu more space above", + "context menu too big either side", + "context menu larger than screen", + "context menu flips horizontally on osx"]; +function getScreenXY(element) +{ + var screenX, screenY; + var mouseFn = function(event) { + screenX = event.screenX - 1; + screenY = event.screenY - 1; + } + + // a hacky way to get the screen position of an element without using the box object + window.addEventListener("mousedown", mouseFn, false); + synthesizeMouse(element, 1, 1, { }); + window.removeEventListener("mousedown", mouseFn, false); + + return [screenX, screenY]; +} + +function hidePopup() { + window.requestAnimationFrame( + function() { + setTimeout( + function() { + document.getElementById("popup").hidePopup(); + }, 0); + }); +} + +function runTests() +{ + [, gScreenY] = getScreenXY(document.documentElement); + nextTest(); +} + +function nextTest() +{ + gOverflowed = false, gUnderflowed = false; + + var y = screen.height; + if (gTestIndex == 1) // open with bottom overlap test: + y -= 100; + else + y /= 2; + + var popup = document.getElementById("popup"); + if (gTestIndex == 2) { + // add some more menuitems so that scrolling will be necessary + var moreItemCount = Math.round(screen.height / popup.firstChild.getBoundingClientRect().height); + for (var t = 1; t <= moreItemCount; t++) { + var menu = document.createElement("menuitem"); + menu.setAttribute("label", "More" + t); + popup.appendChild(menu); + } + } + else if (gTestIndex == 4) { + // remove the items added in test 2 above + while (popup.childNodes.length > 15) + popup.removeChild(popup.lastChild); + } + + window.requestAnimationFrame(function() { + setTimeout( + function() { + popup.openPopupAtScreen(100, y, false); + }, 0); + }); +} + +function popupShown() +{ + if (gTests[gTestIndex] == "menu movement") + return testPopupMovement(); + + if (gContextMenuTests) + return contextMenuPopupShown(); + + var popup = document.getElementById("popup"); + var rect = popup.getBoundingClientRect(); + var sbo = document.getAnonymousNodes(popup)[0].scrollBoxObject; + var expectedScrollPos = 0; + + if (gTestIndex == 0) { + // the popup should be in the center of the screen + // note that if the height is odd, the y-offset will have been rounded + // down when we pass the fractional value to openPopupAtScreen above. + is(Math.round(rect.top) + gScreenY, Math.floor(screen.height / 2), + gTests[gTestIndex] + " top"); + ok(Math.round(rect.bottom) + gScreenY < screen.height, + gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow") + } + else if (gTestIndex == 1) { + // the popup was supposed to open 100 pixels from the bottom, but that + // would put it off screen so ... + if (platformIsMac()) { + // On OSX the popup is constrained so it remains within the + // bounds of the screen + ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top"); + is(Math.round(rect.bottom) + gScreenY, screen.availTop + screen.availHeight, gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow"); + } + else { + // On other platforms the menu should be flipped to have its bottom + // edge 100 pixels from the bottom + ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top"); + is(Math.round(rect.bottom) + gScreenY, screen.height - 100, + gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow"); + } + } + else if (gTestIndex == 2) { + // the popup is too large so ensure that it is on screen + ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top"); + ok(Math.round(rect.bottom) + gScreenY <= screen.height, gTests[gTestIndex] + " bottom"); + ok(gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow") + + sbo.scrollTo(0, 40); + expectedScrollPos = 40; + } + else if (gTestIndex == 3) { + expectedScrollPos = 40; + } + else if (gTestIndex == 4) { + // note that if the height is odd, the y-offset will have been rounded + // down when we pass the fractional value to openPopupAtScreen above. + is(Math.round(rect.top) + gScreenY, Math.floor(screen.height / 2), + gTests[gTestIndex] + " top"); + ok(Math.round(rect.bottom) + gScreenY < screen.height, + gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && gUnderflowed, gTests[gTestIndex] + " overflow"); + } + + is(sbo.positionY, expectedScrollPos, "menu scroll position"); + + hidePopup(); +} + +function is(l, r, n) { window.opener.wrappedJSObject.SimpleTest.is(l,r,n); } +function ok(v, n) { window.opener.wrappedJSObject.SimpleTest.ok(v,n); } + +var oldx, oldy, waitSteps = 0; +function moveWindowTo(x, y, callback, arg) +{ + if (!waitSteps) { + oldx = window.screenX; + oldy = window.screenY; + window.moveTo(x, y); + + waitSteps++; + setTimeout(moveWindowTo, 100, x, y, callback, arg); + return; + } + + if (window.screenX == oldx && window.screenY == oldy) { + if (waitSteps++ > 10) { + ok(false, "Window never moved properly to " + x + "," + y); + window.opener.wrappedJSObject.SimpleTest.finish(); + window.close(); + } + + setTimeout(moveWindowTo, 100, x, y, callback, arg); + } + else { + waitSteps = 0; + callback(arg); + } +} + +function popupHidden() +{ + gTestIndex++; + if (gTestIndex == gTests.length) { + window.opener.wrappedJSObject.SimpleTest.finish(); + window.close(); + } + else if (gTests[gTestIndex] == "context menu enough space below") { + gContextMenuTests = true; + moveWindowTo(window.screenX, screen.availTop + 10, + () => synthesizeMouse(document.getElementById("label"), 4, 4, { type: "contextmenu", button: 2 })); + } + else if (gTests[gTestIndex] == "menu movement") { + document.getElementById("popup").openPopup( + document.getElementById("label"), "after_start", 0, 0, false, false); + } + else if (gTests[gTestIndex] == "panel movement") { + document.getElementById("panel").openPopup( + document.getElementById("label"), "after_start", 0, 0, false, false); + } + else if (gContextMenuTests) { + contextMenuPopupHidden(); + } + else { + nextTest(); + } +} + +function contextMenuPopupShown() +{ + var popup = document.getElementById("popup"); + var rect = popup.getBoundingClientRect(); + var labelrect = document.getElementById("label").getBoundingClientRect(); + + // Click to open popup in popupHidden() occurs at (4,4) in label's coordinate space + var clickX = clickY = 4; + + var testPopupAppearedRightOfCursor = true; + switch (gTests[gTestIndex]) { + case "context menu enough space below": + is(rect.top, labelrect.top + clickY + (platformIsMac() ? -6 : 2), gTests[gTestIndex] + " top"); + break; + case "context menu more space above": + if (platformIsMac()) { + let screenY; + [, screenY] = getScreenXY(popup); + // Macs constrain their popup menus vertically rather than flip them. + is(screenY, screen.availTop + screen.availHeight - rect.height, gTests[gTestIndex] + " top"); + } else { + is(rect.top, labelrect.top + clickY - rect.height - 2, gTests[gTestIndex] + " top"); + } + + break; + case "context menu too big either side": + [, gScreenY] = getScreenXY(document.documentElement); + // compare against the available size as well as the total size, as some + // platforms allow the menu to overlap os chrome and others do not + var pos = (screen.availTop + screen.availHeight - rect.height) - gScreenY; + var availPos = (screen.top + screen.height - rect.height) - gScreenY; + ok(rect.top == pos || rect.top == availPos, + gTests[gTestIndex] + " top"); + break; + case "context menu larger than screen": + ok(rect.top == -(gScreenY - screen.availTop) || rect.top == -(gScreenY - screen.top), gTests[gTestIndex] + " top"); + break; + case "context menu flips horizontally on osx": + testPopupAppearedRightOfCursor = false; + if (platformIsMac()) { + is(Math.round(rect.right), labelrect.left + clickX - 1, gTests[gTestIndex] + " right"); + } + break; + } + + if (testPopupAppearedRightOfCursor) { + is(rect.left, labelrect.left + clickX + (platformIsMac() ? 1 : 2), gTests[gTestIndex] + " left"); + } + + hidePopup(); +} + +function contextMenuPopupHidden() +{ + var screenAvailBottom = screen.availTop + screen.availHeight; + + if (gTests[gTestIndex] == "context menu more space above") { + moveWindowTo(window.screenX, screenAvailBottom - 80, nextContextMenuTest, -1); + } + else if (gTests[gTestIndex] == "context menu too big either side") { + moveWindowTo(window.screenX, screenAvailBottom / 2 - 80, nextContextMenuTest, screenAvailBottom / 2 + 120); + } + else if (gTests[gTestIndex] == "context menu larger than screen") { + nextContextMenuTest(screen.availHeight + 80); + } + else if (gTests[gTestIndex] == "context menu flips horizontally on osx") { + var popup = document.getElementById("popup"); + var popupWidth = popup.getBoundingClientRect().width; + moveWindowTo(screen.availLeft + screen.availWidth - popupWidth, 100, nextContextMenuTest, -1); + } +} + +function nextContextMenuTest(desiredHeight) +{ + if (desiredHeight >= 0) { + var popup = document.getElementById("popup"); + var height = popup.getBoundingClientRect().height; + var itemheight = document.getElementById("firstitem").getBoundingClientRect().height; + while (height < desiredHeight) { + var menu = document.createElement("menuitem"); + menu.setAttribute("label", "Item"); + popup.appendChild(menu); + height += itemheight; + } + } + + synthesizeMouse(document.getElementById("label"), 4, 4, { type: "contextmenu", button: 2 }); +} + +function testPopupMovement() +{ + var button = document.getElementById("label"); + var isPanelTest = (gTests[gTestIndex] == "panel movement"); + var popup = document.getElementById(isPanelTest ? "panel" : "popup"); + + var screenX, screenY, buttonScreenX, buttonScreenY; + var rect = popup.getBoundingClientRect(); + + var overlapOSChrome = !platformIsMac(); + popup.moveTo(1, 1); + [screenX, screenY] = getScreenXY(popup); + + var expectedx = 1, expectedy = 1; + if (!isPanelTest && !overlapOSChrome) { + if (screen.availLeft >= 1) expectedx = screen.availLeft; + if (screen.availTop >= 1) expectedy = screen.availTop; + } + is(screenX, expectedx, gTests[gTestIndex] + " (1, 1) x"); + is(screenY, expectedy, gTests[gTestIndex] + " (1, 1) y"); + + popup.moveTo(100, 8000); + if (isPanelTest) { + expectedy = 8000; + } + else { + expectedy = (overlapOSChrome ? screen.height + screen.top : screen.availHeight + screen.availTop) - + Math.round(rect.height); + } + + [screenX, screenY] = getScreenXY(popup); + is(screenX, 100, gTests[gTestIndex] + " (100, 8000) x"); + is(screenY, expectedy, gTests[gTestIndex] + " (100, 8000) y"); + + popup.moveTo(6000, 100); + + if (isPanelTest) { + expectedx = 6000; + } + else { + expectedx = (overlapOSChrome ? screen.width + screen.left : screen.availWidth + screen.availLeft) - + Math.round(rect.width); + } + + [screenX, screenY] = getScreenXY(popup); + is(screenX, expectedx, gTests[gTestIndex] + " (6000, 100) x"); + is(screenY, 100, gTests[gTestIndex] + " (6000, 100) y"); + + is(popup.left, "", gTests[gTestIndex] + " left is empty after moving"); + is(popup.top, "", gTests[gTestIndex] + " top is empty after moving"); + popup.setAttribute("left", "80"); + popup.setAttribute("top", "82"); + [screenX, screenY] = getScreenXY(popup); + is(screenX, 80, gTests[gTestIndex] + " set left and top x"); + is(screenY, 82, gTests[gTestIndex] + " set left and top y"); + popup.moveTo(95, 98); + [screenX, screenY] = getScreenXY(popup); + is(screenX, 95, gTests[gTestIndex] + " move after set left and top x"); + is(screenY, 98, gTests[gTestIndex] + " move after set left and top y"); + is(popup.left, "95", gTests[gTestIndex] + " left is set after moving"); + is(popup.top, "98", gTests[gTestIndex] + " top is set after moving"); + popup.removeAttribute("left"); + popup.removeAttribute("top"); + + popup.moveTo(-1, -1); + [screenX, screenY] = getScreenXY(popup); + + expectedx = (overlapOSChrome ? screen.left : screen.availLeft); + expectedy = (overlapOSChrome ? screen.top : screen.availTop); + + is(screenX, expectedx, gTests[gTestIndex] + " move after set left and top x to -1"); + is(screenY, expectedy, gTests[gTestIndex] + " move after set left and top y to -1"); + is(popup.left, "", gTests[gTestIndex] + " left is not set after moving to -1"); + is(popup.top, "", gTests[gTestIndex] + " top is not set after moving to -1"); + + popup.hidePopup(); +} + +function platformIsMac() +{ + return navigator.platform.indexOf("Mac") > -1; +} + +window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTests, window); + +]]> +</script> + +<button id="label" label="OK" context="popup"/> +<menupopup id="popup" onpopupshown="popupShown();" onpopuphidden="popupHidden();" + onoverflow="gOverflowed = true" onunderflow="gUnderflowed = true;"> + <menuitem id="firstitem" label="1"/> + <menuitem label="2"/> + <menuitem label="3"/> + <menuitem label="4"/> + <menuitem label="5"/> + <menuitem label="6"/> + <menuitem label="7"/> + <menuitem label="8"/> + <menuitem label="9"/> + <menuitem label="10"/> + <menuitem label="11"/> + <menuitem label="12"/> + <menuitem label="13"/> + <menuitem label="14"/> + <menuitem label="15"/> +</menupopup> + +<panel id="panel" onpopupshown="testPopupMovement();" onpopuphidden="popupHidden();" style="margin: 0"> + <button label="OK"/> +</panel> + +</window> diff --git a/toolkit/content/tests/chrome/window_panel.xul b/toolkit/content/tests/chrome/window_panel.xul new file mode 100644 index 0000000000..b99b52dfaf --- /dev/null +++ b/toolkit/content/tests/chrome/window_panel.xul @@ -0,0 +1,312 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for panels + --> +<window title="Titlebar" width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<tree id="tree" seltype="single" width="100" height="100"> + <treecols> + <treecol flex="1"/> + <treecol flex="1"/> + </treecols> + <treechildren id="treechildren"> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + </treechildren> +</tree> + + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var currentTest = null; + +function ok(condition, message) { + window.opener.wrappedJSObject.SimpleTest.ok(condition, message); +} + +function is(left, right, message) { + window.opener.wrappedJSObject.SimpleTest.is(left, right, message); +} + +function test_panels() +{ + checkTreeCoords(); + + addEventListener("popupshowing", popupShowing, false); + addEventListener("popupshown", popupShown, false); + addEventListener("popuphidden", nextTest, false); + nextTest(); +} + +function nextTest() +{ + if (!tests.length) { + window.close(); + window.opener.wrappedJSObject.SimpleTest.finish(); + return; + } + + currentTest = tests.shift(); + var panel = createPanel(currentTest.attrs); + currentTest.test(panel); +} + +function popupShowing(event) +{ + var rect = event.target.getOuterScreenRect(); + ok(!rect.left && !rect.top && !rect.width && !rect.height, + currentTest.testname + " empty rectangle during popupshowing"); +} + +var waitSteps = 0; +function popupShown(event) +{ + var panel = event.target; + + if (waitSteps > 0 && navigator.platform.indexOf("Linux") >= 0 && + panel.boxObject.screenY == 210) { + waitSteps--; + setTimeout(popupShown, 10, event); + return; + } + + currentTest.result(currentTest.testname + " ", panel); + panel.hidePopup(); +} + +function createPanel(attrs) +{ + var panel = document.createElement("panel"); + for (var a in attrs) { + panel.setAttribute(a, attrs[a]); + } + + var button = document.createElement("button"); + panel.appendChild(button); + button.label = "OK"; + button.width = 120; + button.height = 40; + button.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;"); + panel.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;"); + return document.documentElement.appendChild(panel); +} + +function checkTreeCoords() +{ + var tree = $("tree"); + var treechildren = $("treechildren"); + tree.currentIndex = 0; + tree.treeBoxObject.scrollToRow(0); + synthesizeMouse(treechildren, 10, tree.treeBoxObject.rowHeight + 2, { }); + is(tree.currentIndex, 1, "tree selection"); + + tree.treeBoxObject.scrollToRow(2); + synthesizeMouse(treechildren, 10, tree.treeBoxObject.rowHeight + 2, { }); + is(tree.currentIndex, 3, "tree selection after scroll"); +} + +var tests = [ + { + testname: "normal panel", + attrs: { }, + test: function(panel) { + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 0, this.testname + " screen left before open"); + is(screenRect.top, 0, this.testname + " screen top before open"); + is(screenRect.width, 0, this.testname + " screen width before open"); + is(screenRect.height, 0, this.testname + " screen height before open"); + + panel.openPopupAtScreen(200, 210); + }, + result: function(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, 200 - mozInnerScreenX, testname + "left"); + is(panelrect.top, 210 - mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 200, testname + " screen left"); + is(screenRect.top, 210, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, + { + // only noautohide panels support titlebars, so one shouldn't be shown here + testname: "autohide panel with titlebar", + attrs: { titlebar: "normal" }, + test: function(panel) { + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 0, this.testname + " screen left before open"); + is(screenRect.top, 0, this.testname + " screen top before open"); + is(screenRect.width, 0, this.testname + " screen width before open"); + is(screenRect.height, 0, this.testname + " screen height before open"); + + panel.openPopupAtScreen(200, 210); + }, + result: function(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, 200 - mozInnerScreenX, testname + "left"); + is(panelrect.top, 210 - mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 200, testname + " screen left"); + is(screenRect.top, 210, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, + { + testname: "noautohide panel with titlebar", + attrs: { noautohide: true, titlebar: "normal" }, + test: function(panel) { + waitSteps = 25; + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 0, this.testname + " screen left before open"); + is(screenRect.top, 0, this.testname + " screen top before open"); + is(screenRect.width, 0, this.testname + " screen width before open"); + is(screenRect.height, 0, this.testname + " screen height before open"); + + panel.openPopupAtScreen(200, 210); + }, + result: function(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + ok(panelrect.left >= 200 - mozInnerScreenX, testname + "left"); + if (navigator.platform.indexOf("Linux") < 0) { + ok(panelrect.top >= 210 - mozInnerScreenY + 10, testname + "top greater"); + } + ok(panelrect.top <= 210 - mozInnerScreenY + 32, testname + "top less"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + if (navigator.platform.indexOf("Linux") < 0) { + is(screenRect.left, 200, testname + " screen left"); + is(screenRect.top, 210, testname + " screen top"); + } + ok(screenRect.width >= 120 && screenRect.width <= 140, testname + " screen width"); + ok(screenRect.height >= 40 && screenRect.height <= 80, testname + " screen height"); + + var gotMouseEvent = false; + function mouseMoved(event) + { + is(event.clientY, panelrect.top + 10, + "popup clientY"); + is(event.screenY, panel.boxObject.screenY + 10, + "popup screenY"); + is(event.originalTarget, panel.firstChild, "popup target"); + gotMouseEvent = true; + } + + panel.addEventListener("mousemove", mouseMoved, true); + synthesizeMouse(panel, 10, 10, { type: "mousemove" }); + ok(gotMouseEvent, "mouse event on panel"); + panel.removeEventListener("mousemove", mouseMoved, true); + + var tree = $("tree"); + tree.currentIndex = 0; + panel.appendChild(tree); + checkTreeCoords(); + } + }, + { + testname: "noautohide panel with backdrag", + attrs: { noautohide: true, backdrag: "true" }, + test: function(panel) { + var label = document.createElement("label"); + label.id = "backdragspot"; + label.setAttribute("value", "Hello There"); + panel.appendChild(label); + panel.openPopupAtScreen(200, 230); + }, + result: function(testname, panel) { + var oldrect = panel.getOuterScreenRect(); + + // Linux uses native window moving + if (navigator.platform.indexOf("Linux") == -1) { + var backdragspot = document.getElementById("backdragspot"); + synthesizeMouse(backdragspot, 5, 5, { type: "mousedown" }); + synthesizeMouse(backdragspot, 15, 20, { type: "mousemove" }); + synthesizeMouse(backdragspot, 15, 20, { type: "mouseup" }); + + is(panel.getOuterScreenRect().left, 210, testname + "left"); + is(panel.getOuterScreenRect().top, 245, testname + "top"); + } + } + }, + { + // The panel should be allowed to appear and remain offscreen + testname: "normal panel with flip='none' off-screen", + attrs: { "flip": "none" }, + test: function(panel) { + panel.openPopup(document.documentElement, "", -100 - mozInnerScreenX, -100 - mozInnerScreenY, false, false, null); + }, + result: function(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, -100 - mozInnerScreenX, testname + "left"); + is(panelrect.top, -100 - mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, -100, testname + " screen left"); + is(screenRect.top, -100, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, + { + // The panel should be allowed to remain offscreen after moving and it should follow the anchor + testname: "normal panel with flip='none' moved off-screen", + attrs: { "flip": "none" }, + test: function(panel) { + panel.openPopup(document.documentElement, "", -100 - mozInnerScreenX, -100 - mozInnerScreenY, false, false, null); + window.moveBy(-50, -50); + }, + result: function(testname, panel) { + if (navigator.platform.indexOf("Linux") >= 0) { + // The window position doesn't get updated immediately on Linux. + return; + } + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, -150 - mozInnerScreenX, testname + "left"); + is(panelrect.top, -150 - mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, -150, testname + " screen left"); + is(screenRect.top, -150, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, +]; + +window.opener.wrappedJSObject.SimpleTest.waitForFocus(test_panels, window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/window_panel_focus.xul b/toolkit/content/tests/chrome/window_panel_focus.xul new file mode 100644 index 0000000000..6ac1abdc0b --- /dev/null +++ b/toolkit/content/tests/chrome/window_panel_focus.xul @@ -0,0 +1,132 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Panel Focus Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<checkbox id="b1" label="Item 1"/> + +<!-- Focus should be in this order: 2 6 3 8 1 4 5 7 9 --> +<panel id="panel" norestorefocus="true" onpopupshown="panelShown()" onpopuphidden="panelHidden()"> + <button id="t1" label="Button One"/> + <button id="t2" tabindex="1" label="Button Two" onblur="gButtonBlur++;"/> + <button id="t3" tabindex="2" label="Button Three"/> + <button id="t4" tabindex="0" label="Button Four"/> + <button id="t5" label="Button Five"/> + <button id="t6" tabindex="1" label="Button Six"/> + <button id="t7" label="Button Seven"/> + <button id="t8" tabindex="4" label="Button Eight"/> + <button id="t9" label="Button Nine"/> +</panel> + +<panel id="noautofocusPanel" noautofocus="true" + onpopupshown="noautofocusPanelShown()" onpopuphidden="noautofocusPanelHidden()"> + <textbox id="tb3"/> +</panel> + +<checkbox id="b2" label="Item 2" popup="panel" onblur="gButtonBlur++;"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var gButtonBlur = 0; + +function showPanel() +{ + // click on the document so that the window has focus + synthesizeMouse(document.documentElement, 1, 1, { }); + + // focus the button + synthesizeKeyExpectEvent("VK_TAB", { }, $("b1"), "focus", "button focus"); + // tabbing again should skip the popup + synthesizeKeyExpectEvent("VK_TAB", { }, $("b2"), "focus", "popup skipped in focus navigation"); + + $("panel").openPopup(null, "", 10, 10, false, false); +} + +function panelShown() +{ + // the focus on the button should have been removed when the popup was opened + is(gButtonBlur, 1, "focus removed when popup opened"); + + // press tab numerous times to cycle through the buttons. The t2 button will + // be blurred twice, so gButtonBlur will be 3 afterwards. + synthesizeKeyExpectEvent("VK_TAB", { }, $("t2"), "focus", "tabindex 1"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("t6"), "focus", "tabindex 2"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("t3"), "focus", "tabindex 3"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("t8"), "focus", "tabindex 4"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("t1"), "focus", "tabindex 5"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("t4"), "focus", "tabindex 6"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("t5"), "focus", "tabindex 7"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("t7"), "focus", "tabindex 8"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("t9"), "focus", "tabindex 9"); + synthesizeKeyExpectEvent("VK_TAB", { }, $("t2"), "focus", "tabindex 10"); + + synthesizeKeyExpectEvent("VK_TAB", { shiftKey: true }, $("t9"), "focus", "back tabindex 1"); + synthesizeKeyExpectEvent("VK_TAB", { shiftKey: true }, $("t7"), "focus", "back tabindex 2"); + synthesizeKeyExpectEvent("VK_TAB", { shiftKey: true }, $("t5"), "focus", "back tabindex 3"); + synthesizeKeyExpectEvent("VK_TAB", { shiftKey: true }, $("t4"), "focus", "back tabindex 4"); + synthesizeKeyExpectEvent("VK_TAB", { shiftKey: true }, $("t1"), "focus", "back tabindex 5"); + synthesizeKeyExpectEvent("VK_TAB", { shiftKey: true }, $("t8"), "focus", "back tabindex 6"); + synthesizeKeyExpectEvent("VK_TAB", { shiftKey: true }, $("t3"), "focus", "back tabindex 7"); + synthesizeKeyExpectEvent("VK_TAB", { shiftKey: true }, $("t6"), "focus", "back tabindex 8"); + synthesizeKeyExpectEvent("VK_TAB", { shiftKey: true }, $("t2"), "focus", "back tabindex 9"); + + is(gButtonBlur, 3, "blur events fired within popup"); + + synthesizeKey("VK_ESCAPE", { }); +} + +function ok(condition, message) { + window.opener.wrappedJSObject.SimpleTest.ok(condition, message); +} + +function is(left, right, message) { + window.opener.wrappedJSObject.SimpleTest.is(left, right, message); +} + +function panelHidden() +{ + // closing the popup should have blurred the focused element + is(gButtonBlur, 4, "focus removed when popup closed"); + + // now that the panel is hidden, pressing tab should focus the elements in + // the main window again + synthesizeKeyExpectEvent("VK_TAB", { }, $("b1"), "focus", "focus after popup closed"); + + $("noautofocusPanel").openPopup(null, "", 10, 10, false, false); +} + +function noautofocusPanelShown() +{ + // with noautofocus="true", the focus should not be removed when the panel is + // opened, so key events should still be fired at the checkbox. + synthesizeKeyExpectEvent("VK_SPACE", { }, $("b1"), "command", "noautofocus"); + $("noautofocusPanel").hidePopup(); +} + +function noautofocusPanelHidden() +{ + window.close(); + window.opener.wrappedJSObject.SimpleTest.finish(); +} + +window.opener.wrappedJSObject.SimpleTest.waitForFocus(showPanel, window); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_anchor.xul b/toolkit/content/tests/chrome/window_popup_anchor.xul new file mode 100644 index 0000000000..45f5fe365a --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_anchor.xul @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Anchor Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script> +function runTests() +{ + frames[0].openPopup(); +} + +window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTests, window); +</script> + +<spacer height="13"/> +<button id="outerbutton" label="Button One" style="margin-left: 6px; -moz-appearance: none;"/> +<hbox> + <spacer width="20"/> + <deck> + <vbox> + <iframe id="frame" style="margin-left: 60px; margin-top: 10px; border-left: 17px solid red; padding-left: 0 !important; padding-top: 3px;" + width="250" height="80" src="frame_popup_anchor.xul"/> + </vbox> + </deck> +</hbox> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_anchoratrect.xul b/toolkit/content/tests/chrome/window_popup_anchoratrect.xul new file mode 100644 index 0000000000..ff37afee75 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_anchoratrect.xul @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onpopupshown="popupshown(event.target)" onpopuphidden="nextTest()"> + +<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<label value="Popup Test"/> + +<menupopup id="popup"> + <menuitem label="One"/> + <menuitem label="Two"/> +</menupopup> + +<panel id="panel" noautohide="true" height="20"> + <label value="OK"/> +</panel> + +<script> +<![CDATA[ + +let menupopup; + +let tests = [ + { + test: () => menupopup.openPopupAtScreenRect("after_start", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 150, "popup at screen position x"); + is(rect.top, 290, "popup at screen position y"); + } + }, + { + test: () => menupopup.openPopupAtScreenRect("after_start", 150, 350, 30, 9000), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 150, "flipped popup at screen position x"); + is(rect.bottom, 350, "flipped popup at screen position y"); + } + }, + { + test: () => menupopup.openPopupAtScreenRect("end_before", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 180, "popup at end_before screen position x"); + is(rect.top, 250, "popup at end_before screen position y"); + } + }, + { + test: () => $("panel").openPopupAtScreenRect("after_start", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 150, "panel at screen position x"); + is(rect.top, 290, "panel at screen position y"); + } + }, + { + test: () => $("panel").openPopupAtScreenRect("before_start", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 150, "panel at before_start screen position x"); + is(rect.bottom, 250, "panel at before_start screen position y"); + } + }, +]; + +function runTest(id) +{ + menupopup = $("popup"); + nextTest(); +} + +function nextTest() +{ + if (!tests.length) { + window.close(); + window.opener.SimpleTest.finish(); + return; + } + + tests[0].test(); +} + +function popupshown(popup) +{ + tests[0].verify(popup); + tests.shift(); + popup.hidePopup(); +} + +function is(left, right, message) +{ + window.opener.SimpleTest.is(left, right, message); +} + +function ok(value, message) +{ + window.opener.SimpleTest.ok(value, message); +} + +window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTest, window); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_attribute.xul b/toolkit/content/tests/chrome/window_popup_attribute.xul new file mode 100644 index 0000000000..9316d31c4a --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_attribute.xul @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Attribute Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + <script type="application/javascript" src="popup_trigger.js"></script> + +<script> +window.opener.SimpleTest.waitForFocus(runTests, window); +</script> + +<hbox style="margin-left: 200px; margin-top: 270px;"> + <label id="trigger" popup="thepopup" value="Popup" height="60"/> +</hbox> +<!-- this frame is used to check that document.popupNode + is inaccessible from different sources --> +<iframe id="childframe" type="content" width="10" height="10" + src="http://sectest2.example.org:80/chrome/toolkit/content/tests/chrome/popup_childframe_node.xul"/> + +<menupopup id="thepopup"> + <menuitem id="item1" label="First"/> + <menuitem id="item2" label="Main Item"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem id="item3" label="Third"/> + <menuitem id="one" label="One"/> + <menuitem id="fancier" label="Fancier Menu"/> + <menu id="submenu" label="Only Menu"> + <menupopup id="submenupopup"> + <menuitem id="submenuitem" label="Test Submenu"/> + </menupopup> + </menu> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="secondlast" label="Second Last Menu" accesskey="T"/> + <menuitem id="last" label="One Other Menu"/> +</menupopup> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_button.xul b/toolkit/content/tests/chrome/window_popup_button.xul new file mode 100644 index 0000000000..125e6886cb --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_button.xul @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + <script type="application/javascript" src="popup_trigger.js"></script> + +<script> +window.opener.SimpleTest.waitForFocus(runTests, window); +</script> + +<hbox style="margin-left: 200px; margin-top: 270px;"> + <button id="trigger" type="menu" label="Popup" width="100" height="50"> + <menupopup id="thepopup"> + <menuitem id="item1" label="First"/> + <menuitem id="item2" label="Main Item"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem id="item3" label="Third"/> + <menuitem id="one" label="One"/> + <menuitem id="fancier" label="Fancier Menu"/> + <menu id="submenu" label="Only Menu"> + <menupopup id="submenupopup"> + <menuitem id="submenuitem" label="Test Submenu"/> + </menupopup> + </menu> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="secondlast" label="Second Last Menu" accesskey="T"/> + <menuitem id="last" label="One Other Menu"/> + </menupopup> + </button> +</hbox> + +<!-- this frame is used to check that document.popupNode + is inaccessible from different sources --> +<iframe id="childframe" type="content" width="10" height="10" + src="http://sectest2.example.org:80/chrome/toolkit/content/tests/chrome/popup_childframe_node.xul"/> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_preventdefault_chrome.xul b/toolkit/content/tests/chrome/window_popup_preventdefault_chrome.xul new file mode 100644 index 0000000000..4d10d7fc75 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_preventdefault_chrome.xul @@ -0,0 +1,113 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Prevent Default Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<!-- + This tests checks that preventDefault can be called on a popupshowing + event or popuphiding event to prevent the default behaviour. + --> + +<script> + +var gBlockShowing = true; +var gBlockHiding = true; +var gShownNotAllowed = true; +var gHiddenNotAllowed = true; + +var fm = Components.classes["@mozilla.org/focus-manager;1"]. + getService(Components.interfaces.nsIFocusManager); + +var is = function(l, r, v) { window.opener.wrappedJSObject.SimpleTest.is(l, r, v); } +var isnot = function(l, r, v) { window.opener.wrappedJSObject.SimpleTest.isnot(l, r, v); } + +function runTest() +{ + var menu = document.getElementById("menu"); + + is(fm.activeWindow, window, "active window at start"); + is(fm.focusedWindow, window, "focused window at start"); + + is(window.windowState, window.STATE_NORMAL, "window is normal"); + // the minimizing test sometimes fails on Linux so don't test it there + if (navigator.platform.indexOf("Lin") == 0) { + menu.open = true; + return; + } + window.minimize(); + is(window.windowState, window.STATE_MINIMIZED, "window is minimized"); + + isnot(fm.activeWindow, window, "active window after minimize"); + isnot(fm.focusedWindow, window, "focused window after minimize"); + + menu.open = true; + + setTimeout(runTestAfterMinimize, 0); +} + +function runTestAfterMinimize() +{ + var menu = document.getElementById("menu"); + is(menu.firstChild.state, "closed", "popup not opened when window minimized"); + + window.restore(); + is(window.windowState, window.STATE_NORMAL, "window is restored"); + + is(fm.activeWindow, window, "active window after restore"); + is(fm.focusedWindow, window, "focused window after restore"); + + menu.open = true; +} + +function popupShowing(event) +{ + if (gBlockShowing) { + event.preventDefault(); + gBlockShowing = false; + setTimeout(function() { + gShownNotAllowed = false; + document.getElementById("menu").open = true; + }, 3000, true); + } +} + +function popupShown() +{ + window.opener.wrappedJSObject.SimpleTest.ok(!gShownNotAllowed, "popupshowing preventDefault"); + document.getElementById("menu").open = false; +} + +function popupHiding(event) +{ + if (gBlockHiding) { + event.preventDefault(); + gBlockHiding = false; + setTimeout(function() { + gHiddenNotAllowed = false; + document.getElementById("menu").open = false; + }, 3000, true); + } +} + +function popupHidden() +{ + window.opener.wrappedJSObject.SimpleTest.ok(!gHiddenNotAllowed, "popuphiding preventDefault"); + window.opener.wrappedJSObject.SimpleTest.finish(); + window.close(); +} + +window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTest, window); +</script> + +<button id="menu" type="menu" label="Menu"> + <menupopup onpopupshowing="popupShowing(event);" + onpopupshown="popupShown();" + onpopuphiding="popupHiding(event);" + onpopuphidden="popupHidden();"> + <menuitem label="Item"/> + </menupopup> +</button> + + +</window> diff --git a/toolkit/content/tests/chrome/window_preferences.xul b/toolkit/content/tests/chrome/window_preferences.xul new file mode 100644 index 0000000000..25ee4b5b29 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences.xul @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window +--> +<prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="preferences window" + windowtype="test:preferences" + buttons="accept,cancel" + onload="RunTest(window.arguments)" +> + <script type="application/javascript"> + <![CDATA[ + function RunTest(aArgs) + { + // run test + aArgs[0](this); + // close dialog + document.documentElement[aArgs[1] ? "acceptDialog" : "cancelDialog"](); + } + ]]> + </script> + + <prefpane id="sample_pane" label="Sample Prefpane"> + <preferences id="sample_preferences"> + <!-- one of each type known to <preferences>.valueFromPreferences --> + <preference id ="tests.static_preference_int" + name="tests.static_preference_int" + type="int"/> + <preference id ="tests.static_preference_bool" + name="tests.static_preference_bool" + type="bool"/> + <preference id ="tests.static_preference_string" + name="tests.static_preference_string" + type="string"/> + <preference id ="tests.static_preference_wstring" + name="tests.static_preference_wstring" + type="wstring"/> + <preference id ="tests.static_preference_unichar" + name="tests.static_preference_unichar" + type="unichar"/> + <preference id ="tests.static_preference_file" + name="tests.static_preference_file" + type="file"/> + </preferences> + + <!-- one element for each preference type above --> + <hbox> + <label flex="1" value="int"/> + <textbox id="static_element_int" preference="tests.static_preference_int"/> + </hbox> + <hbox> + <label flex="1" value="bool"/> + <checkbox id="static_element_bool" preference="tests.static_preference_bool"/> + </hbox> + <hbox> + <label flex="1" value="string"/> + <textbox id="static_element_string" preference="tests.static_preference_string"/> + </hbox> + <hbox> + <label flex="1" value="wstring"/> + <textbox id="static_element_wstring" preference="tests.static_preference_wstring"/> + </hbox> + <hbox> + <label flex="1" value="unichar"/> + <textbox id="static_element_unichar" preference="tests.static_preference_unichar"/> + </hbox> + <hbox> + <label flex="1" value="file"/> + <textbox id="static_element_file" preference="tests.static_preference_file"/> + </hbox> + </prefpane> +</prefwindow> diff --git a/toolkit/content/tests/chrome/window_preferences2.xul b/toolkit/content/tests/chrome/window_preferences2.xul new file mode 100644 index 0000000000..87158c9c7c --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences2.xul @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window +--> +<prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="preferences window" + windowtype="test:preferences2" + buttons="accept,cancel" + onload="RunTest(window.arguments)" +> + <script type="application/javascript"> + <![CDATA[ + function RunTest(aArgs) + { + // open child + document.documentElement.openSubDialog("window_preferences3.xul", "", {test: aArgs[0], accept: aArgs[1]}); + // close dialog + document.documentElement[aArgs[1] ? "acceptDialog" : "cancelDialog"](); + } + ]]> + </script> + + <prefpane id="sample_pane" label="Sample Prefpane"/> +</prefwindow> diff --git a/toolkit/content/tests/chrome/window_preferences3.xul b/toolkit/content/tests/chrome/window_preferences3.xul new file mode 100644 index 0000000000..c37893a678 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences3.xul @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window +--> +<prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="preferences window" + windowtype="test:preferences3" + buttons="accept,cancel" + onload="RunTest(window.arguments)" + type="child" +> + <script type="application/javascript"> + <![CDATA[ + function RunTest(aArgs) + { + // run test + aArgs[0].test(this); + // close dialog + document.documentElement[aArgs[0].accept ? "acceptDialog" : "cancelDialog"](); + } + ]]> + </script> + + <prefpane id="sample_pane" label="Sample Prefpane"> + <preferences id="sample_preferences"> + <!-- one of each type known to <preferences>.valueFromPreferences --> + <preference id ="tests.static_preference_int" + name="tests.static_preference_int" + type="int"/> + <preference id ="tests.static_preference_bool" + name="tests.static_preference_bool" + type="bool"/> + <preference id ="tests.static_preference_string" + name="tests.static_preference_string" + type="string"/> + <preference id ="tests.static_preference_wstring" + name="tests.static_preference_wstring" + type="wstring"/> + <preference id ="tests.static_preference_unichar" + name="tests.static_preference_unichar" + type="unichar"/> + <preference id ="tests.static_preference_file" + name="tests.static_preference_file" + type="file"/> + </preferences> + + <!-- one element for each preference type above --> + <hbox> + <label flex="1" value="int"/> + <textbox id="static_element_int" preference="tests.static_preference_int"/> + </hbox> + <hbox> + <label flex="1" value="bool"/> + <checkbox id="static_element_bool" preference="tests.static_preference_bool"/> + </hbox> + <hbox> + <label flex="1" value="string"/> + <textbox id="static_element_string" preference="tests.static_preference_string"/> + </hbox> + <hbox> + <label flex="1" value="wstring"/> + <textbox id="static_element_wstring" preference="tests.static_preference_wstring"/> + </hbox> + <hbox> + <label flex="1" value="unichar"/> + <textbox id="static_element_unichar" preference="tests.static_preference_unichar"/> + </hbox> + <hbox> + <label flex="1" value="file"/> + <textbox id="static_element_file" preference="tests.static_preference_file"/> + </hbox> + </prefpane> +</prefwindow> diff --git a/toolkit/content/tests/chrome/window_preferences_beforeaccept.xul b/toolkit/content/tests/chrome/window_preferences_beforeaccept.xul new file mode 100644 index 0000000000..ba200b6149 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_beforeaccept.xul @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window with beforeaccept +--> +<prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="preferences window" + width="300" height="300" + windowtype="test:preferences" + buttons="accept,cancel" + onbeforeaccept="return beforeAccept();" + onload="onDialogLoad();" +> + <script type="application/javascript"> + <![CDATA[ + function onDialogLoad() { + var pref = document.getElementById("tests.beforeaccept.dialogShown"); + pref.value = true; + + // call the onload handler we were passed + window.arguments[0](); + } + + function beforeAccept() { + var beforeAcceptPref = document.getElementById("tests.beforeaccept.called"); + var oldValue = beforeAcceptPref.value; + beforeAcceptPref.value = true; + + return !!oldValue; + } + ]]> + </script> + + <prefpane id="sample_pane" label="Sample Prefpane"> + <preferences id="sample_preferences"> + <preference id="tests.beforeaccept.called" + name="tests.beforeaccept.called" + type="bool"/> + <preference id="tests.beforeaccept.dialogShown" + name="tests.beforeaccept.dialogShown" + type="bool"/> + </preferences> + </prefpane> + <label>Test Prefpane</label> +</prefwindow> diff --git a/toolkit/content/tests/chrome/window_preferences_commandretarget.xul b/toolkit/content/tests/chrome/window_preferences_commandretarget.xul new file mode 100644 index 0000000000..77c6fd18ce --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_commandretarget.xul @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window. This particular test ensures that + a checkbox with a command attribute properly updates even though the command + event gets retargeted. +--> +<prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="preferences window" + windowtype="test:preferences" + buttons="accept,cancel" + onload="RunTest(window.arguments)"> + <script type="application/javascript"> + <![CDATA[ + function RunTest(aArgs) + { + aArgs[0](this); + document.documentElement.cancelDialog(); + } + ]]> + </script> + + <prefpane id="sample_pane" label="Sample Prefpane"> + <preferences id="sample_preferences"> + <preference id="tests.static_preference_bool" + name="tests.static_preference_bool" + type="bool"/> + </preferences> + + <commandset> + <command id="cmd_test" preference="tests.static_preference_bool"/> + </commandset> + + <checkbox id="checkbox" label="Enable Option" preference="tests.static_preference_bool" command="cmd_test"/> + </prefpane> +</prefwindow> diff --git a/toolkit/content/tests/chrome/window_preferences_onsyncfrompreference.xul b/toolkit/content/tests/chrome/window_preferences_onsyncfrompreference.xul new file mode 100644 index 0000000000..e0366f9895 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_onsyncfrompreference.xul @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- 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/. --> +<!-- + XUL Widget Test for preferences window with onsyncfrompreference + This test ensures that onsyncfrompreference handlers are called after all the + values of the corresponding preference element have been set correctly +--> +<prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="preferences window" + width="300" height="300" + windowtype="test:preferences"> + + <prefpane id="sample_pane" label="Sample Prefpane"> + <preferences id="sample_preferences"> + <preference id="tests.onsyncfrompreference.pref1" + name="tests.onsyncfrompreference.pref1" + type="int"/> + <preference id="tests.onsyncfrompreference.pref2" + name="tests.onsyncfrompreference.pref2" + type="int"/> + <preference id="tests.onsyncfrompreference.pref3" + name="tests.onsyncfrompreference.pref3" + type="int"/> + </preferences> + </prefpane> + <label>Test Prefpane</label> + <checkbox id="check1" label="Label1" + preference="tests.onsyncfrompreference.pref1" + onsyncfrompreference="return window.arguments[0]();" + onsynctopreference="return 1;"/> + <checkbox id="check2" label="Label2" + preference="tests.onsyncfrompreference.pref2" + onsyncfrompreference="return window.arguments[0]();" + onsynctopreference="return 1;"/> + <checkbox id="check3" label="Label3" + preference="tests.onsyncfrompreference.pref3" + onsyncfrompreference="return window.arguments[0]();" + onsynctopreference="return 1;"/> +</prefwindow> diff --git a/toolkit/content/tests/chrome/window_screenPosSize.xul b/toolkit/content/tests/chrome/window_screenPosSize.xul new file mode 100644 index 0000000000..accc10d8f1 --- /dev/null +++ b/toolkit/content/tests/chrome/window_screenPosSize.xul @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Window Open Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + screenX="80" + screenY="80" + height="300" + width="300" + persist="screenX screenY height width"> + +<body xmlns="http://www.w3.org/1999/xhtml"> + +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_showcaret.xul b/toolkit/content/tests/chrome/window_showcaret.xul new file mode 100644 index 0000000000..cb26658a11 --- /dev/null +++ b/toolkit/content/tests/chrome/window_showcaret.xul @@ -0,0 +1,10 @@ +<?xml version='1.0'?> + +<?xml-stylesheet href='chrome://global/skin' type='text/css'?> + +<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'> + +<hbox style='-moz-user-focus: normal;' width='20' height='20'/> +<textbox/> + +</window> diff --git a/toolkit/content/tests/chrome/window_subframe_origin.xul b/toolkit/content/tests/chrome/window_subframe_origin.xul new file mode 100644 index 0000000000..a060929a64 --- /dev/null +++ b/toolkit/content/tests/chrome/window_subframe_origin.xul @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="window" title="Subframe Origin Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<iframe + style="margin-left:20px; margin-top:20px; min-height:300px; max-width:300px; max-height:300px; border:solid 1px black;" + src="frame_subframe_origin_subframe1.xul"></iframe> +<caption id="parentcap" label=""/> + +<script> + +// Fire a mouse move event aimed at this window, and check to be +// sure the client coords translate from widget to the dom correctly. + +function runTests() +{ + synthesizeMouse(document.getElementById("window"), 1, 2, { type: "mousemove" }); +} + +window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTests, window); + +function mouseMove(e) { + var element = e.target; + var el = document.getElementById("parentcap"); + el.label = "client: (" + e.clientX + "," + e.clientY + ")"; + window.opener.wrappedJSObject.SimpleTest.is(e.clientX, 1, "mouse event clientX"); + window.opener.wrappedJSObject.SimpleTest.is(e.clientY, 2, "mouse event clientY"); + // fire the next test on the sub frame + frames[0].runTests(); +} + +window.addEventListener("mousemove",mouseMove, false); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/window_titlebar.xul b/toolkit/content/tests/chrome/window_titlebar.xul new file mode 100644 index 0000000000..e27782153b --- /dev/null +++ b/toolkit/content/tests/chrome/window_titlebar.xul @@ -0,0 +1,223 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for the titlebar element and window dragging + --> +<window title="Titlebar" width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <titlebar id="titlebar"> + <label id="label" value="Titlebar"/> + </titlebar> + + <!-- a non-noautohide panel is treated as anchored --> + <panel id="panel" onpopupshown="popupshown(this, false)" onpopuphidden="popuphidden('panelnoautohide')"> + <titlebar> + <label id="panellabel" value="Titlebar"/> + </titlebar> + </panel> + + <panel id="panelnoautohide" noautohide="true" + onpopupshown="popupshown(this, false)" onpopuphidden="popuphidden('panelanchored')"> + <titlebar> + <label id="panellabelnoautohide" value="Titlebar"/> + </titlebar> + </panel> + + <panel id="panelanchored" noautohide="true" + onpopupshown="popupshown(this, true)" onpopuphidden="popuphidden('paneltop')"> + <titlebar> + <label id="panellabelanchored" value="Titlebar"/> + </titlebar> + </panel> + + <panel id="paneltop" noautohide="true" level="top" + onpopupshown="popupshown(this, false)" onpopuphidden="popuphidden('panelfloating')"> + <titlebar> + <label id="panellabeltop" value="Titlebar"/> + </titlebar> + </panel> + + <panel id="panelfloating" noautohide="true" level="floating" + onpopupshown="popupshown(this, false)" onpopuphidden="popuphidden('')"> + <titlebar> + <label id="panellabelfloating" value="Titlebar"/> + </titlebar> + </panel> + + <button id="button" label="OK"/> + +<script> +<![CDATA[ + +var SimpleTest = window.opener.wrappedJSObject.SimpleTest; + +SimpleTest.waitForFocus(test_titlebar, window); + +var mouseDownTarget; +var origoldx, origoldy, oldx, oldy, waitSteps = 0; +function waitForWindowMove(element, x, y, callback, arg, panel, anchored) +{ + var isPanelMove = (element.id != "label"); + + if (!waitSteps) { + oldx = isPanelMove ? panel.getBoundingClientRect().left : window.screenX; + oldy = isPanelMove ? panel.getBoundingClientRect().top : window.screenY; + synthesizeMouse(element, x, y, { type: "mousemove" }); + } + + var newx = isPanelMove ? panel.getBoundingClientRect().left : window.screenX; + var newy = isPanelMove ? panel.getBoundingClientRect().top : window.screenY; + if (newx == oldx && newy == oldy) { + if (waitSteps++ > 10) { + SimpleTest.is(window.screenX + "," + window.screenY, oldx + "," + oldy + " ", + "Window never moved properly to " + x + "," + y + (panel ? " " + panel.id : "")); + window.opener.wrappedJSObject.SimpleTest.finish(); + window.close(); + return; + } + + setTimeout(waitForWindowMove, 100, element, x, y, callback, arg, panel, anchored); + } + else { + waitSteps = 0; + + // on Linux, we need to wait a bit for the popup to be moved as well + if (navigator.platform.indexOf("Linux") >= 0) { + setTimeout(callback, 0, arg, panel, anchored); + } + else { + callback(arg, panel, anchored); + } + } +} + +function test_titlebar() +{ + var titlebar = document.getElementById("titlebar"); + var label = document.getElementById("label"); + + origoldx = window.screenX; + origoldy = window.screenY; + + var mousedownListener = event => mouseDownTarget = event.originalTarget; + window.addEventListener("mousedown", mousedownListener, false); + synthesizeMouse(label, 2, 2, { type: "mousedown" }); + SimpleTest.is(mouseDownTarget, titlebar, "movedown on titlebar"); + waitForWindowMove(label, 22, 22, test_titlebar_step2, mousedownListener); +} + +function test_titlebar_step2(mousedownListener) +{ + var titlebar = document.getElementById("titlebar"); + var label = document.getElementById("label"); + + SimpleTest.is(window.screenX, origoldx + 20, "move window horizontal"); + SimpleTest.is(window.screenY, origoldy + 20, "move window vertical"); + synthesizeMouse(label, 22, 22, { type: "mouseup" }); + + // with allowEvents set to true, the mouse should target the label instead + // and not move the window + titlebar.allowEvents = true; + + synthesizeMouse(label, 2, 2, { type: "mousedown" }); + SimpleTest.is(mouseDownTarget, label, "movedown on titlebar with allowevents"); + synthesizeMouse(label, 22, 22, { type: "mousemove" }); + SimpleTest.is(window.screenX, origoldx + 20, "mouse on label move window horizontal"); + SimpleTest.is(window.screenY, origoldy + 20, "mouse on label move window vertical"); + synthesizeMouse(label, 22, 22, { type: "mouseup" }); + + window.removeEventListener("mousedown", mousedownListener, false); + + document.getElementById("panel").openPopupAtScreen(window.screenX + 50, window.screenY + 60, false); +} + +function popupshown(panel, anchored) +{ + var rect = panel.getBoundingClientRect(); + + // skip this check for non-noautohide panels + if (panel.id == "panel") { + var panellabel = panel.firstChild.firstChild; + synthesizeMouse(panellabel, 2, 2, { type: "mousedown" }); + waitForWindowMove(panellabel, 22, 22, popupshown_step3, rect, panel, anchored); + return; + } + + // now, try moving the window. If anchored, the popup should move with the + // window. If not anchored, the popup should remain at its current screen location. + window.moveBy(10, 10); + waitSteps = 1; + waitForWindowMove(document.getElementById("label"), 1, 1, popupshown_step2, rect, panel, anchored); +} + +function popupshown_step2(oldrect, panel, anchored) +{ + var newrect = panel.getBoundingClientRect(); + + // The window movement that occured long ago at the beginning of the test + // on Linux is delayed and there isn't any way to tell when the move + // actually happened. This causes the checks here to fail. Instead, just + // wait a bit for the test to be ready. + if (navigator.platform.indexOf("Linux") >= 0 && + newrect.left != oldrect.left - (anchored ? 0 : 10)) { + setTimeout(popupshown_step2, 10, oldrect, panel, anchored); + return; + } + + // anchored popups should still be at the same offset. Non-anchored popups will + // now be offset by 10 pixels less. + SimpleTest.is(newrect.left, oldrect.left - (anchored ? 0 : 10), + panel.id + " horizontal after window move"); + SimpleTest.is(newrect.top, oldrect.top - (anchored ? 0 : 10), + panel.id + " vertical after window move"); + + var panellabel = panel.firstChild.firstChild; + synthesizeMouse(panellabel, 2, 2, { type: "mousedown" }); + waitForWindowMove(panellabel, 22, 22, popupshown_step3, newrect, panel, anchored); +} + +function popupshown_step3(oldrect, panel, anchored) +{ + // skip this check on Linux for the same window positioning reasons as above + if (navigator.platform.indexOf("Linux") == -1 || (panel.id != "panelanchored" && panel.id != "paneltop")) { + // next, drag the titlebar in the panel + var newrect = panel.getBoundingClientRect(); + SimpleTest.is(newrect.left, oldrect.left + 20, panel.id + " move popup horizontal"); + SimpleTest.is(newrect.top, oldrect.top + 20, panel.id + " move popup vertical"); + synthesizeMouse(document.getElementById("panellabel"), 22, 22, { type: "mouseup" }); + + synthesizeMouse(document.getElementById("button"), 5, 5, { type: "mousemove" }); + newrect = panel.getBoundingClientRect(); + SimpleTest.is(newrect.left, oldrect.left + 20, panel.id + " horizontal after mouse on button"); + SimpleTest.is(newrect.top, oldrect.top + 20, panel.id + " vertical after mouse on button"); + } + else { + synthesizeMouse(document.getElementById("panellabel"), 22, 22, { type: "mouseup" }); + } + + panel.hidePopup(); +} + +function popuphidden(nextPopup) +{ + if (nextPopup) { + var panel = document.getElementById(nextPopup); + if (panel.id == "panelnoautohide") { + panel.openPopupAtScreen(window.screenX + 50, window.screenY + 60, false); + } + else { + panel.openPopup(document.getElementById("button"), "after_start"); + } + } + else + window.opener.wrappedJSObject.done(window); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/window_tooltip.xul b/toolkit/content/tests/chrome/window_tooltip.xul new file mode 100644 index 0000000000..087c91c3e7 --- /dev/null +++ b/toolkit/content/tests/chrome/window_tooltip.xul @@ -0,0 +1,311 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tooltip Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + <script type="application/javascript" src="popup_shared.js"></script> + +<tooltip id="thetooltip"> + <label id="label" value="This is a tooltip"/> +</tooltip> + +<box id="parent" tooltiptext="Box Tooltip" style="margin: 10px"> + <button id="withtext" label="Tooltip Text" tooltiptext="Button Tooltip" + style="-moz-appearance: none; padding: 0;"/> + <button id="without" label="No Tooltip" style="-moz-appearance: none; padding: 0;"/> + <!-- remove the native theme and borders to avoid some platform + specific sizing differences --> + <button id="withtooltip" label="Tooltip Element" tooltip="thetooltip" + class="plain" style="-moz-appearance: none; padding: 0;"/> + <iframe id="childframe" type="content" width="10" height="10" + src="http://sectest2.example.org:80/chrome/toolkit/content/tests/chrome/popup_childframe_node.xul"/> +</box> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var gOriginalWidth = -1; +var gOriginalHeight = -1; +var gButton = null; + +function runTest() +{ + startPopupTests(popupTests); +} + +function checkCoords(event) +{ + // all but one test open the tooltip at the button location offset by 6 + // in each direction. Test 5 opens it at 4 in each direction. + var mod = (gTestIndex == 5) ? 4 : 6; + + var rect = gButton.getBoundingClientRect(); + var popupstyle = window.getComputedStyle(gButton, ""); + is(event.clientX, Math.round(rect.left + mod), + "step " + (gTestIndex + 1) + " clientX"); + is(event.clientY, Math.round(rect.top + mod), + "step " + (gTestIndex + 1) + " clientY"); + ok(event.screenX > 0, "step " + (gTestIndex + 1) + " screenX"); + ok(event.screenY > 0, "step " + (gTestIndex + 1) + " screenY"); +} + +var popupTests = [ +{ + testname: "hover tooltiptext attribute", + events: [ "popupshowing #tooltip", "popupshown #tooltip" ], + test: function() { + gButton = document.getElementById("withtext"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + } +}, +{ + testname: "close tooltip", + events: [ "popuphiding #tooltip", "popuphidden #tooltip", + "DOMMenuInactive #tooltip" ], + test: function() { + disableNonTestMouse(true); + synthesizeMouse(document.documentElement, 2, 2, { type: "mousemove" }); + disableNonTestMouse(false); + } +}, +{ + testname: "hover inherited tooltip", + events: [ "popupshowing #tooltip", "popupshown #tooltip" ], + test: function() { + gButton = document.getElementById("without"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + } +}, +{ + testname: "hover tooltip attribute", + events: [ "popuphiding #tooltip", "popuphidden #tooltip", + "DOMMenuInactive #tooltip", + "popupshowing thetooltip", "popupshown thetooltip" ], + test: function() { + gButton = document.getElementById("withtooltip"); + gExpectedTriggerNode = gButton; + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result: function(testname) { + var tooltip = document.getElementById("thetooltip"); + gExpectedTriggerNode = null; + is(tooltip.triggerNode, gButton, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + is(document.tooltipNode, gButton, testname + " document.tooltipNode"); + + var child = $("childframe").contentDocument; + var evt = child.createEvent("Event"); + evt.initEvent("click", true, true); + child.documentElement.dispatchEvent(evt); + is(child.documentElement.getAttribute("data"), "xnull", + "cannot get tooltipNode from other document"); + + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = tooltip.getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip"), ""); + + is(Math.round(rect.left), + Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 6), + testname + " left position of tooltip"); + is(Math.round(rect.top), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6), + testname + " top position of tooltip"); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + gOriginalWidth = rect.right - rect.left; + gOriginalHeight = rect.bottom - rect.top; + } +}, +{ + testname: "click to close tooltip", + events: [ "popuphiding thetooltip", "popuphidden thetooltip", + "command withtooltip", "DOMMenuInactive thetooltip" ], + test: function() { + gButton = document.getElementById("withtooltip"); + synthesizeMouse(gButton, 2, 2, { }); + }, + result: function(testname) { + var tooltip = document.getElementById("thetooltip"); + is(tooltip.triggerNode, null, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + is(document.tooltipNode, null, testname + " document.tooltipNode"); + } +}, +{ + testname: "hover tooltip after size increased", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + test: function() { + var label = document.getElementById("label"); + label.removeAttribute("value"); + label.textContent = "This is a longer tooltip than before\nIt has multiple lines\nIt is testing tooltip sizing\n"; + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result: function(testname) { + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip"), ""); + var buttonstyle = window.getComputedStyle(document.getElementById("withtooltip"), ""); + + is(Math.round(rect.left), + Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 4), + testname + " left position of tooltip"); + is(Math.round(rect.top), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 4), + testname + " top position of tooltip"); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + // make sure that the tooltip is larger than it was before by just + // checking against the original height plus an arbitrary 15 pixels + ok(gOriginalWidth + 15 < rect.right - rect.left, testname + " tooltip is wider"); + ok(gOriginalHeight + 15 < rect.bottom - rect.top, testname + " tooltip is taller"); + } +}, +{ + testname: "close tooltip with hidePopup", + events: [ "popuphiding thetooltip", "popuphidden thetooltip", + "DOMMenuInactive thetooltip" ], + test: function() { + document.getElementById("thetooltip").hidePopup(); + }, +}, +{ + testname: "hover tooltip after size decreased", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + autohide: "thetooltip", + test: function() { + var label = document.getElementById("label"); + label.value = "This is a tooltip"; + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result: function(testname) { + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip"), ""); + var buttonstyle = window.getComputedStyle(document.getElementById("withtooltip"), ""); + + is(Math.round(rect.left), + Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 6), + testname + " left position of tooltip"); + is(Math.round(rect.top), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6), + testname + " top position of tooltip"); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + is(gOriginalWidth, rect.right - rect.left, testname + " tooltip is original width"); + is(gOriginalHeight, rect.bottom - rect.top, testname + " tooltip is original height"); + } +}, +{ + testname: "hover tooltip at bottom edge of screen", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + autohide: "thetooltip", + condition: function() { + // Only checking OSX here because on other platforms popups and tooltips behave the same way + // when there's not enough space to show them below (by flipping vertically) + // However, on OSX most popups are not flipped but tooltips are. + return navigator.platform.indexOf("Mac") > -1; + }, + test: function() { + var buttonRect = document.getElementById("withtext").getBoundingClientRect(); + var windowY = screen.height - + (window.mozInnerScreenY - window.screenY ) - buttonRect.bottom; + + moveWindowTo(window.screenX, windowY, function() { + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + disableNonTestMouse(false); + }); + }, + result: function(testname) { + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip"), ""); + + is(Math.round(rect.y + rect.height), + Math.round(buttonrect.top + 4 - parseFloat(popupstyle.marginTop)), + testname + " position of tooltip above button"); + } +} + +]; + +var waitSteps = 0; +function moveWindowTo(x, y, callback, arg) +{ + if (!waitSteps) { + oldx = window.screenX; + oldy = window.screenY; + window.moveTo(x, y); + + waitSteps++; + setTimeout(moveWindowTo, 100, x, y, callback, arg); + return; + } + + if (window.screenX == oldx && window.screenY == oldy) { + if (waitSteps++ > 10) { + ok(false, "Window never moved properly to " + x + "," + y); + window.opener.wrappedJSObject.SimpleTest.finish(); + window.close(); + } + + setTimeout(moveWindowTo, 100, x, y, callback, arg); + } + else { + waitSteps = 0; + callback(arg); + } +} + +window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTest, window); +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/xul_selectcontrol.js b/toolkit/content/tests/chrome/xul_selectcontrol.js new file mode 100644 index 0000000000..d6518c1503 --- /dev/null +++ b/toolkit/content/tests/chrome/xul_selectcontrol.js @@ -0,0 +1,390 @@ +// This script is used to test elements that implement +// nsIDOMXULSelectControlElement. This currently is the following elements: +// listbox, menulist, radiogroup, richlistbox, tabs +// +// flag behaviours that differ for certain elements +// allow-other-value - alternate values for the value property may be used +// besides those in the list +// other-value-clears-selection - alternative values for the value property +// clears the selected item +// selection-required - an item must be selected in the list, unless there +// aren't any to select +// activate-disabled-menuitem - disabled menuitems can be highlighted +// select-keynav-wraps - key navigation over a selectable list wraps +// select-extended-keynav - home, end, page up and page down keys work to +// navigate over a selectable list +// keynav-leftright - key navigation is left/right rather than up/down +// The win:, mac: and gtk: or other prefixes may be used for platform specific behaviour +var behaviours = { + menu: "win:activate-disabled-menuitem activate-disabled-menuitem-mousemove select-keynav-wraps select-extended-keynav", + menulist: "allow-other-value other-value-clears-selection", + listbox: "select-extended-keynav", + richlistbox: "select-extended-keynav", + radiogroup: "select-keynav-wraps dont-select-disabled allow-other-value", + tabs: "select-extended-keynav mac:select-keynav-wraps allow-other-value selection-required keynav-leftright" +}; + +function behaviourContains(tag, behaviour) +{ + var platform = "none:"; + if (navigator.platform.indexOf("Mac") >= 0) + platform = "mac:"; + else if (navigator.platform.indexOf("Win") >= 0) + platform = "win:"; + else if (navigator.platform.indexOf("X") >= 0) + platform = "gtk:"; + + var re = new RegExp("\\s" + platform + behaviour + "\\s|\\s" + behaviour + "\\s"); + return re.test(" " + behaviours[tag] + " "); +} + +function test_nsIDOMXULSelectControlElement(element, childtag, testprefix) +{ + var testid = (testprefix) ? testprefix + " " : ""; + testid += element.localName + " nsIDOMXULSelectControlElement "; + + // editable menulists use the label as the value instead + var firstvalue = "first", secondvalue = "second", fourthvalue = "fourth"; + if (element.localName == "menulist" && element.editable) { + firstvalue = "First Item"; + secondvalue = "Second Item" + fourthvalue = "Fourth Item"; + } + + // 'initial' - check if the initial state of the element is correct + test_nsIDOMXULSelectControlElement_States(element, testid + "initial", 0, null, -1, ""); + + test_nsIDOMXULSelectControlElement_init(element, testid); + + // 'appendItem' - check if appendItem works to add a new item + var firstitem = element.appendItem("First Item", "first"); + is(firstitem.localName, childtag, + testid + "appendItem - first item is " + childtag); + test_nsIDOMXULSelectControlElement_States(element, testid + "appendItem", 1, null, -1, ""); + + is(firstitem.control, element, testid + "control"); + + // 'selectedIndex' - check if an item may be selected + element.selectedIndex = 0; + test_nsIDOMXULSelectControlElement_States(element, testid + "selectedIndex", 1, firstitem, 0, firstvalue); + + // 'appendItem 2' - check if a second item may be added + var seconditem = element.appendItem("Second Item", "second"); + test_nsIDOMXULSelectControlElement_States(element, testid + "appendItem 2", 2, firstitem, 0, firstvalue); + + // 'selectedItem' - check if the second item may be selected + element.selectedItem = seconditem; + test_nsIDOMXULSelectControlElement_States(element, testid + "selectedItem", 2, seconditem, 1, secondvalue); + + // 'selectedIndex 2' - check if selectedIndex may be set to -1 to deselect items + var selectionRequired = behaviourContains(element.localName, "selection-required"); + element.selectedIndex = -1; + test_nsIDOMXULSelectControlElement_States(element, testid + "selectedIndex 2", 2, + selectionRequired ? seconditem : null, selectionRequired ? 1 : -1, + selectionRequired ? secondvalue : ""); + + // 'selectedItem 2' - check if the selectedItem property may be set to null + element.selectedIndex = 1; + element.selectedItem = null; + test_nsIDOMXULSelectControlElement_States(element, testid + "selectedItem 2", 2, + selectionRequired ? seconditem : null, selectionRequired ? 1 : -1, + selectionRequired ? secondvalue : ""); + + // 'getIndexOfItem' - check if getIndexOfItem returns the right index + is(element.getIndexOfItem(firstitem), 0, testid + "getIndexOfItem - first item at index 0"); + is(element.getIndexOfItem(seconditem), 1, testid + "getIndexOfItem - second item at index 1"); + + var otheritem = element.ownerDocument.createElement(childtag); + is(element.getIndexOfItem(otheritem), -1, testid + "getIndexOfItem - other item not found"); + + // 'getItemAtIndex' - check if getItemAtIndex returns the right item + is(element.getItemAtIndex(0), firstitem, testid + "getItemAtIndex - index 0 is first item"); + is(element.getItemAtIndex(1), seconditem, testid + "getItemAtIndex - index 0 is second item"); + is(element.getItemAtIndex(-1), null, testid + "getItemAtIndex - index -1 is null"); + is(element.getItemAtIndex(2), null, testid + "getItemAtIndex - index 2 is null"); + + // check if setting the value changes the selection + element.value = firstvalue; + test_nsIDOMXULSelectControlElement_States(element, testid + "set value 1", 2, firstitem, 0, firstvalue); + element.value = secondvalue; + test_nsIDOMXULSelectControlElement_States(element, testid + "set value 2", 2, seconditem, 1, secondvalue); + // setting the value attribute to one not in the list doesn't change the selection. + // The value is only changed for elements which support having a value other than the + // selection. + element.value = "other"; + var allowOtherValue = behaviourContains(element.localName, "allow-other-value"); + var otherValueClearsSelection = behaviourContains(element.localName, "other-value-clears-selection"); + test_nsIDOMXULSelectControlElement_States(element, testid + "set value other", 2, + otherValueClearsSelection ? null : seconditem, + otherValueClearsSelection ? -1 : 1, + allowOtherValue ? "other" : secondvalue); + if (allowOtherValue) + element.value = ""; + + // 'removeItemAt' - check if removeItemAt removes the right item + if (selectionRequired) + element.value = secondvalue; + else + element.selectedIndex = -1; + + var removeditem = element.removeItemAt(0); + is(removeditem, firstitem, testid + "removeItemAt return value"); + test_nsIDOMXULSelectControlElement_States(element, testid + "removeItemAt", 1, + selectionRequired ? seconditem : null, selectionRequired ? 0 : -1, + selectionRequired ? secondvalue : ""); + + is(removeditem.control, undefined, testid + "control not set"); + + var thirditem = element.appendItem("Third Item", "third"); + var fourthitem = element.appendItem("Fourth Item", fourthvalue); + var fifthitem = element.appendItem("Fifth Item", "fifth"); + + // 'removeItemAt 2' - check if removeItemAt removes the selected item and + // adjusts the selection to the next item + element.selectedItem = thirditem; + is(element.removeItemAt(1), thirditem, testid + "removeItemAt 2 return value"); + + // radio buttons don't handle removing quite right due to XBL issues, + // so disable testing some of these remove tests for now - bug 367400 + var isnotradio = (element.localName != "radiogroup"); + // XXXndeakin disable these tests for all widgets for now. They require bug 331513. + isnotradio = false; + if (isnotradio) + test_nsIDOMXULSelectControlElement_States(element, testid + "removeItemAt 2", 3, fourthitem, 1, fourthvalue); + + // 'removeItemAt 3' - check if removeItemAt adjusts the selection + // if an earlier item is removed + element.selectedItem = fourthitem; + element.removeItemAt(0); + test_nsIDOMXULSelectControlElement_States(element, testid + "removeItemAt 3", 2, fourthitem, 0, fourthvalue); + + // 'removeItemAt 4' - check if removeItemAt adjusts the selection if the + // last item is selected and removed + element.selectedItem = fifthitem; + element.removeItemAt(1); + if (isnotradio) + test_nsIDOMXULSelectControlElement_States(element, testid + "removeItemAt 4", 1, fourthitem, 0, fourthvalue); + + // 'removeItemAt 5' - check that removeItemAt doesn't fail when removing invalid items + is(element.removeItemAt(-1), null, testid + "removeItemAt 5 return value"); + if (isnotradio) + test_nsIDOMXULSelectControlElement_States(element, testid + "removeItemAt 5", 1, fourthitem, 0, fourthvalue); + + // 'removeItemAt 6' - check that removeItemAt doesn't fail when removing invalid items + is(element.removeItemAt(1), null, testid + "removeItemAt 6 return value"); + is("item removed", "item removed", testid + "removeItemAt 6"); + if (isnotradio) + test_nsIDOMXULSelectControlElement_States(element, testid + "removeItemAt 6", 1, fourthitem, 0, fourthvalue); + + // 'insertItemAt' - check if insertItemAt inserts items at the right locations + element.selectedIndex = 0; + test_nsIDOMXULSelectControlElement_insertItemAt(element, 0, 0, testid, 5); + test_nsIDOMXULSelectControlElement_insertItemAt(element, 2, 2, testid, 6); + test_nsIDOMXULSelectControlElement_insertItemAt(element, -1, 3, testid, 7); + test_nsIDOMXULSelectControlElement_insertItemAt(element, 6, 4, testid, 8); + + element.selectedIndex = 0; + fourthitem.disabled = true; + element.selectedIndex = 1; + test_nsIDOMXULSelectControlElement_States(element, testid + "selectedIndex disabled", 5, fourthitem, 1, fourthvalue); + + element.selectedIndex = 0; + element.selectedItem = fourthitem; + test_nsIDOMXULSelectControlElement_States(element, testid + "selectedIndex disabled", 5, fourthitem, 1, fourthvalue); + + // 'removeall' - check if all items are removed + while (element.itemCount) + element.removeItemAt(0); + if (isnotradio) + test_nsIDOMXULSelectControlElement_States(element, testid + "remove all", 0, null, -1, + allowOtherValue ? "number8" : ""); +} + +function test_nsIDOMXULSelectControlElement_init(element, testprefix) +{ + // editable menulists use the label as the value + var isEditable = (element.localName == "menulist" && element.editable); + + var id = element.id; + element = document.getElementById(id + "-initwithvalue"); + if (element) { + var seconditem = element.getItemAtIndex(1); + test_nsIDOMXULSelectControlElement_States(element, testprefix + " value initialization", + 3, seconditem, 1, + isEditable ? seconditem.label : seconditem.value); + } + + element = document.getElementById(id + "-initwithselected"); + if (element) { + var thirditem = element.getItemAtIndex(2); + test_nsIDOMXULSelectControlElement_States(element, testprefix + " selected initialization", + 3, thirditem, 2, + isEditable ? thirditem.label : thirditem.value); + } +} + +function test_nsIDOMXULSelectControlElement_States(element, testid, + expectedcount, expecteditem, + expectedindex, expectedvalue) +{ + // need an itemCount property here + var count = element.itemCount; + is(count, expectedcount, testid + " item count"); + is(element.selectedItem, expecteditem, testid + " selectedItem"); + is(element.selectedIndex, expectedindex, testid + " selectedIndex"); + is(element.value, expectedvalue, testid + " value"); + if (element.selectedItem) { + is(element.selectedItem.selected, true, + testid + " selectedItem marked as selected"); + } +} + +function test_nsIDOMXULSelectControlElement_insertItemAt(element, index, expectedindex, testid, number) +{ + var expectedCount = element.itemCount; + var expectedSelItem = element.selectedItem; + var expectedSelIndex = element.selectedIndex; + var expectedSelValue = element.value; + + var newitem = element.insertItemAt(index, "Item " + number, "number" + number); + is(element.getIndexOfItem(newitem), expectedindex, + testid + "insertItemAt " + expectedindex + " - get inserted item"); + expectedCount++; + if (expectedSelIndex >= expectedindex) + expectedSelIndex++; + + test_nsIDOMXULSelectControlElement_States(element, testid + "insertItemAt " + index, + expectedCount, expectedSelItem, + expectedSelIndex, expectedSelValue); + return newitem; +} + +/** test_nsIDOMXULSelectControlElement_UI + * + * Test the UI aspects of an element which implements nsIDOMXULSelectControlElement + * + * Parameters: + * element - element to test + */ +function test_nsIDOMXULSelectControlElement_UI(element, testprefix) +{ + var testid = (testprefix) ? testprefix + " " : ""; + testid += element.localName + " nsIDOMXULSelectControlElement UI "; + + while (element.itemCount) + element.removeItemAt(0); + + var firstitem = element.appendItem("First Item", "first"); + var seconditem = element.appendItem("Second Item", "second"); + + // 'mouse select' - check if clicking an item selects it + synthesizeMouseExpectEvent(firstitem, 2, 2, {}, element, "select", testid + "mouse select"); + test_nsIDOMXULSelectControlElement_States(element, testid + "mouse select", 2, firstitem, 0, "first"); + + synthesizeMouseExpectEvent(seconditem, 2, 2, {}, element, "select", testid + "mouse select 2"); + test_nsIDOMXULSelectControlElement_States(element, testid + "mouse select 2", 2, seconditem, 1, "second"); + + // make sure the element is focused so keyboard navigation will apply + element.selectedIndex = 1; + element.focus(); + + var navLeftRight = behaviourContains(element.localName, "keynav-leftright"); + var backKey = navLeftRight ? "VK_LEFT" : "VK_UP"; + var forwardKey = navLeftRight ? "VK_RIGHT" : "VK_DOWN"; + + // 'key select' - check if keypresses move between items + synthesizeKeyExpectEvent(backKey, {}, element, "select", testid + "key up"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key up", 2, firstitem, 0, "first"); + + var keyWrap = behaviourContains(element.localName, "select-keynav-wraps"); + + var expectedItem = keyWrap ? seconditem : firstitem; + var expectedIndex = keyWrap ? 1 : 0; + var expectedValue = keyWrap ? "second" : "first"; + synthesizeKeyExpectEvent(backKey, {}, keyWrap ? element : null, "select", testid + "key up 2"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key up 2", 2, + expectedItem, expectedIndex, expectedValue); + + element.selectedIndex = 0; + synthesizeKeyExpectEvent(forwardKey, {}, element, "select", testid + "key down"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key down", 2, seconditem, 1, "second"); + + expectedItem = keyWrap ? firstitem : seconditem; + expectedIndex = keyWrap ? 0 : 1; + expectedValue = keyWrap ? "first" : "second"; + synthesizeKeyExpectEvent(forwardKey, {}, keyWrap ? element : null, "select", testid + "key down 2"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key down 2", 2, + expectedItem, expectedIndex, expectedValue); + + var thirditem = element.appendItem("Third Item", "third"); + var fourthitem = element.appendItem("Fourth Item", "fourth"); + if (behaviourContains(element.localName, "select-extended-keynav")) { + var fifthitem = element.appendItem("Fifth Item", "fifth"); + var sixthitem = element.appendItem("Sixth Item", "sixth"); + + synthesizeKeyExpectEvent("VK_END", {}, element, "select", testid + "key end"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key end", 6, sixthitem, 5, "sixth"); + + synthesizeKeyExpectEvent("VK_HOME", {}, element, "select", testid + "key home"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key home", 6, firstitem, 0, "first"); + + synthesizeKeyExpectEvent("VK_PAGE_DOWN", {}, element, "select", testid + "key page down"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key page down", 6, fourthitem, 3, "fourth"); + synthesizeKeyExpectEvent("VK_PAGE_DOWN", {}, element, "select", testid + "key page down to end"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key page down to end", 6, sixthitem, 5, "sixth"); + + synthesizeKeyExpectEvent("VK_PAGE_UP", {}, element, "select", testid + "key page up"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key page up", 6, thirditem, 2, "third"); + synthesizeKeyExpectEvent("VK_PAGE_UP", {}, element, "select", testid + "key page up to start"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key page up to start", 6, firstitem, 0, "first"); + + element.removeItemAt(5); + element.removeItemAt(4); + } + + // now test whether a disabled item works. + element.selectedIndex = 0; + seconditem.disabled = true; + + var dontSelectDisabled = (behaviourContains(element.localName, "dont-select-disabled")); + + // 'mouse select' - check if clicking an item selects it + synthesizeMouseExpectEvent(seconditem, 2, 2, {}, element, + dontSelectDisabled ? "!select" : "select", + testid + "mouse select disabled"); + test_nsIDOMXULSelectControlElement_States(element, testid + "mouse select disabled", 4, + dontSelectDisabled ? firstitem: seconditem, dontSelectDisabled ? 0 : 1, + dontSelectDisabled ? "first" : "second"); + + if (dontSelectDisabled) { + // test whether disabling an item won't allow it to be selected + synthesizeKeyExpectEvent(forwardKey, {}, element, "select", testid + "key down disabled"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key down disabled", 4, thirditem, 2, "third"); + + synthesizeKeyExpectEvent(backKey, {}, element, "select", testid + "key up disabled"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key up disabled", 4, firstitem, 0, "first"); + + element.selectedIndex = 2; + firstitem.disabled = true; + + synthesizeKeyExpectEvent(backKey, {}, keyWrap ? element : null, "select", testid + "key up disabled 2"); + expectedItem = keyWrap ? fourthitem : thirditem; + expectedIndex = keyWrap ? 3 : 2; + expectedValue = keyWrap ? "fourth" : "third"; + test_nsIDOMXULSelectControlElement_States(element, testid + "key up disabled 2", 4, + expectedItem, expectedIndex, expectedValue); + } + else { + // in this case, disabled items should behave the same as non-disabled items. + element.selectedIndex = 0; + synthesizeKeyExpectEvent(forwardKey, {}, element, "select", testid + "key down disabled"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key down disabled", 4, seconditem, 1, "second"); + synthesizeKeyExpectEvent(forwardKey, {}, element, "select", testid + "key down disabled again"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key down disabled again", 4, thirditem, 2, "third"); + + synthesizeKeyExpectEvent(backKey, {}, element, "select", testid + "key up disabled"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key up disabled", 4, seconditem, 1, "second"); + synthesizeKeyExpectEvent(backKey, {}, element, "select", testid + "key up disabled again"); + test_nsIDOMXULSelectControlElement_States(element, testid + "key up disabled again", 4, firstitem, 0, "first"); + } +} diff --git a/toolkit/content/tests/fennec-tile-testapp/application.ini b/toolkit/content/tests/fennec-tile-testapp/application.ini new file mode 100644 index 0000000000..510d481809 --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/application.ini @@ -0,0 +1,11 @@ +[App] +Vendor=venderr +Name=tile +Version=1.0 +BuildID=20060101 +Copyright=Copyright (c) 2006 Mark Finkle +ID=xulapp@starkravingfinkle.org + +[Gecko] +MinVersion=1.8 +MaxVersion=2.0 diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/chrome.manifest b/toolkit/content/tests/fennec-tile-testapp/chrome/chrome.manifest new file mode 100644 index 0000000000..118354c81a --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/chrome.manifest @@ -0,0 +1 @@ +content tile file:content/ diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/BrowserView.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/BrowserView.js new file mode 100644 index 0000000000..c498810df3 --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/BrowserView.js @@ -0,0 +1,694 @@ +// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 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/. */ + +var Ci = Components.interfaces; + +// --- REMOVE --- +var noop = function() {}; +var endl = '\n'; +// -------------- + +function BrowserView(container, visibleRect) { + bindAll(this); + this.init(container, visibleRect); +} + +/** + * A BrowserView maintains state of the viewport (browser, zoom level, + * dimensions) and the visible rectangle into the viewport, for every + * browser it is given (cf setBrowser()). In updates to the viewport state, + * a BrowserView (using its TileManager) renders parts of the page quasi- + * intelligently, with guarantees of having rendered and appended all of the + * visible browser content (aka the "critical rectangle"). + * + * State is characterized in large part by two rectangles (and an implicit third): + * - Viewport: Always rooted at the origin, ie with (left, top) at (0, 0). The + * width and height (right and bottom) of this rectangle are that of the + * current viewport, which corresponds more or less to the transformed + * browser content (scaled by zoom level). + * - Visible: Corresponds to the client's viewing rectangle in viewport + * coordinates. Has (top, left) corresponding to position, and width & height + * corresponding to the clients viewing dimensions. Take note that the top + * and left of the visible rect are per-browser state, but that the width + * and height persist across setBrowser() calls. This is best explained by + * a simple example: user views browser A, pans to position (x0, y0), switches + * to browser B, where she finds herself at position (x1, y1), tilts her + * device so that visible rectangle's width and height change, and switches + * back to browser A. She expects to come back to position (x0, y0), but her + * device remains tilted. + * - Critical (the implicit one): The critical rectangle is the (possibly null) + * intersection of the visible and viewport rectangles. That is, it is that + * region of the viewport which is visible to the user. We care about this + * because it tells us which region must be rendered as soon as it is dirtied. + * The critical rectangle is mostly state that we do not keep in BrowserView + * but that our TileManager maintains. + * + * Example rectangle state configurations: + * + * + * +-------------------------------+ + * |A | + * | | + * | | + * | | + * | +----------------+ | + * | |B,C | | + * | | | | + * | | | | + * | | | | + * | +----------------+ | + * | | + * | | + * | | + * | | + * | | + * +-------------------------------+ + * + * + * A = viewport ; at (0, 0) + * B = visible ; at (x, y) where x > 0, y > 0 + * C = critical ; at (x, y) + * + * + * + * +-------------------------------+ + * |A | + * | | + * | | + * | | + * +----+-----------+ | + * |B .C | | + * | . | | + * | . | | + * | . | | + * +----+-----------+ | + * | | + * | | + * | | + * | | + * | | + * +-------------------------------+ + * + * + * A = viewport ; at (0, 0) + * B = visible ; at (x, y) where x < 0, y > 0 + * C = critical ; at (0, y) + * + * + * Maintaining per-browser state is a little bit of a hack involving attaching + * an object as the obfuscated dynamic JS property of the browser object, that + * hopefully no one but us will touch. See getViewportStateFromBrowser() for + * the property name. + */ +BrowserView.prototype = ( +function() { + + // ----------------------------------------------------------- + // Privates + // + + const kZoomLevelMin = 0.2; + const kZoomLevelMax = 4.0; + const kZoomLevelPrecision = 10000; + + function visibleRectToCriticalRect(visibleRect, browserViewportState) { + return visibleRect.intersect(browserViewportState.viewportRect); + } + + function clampZoomLevel(zl) { + let bounded = Math.min(Math.max(kZoomLevelMin, zl), kZoomLevelMax); + return Math.round(bounded * kZoomLevelPrecision) / kZoomLevelPrecision; + } + + function pageZoomLevel(visibleRect, browserW, browserH) { + return clampZoomLevel(visibleRect.width / browserW); + } + + function seenBrowser(browser) { + return !!(browser.__BrowserView__vps); + } + + function initBrowserState(browser, visibleRect) { + let [browserW, browserH] = getBrowserDimensions(browser); + + let zoomLevel = pageZoomLevel(visibleRect, browserW, browserH); + let viewportRect = (new wsRect(0, 0, browserW, browserH)).scale(zoomLevel, zoomLevel); + + dump('--- initing browser to ---' + endl); + browser.__BrowserView__vps = new BrowserView.BrowserViewportState(viewportRect, + visibleRect.x, + visibleRect.y, + zoomLevel); + dump(browser.__BrowserView__vps.toString() + endl); + dump('--------------------------' + endl); + } + + function getViewportStateFromBrowser(browser) { + return browser.__BrowserView__vps; + } + + function getBrowserDimensions(browser) { + return [browser.scrollWidth, browser.scrollHeight]; + } + + function getContentScrollValues(browser) { + let cwu = getBrowserDOMWindowUtils(browser); + let scrollX = {}; + let scrollY = {}; + cwu.getScrollXY(false, scrollX, scrollY); + + return [scrollX.value, scrollY.value]; + } + + function getBrowserDOMWindowUtils(browser) { + return browser.contentWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + } + + function getNewBatchOperationState() { + return { + viewportSizeChanged: false, + dirtyAll: false + }; + } + + function clampViewportWH(width, height, visibleRect) { + let minW = visibleRect.width; + let minH = visibleRect.height; + return [Math.max(width, minW), Math.max(height, minH)]; + } + + function initContainer(container, visibleRect) { + container.style.width = visibleRect.width + 'px'; + container.style.height = visibleRect.height + 'px'; + container.style.overflow = '-moz-hidden-unscrollable'; + } + + function resizeContainerToViewport(container, viewportRect) { + container.style.width = viewportRect.width + 'px'; + container.style.height = viewportRect.height + 'px'; + } + + // !!! --- RESIZE HACK BEGIN ----- + function simulateMozAfterSizeChange(browser, width, height) { + let ev = document.createElement("MouseEvents"); + ev.initEvent("FakeMozAfterSizeChange", false, false, window, 0, width, height); + browser.dispatchEvent(ev); + } + // !!! --- RESIZE HACK END ------- + + // --- Change of coordinates functions --- // + + + // The following returned object becomes BrowserView.prototype + return { + + // ----------------------------------------------------------- + // Public instance methods + // + + init: function init(container, visibleRect) { + this._batchOps = []; + this._container = container; + this._browserViewportState = null; + this._renderMode = 0; + this._tileManager = new TileManager(this._appendTile, this._removeTile, this); + this.setVisibleRect(visibleRect); + + // !!! --- RESIZE HACK BEGIN ----- + // remove this eventually + this._resizeHack = { + maxSeenW: 0, + maxSeenH: 0 + }; + // !!! --- RESIZE HACK END ------- + }, + + setVisibleRect: function setVisibleRect(r) { + let bvs = this._browserViewportState; + let vr = this._visibleRect; + + if (!vr) + this._visibleRect = vr = r.clone(); + else + vr.copyFrom(r); + + if (bvs) { + bvs.visibleX = vr.left; + bvs.visibleY = vr.top; + + // reclamp minimally to the new visible rect + // this.setViewportDimensions(bvs.viewportRect.right, bvs.viewportRect.bottom); + } else + this._viewportChanged(false, false); + }, + + getVisibleRect: function getVisibleRect() { + return this._visibleRect.clone(); + }, + + getVisibleRectX: function getVisibleRectX() { return this._visibleRect.x; }, + getVisibleRectY: function getVisibleRectY() { return this._visibleRect.y; }, + getVisibleRectWidth: function getVisibleRectWidth() { return this._visibleRect.width; }, + getVisibleRectHeight: function getVisibleRectHeight() { return this._visibleRect.height; }, + + setViewportDimensions: function setViewportDimensions(width, height, causedByZoom) { + let bvs = this._browserViewportState; + let vis = this._visibleRect; + + if (!bvs) + return; + + // [width, height] = clampViewportWH(width, height, vis); + bvs.viewportRect.right = width; + bvs.viewportRect.bottom = height; + + // XXX we might not want the user's page to disappear from under them + // at this point, which could happen if the container gets resized such + // that visible rect becomes entirely outside of viewport rect. might + // be wise to define what UX should be in this case, like a move occurs. + // then again, we could also argue this is the responsibility of the + // caller who would do such a thing... + + this._viewportChanged(true, !!causedByZoom); + }, + + setZoomLevel: function setZoomLevel(zl) { + let bvs = this._browserViewportState; + + if (!bvs) + return; + + let newZL = clampZoomLevel(zl); + + if (newZL != bvs.zoomLevel) { + let browserW = this.viewportToBrowser(bvs.viewportRect.right); + let browserH = this.viewportToBrowser(bvs.viewportRect.bottom); + bvs.zoomLevel = newZL; // side-effect: now scale factor in transformations is newZL + this.setViewportDimensions(this.browserToViewport(browserW), + this.browserToViewport(browserH)); + } + }, + + getZoomLevel: function getZoomLevel() { + let bvs = this._browserViewportState; + if (!bvs) + return undefined; + + return bvs.zoomLevel; + }, + + beginBatchOperation: function beginBatchOperation() { + this._batchOps.push(getNewBatchOperationState()); + this.pauseRendering(); + }, + + commitBatchOperation: function commitBatchOperation() { + let bops = this._batchOps; + + if (bops.length == 0) + return; + + let opState = bops.pop(); + this._viewportChanged(opState.viewportSizeChanged, opState.dirtyAll); + this.resumeRendering(); + }, + + discardBatchOperation: function discardBatchOperation() { + let bops = this._batchOps; + bops.pop(); + this.resumeRendering(); + }, + + discardAllBatchOperations: function discardAllBatchOperations() { + let bops = this._batchOps; + while (bops.length > 0) + this.discardBatchOperation(); + }, + + moveVisibleBy: function moveVisibleBy(dx, dy) { + let vr = this._visibleRect; + let vs = this._browserViewportState; + + this.onBeforeVisibleMove(dx, dy); + this.onAfterVisibleMove(dx, dy); + }, + + moveVisibleTo: function moveVisibleTo(x, y) { + let visibleRect = this._visibleRect; + let dx = x - visibleRect.x; + let dy = y - visibleRect.y; + this.moveBy(dx, dy); + }, + + /** + * Calls to this function need to be one-to-one with calls to + * resumeRendering() + */ + pauseRendering: function pauseRendering() { + this._renderMode++; + }, + + /** + * Calls to this function need to be one-to-one with calls to + * pauseRendering() + */ + resumeRendering: function resumeRendering(renderNow) { + if (this._renderMode > 0) + this._renderMode--; + + if (renderNow || this._renderMode == 0) + this._tileManager.criticalRectPaint(); + }, + + isRendering: function isRendering() { + return (this._renderMode == 0); + }, + + /** + * @param dx Guess delta to destination x coordinate + * @param dy Guess delta to destination y coordinate + */ + onBeforeVisibleMove: function onBeforeVisibleMove(dx, dy) { + let vs = this._browserViewportState; + let vr = this._visibleRect; + + let destCR = visibleRectToCriticalRect(vr.clone().translate(dx, dy), vs); + + this._tileManager.beginCriticalMove(destCR); + }, + + /** + * @param dx Actual delta to destination x coordinate + * @param dy Actual delta to destination y coordinate + */ + onAfterVisibleMove: function onAfterVisibleMove(dx, dy) { + let vs = this._browserViewportState; + let vr = this._visibleRect; + + vr.translate(dx, dy); + vs.visibleX = vr.left; + vs.visibleY = vr.top; + + let cr = visibleRectToCriticalRect(vr, vs); + + this._tileManager.endCriticalMove(cr, this.isRendering()); + }, + + setBrowser: function setBrowser(browser, skipZoom) { + let currentBrowser = this._browser; + + let browserChanged = (currentBrowser !== browser); + + if (currentBrowser) { + currentBrowser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false); + + // !!! --- RESIZE HACK BEGIN ----- + // change to the real event type and perhaps refactor the handler function name + currentBrowser.removeEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false); + // !!! --- RESIZE HACK END ------- + + this.discardAllBatchOperations(); + + currentBrowser.setAttribute("type", "content"); + currentBrowser.docShell.isOffScreenBrowser = false; + } + + this._restoreBrowser(browser); + + browser.setAttribute("type", "content-primary"); + + this.beginBatchOperation(); + + browser.addEventListener("MozAfterPaint", this.handleMozAfterPaint, false); + + // !!! --- RESIZE HACK BEGIN ----- + // change to the real event type and perhaps refactor the handler function name + browser.addEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false); + // !!! --- RESIZE HACK END ------- + + if (!skipZoom) { + browser.docShell.isOffScreenBrowser = true; + this.zoomToPage(); + } + + this._viewportChanged(browserChanged, browserChanged); + + this.commitBatchOperation(); + }, + + handleMozAfterPaint: function handleMozAfterPaint(ev) { + let browser = this._browser; + let tm = this._tileManager; + let vs = this._browserViewportState; + + let [scrollX, scrollY] = getContentScrollValues(browser); + let clientRects = ev.clientRects; + + // !!! --- RESIZE HACK BEGIN ----- + // remove this, cf explanation in loop below + let hack = this._resizeHack; + let hackSizeChanged = false; + // !!! --- RESIZE HACK END ------- + + let rects = []; + // loop backwards to avoid xpconnect penalty for .length + for (let i = clientRects.length - 1; i >= 0; --i) { + let e = clientRects.item(i); + let r = new wsRect(e.left + scrollX, + e.top + scrollY, + e.width, e.height); + + this.browserToViewportRect(r); + r.round(); + + if (r.right < 0 || r.bottom < 0) + continue; + + // !!! --- RESIZE HACK BEGIN ----- + // remove this. this is where we make 'lazy' calculations + // that hint at a browser size change and fake the size change + // event dispach + if (r.right > hack.maxW) { + hack.maxW = rect.right; + hackSizeChanged = true; + } + if (r.bottom > hack.maxH) { + hack.maxH = rect.bottom; + hackSizeChanged = true; + } + // !!! --- RESIZE HACK END ------- + + r.restrictTo(vs.viewportRect); + rects.push(r); + } + + // !!! --- RESIZE HACK BEGIN ----- + // remove this, cf explanation in loop above + if (hackSizeChanged) + simulateMozAfterSizeChange(browser, hack.maxW, hack.maxH); + // !!! --- RESIZE HACK END ------- + + tm.dirtyRects(rects, this.isRendering()); + }, + + handleMozAfterSizeChange: function handleMozAfterPaint(ev) { + // !!! --- RESIZE HACK BEGIN ----- + // get the correct properties off of the event, these are wrong because + // we're using a MouseEvent since it has an X and Y prop of some sort and + // we piggyback on that. + let w = ev.screenX; + let h = ev.screenY; + // !!! --- RESIZE HACK END ------- + + this.setViewportDimensions(w, h); + }, + + zoomToPage: function zoomToPage() { + let browser = this._browser; + + if (!browser) + return; + + let [w, h] = getBrowserDimensions(browser); + this.setZoomLevel(pageZoomLevel(this._visibleRect, w, h)); + }, + + zoom: function zoom(aDirection) { + if (aDirection == 0) + return; + + var zoomDelta = 0.05; // 1/20 + if (aDirection >= 0) + zoomDelta *= -1; + + this.zoomLevel = this._zoomLevel + zoomDelta; + }, + + viewportToBrowser: function viewportToBrowser(x) { + let bvs = this._browserViewportState; + + if (!bvs) + throw "No browser is set"; + + return x / bvs.zoomLevel; + }, + + browserToViewport: function browserToViewport(x) { + let bvs = this._browserViewportState; + + if (!bvs) + throw "No browser is set"; + + return x * bvs.zoomLevel; + }, + + viewportToBrowserRect: function viewportToBrowserRect(rect) { + let f = this.viewportToBrowser(1.0); + return rect.scale(f, f); + }, + + browserToViewportRect: function browserToViewportRect(rect) { + let f = this.browserToViewport(1.0); + return rect.scale(f, f); + }, + + browserToViewportCanvasContext: function browserToViewportCanvasContext(ctx) { + let f = this.browserToViewport(1.0); + ctx.scale(f, f); + }, + + + // ----------------------------------------------------------- + // Private instance methods + // + + _restoreBrowser: function _restoreBrowser(browser) { + let vr = this._visibleRect; + + if (!seenBrowser(browser)) + initBrowserState(browser, vr); + + let bvs = getViewportStateFromBrowser(browser); + + this._contentWindow = browser.contentWindow; + this._browser = browser; + this._browserViewportState = bvs; + vr.left = bvs.visibleX; + vr.top = bvs.visibleY; + this._tileManager.setBrowser(browser); + }, + + _viewportChanged: function _viewportChanged(viewportSizeChanged, dirtyAll) { + let bops = this._batchOps; + + if (bops.length > 0) { + let opState = bops[bops.length - 1]; + + if (viewportSizeChanged) + opState.viewportSizeChanged = viewportSizeChanged; + + if (dirtyAll) + opState.dirtyAll = dirtyAll; + + return; + } + + let bvs = this._browserViewportState; + let vis = this._visibleRect; + + // !!! --- RESIZE HACK BEGIN ----- + // We want to uncomment this for perf, but we can't with the hack in place + // because the mozAfterPaint gives us rects that we use to create the + // fake mozAfterResize event, so we can't just clear things. + /* + if (dirtyAll) { + // We're about to mark the entire viewport dirty, so we can clear any + // queued afterPaint events that will cause redundant draws + getBrowserDOMWindowUtils(this._browser).clearMozAfterPaintEvents(); + } + */ + // !!! --- RESIZE HACK END ------- + + if (bvs) { + resizeContainerToViewport(this._container, bvs.viewportRect); + + this._tileManager.viewportChangeHandler(bvs.viewportRect, + visibleRectToCriticalRect(vis, bvs), + viewportSizeChanged, + dirtyAll); + } + }, + + _appendTile: function _appendTile(tile) { + let canvas = tile.getContentImage(); + + /* + canvas.style.position = "absolute"; + canvas.style.left = tile.x + "px"; + canvas.style.top = tile.y + "px"; + */ + + canvas.setAttribute("style", "position: absolute; left: " + tile.boundRect.left + "px; " + "top: " + tile.boundRect.top + "px;"); + + this._container.appendChild(canvas); + + // dump('++ ' + tile.toString(true) + endl); + }, + + _removeTile: function _removeTile(tile) { + let canvas = tile.getContentImage(); + + this._container.removeChild(canvas); + + // dump('-- ' + tile.toString(true) + endl); + } + + }; + +} +)(); + + +// ----------------------------------------------------------- +// Helper structures +// + +BrowserView.BrowserViewportState = function(viewportRect, + visibleX, + visibleY, + zoomLevel) { + + this.init(viewportRect, visibleX, visibleY, zoomLevel); +}; + +BrowserView.BrowserViewportState.prototype = { + + init: function init(viewportRect, visibleX, visibleY, zoomLevel) { + this.viewportRect = viewportRect; + this.visibleX = visibleX; + this.visibleY = visibleY; + this.zoomLevel = zoomLevel; + }, + + clone: function clone() { + return new BrowserView.BrowserViewportState(this.viewportRect, + this.visibleX, + this.visibleY, + this.zoomLevel); + }, + + toString: function toString() { + let props = ['\tviewportRect=' + this.viewportRect.toString(), + '\tvisibleX=' + this.visibleX, + '\tvisibleY=' + this.visibleY, + '\tzoomLevel=' + this.zoomLevel]; + + return '[BrowserViewportState] {\n' + props.join(',\n') + '\n}'; + } + +}; + diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/FooScript.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/FooScript.js new file mode 100644 index 0000000000..49cbbed664 --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/FooScript.js @@ -0,0 +1,352 @@ +var noop = function() {}; +Browser = { + updateViewportSize: noop + /** *********************************************************** + function + let browser = document.getElementById("googlenews"); + let cdoc = browser.contentDocument; + + // These might not exist yet depending on page load state + var body = cdoc.body || {}; + var html = cdoc.documentElement || {}; + + var w = Math.max(body.scrollWidth || 0, html.scrollWidth); + var h = Math.max(body.scrollHeight || 0, html.scrollHeight); + + window.tileManager.viewportHandler(new wsRect(0, 0, w, h), + window.innerWidth, + new wsRect(0, 0, window.innerWidth, window.innerHeight), + false); + *************************************************************/ +}; +var ws = { + beginUpdateBatch: noop, + panTo: noop, + endUpdateBatch: noop +}; +var Ci = Components.interfaces; +var bv = null; +var endl = "\n"; + + +function BrowserView() { + this.init(); + bindAll(this); +} + +BrowserView.prototype = { + + // --- PROPERTIES --- + // public: + // init() + // getViewportInnerBoundsRect(dx, dy) + // tileManager + // scrollbox + // + // private: + // _scrollbox + // _leftbar + // _rightbar + // _topbar + // _browser + // _tileManager + // _viewportRect + // _viewportInnerBoundsRect + // + + get tileManager() { return this._tileManager; }, + get scrollbox() { return this._scrollbox; }, + + init: function init() { + let scrollbox = document.getElementById("scrollbox").boxObject; + this._scrollbox = scrollbox; + + let leftbar = document.getElementById("left_sidebar"); + let rightbar = document.getElementById("right_sidebar"); + let topbar = document.getElementById("top_urlbar"); + this._leftbar = leftbar; + this._rightbar = rightbar; + this._topbar = topbar; + + scrollbox.scrollTo(Math.round(leftbar.getBoundingClientRect().right), 0); + + let tileContainer = document.getElementById("tile_container"); + tileContainer.addEventListener("mousedown", onMouseDown, true); + tileContainer.addEventListener("mouseup", onMouseUp, true); + tileContainer.addEventListener("mousemove", onMouseMove, true); + this._tileContainer = tileContainer; + + let tileManager = new TileManager(this.appendTile, this.removeTile, window.innerWidth); + this._tileManager = tileManager; + + let browser = document.getElementById("googlenews"); + this.setCurrentBrowser(browser, false); // sets this._browser + + let cdoc = browser.contentDocument; + + // These might not exist yet depending on page load state + let body = cdoc.body || {}; + let html = cdoc.documentElement || {}; + + let w = Math.max(body.scrollWidth || 0, html.scrollWidth); + let h = Math.max(body.scrollHeight || 0, html.scrollHeight); + + let viewportRect = new wsRect(0, 0, w, h); + this._viewportRect = viewportRect; + + let viewportInnerBoundsRect = this.getViewportInnerBoundsRect(); + this._viewportInnerBoundsRect = viewportInnerBoundsRect; + + tileManager.viewportHandler(viewportRect, + window.innerWidth, + viewportInnerBoundsRect, + true); + }, + + resizeTileContainer: function resizeTileContainer() { + + }, + + scrollboxToViewportRect: function scrollboxToViewportRect(rect, clip) { + let leftbar = this._leftbar.getBoundingClientRect(); + let rightbar = this._rightbar.getBoundingClientRect(); + let topbar = this._topbar.getBoundingClientRect(); + + let xtrans = -leftbar.width; + let ytrans = -topbar.height; + let x = rect.x + xtrans; + let y = rect.y + ytrans; + + // XXX we're cheating --- this is not really a clip, but its the only + // way this function is used + rect.x = (clip) ? Math.max(x, 0) : x; + rect.y = (clip) ? Math.max(y, 0) : y; + + return rect; + }, + + getScrollboxPosition: function getScrollboxPosition() { + return [this._scrollbox.positionX, this._scrollbox.positionY]; + }, + + getViewportInnerBoundsRect: function getViewportInnerBoundsRect(dx, dy) { + if (!dx) dx = 0; + if (!dy) dy = 0; + + let w = window.innerWidth; + let h = window.innerHeight; + + let leftbar = this._leftbar.getBoundingClientRect(); + let rightbar = this._rightbar.getBoundingClientRect(); + let topbar = this._topbar.getBoundingClientRect(); + + let leftinner = Math.max(leftbar.right - dx, 0); + let rightinner = Math.min(rightbar.left - dx, w); + let topinner = Math.max(topbar.bottom - dy, 0); + + let [x, y] = this.getScrollboxPosition(); + + return this.scrollboxToViewportRect(new wsRect(x + dx, y + dy, rightinner - leftinner, h - topinner), + true); + }, + + appendTile: function appendTile(tile) { + let canvas = tile.contentImage; + + canvas.style.position = "absolute"; + canvas.style.left = tile.x + "px"; + canvas.style.top = tile.y + "px"; + + let tileContainer = document.getElementById("tile_container"); + tileContainer.appendChild(canvas); + + dump('++ ' + tile.toString() + endl); + }, + + removeTile: function removeTile(tile) { + let canvas = tile.contentImage; + + let tileContainer = document.getElementById("tile_container"); + tileContainer.removeChild(canvas); + + dump('-- ' + tile.toString() + endl); + }, + + scrollBy: function scrollBy(dx, dy) { + // TODO + this.onBeforeScroll(); + this.onAfterScroll(); + }, + + // x: current x + // y: current y + // dx: delta to get to x from current x + // dy: delta to get to y from current y + onBeforeScroll: function onBeforeScroll(x, y, dx, dy) { + this.tileManager.onBeforeScroll(this.getViewportInnerBoundsRect(dx, dy)); + + // shouldn't update margin if it doesn't need to be changed + let sidebars = document.getElementsByClassName("sidebar"); + for (let i = 0; i < sidebars.length; i++) { + let sidebar = sidebars[i]; + sidebar.style.margin = (y + dy) + "px 0px 0px 0px"; + } + + let urlbar = document.getElementById("top_urlbar"); + urlbar.style.margin = "0px 0px 0px " + (x + dx) + "px"; + }, + + onAfterScroll: function onAfterScroll(x, y, dx, dy) { + this.tileManager.onAfterScroll(this.getViewportInnerBoundsRect()); + }, + + setCurrentBrowser: function setCurrentBrowser(browser, skipZoom) { + let currentBrowser = this._browser; + if (currentBrowser) { + // backup state + currentBrowser.mZoomLevel = this.zoomLevel; + currentBrowser.mPanX = ws._viewingRect.x; + currentBrowser.mPanY = ws._viewingRect.y; + + // stop monitor paint events for this browser + currentBrowser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false); + currentBrowser.setAttribute("type", "content"); + currentBrowser.docShell.isOffScreenBrowser = false; + } + + browser.setAttribute("type", "content-primary"); + if (!skipZoom) + browser.docShell.isOffScreenBrowser = true; + + // start monitoring paint events for this browser + browser.addEventListener("MozAfterPaint", this.handleMozAfterPaint, false); + + this._browser = browser; + + // endLoading(and startLoading in most cases) calls zoom anyway + if (!skipZoom) { + this.zoomToPage(); + } + + if ("mZoomLevel" in browser) { + // restore last state + ws.beginUpdateBatch(); + ws.panTo(browser.mPanX, browser.mPanY); + this.zoomLevel = browser.mZoomLevel; + ws.endUpdateBatch(true); + + // drop the cache + delete browser.mZoomLevel; + delete browser.mPanX; + delete browser.mPanY; + } + + this.tileManager.browser = browser; + }, + + handleMozAfterPaint: function handleMozAfterPaint(ev) { + this.tileManager.handleMozAfterPaint(ev); + }, + + zoomToPage: function zoomToPage() { + /** ****************************************************** + let needToPanToTop = this._needToPanToTop; + // Ensure pages are panned at the top before zooming/painting + // combine the initial pan + zoom into a transaction + if (needToPanToTop) { + ws.beginUpdateBatch(); + this._needToPanToTop = false; + ws.panTo(0, -BrowserUI.toolbarH); + } + // Adjust the zoomLevel to fit the page contents in our window width + let [contentW, ] = this._contentAreaDimensions; + let fakeW = this._fakeWidth; + + if (contentW > fakeW) + this.zoomLevel = fakeW / contentW; + + if (needToPanToTop) + ws.endUpdateBatch(); + ********************************************************/ + } + +}; + + +function onResize(e) { + let browser = document.getElementById("googlenews"); + let cdoc = browser.contentDocument; + + // These might not exist yet depending on page load state + var body = cdoc.body || {}; + var html = cdoc.documentElement || {}; + + var w = Math.max(body.scrollWidth || 0, html.scrollWidth); + var h = Math.max(body.scrollHeight || 0, html.scrollHeight); + + if (bv) + bv.tileManager.viewportHandler(new wsRect(0, 0, w, h), + window.innerWidth, + bv.getViewportInnerBoundsRect(), + true); +} + +function onMouseDown(e) { + window._isDragging = true; + window._dragStart = {x: e.clientX, y: e.clientY}; + + bv.tileManager.startPanning(); +} + +function onMouseUp() { + window._isDragging = false; + + bv.tileManager.endPanning(); +} + +function onMouseMove(e) { + if (window._isDragging) { + let scrollbox = bv.scrollbox; + + let x = scrollbox.positionX; + let y = scrollbox.positionY; + let w = scrollbox.scrolledWidth; + let h = scrollbox.scrolledHeight; + + let dx = window._dragStart.x - e.clientX; + let dy = window._dragStart.y - e.clientY; + + // XXX if max(x, 0) > scrollwidth we shouldn't do anything (same for y/height) + let newX = Math.max(x + dx, 0); + let newY = Math.max(y + dy, 0); + + if (newX < w || newY < h) { + // clip dx and dy to prevent us from going below 0 + dx = Math.max(dx, -x); + dy = Math.max(dy, -y); + + bv.onBeforeScroll(x, y, dx, dy); + + /* dump("==========scroll==========" + endl); + dump("delta: " + dx + "," + dy + endl); + let xx = {}; + let yy = {}; + scrollbox.getPosition(xx, yy); + dump(xx.value + "," + yy.value + endl);*/ + + scrollbox.scrollBy(dx, dy); + + /* scrollbox.getPosition(xx, yy); + dump(xx.value + "," + yy.value + endl); + dump("==========================" + endl);*/ + + bv.onAfterScroll(); + } + } + + window._dragStart = {x: e.clientX, y: e.clientY}; +} + +function onLoad() { + bv = new BrowserView(); +} diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/TileManager.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/TileManager.js new file mode 100644 index 0000000000..52beb6e364 --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/TileManager.js @@ -0,0 +1,1018 @@ +// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 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/. */ + +const kXHTMLNamespaceURI = "http://www.w3.org/1999/xhtml"; + +// base-2 exponent for width, height of a single tile. +const kTileExponentWidth = 7; +const kTileExponentHeight = 7; +const kTileWidth = Math.pow(2, kTileExponentWidth); // 2^7 = 128 +const kTileHeight = Math.pow(2, kTileExponentHeight); // 2^7 = 128 +const kLazyRoundTimeCap = 500; // millis + + +function bind(f, thisObj) { + return function() { + return f.apply(thisObj, arguments); + }; +} + +function bindSome(instance, methodNames) { + for (let methodName of methodNames) + if (methodName in instance) + instance[methodName] = bind(instance[methodName], instance); +} + +function bindAll(instance) { + for (let key in instance) + if (instance[key] instanceof Function) + instance[key] = bind(instance[key], instance); +} + + +/** + * The Tile Manager! + * + * @param appendTile The function the tile manager should call in order to + * "display" a tile (e.g. append it to the DOM). The argument to this + * function is a TileManager.Tile object. + * @param removeTile The function the tile manager should call in order to + * "undisplay" a tile (e.g. remove it from the DOM). The argument to this + * function is a TileManager.Tile object. + * @param fakeWidth The width of the widest possible visible rectangle, e.g. + * the width of the screen. This is used in setting the zoomLevel. + */ +function TileManager(appendTile, removeTile, browserView) { + // backref to the BrowserView object that owns us + this._browserView = browserView; + + // callbacks to append / remove a tile to / from the parent + this._appendTile = appendTile; + this._removeTile = removeTile; + + // tile cache holds tile objects and pools them under a given capacity + let self = this; + this._tileCache = new TileManager.TileCache(function(tile) { self._removeTileSafe(tile); }, + -1, -1, 110); + + // Rectangle within the viewport that is visible to the user. It is "critical" + // in the sense that it must be rendered as soon as it becomes dirty + this._criticalRect = null; + + // Current <browser> DOM element, holding the content we wish to render. + // This is null when no browser is attached + this._browser = null; + + // if we have an outstanding paint timeout, its value is stored here + // for cancelling when we end page loads + // this._drawTimeout = 0; + this._pageLoadResizerTimeout = 0; + + // timeout of the non-visible-tiles-crawler to cache renders from the browser + this._idleTileCrawlerTimeout = 0; + + // object that keeps state on our current lazyload crawl + this._crawler = null; + + // the max right coordinate we've seen from paint events + // while we were loading a page. If we see something that's bigger than + // our width, we'll trigger a page zoom. + this._pageLoadMaxRight = 0; + this._pageLoadMaxBottom = 0; + + // Tells us to pan to top before first draw + this._needToPanToTop = false; +} + +TileManager.prototype = { + + setBrowser: function setBrowser(b) { this._browser = b; }, + + // This is the callback fired by our client whenever the viewport + // changed somehow (or didn't change but someone asked it to update). + viewportChangeHandler: function viewportChangeHandler(viewportRect, + criticalRect, + boundsSizeChanged, + dirtyAll) { + // !!! --- DEBUG BEGIN ----- + dump("***vphandler***\n"); + dump(viewportRect.toString() + "\n"); + dump(criticalRect.toString() + "\n"); + dump(boundsSizeChanged + "\n"); + dump(dirtyAll + "\n***************\n"); + // !!! --- DEBUG END ------- + + let tc = this._tileCache; + + tc.iBound = Math.ceil(viewportRect.right / kTileWidth); + tc.jBound = Math.ceil(viewportRect.bottom / kTileHeight); + + if (!criticalRect || !criticalRect.equals(this._criticalRect)) { + this.beginCriticalMove(criticalRect); + this.endCriticalMove(criticalRect, !boundsSizeChanged); + } + + if (boundsSizeChanged) { + // TODO fastpath if !dirtyAll + this.dirtyRects([viewportRect.clone()], true); + } + }, + + dirtyRects: function dirtyRects(rects, doCriticalRender) { + let criticalIsDirty = false; + let criticalRect = this._criticalRect; + + for (let rect of rects) { + this._tileCache.forEachIntersectingRect(rect, false, this._dirtyTile, this); + + if (criticalRect && rect.intersects(criticalRect)) + criticalIsDirty = true; + } + + if (criticalIsDirty && doCriticalRender) + this.criticalRectPaint(); + }, + + criticalRectPaint: function criticalRectPaint() { + let cr = this._criticalRect; + + if (cr) { + let [ctrx, ctry] = cr.centerRounded(); + this.recenterEvictionQueue(ctrx, ctry); + this._renderAppendHoldRect(cr); + } + }, + + beginCriticalMove2: function beginCriticalMove(destCriticalRect) { + let start = Date.now(); + function appendNonDirtyTile(tile) { + if (!tile.isDirty()) + this._appendTileSafe(tile); + } + + if (destCriticalRect) + this._tileCache.forEachIntersectingRect(destCriticalRect, false, appendNonDirtyTile, this); + let end = Date.now(); + dump("start: " + (end-start) + "\n") + }, + + beginCriticalMove: function beginCriticalMove(destCriticalRect) { + /* + function appendNonDirtyTile(tile) { + if (!tile.isDirty()) + this._appendTileSafe(tile); + } + */ + + let start = Date.now(); + + if (destCriticalRect) { + + let rect = destCriticalRect; + + let create = false; + + // this._tileCache.forEachIntersectingRect(destCriticalRect, false, appendNonDirtyTile, this); + let visited = {}; + let evictGuard = null; + if (create) { + evictGuard = function evictGuard(tile) { + return !visited[tile.toString()]; + }; + } + + let starti = rect.left >> kTileExponentWidth; + let endi = rect.right >> kTileExponentWidth; + + let startj = rect.top >> kTileExponentHeight; + let endj = rect.bottom >> kTileExponentHeight; + + let tile = null; + let tc = this._tileCache; + + for (var j = startj; j <= endj; ++j) { + for (var i = starti; i <= endi; ++i) { + + // 'this' for getTile needs to be tc + + // tile = this.getTile(i, j, create, evictGuard); + // if (!tc.inBounds(i, j)) { + if (0 <= i && 0 <= j && i <= tc.iBound && j <= tc.jBound) { + // return null; + break; + } + + tile = null; + + // if (tc._isOccupied(i, j)) { + if (tc._tiles[i] && tc._tiles[i][j]) { + tile = tc._tiles[i][j]; + } else if (create) { + // NOTE: create is false here + tile = tc._createTile(i, j, evictionGuard); + if (tile) tile.markDirty(); + } + + if (tile) { + visited[tile.toString()] = true; + // fn.call(thisObj, tile); + // function appendNonDirtyTile(tile) { + // if (!tile.isDirty()) + if (!tile._dirtyTileCanvas) { + // this._appendTileSafe(tile); + if (!tile._appended) { + let astart = Date.now(); + this._appendTile(tile); + tile._appended = true; + let aend = Date.now(); + dump("append: " + (aend - astart) + "\n"); + } + } + // } + } + } + } + } + + let end = Date.now(); + dump("start: " + (end-start) + "\n") + }, + + endCriticalMove: function endCriticalMove(destCriticalRect, doCriticalPaint) { + let start = Date.now(); + + let tc = this._tileCache; + let cr = this._criticalRect; + + let dcr = destCriticalRect.clone(); + + let f = function releaseOldTile(tile) { + // release old tile + if (!tile.boundRect.intersects(dcr)) + tc.releaseTile(tile); + } + + if (cr) + tc.forEachIntersectingRect(cr, false, f, this); + + this._holdRect(destCriticalRect); + + if (cr) + cr.copyFrom(destCriticalRect); + else + this._criticalRect = cr = destCriticalRect; + + let crpstart = Date.now(); + if (doCriticalPaint) + this.criticalRectPaint(); + dump(" crp: " + (Date.now() - crpstart) + "\n"); + + let end = Date.now(); + dump("end: " + (end - start) + "\n"); + }, + + restartLazyCrawl: function restartLazyCrawl(startRectOrQueue) { + if (!startRectOrQueue || startRectOrQueue instanceof Array) { + this._crawler = new TileManager.CrawlIterator(this._tileCache); + + if (startRectOrQueue) { + let len = startRectOrQueue.length; + for (let k = 0; k < len; ++k) + this._crawler.enqueue(startRectOrQueue[k].i, startRectOrQueue[k].j); + } + } else { + this._crawler = new TileManager.CrawlIterator(this._tileCache, startRectOrQueue); + } + + if (!this._idleTileCrawlerTimeout) + this._idleTileCrawlerTimeout = setTimeout(this._idleTileCrawler, 2000, this); + }, + + stopLazyCrawl: function stopLazyCrawl() { + this._idleTileCrawlerTimeout = 0; + this._crawler = null; + + let cr = this._criticalRect; + if (cr) { + let [ctrx, ctry] = cr.centerRounded(); + this.recenterEvictionQueue(ctrx, ctry); + } + }, + + recenterEvictionQueue: function recenterEvictionQueue(ctrx, ctry) { + let ctri = ctrx >> kTileExponentWidth; + let ctrj = ctry >> kTileExponentHeight; + + function evictFarTiles(a, b) { + let dista = Math.max(Math.abs(a.i - ctri), Math.abs(a.j - ctrj)); + let distb = Math.max(Math.abs(b.i - ctri), Math.abs(b.j - ctrj)); + return dista - distb; + } + + this._tileCache.sortEvictionQueue(evictFarTiles); + }, + + _renderTile: function _renderTile(tile) { + if (tile.isDirty()) + tile.render(this._browser, this._browserView); + }, + + _appendTileSafe: function _appendTileSafe(tile) { + if (!tile._appended) { + this._appendTile(tile); + tile._appended = true; + } + }, + + _removeTileSafe: function _removeTileSafe(tile) { + if (tile._appended) { + this._removeTile(tile); + tile._appended = false; + } + }, + + _dirtyTile: function _dirtyTile(tile) { + if (!this._criticalRect || !tile.boundRect.intersects(this._criticalRect)) + this._removeTileSafe(tile); + + tile.markDirty(); + + if (this._crawler) + this._crawler.enqueue(tile.i, tile.j); + }, + + _holdRect: function _holdRect(rect) { + this._tileCache.holdTilesIntersectingRect(rect); + }, + + _releaseRect: function _releaseRect(rect) { + this._tileCache.releaseTilesIntersectingRect(rect); + }, + + _renderAppendHoldRect: function _renderAppendHoldRect(rect) { + function renderAppendHoldTile(tile) { + if (tile.isDirty()) + this._renderTile(tile); + + this._appendTileSafe(tile); + this._tileCache.holdTile(tile); + } + + this._tileCache.forEachIntersectingRect(rect, true, renderAppendHoldTile, this); + }, + + _idleTileCrawler: function _idleTileCrawler(self) { + if (!self) self = this; + dump('crawl pass.\n'); + let itered = 0, rendered = 0; + + let start = Date.now(); + let comeAgain = true; + + while ((Date.now() - start) <= kLazyRoundTimeCap) { + let tile = self._crawler.next(); + + if (!tile) { + comeAgain = false; + break; + } + + if (tile.isDirty()) { + self._renderTile(tile); + ++rendered; + } + ++itered; + } + + dump('crawl itered:' + itered + ' rendered:' + rendered + '\n'); + + if (comeAgain) { + self._idleTileCrawlerTimeout = setTimeout(self._idleTileCrawler, 2000, self); + } else { + self.stopLazyCrawl(); + dump('crawl end\n'); + } + } + +}; + + +/** + * The tile cache used by the tile manager to hold and index all + * tiles. Also responsible for pooling tiles and maintaining the + * number of tiles under given capacity. + * + * @param onBeforeTileDetach callback set by the TileManager to call before + * we must "detach" a tile from a tileholder due to needing it elsewhere or + * having to discard it on capacity decrease + * @param capacity the initial capacity of the tile cache, i.e. the max number + * of tiles the cache can have allocated + */ +TileManager.TileCache = function TileCache(onBeforeTileDetach, iBound, jBound, capacity) { + if (arguments.length <= 3 || capacity < 0) + capacity = Infinity; + + // We track all pooled tiles in a 2D array (row, column) ordered as + // they "appear on screen". The array is a grid that functions for + // storage of the tiles and as a lookup map. Each array entry is + // a reference to the tile occupying that space ("tileholder"). Entries + // are not unique, so a tile could be referenced by multiple array entries, + // i.e. a tile could "span" many tile placeholders (e.g. if we merge + // neighbouring tiles). + this._tiles = []; + + // holds the same tiles that _tiles holds, but as contiguous array + // elements, for pooling tiles for reuse under finite capacity + this._tilePool = (capacity == Infinity) ? new Array() : new Array(capacity); + + this._capacity = capacity; + this._nTiles = 0; + this._numFree = 0; + + this._onBeforeTileDetach = onBeforeTileDetach; + + this.iBound = iBound; + this.jBound = jBound; +}; + +TileManager.TileCache.prototype = { + + get size() { return this._nTiles; }, + get numFree() { return this._numFree; }, + + // A comparison function that will compare all free tiles as greater + // than all non-free tiles. Useful because, for instance, to shrink + // the tile pool when capacity is lowered, we want to remove all tiles + // at the new cap and beyond, favoring removal of free tiles first. + evictionCmp: function freeTilesLast(a, b) { + if (a.free == b.free) return (a.j == b.j) ? b.i - a.i : b.j - a.j; + return (a.free) ? 1 : -1; + }, + + getCapacity: function getCapacity() { return this._capacity; }, + + setCapacity: function setCapacity(newCap, skipEvictionQueueSort) { + if (newCap < 0) + throw "Cannot set a negative tile cache capacity"; + + if (newCap == Infinity) { + this._capacity = Infinity; + return; + } else if (this._capacity == Infinity) { + // pretend we had a finite capacity all along and proceed normally + this._capacity = this._tilePool.length; + } + + let rem = null; + + if (newCap < this._capacity) { + // This case is obnoxious. We're decreasing our capacity which means + // we may have to get rid of tiles. Depending on our eviction comparator, + // we probably try to get rid free tiles first, but we might have to throw + // out some nonfree ones too. Note that "throwing out" means the cache + // won't keep them, and they'll get GC'ed as soon as all other refholders + // let go of their refs to the tile. + if (!skipEvictionQueueSort) + this.sortEvictionQueue(); + + rem = this._tilePool.splice(newCap, this._tilePool.length); + + } else { + // This case is win. Extend our tile pool array with new empty space. + this._tilePool.push.apply(this._tilePool, new Array(newCap - this._capacity)); + } + + // update state in the case that we threw things out. + let nTilesDeleted = this._nTiles - newCap; + if (nTilesDeleted > 0) { + let nFreeDeleted = 0; + for (let k = 0; k < nTilesDeleted; ++k) { + if (rem[k].free) + nFreeDeleted++; + + this._detachTile(rem[k].i, rem[k].j); + } + + this._nTiles -= nTilesDeleted; + this._numFree -= nFreeDeleted; + } + + this._capacity = newCap; + }, + + _isOccupied: function _isOccupied(i, j) { + return !!(this._tiles[i] && this._tiles[i][j]); + }, + + _detachTile: function _detachTile(i, j) { + let tile = null; + if (this._isOccupied(i, j)) { + tile = this._tiles[i][j]; + + if (this._onBeforeTileDetach) + this._onBeforeTileDetach(tile); + + this.releaseTile(tile); + delete this._tiles[i][j]; + } + return tile; + }, + + _reassignTile: function _reassignTile(tile, i, j) { + this._detachTile(tile.i, tile.j); // detach + tile.init(i, j); // re-init + this._tiles[i][j] = tile; // attach + return tile; + }, + + _evictTile: function _evictTile(evictionGuard) { + let k = this._nTiles - 1; + let pool = this._tilePool; + let victim = null; + + for (; k >= 0; --k) { + if (pool[k].free && + (!evictionGuard || evictionGuard(pool[k]))) + { + victim = pool[k]; + break; + } + } + + return victim; + }, + + _createTile: function _createTile(i, j, evictionGuard) { + if (!this._tiles[i]) + this._tiles[i] = []; + + let tile = null; + + if (this._nTiles < this._capacity) { + // either capacity is infinite, or we still have room to allocate more + tile = new TileManager.Tile(i, j); + this._tiles[i][j] = tile; + this._tilePool[this._nTiles++] = tile; + this._numFree++; + + } else { + // assert: nTiles == capacity + dump("\nevicting\n"); + tile = this._evictTile(evictionGuard); + if (tile) + this._reassignTile(tile, i, j); + } + + return tile; + }, + + inBounds: function inBounds(i, j) { + return 0 <= i && 0 <= j && i <= this.iBound && j <= this.jBound; + }, + + sortEvictionQueue: function sortEvictionQueue(cmp) { + if (!cmp) cmp = this.evictionCmp; + this._tilePool.sort(cmp); + }, + + /** + * Get a tile by its indices + * + * @param i Column + * @param j Row + * @param create Flag true if the tile should be created in case there is no + * tile at (i, j) + * @param reuseCondition Boolean-valued function to restrict conditions under + * which an old tile may be reused for creating this one. This can happen if + * the cache has reached its capacity and must reuse existing tiles in order to + * create this one. The function is given a Tile object as its argument and + * returns true if the tile is OK for reuse. This argument has no effect if the + * create argument is false. + */ + getTile: function getTile(i, j, create, evictionGuard) { + if (!this.inBounds(i, j)) + return null; + + let tile = null; + + if (this._isOccupied(i, j)) { + tile = this._tiles[i][j]; + } else if (create) { + tile = this._createTile(i, j, evictionGuard); + if (tile) tile.markDirty(); + } + + return tile; + }, + + /** + * Look up (possibly creating) a tile from its viewport coordinates. + * + * @param x + * @param y + * @param create Flag true if the tile should be created in case it doesn't + * already exist at the tileholder corresponding to (x, y) + */ + tileFromPoint: function tileFromPoint(x, y, create) { + let i = x >> kTileExponentWidth; + let j = y >> kTileExponentHeight; + + return this.getTile(i, j, create); + }, + + /** + * Hold a tile (i.e. mark it non-free). Returns true if the operation + * actually did something, false elsewise. + */ + holdTile: function holdTile(tile) { + if (tile && tile.free) { + tile._hold(); + this._numFree--; + return true; + } + return false; + }, + + /** + * Release a tile (i.e. mark it free). Returns true if the operation + * actually did something, false elsewise. + */ + releaseTile: function releaseTile(tile) { + if (tile && !tile.free) { + tile._release(); + this._numFree++; + return true; + } + return false; + }, + + // XXX the following two functions will iterate through duplicate tiles + // once we begin to merge tiles. + /** + * Fetch all tiles that share at least one point with this rect. If `create' + * is true then any tileless tileholders will have tiles created for them. + */ + tilesIntersectingRect: function tilesIntersectingRect(rect, create) { + let dx = (rect.right % kTileWidth) - (rect.left % kTileWidth); + let dy = (rect.bottom % kTileHeight) - (rect.top % kTileHeight); + let tiles = []; + + for (let y = rect.top; y <= rect.bottom - dy; y += kTileHeight) { + for (let x = rect.left; x <= rect.right - dx; x += kTileWidth) { + let tile = this.tileFromPoint(x, y, create); + if (tile) + tiles.push(tile); + } + } + + return tiles; + }, + + forEachIntersectingRect: function forEachIntersectingRect(rect, create, fn, thisObj) { + let visited = {}; + let evictGuard = null; + if (create) { + evictGuard = function evictGuard(tile) { + return !visited[tile.toString()]; + }; + } + + let starti = rect.left >> kTileExponentWidth; + let endi = rect.right >> kTileExponentWidth; + + let startj = rect.top >> kTileExponentHeight; + let endj = rect.bottom >> kTileExponentHeight; + + let tile = null; + for (var j = startj; j <= endj; ++j) { + for (var i = starti; i <= endi; ++i) { + tile = this.getTile(i, j, create, evictGuard); + if (tile) { + visited[tile.toString()] = true; + fn.call(thisObj, tile); + } + } + } + }, + + holdTilesIntersectingRect: function holdTilesIntersectingRect(rect) { + this.forEachIntersectingRect(rect, false, this.holdTile, this); + }, + + releaseTilesIntersectingRect: function releaseTilesIntersectingRect(rect) { + this.forEachIntersectingRect(rect, false, this.releaseTile, this); + } + +}; + + + +TileManager.Tile = function Tile(i, j) { + // canvas element is where we keep paint data from browser for this tile + this._canvas = document.createElementNS(kXHTMLNamespaceURI, "canvas"); + this._canvas.setAttribute("width", String(kTileWidth)); + this._canvas.setAttribute("height", String(kTileHeight)); + this._canvas.setAttribute("moz-opaque", "true"); + // this._canvas.style.border = "1px solid red"; + + this.init(i, j); // defines more properties, cf below +}; + +TileManager.Tile.prototype = { + + // essentially, this is part of constructor code, but since we reuse tiles + // in the tile cache, this is here so that we can reinitialize tiles when we + // reuse them + init: function init(i, j) { + if (!this.boundRect) + this.boundRect = new wsRect(i * kTileWidth, j * kTileHeight, kTileWidth, kTileHeight); + else + this.boundRect.setRect(i * kTileWidth, j * kTileHeight, kTileWidth, kTileHeight); + + // indices! + this.i = i; + this.j = j; + + // flags true if we need to repaint our own local canvas + this._dirtyTileCanvas = false; + + // keep a dirty rectangle (i.e. only part of the tile is dirty) + this._dirtyTileCanvasRect = null; + + // flag used by TileManager to avoid re-appending tiles that have already + // been appended + this._appended = false; + + // We keep tile objects around after their use for later reuse, so this + // flags true for an unused pooled tile. We don't actually care about + // this from within the Tile prototype, it is here for the cache to use. + this.free = true; + }, + + // viewport coordinates + get x() { return this.boundRect.left; }, + get y() { return this.boundRect.top; }, + + // the actual canvas that holds the most recently rendered image of this + // canvas + getContentImage: function getContentImage() { return this._canvas; }, + + isDirty: function isDirty() { return this._dirtyTileCanvas; }, + + /** + * Mark this entire tile as dirty (i.e. the whole tile needs to be rendered + * on next render). + */ + markDirty: function markDirty() { this.updateDirtyRegion(); }, + + unmarkDirty: function unmarkDirty() { + this._dirtyTileCanvasRect = null; + this._dirtyTileCanvas = false; + }, + + /** + * This will mark dirty at least everything in dirtyRect (which must be + * specified in canvas coordinates). If dirtyRect is not given then + * the entire tile is marked dirty. + */ + updateDirtyRegion: function updateDirtyRegion(dirtyRect) { + if (!dirtyRect) { + + if (!this._dirtyTileCanvasRect) + this._dirtyTileCanvasRect = this.boundRect.clone(); + else + this._dirtyTileCanvasRect.copyFrom(this.boundRect); + + } else if (!this._dirtyTileCanvasRect) { + this._dirtyTileCanvasRect = dirtyRect.intersect(this.boundRect); + } else if (dirtyRect.intersects(this.boundRect)) { + this._dirtyTileCanvasRect.expandToContain(dirtyRect.intersect(this.boundRect)); + } + + // TODO if after the above, the dirty rectangle is large enough, + // mark the whole tile dirty. + + if (this._dirtyTileCanvasRect) + this._dirtyTileCanvas = true; + }, + + /** + * Actually draw the browser content into the dirty region of this + * tile. This requires us to actually draw with the + * nsIDOMCanvasRenderingContext2D object's drawWindow method, which + * we expect to be a relatively heavy operation. + * + * You likely want to check if the tile isDirty() before asking it + * to render, as this will cause the entire tile to re-render in the + * case that it is not dirty. + */ + render: function render(browser, browserView) { + if (!this.isDirty()) + this.markDirty(); + + let rect = this._dirtyTileCanvasRect; + + let x = rect.left - this.boundRect.left; + let y = rect.top - this.boundRect.top; + + // content process is not being scaled, so don't scale our rect either + // browserView.viewportToBrowserRect(rect); + // rect.round(); // snap outward to get whole "pixel" (in browser coords) + + let ctx = this._canvas.getContext("2d"); + ctx.save(); + + browserView.browserToViewportCanvasContext(ctx); + + ctx.translate(x, y); + + let cw = browserView._contentWindow; + // let cw = browser.contentWindow; + ctx.asyncDrawXULElement(browserView._browser, + rect.left, rect.top, + rect.right - rect.left, rect.bottom - rect.top, + "grey", + (ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_CARET)); + + ctx.restore(); + + this.unmarkDirty(); + }, + + toString: function toString(more) { + if (more) { + return 'Tile(' + [this.i, + this.j, + "dirty=" + this.isDirty(), + "boundRect=" + this.boundRect].join(', ') + + ')'; + } + + return 'Tile(' + this.i + ', ' + this.j + ')'; + }, + + _hold: function hold() { this.free = false; }, + _release: function release() { this.free = true; } + +}; + + +/** + * A CrawlIterator is in charge of creating and returning subsequent tiles "crawled" + * over as we render tiles lazily. It supports iterator semantics so you can use + * CrawlIterator objects in for..in loops. + * + * Currently the CrawlIterator is built to expand a rectangle iteratively and return + * subsequent tiles that intersect the boundary of the rectangle. Each expansion of + * the rectangle is one unit of tile dimensions in each direction. This is repeated + * until all tiles from elsewhere have been reused (assuming the cache has finite + * capacity) in this crawl, so that we don't start reusing tiles from the beginning + * of our crawl. Afterward, the CrawlIterator enters a state where it operates as a + * FIFO queue, and calls to next() simply dequeue elements, which must be added with + * enqueue(). + * + * @param tileCache The TileCache over whose tiles this CrawlIterator will crawl + * @param startRect [optional] The rectangle that we grow in the first (rectangle + * expansion) iteration state. + */ +TileManager.CrawlIterator = function CrawlIterator(tileCache, startRect) { + this._tileCache = tileCache; + this._stepRect = startRect; + + // used to remember tiles that we've reused during this crawl + this._visited = {}; + + // filters the tiles we've already reused once from being considered victims + // for reuse when we ask the tile cache to create a new tile + let visited = this._visited; + this._notVisited = function(tile) { return !visited[tile]; }; + + // a generator that generates tile indices corresponding to tiles intersecting + // the boundary of an expanding rectangle + this._crawlIndices = !startRect ? null : (function indicesGenerator(rect, tc) { + let outOfBounds = false; + while (!outOfBounds) { + // expand rect + rect.left -= kTileWidth; + rect.right += kTileWidth; + rect.top -= kTileHeight; + rect.bottom += kTileHeight; + + let dx = (rect.right % kTileWidth) - (rect.left % kTileWidth); + let dy = (rect.bottom % kTileHeight) - (rect.top % kTileHeight); + + outOfBounds = true; + + // top, bottom borders + for (let y of [rect.top, rect.bottom]) { + for (let x = rect.left; x <= rect.right - dx; x += kTileWidth) { + let i = x >> kTileExponentWidth; + let j = y >> kTileExponentHeight; + if (tc.inBounds(i, j)) { + outOfBounds = false; + yield [i, j]; + } + } + } + + // left, right borders + for (let x of [rect.left, rect.right]) { + for (let y = rect.top; y <= rect.bottom - dy; y += kTileHeight) { + let i = x >> kTileExponentWidth; + let j = y >> kTileExponentHeight; + if (tc.inBounds(i, j)) { + outOfBounds = false; + yield [i, j]; + } + } + } + } + })(this._stepRect, this._tileCache), // instantiate the generator + + // after we finish the rectangle iteration state, we enter the FIFO queue state + this._queueState = !startRect; + this._queue = []; + + // used to prevent tiles from being enqueued twice --- "patience, we'll get to + // it in a moment" + this._enqueued = {}; +}; + +TileManager.CrawlIterator.prototype = { + __iterator__: function*() { + while (true) { + let tile = this.next(); + if (!tile) break; + yield tile; + } + }, + + becomeQueue: function becomeQueue() { + this._queueState = true; + }, + + unbecomeQueue: function unbecomeQueue() { + this._queueState = false; + }, + + next: function next() { + if (this._queueState) + return this.dequeue(); + + let tile = null; + + if (this._crawlIndices) { + try { + let [i, j] = this._crawlIndices.next(); + tile = this._tileCache.getTile(i, j, true, this._notVisited); + } catch (e) { + if (!(e instanceof StopIteration)) + throw e; + } + } + + if (tile) { + this._visited[tile] = true; + } else { + this.becomeQueue(); + return this.next(); + } + + return tile; + }, + + dequeue: function dequeue() { + let tile = null; + do { + let idx = this._queue.shift(); + if (!idx) + return null; + + delete this._enqueued[idx]; + let [i, j] = this._unstrIndices(idx); + tile = this._tileCache.getTile(i, j, false); + + } while (!tile); + + return tile; + }, + + enqueue: function enqueue(i, j) { + let idx = this._strIndices(i, j); + if (!this._enqueued[idx]) { + this._queue.push(idx); + this._enqueued[idx] = true; + } + }, + + _strIndices: function _strIndices(i, j) { + return i + "," + j; + }, + + _unstrIndices: function _unstrIndices(str) { + return str.split(','); + } + +}; diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/WidgetStack.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/WidgetStack.js new file mode 100644 index 0000000000..69288e725f --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/WidgetStack.js @@ -0,0 +1,1438 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +var gWsDoLog = false; +var gWsLogDiv = null; + +function logbase() { + if (!gWsDoLog) + return; + + if (gWsLogDiv == null && "console" in window) { + console.log.apply(console, arguments); + } else { + var s = ""; + for (var i = 0; i < arguments.length; i++) { + s += arguments[i] + " "; + } + s += "\n"; + if (gWsLogDiv) { + gWsLogDiv.appendChild(document.createElementNS("http://www.w3.org/1999/xhtml", "br")); + gWsLogDiv.appendChild(document.createTextNode(s)); + } + + dump(s); + } +} + +function dumpJSStack(stopAtNamedFunction) { + let caller = Components.stack.caller; + dump("\tStack: " + caller.name); + while ((caller = caller.caller)) { + dump(" <- " + caller.name); + if (stopAtNamedFunction && caller.name != "anonymous") + break; + } + dump("\n"); +} + +function log() { + // logbase.apply(window, arguments); +} + +function log2() { + // logbase.apply(window, arguments); +} + +var reportError = log; + +/* + * wsBorder class + * + * Simple container for top,left,bottom,right "border" values + */ +function wsBorder(t, l, b, r) { + this.setBorder(t, l, b, r); +} + +wsBorder.prototype = { + + setBorder: function (t, l, b, r) { + this.top = t; + this.left = l; + this.bottom = b; + this.right = r; + }, + + toString: function () { + return "[l:" + this.left + ",t:" + this.top + ",r:" + this.right + ",b:" + this.bottom + "]"; + } +}; + +/* + * wsRect class + * + * Rectangle class, with both x/y/w/h and t/l/b/r accessors. + */ +function wsRect(x, y, w, h) { + this.left = x; + this.top = y; + this.right = x+w; + this.bottom = y+h; +} + +wsRect.prototype = { + + get x() { return this.left; }, + get y() { return this.top; }, + get width() { return this.right - this.left; }, + get height() { return this.bottom - this.top; }, + set x(v) { + let diff = this.left - v; + this.left = v; + this.right -= diff; + }, + set y(v) { + let diff = this.top - v; + this.top = v; + this.bottom -= diff; + }, + set width(v) { this.right = this.left + v; }, + set height(v) { this.bottom = this.top + v; }, + + setRect: function(x, y, w, h) { + this.left = x; + this.top = y; + this.right = x+w; + this.bottom = y+h; + + return this; + }, + + setBounds: function(t, l, b, r) { + this.top = t; + this.left = l; + this.bottom = b; + this.right = r; + + return this; + }, + + equals: function equals(r) { + return (r != null && + this.top == r.top && + this.left == r.left && + this.bottom == r.bottom && + this.right == r.right); + }, + + clone: function clone() { + return new wsRect(this.left, this.top, this.right - this.left, this.bottom - this.top); + }, + + center: function center() { + return [this.left + (this.right - this.left) / 2, + this.top + (this.bottom - this.top) / 2]; + }, + + centerRounded: function centerRounded() { + return this.center().map(Math.round); + }, + + copyFrom: function(r) { + this.top = r.top; + this.left = r.left; + this.bottom = r.bottom; + this.right = r.right; + + return this; + }, + + copyFromTLBR: function(r) { + this.left = r.left; + this.top = r.top; + this.right = r.right; + this.bottom = r.bottom; + + return this; + }, + + translate: function(x, y) { + this.left += x; + this.right += x; + this.top += y; + this.bottom += y; + + return this; + }, + + // return a new wsRect that is the union of that one and this one + union: function(rect) { + let l = Math.min(this.left, rect.left); + let r = Math.max(this.right, rect.right); + let t = Math.min(this.top, rect.top); + let b = Math.max(this.bottom, rect.bottom); + + return new wsRect(l, t, r-l, b-t); + }, + + toString: function() { + return "[" + this.x + "," + this.y + "," + this.width + "," + this.height + "]"; + }, + + expandBy: function(b) { + this.left += b.left; + this.right += b.right; + this.top += b.top; + this.bottom += b.bottom; + return this; + }, + + contains: function(other) { + return !!(other.left >= this.left && + other.right <= this.right && + other.top >= this.top && + other.bottom <= this.bottom); + }, + + intersect: function(r2) { + let xmost1 = this.right; + let xmost2 = r2.right; + + let x = Math.max(this.left, r2.left); + + let temp = Math.min(xmost1, xmost2); + if (temp <= x) + return null; + + let width = temp - x; + + let ymost1 = this.bottom; + let ymost2 = r2.bottom; + let y = Math.max(this.top, r2.top); + + temp = Math.min(ymost1, ymost2); + if (temp <= y) + return null; + + let height = temp - y; + + return new wsRect(x, y, width, height); + }, + + intersects: function(other) { + let xok = (other.left > this.left && other.left < this.right) || + (other.right > this.left && other.right < this.right) || + (other.left <= this.left && other.right >= this.right); + let yok = (other.top > this.top && other.top < this.bottom) || + (other.bottom > this.top && other.bottom < this.bottom) || + (other.top <= this.top && other.bottom >= this.bottom); + return xok && yok; + }, + + /** + * Similar to (and most code stolen from) intersect(). A restriction + * is an intersection, but this modifies the receiving object instead + * of returning a new rect. + */ + restrictTo: function restrictTo(r2) { + let xmost1 = this.right; + let xmost2 = r2.right; + + let x = Math.max(this.left, r2.left); + + let temp = Math.min(xmost1, xmost2); + if (temp <= x) + throw "Intersection is empty but rects cannot be empty"; + + let width = temp - x; + + let ymost1 = this.bottom; + let ymost2 = r2.bottom; + let y = Math.max(this.top, r2.top); + + temp = Math.min(ymost1, ymost2); + if (temp <= y) + throw "Intersection is empty but rects cannot be empty"; + + let height = temp - y; + + return this.setRect(x, y, width, height); + }, + + /** + * Similar to (and most code stolen from) union(). An extension is a + * union (in our sense of the term, not the common set-theoretic sense), + * but this modifies the receiving object instead of returning a new rect. + * Effectively, this rectangle is expanded minimally to contain all of the + * other rect. "Expanded minimally" means that the rect may shrink if + * given a strict subset rect as the argument. + */ + expandToContain: function extendTo(rect) { + let l = Math.min(this.left, rect.left); + let r = Math.max(this.right, rect.right); + let t = Math.min(this.top, rect.top); + let b = Math.max(this.bottom, rect.bottom); + + return this.setRect(l, t, r-l, b-t); + }, + + round: function round(scale) { + if (!scale) scale = 1; + + this.left = Math.floor(this.left * scale) / scale; + this.top = Math.floor(this.top * scale) / scale; + this.right = Math.ceil(this.right * scale) / scale; + this.bottom = Math.ceil(this.bottom * scale) / scale; + + return this; + }, + + scale: function scale(xscl, yscl) { + this.left *= xscl; + this.right *= xscl; + this.top *= yscl; + this.bottom *= yscl; + + return this; + } +}; + +/* + * The "Widget Stack" + * + * Manages a <xul:stack>'s children, allowing them to be dragged around + * the stack, subject to specified constraints. Optionally supports + * one widget designated as the viewport, which can be panned over a virtual + * area without needing to draw that area entirely. The viewport widget + * is designated by a 'viewport' attribute on the child element. + * + * Widgets are subject to various constraints, specified in xul via the + * 'constraint' attribute. Current constraints are: + * ignore-x: When panning, ignore any changes to the widget's x position + * ignore-y: When panning, ignore any changes to the widget's y position + * vp-relative: This widget's position should be claculated relative to + * the viewport widget. It will always keep the same offset from that + * widget as initially laid out, regardless of changes to the viewport + * bounds. + * frozen: This widget is in a fixed position and should never pan. + */ +function WidgetStack(el, ew, eh) { + this.init(el, ew, eh); +} + +WidgetStack.prototype = { + // the <stack> element + _el: null, + + // object indexed by widget id, with state struct for each object (see _addNewWidget) + _widgetState: null, + + // any barriers + _barriers: null, + + // If a viewport widget is present, this will point to its state object; + // otherwise null. + _viewport: null, + + // a wsRect; the inner bounds of the viewport content + _viewportBounds: null, + // a wsBorder; the overflow area to the side of the bounds where our + // viewport-relative widgets go + _viewportOverflow: null, + + // a wsRect; the viewportBounds expanded by the viewportOverflow + _pannableBounds: null, + get pannableBounds() { + if (!this._pannableBounds) { + this._pannableBounds = this._viewportBounds.clone() + .expandBy(this._viewportOverflow); + } + return this._pannableBounds.clone(); + }, + + // a wsRect; the currently visible part of pannableBounds. + _viewingRect: null, + + // the amount of current global offset applied to all widgets (whether + // static or not). Set via offsetAll(). Can be used to push things + // out of the way for overlaying some other UI. + globalOffsetX: 0, + globalOffsetY: 0, + + // if true (default), panning is constrained to the pannable bounds. + _constrainToViewport: true, + + _viewportUpdateInterval: -1, + _viewportUpdateTimeout: -1, + + _viewportUpdateHandler: null, + _panHandler: null, + + _dragState: null, + + _skipViewportUpdates: 0, + _forceViewportUpdate: false, + + // + // init: + // el: the <stack> element whose children are to be managed + // + init: function (el, ew, eh) { + this._el = el; + this._widgetState = {}; + this._barriers = []; + + let rect = this._el.getBoundingClientRect(); + let width = rect.width; + let height = rect.height; + + if (ew != undefined && eh != undefined) { + width = ew; + height = eh; + } + + this._viewportOverflow = new wsBorder(0, 0, 0, 0); + + this._viewingRect = new wsRect(0, 0, width, height); + + // listen for DOMNodeInserted/DOMNodeRemoved/DOMAttrModified + let children = this._el.childNodes; + for (let i = 0; i < children.length; i++) { + let c = this._el.childNodes[i]; + if (c.tagName == "spacer") + this._addNewBarrierFromSpacer(c); + else + this._addNewWidget(c); + } + + // this also updates the viewportOverflow and pannableBounds + this._updateWidgets(); + + if (this._viewport) { + this._viewportBounds = new wsRect(0, 0, this._viewport.rect.width, this._viewport.rect.height); + } else { + this._viewportBounds = new wsRect(0, 0, 0, 0); + } + }, + + // moveWidgetBy: move the widget with the given id by x,y. Should + // not be used on vp-relative or otherwise frozen widgets (using it + // on the x coordinate for x-ignore widgets and similarily for y is + // ok, as long as the other coordinate remains 0.) + moveWidgetBy: function (wid, x, y) { + let state = this._getState(wid); + + state.rect.x += x; + state.rect.y += y; + + this._commitState(state); + }, + + // panBy: pan the entire set of widgets by the given x and y amounts. + // This does the same thing as if the user dragged by the given amount. + // If this is called with an outstanding drag, weirdness might happen, + // but it also might work, so not disabling that. + // + // if ignoreBarriers is true, then barriers are ignored for the pan. + panBy: function panBy(dx, dy, ignoreBarriers) { + dx = Math.round(dx); + dy = Math.round(dy); + + if (dx == 0 && dy == 0) + return false; + + let needsDragWrap = !this._dragging; + + if (needsDragWrap) + this.dragStart(0, 0); + + let panned = this._panBy(dx, dy, ignoreBarriers); + + if (needsDragWrap) + this.dragStop(); + + return panned; + }, + + // panTo: pan the entire set of widgets so that the given x,y + // coordinates are in the upper left of the stack. If either is + // null or undefined, only move the other axis + panTo: function panTo(x, y) { + if (x == undefined || x == null) + x = this._viewingRect.x; + if (y == undefined || y == null) + y = this._viewingRect.y; + this.panBy(x - this._viewingRect.x, y - this._viewingRect.y, true); + }, + + // freeze: set a widget as frozen. A frozen widget won't be moved + // in the stack -- its x,y position will still be tracked in the + // state, but the left/top attributes won't be overwritten. Call unfreeze + // to move the widget back to where the ws thinks it should be. + freeze: function (wid) { + let state = this._getState(wid); + + state.frozen = true; + }, + + unfreeze: function (wid) { + let state = this._getState(wid); + if (!state.frozen) + return; + + state.frozen = false; + this._commitState(state); + }, + + // moveFrozenTo: move a frozen widget with id wid to x, y in the stack. + // can only be used on frozen widgets + moveFrozenTo: function (wid, x, y) { + let state = this._getState(wid); + if (!state.frozen) + throw "moveFrozenTo on non-frozen widget " + wid; + + state.widget.setAttribute("left", x); + state.widget.setAttribute("top", y); + }, + + // moveUnfrozenTo: move an unfrozen, pannable widget with id wid to x, y in + // the stack. should only be used on unfrozen widgets when a dynamic change + // in position needs to be made. we basically remove, adjust and re-add + // the widget + moveUnfrozenTo: function (wid, x, y) { + delete this._widgetState[wid]; + let widget = document.getElementById(wid); + if (x) widget.setAttribute("left", x); + if (y) widget.setAttribute("top", y); + this._addNewWidget(widget); + this._updateWidgets(); + }, + + // we're relying on viewportBounds and viewingRect having the same origin + get viewportVisibleRect () { + let rect = this._viewportBounds.intersect(this._viewingRect); + if (!rect) + rect = new wsRect(0, 0, 0, 0); + return rect; + }, + + isWidgetFrozen: function isWidgetFrozen(wid) { + return this._getState(wid).frozen; + }, + + // isWidgetVisible: return true if any portion of widget with id wid is + // visible; otherwise return false. + isWidgetVisible: function (wid) { + let state = this._getState(wid); + let visibleStackRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height); + + return visibleStackRect.intersects(state.rect); + }, + + // getWidgetVisibility: returns the percentage that the widget is visible + getWidgetVisibility: function (wid) { + let state = this._getState(wid); + let visibleStackRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height); + + let visibleRect = visibleStackRect.intersect(state.rect); + if (visibleRect) + return [visibleRect.width / state.rect.width, visibleRect.height / state.rect.height] + + return [0, 0]; + }, + + // offsetAll: add an offset to all widgets + offsetAll: function (x, y) { + this.globalOffsetX += x; + this.globalOffsetY += y; + + for (let wid in this._widgetState) { + let state = this._widgetState[wid]; + state.rect.x += x; + state.rect.y += y; + + this._commitState(state); + } + }, + + // setViewportBounds + // nb: an object containing top, left, bottom, right properties + // OR + // width, height: integer values; origin assumed to be 0,0 + // OR + // top, left, bottom, right: integer values + // + // Set the bounds of the viewport area; that is, set the size of the + // actual content that the viewport widget will be providing a view + // over. For example, in the case of a 100x100 viewport showing a + // view into a 100x500 webpage, the viewport bounds would be + // { top: 0, left: 0, bottom: 500, right: 100 }. + // + // setViewportBounds will move all the viewport-relative widgets into + // place based on the new viewport bounds. + setViewportBounds: function setViewportBounds() { + let oldBounds = this._viewportBounds.clone(); + + if (arguments.length == 1) { + this._viewportBounds.copyFromTLBR(arguments[0]); + } else if (arguments.length == 2) { + this._viewportBounds.setRect(0, 0, arguments[0], arguments[1]); + } else if (arguments.length == 4) { + this._viewportBounds.setBounds(arguments[0], + arguments[1], + arguments[2], + arguments[3]); + } else { + throw "Invalid number of arguments to setViewportBounds"; + } + + let vp = this._viewport; + + let dleft = this._viewportBounds.left - oldBounds.left; + let dright = this._viewportBounds.right - oldBounds.right; + let dtop = this._viewportBounds.top - oldBounds.top; + let dbottom = this._viewportBounds.bottom - oldBounds.bottom; + + // log2("setViewportBounds dltrb", dleft, dtop, dright, dbottom); + + // move all vp-relative widgets to be the right offset from the bounds again + for (let wid in this._widgetState) { + let state = this._widgetState[wid]; + if (state.vpRelative) { + // log2("vpRelative widget", state.id, state.rect.x, dleft, dright); + if (state.vpOffsetXBefore) { + state.rect.x += dleft; + } else { + state.rect.x += dright; + } + + if (state.vpOffsetYBefore) { + state.rect.y += dtop; + } else { + state.rect.y += dbottom; + } + + // log2("vpRelative widget", state.id, state.rect.x, dleft, dright); + this._commitState(state); + } + } + + for (let bid in this._barriers) { + let barrier = this._barriers[bid]; + + // log2("setViewportBounds: looking at barrier", bid, barrier.vpRelative, barrier.type); + + if (barrier.vpRelative) { + if (barrier.type == "vertical") { + let q = "v barrier moving from " + barrier.x + " to "; + if (barrier.vpOffsetXBefore) { + barrier.x += dleft; + } else { + barrier.x += dright; + } + // log2(q += barrier.x); + } else if (barrier.type == "horizontal") { + let q = "h barrier moving from " + barrier.y + " to "; + if (barrier.vpOffsetYBefore) { + barrier.y += dtop; + } else { + barrier.y += dbottom; + } + // log2(q += barrier.y); + } + } + } + + // clear the pannable bounds cache to make sure it gets rebuilt + this._pannableBounds = null; + + // now let's make sure that the viewing rect and inner bounds are still valid + this._adjustViewingRect(); + + this._viewportUpdate(0, 0, true); + }, + + // setViewportHandler + // uh: A function object + // + // The given function object is called at the end of every drag and viewport + // bounds change, passing in the new rect that's to be displayed in the + // viewport. + // + setViewportHandler: function (uh) { + this._viewportUpdateHandler = uh; + }, + + // setPanHandler + // uh: A function object + // + // The given functin object is called whenever elements pan; it provides + // the new area of the pannable bounds that's visible in the stack. + setPanHandler: function (uh) { + this._panHandler = uh; + }, + + // dragStart: start a drag, with the current coordinates being clientX,clientY + dragStart: function dragStart(clientX, clientY) { + log("(dragStart)", clientX, clientY); + + if (this._dragState) { + reportError("dragStart with drag already in progress? what?"); + this._dragState = null; + } + + this._dragState = { }; + + let t = Date.now(); + + this._dragState.barrierState = []; + + this._dragState.startTime = t; + // outer x, that is outer from the viewport coordinates. In stack-relative coords. + this._dragState.outerStartX = clientX; + this._dragState.outerStartY = clientY; + + this._dragCoordsFromClient(clientX, clientY, t); + + this._dragState.outerLastUpdateDX = 0; + this._dragState.outerLastUpdateDY = 0; + + if (this._viewport) { + // create a copy of these so that we can compute + // deltas correctly to update the viewport + this._viewport.dragStartRect = this._viewport.rect.clone(); + } + + this._dragState.dragging = true; + }, + + _viewportDragUpdate: function viewportDragUpdate() { + let vws = this._viewport; + this._viewportUpdate((vws.dragStartRect.x - vws.rect.x), + (vws.dragStartRect.y - vws.rect.y)); + }, + + // dragStop: stop any drag in progress + dragStop: function dragStop() { + log("(dragStop)"); + + if (!this._dragging) + return; + + if (this._viewportUpdateTimeout != -1) + clearTimeout(this._viewportUpdateTimeout); + + this._viewportDragUpdate(); + + this._dragState = null; + }, + + // dragMove: process a mouse move to clientX,clientY for an ongoing drag + dragMove: function dragMove(clientX, clientY) { + if (!this._dragging) + return false; + + this._dragCoordsFromClient(clientX, clientY); + + let panned = this._dragUpdate(); + + if (this._viewportUpdateInterval != -1) { + if (this._viewportUpdateTimeout != -1) + clearTimeout(this._viewportUpdateTimeout); + let self = this; + this._viewportUpdateTimeout = setTimeout(function () { self._viewportDragUpdate(); }, this._viewportUpdateInterval); + } + + return panned; + }, + + // dragBy: process a mouse move by dx,dy for an ongoing drag + dragBy: function dragBy(dx, dy) { + return this.dragMove(this._dragState.outerCurX + dx, this._dragState.outerCurY + dy); + }, + + // updateSize: tell the WidgetStack to update its size, because it + // was either resized or some other event took place. + updateSize: function updateSize(width, height) { + if (width == undefined || height == undefined) { + let rect = this._el.getBoundingClientRect(); + width = rect.width; + height = rect.height; + } + + // update widget rects and viewportOverflow, since the resize might have + // caused them to change (widgets first, since the viewportOverflow depends + // on them). + + // XXX these methods aren't working correctly yet, but they aren't strictly + // necessary in Fennec's default config + // for (let wid in this._widgetState) { + // let s = this._widgetState[wid]; + // this._updateWidgetRect(s); + // } + // this._updateViewportOverflow(); + + this._viewingRect.width = width; + this._viewingRect.height = height; + + // Wrap this call in a batch to ensure that we always call the + // viewportUpdateHandler, even if _adjustViewingRect doesn't trigger a pan. + // If it does, the batch also ensures that we don't call the handler twice. + this.beginUpdateBatch(); + this._adjustViewingRect(); + this.endUpdateBatch(); + }, + + beginUpdateBatch: function startUpdate() { + if (!this._skipViewportUpdates) { + this._startViewportBoundsString = this._viewportBounds.toString(); + this._forceViewportUpdate = false; + } + this._skipViewportUpdates++; + }, + + endUpdateBatch: function endUpdate(aForceRedraw) { + if (!this._skipViewportUpdates) + throw new Error("Unbalanced call to endUpdateBatch"); + + this._forceViewportUpdate = this._forceViewportUpdate || aForceRedraw; + + this._skipViewportUpdates--; + if (this._skipViewportUpdates) + return; + + let boundsSizeChanged = + this._startViewportBoundsString != this._viewportBounds.toString(); + this._callViewportUpdateHandler(boundsSizeChanged || this._forceViewportUpdate); + }, + + // + // Internal code + // + + _updateWidgetRect: function(state) { + // don't need to support updating the viewport rect at the moment + // (we'd need to duplicate the vptarget* code from _addNewWidget if we did) + if (state == this._viewport) + return; + + let w = state.widget; + let x = w.getAttribute("left") || 0; + let y = w.getAttribute("top") || 0; + let rect = w.getBoundingClientRect(); + state.rect = new wsRect(parseInt(x), parseInt(y), + rect.right - rect.left, + rect.bottom - rect.top); + if (w.hasAttribute("widgetwidth") && w.hasAttribute("widgetheight")) { + state.rect.width = parseInt(w.getAttribute("widgetwidth")); + state.rect.height = parseInt(w.getAttribute("widgetheight")); + } + }, + + _dumpRects: function () { + dump("WidgetStack:\n"); + dump("\tthis._viewportBounds: " + this._viewportBounds + "\n"); + dump("\tthis._viewingRect: " + this._viewingRect + "\n"); + dump("\tthis._viewport.viewportInnerBounds: " + this._viewport.viewportInnerBounds + "\n"); + dump("\tthis._viewport.rect: " + this._viewport.rect + "\n"); + dump("\tthis._viewportOverflow: " + this._viewportOverflow + "\n"); + dump("\tthis.pannableBounds: " + this.pannableBounds + "\n"); + }, + + // Ensures that _viewingRect is within _pannableBounds (call this when either + // one is resized) + _adjustViewingRect: function _adjustViewingRect() { + let vr = this._viewingRect; + let pb = this.pannableBounds; + + if (pb.contains(vr)) + return; // nothing to do here + + // don't bother adjusting _viewingRect if it can't fit into + // _pannableBounds + if (vr.height > pb.height || vr.width > pb.width) + return; + + let panX = 0, panY = 0; + if (vr.right > pb.right) + panX = pb.right - vr.right; + else if (vr.left < pb.left) + panX = pb.left - vr.left; + + if (vr.bottom > pb.bottom) + panY = pb.bottom - vr.bottom; + else if (vr.top < pb.top) + panY = pb.top - vr.top; + + this.panBy(panX, panY, true); + }, + + _getState: function (wid) { + let w = this._widgetState[wid]; + if (!w) + throw "Unknown widget id '" + wid + "'; widget not in stack"; + return w; + }, + + get _dragging() { + return this._dragState && this._dragState.dragging; + }, + + _viewportUpdate: function _viewportUpdate(dX, dY, boundsChanged) { + if (!this._viewport) + return; + + this._viewportUpdateTimeout = -1; + + let vws = this._viewport; + let vwib = vws.viewportInnerBounds; + let vpb = this._viewportBounds; + + // recover the amount the inner bounds moved by the amount the viewport + // widget moved, but don't include offsets that we're making up from previous + // drags that didn't affect viewportInnerBounds + let [ignoreX, ignoreY] = this._offsets || [0, 0]; + let rx = dX - ignoreX; + let ry = dY - ignoreY; + + [dX, dY] = this._rectTranslateConstrain(rx, ry, vwib, vpb); + + // record the offsets that correspond to the amount of the drag we're ignoring + // to ensure the viewportInnerBounds remains within the viewportBounds + this._offsets = [dX - rx, dY - ry]; + + // adjust the viewportInnerBounds, and snap the viewport back + vwib.translate(dX, dY); + vws.rect.translate(dX, dY); + this._commitState(vws); + + // update this so that we can call this function again during the same drag + // and get the right values. + vws.dragStartRect = vws.rect.clone(); + + this._callViewportUpdateHandler(boundsChanged); + }, + + _callViewportUpdateHandler: function _callViewportUpdateHandler(boundsChanged) { + if (!this._viewport || !this._viewportUpdateHandler || this._skipViewportUpdates) + return; + + let vwb = this._viewportBounds.clone(); + + let vwib = this._viewport.viewportInnerBounds.clone(); + + let vis = this.viewportVisibleRect; + + vwib.left += this._viewport.offsetLeft; + vwib.top += this._viewport.offsetTop; + vwib.right += this._viewport.offsetRight; + vwib.bottom += this._viewport.offsetBottom; + + this._viewportUpdateHandler.apply(window, [vwb, vwib, vis, boundsChanged]); + }, + + _dragCoordsFromClient: function (cx, cy, t) { + this._dragState.curTime = t ? t : Date.now(); + this._dragState.outerCurX = cx; + this._dragState.outerCurY = cy; + + let dx = this._dragState.outerCurX - this._dragState.outerStartX; + let dy = this._dragState.outerCurY - this._dragState.outerStartY; + this._dragState.outerDX = dx; + this._dragState.outerDY = dy; + }, + + _panHandleBarriers: function (dx, dy) { + // XXX unless the barriers are sorted by position, this will break + // with multiple barriers that are near enough to eachother that a + // drag could cross more than one. + + let vr = this._viewingRect; + + // XXX this just stops at the first horizontal and vertical barrier it finds + + // barrier_[xy] is the barrier that was used to get to the final + // barrier_d[xy] value. if null, no barrier, and dx/dy shouldn't + // be replaced with barrier_d[xy]. + let barrier_y = null, barrier_x = null; + let barrier_dy = 0, barrier_dx = 0; + + for (let i = 0; i < this._barriers.length; i++) { + let b = this._barriers[i]; + + // log2("barrier", i, b.type, b.x, b.y); + + if (dx != 0 && b.type == "vertical") { + if (barrier_x != null) { + delete this._dragState.barrierState[i]; + continue; + } + + let alreadyKnownDistance = this._dragState.barrierState[i] || 0; + + // log2("alreadyKnownDistance", alreadyKnownDistance); + + let dbx = 0; + + // 100 <= 100 && 100-(-5) > 100 + + if ((vr.left <= b.x && vr.left+dx > b.x) || + (vr.left >= b.x && vr.left+dx < b.x)) + { + dbx = b.x - vr.left; + } else if ((vr.right <= b.x && vr.right+dx > b.x) || + (vr.right >= b.x && vr.right+dx < b.x)) + { + dbx = b.x - vr.right; + } else { + delete this._dragState.barrierState[i]; + continue; + } + + let leftoverDistance = dbx - dx; + + // log2("initial dbx", dbx, leftoverDistance); + + let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size; + + if (dist >= 0) { + if (dx < 0) + dbx -= dist; + else + dbx += dist; + delete this._dragState.barrierState[i]; + } else { + dbx = 0; + this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance; + } + + // log2("final dbx", dbx, "state", this._dragState.barrierState[i]); + + if (Math.abs(barrier_dx) <= Math.abs(dbx)) { + barrier_x = b; + barrier_dx = dbx; + + // log2("new barrier_dx", barrier_dx); + } + } + + if (dy != 0 && b.type == "horizontal") { + if (barrier_y != null) { + delete this._dragState.barrierState[i]; + continue; + } + + let alreadyKnownDistance = this._dragState.barrierState[i] || 0; + + // log2("alreadyKnownDistance", alreadyKnownDistance); + + let dby = 0; + + // 100 <= 100 && 100-(-5) > 100 + + if ((vr.top <= b.y && vr.top+dy > b.y) || + (vr.top >= b.y && vr.top+dy < b.y)) + { + dby = b.y - vr.top; + } else if ((vr.bottom <= b.y && vr.bottom+dy > b.y) || + (vr.bottom >= b.y && vr.bottom+dy < b.y)) + { + dby = b.y - vr.bottom; + } else { + delete this._dragState.barrierState[i]; + continue; + } + + let leftoverDistance = dby - dy; + + // log2("initial dby", dby, leftoverDistance); + + let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size; + + if (dist >= 0) { + if (dy < 0) + dby -= dist; + else + dby += dist; + delete this._dragState.barrierState[i]; + } else { + dby = 0; + this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance; + } + + // log2("final dby", dby, "state", this._dragState.barrierState[i]); + + if (Math.abs(barrier_dy) <= Math.abs(dby)) { + barrier_y = b; + barrier_dy = dby; + + // log2("new barrier_dy", barrier_dy); + } + } + } + + if (barrier_x) { + // log2("did barrier_x", barrier_x, "barrier_dx", barrier_dx); + dx = barrier_dx; + } + + if (barrier_y) { + dy = barrier_dy; + } + + return [dx, dy]; + }, + + _panBy: function _panBy(dx, dy, ignoreBarriers) { + let vr = this._viewingRect; + + // check if any barriers would be crossed by this pan, and take them + // into account. do this first. + if (!ignoreBarriers) + [dx, dy] = this._panHandleBarriers(dx, dy); + + // constrain the full drag of the viewingRect to the pannableBounds. + // note that the viewingRect needs to move in the opposite + // direction of the pan, so we fiddle with the signs here (as you + // pan to the upper left, more of the bottom right becomes visible, + // so the viewing rect moves to the bottom right of the virtual surface). + [dx, dy] = this._rectTranslateConstrain(dx, dy, vr, this.pannableBounds); + + // If the net result is that we don't have any room to move, then + // just return. + if (dx == 0 && dy == 0) + return false; + + // the viewingRect moves opposite of the actual pan direction, see above + vr.x += dx; + vr.y += dy; + + // Go through each widget and move it by dx,dy. Frozen widgets + // will be ignored in commitState. + // The widget rects are in real stack space though, so we need to subtract + // our (now negated) dx, dy from their coordinates. + for (let wid in this._widgetState) { + let state = this._widgetState[wid]; + if (!state.ignoreX) + state.rect.x -= dx; + if (!state.ignoreY) + state.rect.y -= dy; + + this._commitState(state); + } + + /* Do not call panhandler during pans within a transaction. + * Those pans always end-up covering up the checkerboard and + * do not require sliding out the location bar + */ + if (!this._skipViewportUpdates && this._panHandler) + this._panHandler.apply(window, [vr.clone(), dx, dy]); + + return true; + }, + + _dragUpdate: function _dragUpdate() { + let dx = this._dragState.outerLastUpdateDX - this._dragState.outerDX; + let dy = this._dragState.outerLastUpdateDY - this._dragState.outerDY; + + this._dragState.outerLastUpdateDX = this._dragState.outerDX; + this._dragState.outerLastUpdateDY = this._dragState.outerDY; + + return this.panBy(dx, dy); + }, + + // + // widget addition/removal + // + _addNewWidget: function (w) { + let wid = w.getAttribute("id"); + if (!wid) { + reportError("WidgetStack: child widget without id!"); + return; + } + + if (w.getAttribute("hidden") == "true") + return; + + let state = { + widget: w, + id: wid, + + viewport: false, + ignoreX: false, + ignoreY: false, + sticky: false, + frozen: false, + vpRelative: false, + + offsetLeft: 0, + offsetTop: 0, + offsetRight: 0, + offsetBottom: 0 + }; + + this._updateWidgetRect(state); + + if (w.hasAttribute("constraint")) { + let cs = w.getAttribute("constraint").split(","); + for (let s of cs) { + if (s == "ignore-x") + state.ignoreX = true; + else if (s == "ignore-y") + state.ignoreY = true; + else if (s == "sticky") + state.sticky = true; + else if (s == "frozen") { + state.frozen = true; + } else if (s == "vp-relative") + state.vpRelative = true; + } + } + + if (w.hasAttribute("viewport")) { + if (this._viewport) + reportError("WidgetStack: more than one viewport canvas in stack!"); + + this._viewport = state; + state.viewport = true; + + if (w.hasAttribute("vptargetx") && w.hasAttribute("vptargety") && + w.hasAttribute("vptargetw") && w.hasAttribute("vptargeth")) + { + let wx = parseInt(w.getAttribute("vptargetx")); + let wy = parseInt(w.getAttribute("vptargety")); + let ww = parseInt(w.getAttribute("vptargetw")); + let wh = parseInt(w.getAttribute("vptargeth")); + + state.offsetLeft = state.rect.left - wx; + state.offsetTop = state.rect.top - wy; + state.offsetRight = state.rect.right - (wx + ww); + state.offsetBottom = state.rect.bottom - (wy + wh); + + state.rect = new wsRect(wx, wy, ww, wh); + } + + // initialize inner bounds to top-left + state.viewportInnerBounds = new wsRect(0, 0, state.rect.width, state.rect.height); + } + + this._widgetState[wid] = state; + + log ("(New widget: " + wid + (state.viewport ? " [viewport]" : "") + " at: " + state.rect + ")"); + }, + + _removeWidget: function (w) { + let wid = w.getAttribute("id"); + delete this._widgetState[wid]; + this._updateWidgets(); + }, + + // updateWidgets: + // Go through all the widgets and figure out their viewport-relative offsets. + // If the widget goes to the left or above the viewport widget, then + // vpOffsetXBefore or vpOffsetYBefore is set. + // See setViewportBounds for use of vpOffset* state variables, and for how + // the actual x and y coords of each widget are calculated based on their offsets + // and the viewport bounds. + _updateWidgets: function () { + let vp = this._viewport; + + let ofRect = this._viewingRect.clone(); + + for (let wid in this._widgetState) { + let state = this._widgetState[wid]; + if (vp && state.vpRelative) { + // compute the vpOffset from 0,0 assuming that the viewport rect is 0,0 + if (state.rect.left >= vp.rect.right) { + state.vpOffsetXBefore = false; + state.vpOffsetX = state.rect.left - vp.rect.width; + } else { + state.vpOffsetXBefore = true; + state.vpOffsetX = state.rect.left - vp.rect.left; + } + + if (state.rect.top >= vp.rect.bottom) { + state.vpOffsetYBefore = false; + state.vpOffsetY = state.rect.top - vp.rect.height; + } else { + state.vpOffsetYBefore = true; + state.vpOffsetY = state.rect.top - vp.rect.top; + } + + log("widget", state.id, "offset", state.vpOffsetX, state.vpOffsetXBefore ? "b" : "a", state.vpOffsetY, state.vpOffsetYBefore ? "b" : "a", "rect", state.rect); + } + } + + this._updateViewportOverflow(); + }, + + // updates the viewportOverflow/pannableBounds + _updateViewportOverflow: function() { + let vp = this._viewport; + if (!vp) + return; + + let ofRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height); + + for (let wid in this._widgetState) { + let state = this._widgetState[wid]; + if (vp && state.vpRelative) { + ofRect.left = Math.min(ofRect.left, state.rect.left); + ofRect.top = Math.min(ofRect.top, state.rect.top); + ofRect.right = Math.max(ofRect.right, state.rect.right); + ofRect.bottom = Math.max(ofRect.bottom, state.rect.bottom); + } + } + + // prevent the viewportOverflow from having positive top/left or negative + // bottom/right values, which would otherwise happen if there aren't widgets + // beyond each of those edges + this._viewportOverflow = new wsBorder( + /* top*/ Math.round(Math.min(ofRect.top, 0)), + /* left*/ Math.round(Math.min(ofRect.left, 0)), + /* bottom*/ Math.round(Math.max(ofRect.bottom - vp.rect.height, 0)), + /* right*/ Math.round(Math.max(ofRect.right - vp.rect.width, 0)) + ); + + // clear the _pannableBounds cache, since it depends on the + // viewportOverflow + this._pannableBounds = null; + }, + + _widgetBounds: function () { + let r = new wsRect(0, 0, 0, 0); + + for (let wid in this._widgetState) { + let state = this._widgetState[wid]; + r = r.union(state.rect); + } + + return r; + }, + + _commitState: function (state) { + // if the widget is frozen, don't actually update its left/top; + // presumably the caller is managing those directly for now. + if (state.frozen) + return; + let w = state.widget; + let l = state.rect.x + state.offsetLeft; + let t = state.rect.y + state.offsetTop; + + // cache left/top to avoid calling setAttribute unnessesarily + if (state._left != l) { + state._left = l; + w.setAttribute("left", l); + } + + if (state._top != t) { + state._top = t; + w.setAttribute("top", t); + } + }, + + // constrain translate of rect by dx dy to bounds; return dx dy that can + // be used to bring rect up to the edge of bounds if we'd go over. + _rectTranslateConstrain: function (dx, dy, rect, bounds) { + let newX, newY; + + // If the rect is larger than the bounds, allow it to increase its overlap + let woverflow = rect.width > bounds.width; + let hoverflow = rect.height > bounds.height; + if (woverflow || hoverflow) { + let intersection = rect.intersect(bounds); + let newIntersection = rect.clone().translate(dx, dy).intersect(bounds); + if (woverflow) + newX = (newIntersection.width > intersection.width) ? rect.x + dx : rect.x; + if (hoverflow) + newY = (newIntersection.height > intersection.height) ? rect.y + dy : rect.y; + } + + // Common case, rect fits within the bounds + // clamp new X to within [bounds.left, bounds.right - rect.width], + // new Y to within [bounds.top, bounds.bottom - rect.height] + if (isNaN(newX)) + newX = Math.min(Math.max(bounds.left, rect.x + dx), bounds.right - rect.width); + if (isNaN(newY)) + newY = Math.min(Math.max(bounds.top, rect.y + dy), bounds.bottom - rect.height); + + return [newX - rect.x, newY - rect.y]; + }, + + // add a new barrier from a <spacer> + _addNewBarrierFromSpacer: function (el) { + let t = el.getAttribute("barriertype"); + + // XXX implement these at some point + // t != "lr" && t != "rl" && + // t != "tb" && t != "bt" && + + if (t != "horizontal" && + t != "vertical") + { + throw "Invalid barrier type: " + t; + } + + let x, y; + + let barrier = {}; + let vp = this._viewport; + + barrier.type = t; + + if (el.getAttribute("left")) + barrier.x = parseInt(el.getAttribute("left")); + else if (el.getAttribute("top")) + barrier.y = parseInt(el.getAttribute("top")); + else + throw "Barrier without top or left attribute"; + + if (el.getAttribute("size")) + barrier.size = parseInt(el.getAttribute("size")); + else + barrier.size = 10; + + if (el.hasAttribute("constraint")) { + let cs = el.getAttribute("constraint").split(","); + for (let s of cs) { + if (s == "ignore-x") + barrier.ignoreX = true; + else if (s == "ignore-y") + barrier.ignoreY = true; + else if (s == "sticky") + barrier.sticky = true; + else if (s == "frozen") { + barrier.frozen = true; + } else if (s == "vp-relative") + barrier.vpRelative = true; + } + } + + if (barrier.vpRelative) { + if (barrier.type == "vertical") { + if (barrier.x >= vp.rect.right) { + barrier.vpOffsetXBefore = false; + barrier.vpOffsetX = barrier.x - vp.rect.right; + } else { + barrier.vpOffsetXBefore = true; + barrier.vpOffsetX = barrier.x - vp.rect.left; + } + } else if (barrier.type == "horizontal") { + if (barrier.y >= vp.rect.bottom) { + barrier.vpOffsetYBefore = false; + barrier.vpOffsetY = barrier.y - vp.rect.bottom; + } else { + barrier.vpOffsetYBefore = true; + barrier.vpOffsetY = barrier.y - vp.rect.top; + } + + // log2("h barrier relative", barrier.vpOffsetYBefore, barrier.vpOffsetY); + } + } + + this._barriers.push(barrier); + } +}; diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/firefoxOverlay.xul b/toolkit/content/tests/fennec-tile-testapp/chrome/content/firefoxOverlay.xul new file mode 100644 index 0000000000..612f8bb9ff --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/firefoxOverlay.xul @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="chrome://tile/skin/overlay.css" type="text/css"?> +<!DOCTYPE overlay SYSTEM "chrome://tile/locale/tile.dtd"> +<overlay id="tile-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="overlay.js"/> + <stringbundleset id="stringbundleset"> + <stringbundle id="tile-strings" src="chrome://tile/locale/tile.properties"/> + </stringbundleset> + + <menupopup id="menu_ToolsPopup"> + <menuitem id="tile-hello" label="&tile.label;" + oncommand="tile.onMenuItemCommand(event);"/> + </menupopup> +</overlay> diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/foo.xul b/toolkit/content/tests/fennec-tile-testapp/chrome/content/foo.xul new file mode 100644 index 0000000000..cdc01658a0 --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/foo.xul @@ -0,0 +1,460 @@ +<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="onAlmostLoad();"
+ style="background-color:white;"
+ width="800"
+ height="480"
+ onresize="onResize();"
+ onkeypress="onKeyPress(event);">
+
+<script type="application/javascript" src="WidgetStack.js"/>
+<script type="application/javascript" src="TileManager.js"/>
+<script type="application/javascript" src="BrowserView.js"/>
+<script type="application/javascript">
+<![CDATA[
+
+// We do not endorse the use of globals, but this is just a closed lab
+// environment. What could possibly go wrong? ...
+let bv = null;
+let scrollbox = null;
+let leftbar = null;
+let rightbar = null;
+let topbar = null;
+
+function debug() {
+ let w = scrollbox.scrolledWidth;
+ let h = scrollbox.scrolledHeight;
+ let container = document.getElementById("tile_container");
+ let [x, y] = getScrollboxPosition();
+ if (bv) {
+ dump('----------------------DEBUG!-------------------------\n');
+ dump(bv._browserViewportState.toString() + endl);
+
+ dump(endl);
+
+ let cr = bv._tileManager._criticalRect;
+ dump('criticalRect from BV: ' + (cr ? cr.toString() : null) + endl);
+ dump('visibleRect from BV : ' + bv._visibleRect.toString() + endl);
+ dump('visibleRect from foo: ' + scrollboxToViewportRect(getVisibleRect()) + endl);
+
+ dump(endl);
+
+ dump('container width,height from BV: ' + bv._container.style.width + ', '
+ + bv._container.style.height + endl);
+ dump('container width,height via DOM: ' + container.style.width + ', '
+ + container.style.height + endl);
+
+ dump(endl);
+
+ dump('scrollbox position : ' + x + ', ' + y + endl);
+ dump('scrollbox scrolledsize: ' + w + ', ' + h + endl);
+
+ dump(endl);
+
+ dump('tilecache capacity: ' + bv._tileManager._tileCache.getCapacity() + endl);
+ dump('tilecache size : ' + bv._tileManager._tileCache.size + endl);
+ dump('tilecache numFree : ' + bv._tileManager._tileCache.numFree + endl);
+ dump('tilecache iBound : ' + bv._tileManager._tileCache.iBound + endl);
+ dump('tilecache jBound : ' + bv._tileManager._tileCache.jBound + endl);
+ dump('tilecache _lru : ' + bv._tileManager._tileCache._lru + endl);
+
+ dump('-----------------------------------------------------\n');
+ }
+}
+
+function debugTile(i, j) {
+ let tc = bv._tileManager._tileCache;
+ let t = tc.getTile(i, j);
+
+ dump('------ DEBUGGING TILE (' + i + ',' + j + ') --------\n');
+
+ dump('in bounds: ' + tc.inBounds(i, j) + endl);
+ dump('occupied : ' + tc._isOccupied(i, j) + endl);
+ if (t)
+ {
+ dump('toString : ' + t.toString(true) + endl);
+ dump('free : ' + t.free + endl);
+ dump('dirtyRect: ' + t._dirtyTileCanvasRect + endl);
+
+ let len = tc._tilePool.length;
+ for (let k = 0; k < len; ++k)
+ if (tc._tilePool[k] === t)
+ dump('found in tilePool at index ' + k + endl);
+ }
+
+ dump('------------------------------------\n');
+}
+
+function onKeyPress(e) {
+ const a = 97; // debug all critical tiles
+ const c = 99; // set tilecache capacity
+ const d = 100; // debug dump
+ const f = 102; // run noop() through forEachIntersectingRect (for timing)
+ const i = 105; // toggle info click mode
+ const l = 108; // restart lazy crawl
+ const m = 109; // fix mouseout
+ const t = 116; // debug given list of tiles separated by space
+
+ switch (e.charCode) {
+ case d:
+ debug();
+
+ break;
+ case l:
+ bv._tileManager.restartLazyCrawl(bv._tileManager._criticalRect);
+
+ break;
+ case c:
+ let cap = parseInt(window.prompt('new capacity'));
+ bv._tileManager._tileCache.setCapacity(cap);
+
+ break;
+ case f:
+ let noop = function noop() { for (let i = 0; i < 10; ++i); };
+ bv._tileManager._tileCache.forEachIntersectingRect(bv._tileManager._criticalRect,
+ false, noop, window);
+
+ break;
+ case t:
+ let ijstrs = window.prompt('row,col plz').split(' ');
+ for (let ijstr of ijstrs) {
+ let [i, j] = ijstr.split(',').map(x => parseInt(x));
+ debugTile(i, j);
+ }
+
+ break;
+ case a:
+ let cr = bv._tileManager._criticalRect;
+ dump('>>>>>> critical rect is ' + (cr ? cr.toString() : cr) + endl);
+ if (cr) {
+ let starti = cr.left >> kTileExponentWidth;
+ let endi = cr.right >> kTileExponentWidth;
+
+ let startj = cr.top >> kTileExponentHeight;
+ let endj = cr.bottom >> kTileExponentHeight;
+
+ for (var jj = startj; jj <= endj; ++jj)
+ for (var ii = starti; ii <= endi; ++ii)
+ debugTile(ii, jj);
+ }
+
+ break;
+ case i:
+ window.infoMode = !window.infoMode;
+ break;
+ case m:
+ onMouseUp();
+ break;
+ default:
+ break;
+ }
+}
+
+function onResize(e) {
+ if (bv) {
+ bv.beginBatchOperation();
+ bv.setVisibleRect(scrollboxToViewportRect(getVisibleRect()));
+ bv.zoomToPage();
+ bv.commitBatchOperation();
+ }
+}
+
+function onMouseDown(e) {
+ if (window.infoMode) {
+ let [basex, basey] = getScrollboxPosition();
+ let [x, y] = scrollboxToViewportXY(basex + e.clientX, basey + e.clientY);
+ let i = x >> kTileExponentWidth;
+ let j = y >> kTileExponentHeight;
+
+ debugTile(i, j);
+ }
+
+ window._isDragging = true;
+ window._dragStart = {x: e.clientX, y: e.clientY};
+
+ bv.pauseRendering();
+}
+
+function onMouseUp() {
+ window._isDragging = false;
+ bv.resumeRendering();
+}
+
+function onMouseMove(e) {
+ if (window._isDragging) {
+ let x = scrollbox.positionX;
+ let y = scrollbox.positionY;
+ let w = scrollbox.scrolledWidth;
+ let h = scrollbox.scrolledHeight;
+
+ let dx = window._dragStart.x - e.clientX;
+ let dy = window._dragStart.y - e.clientY;
+
+ // XXX if max(x, 0) > scrollwidth we shouldn't do anything (same for y/height)
+ let newX = Math.max(x + dx, 0);
+ let newY = Math.max(y + dy, 0);
+
+ if (newX < w || newY < h) {
+ // clip dx and dy to prevent us from going below 0
+ dx = Math.max(dx, -x);
+ dy = Math.max(dy, -y);
+
+ let oldx = x;
+ let oldy = y;
+
+ bv.onBeforeVisibleMove(dx, dy);
+
+ updateBars(oldx, oldy, dx, dy);
+ scrollbox.scrollBy(dx, dy);
+
+ let [newx, newy] = getScrollboxPosition();
+ let realdx = newx - oldx;
+ let realdy = newy - oldy;
+
+ updateBars(oldx, oldy, realdx, realdy);
+ bv.onAfterVisibleMove(realdx, realdy);
+ }
+ window._dragStart = {x: e.clientX, y: e.clientY};
+ }
+}
+
+function onAlmostLoad() {
+ window._isDragging = false;
+ window.infoMode = false;
+ window.setTimeout(onLoad, 1500);
+}
+
+function onLoad() {
+ // ----------------------------------------------------
+ scrollbox = document.getElementById("scrollbox").boxObject;
+ leftbar = document.getElementById("left_sidebar");
+ rightbar = document.getElementById("right_sidebar");
+ topbar = document.getElementById("top_urlbar");
+ // ----------------------------------------------------
+
+ let initX = Math.round(leftbar.getBoundingClientRect().right);
+ dump('scrolling to ' + initX + endl);
+ scrollbox.scrollTo(initX, 0);
+ let [x, y] = getScrollboxPosition();
+ dump(' scrolled to ' + x + ',' + y + endl);
+
+ let container = document.getElementById("tile_container");
+ container.addEventListener("mousedown", onMouseDown, true);
+ container.addEventListener("mouseup", onMouseUp, true);
+ container.addEventListener("mousemove", onMouseMove, true);
+
+ bv = new BrowserView(container, scrollboxToViewportRect(getVisibleRect()));
+
+ let browser = document.getElementById("googlenews");
+ bv.setBrowser(browser, false);
+}
+
+function updateBars(x, y, dx, dy) {
+return;
+ // shouldn't update margin if it doesn't need to be changed
+ let sidebars = document.getElementsByClassName("sidebar");
+ for (let i = 0; i < sidebars.length; i++) {
+ let sidebar = sidebars[i];
+ sidebar.style.margin = (y + dy) + "px 0px 0px 0px";
+ }
+
+ let urlbar = document.getElementById("top_urlbar");
+ urlbar.style.margin = "0px 0px 0px " + (x + dx) + "px";
+}
+
+function viewportToScrollboxXY(x, y) {
+ return scrollboxToViewportXY(x, y, -1);
+}
+
+function scrollboxToViewportXY(x, y) {
+ if (!x) x = 0;
+ if (!y) y = 0;
+
+ // shield your eyes!
+ let direction = (arguments.length >= 3) ? arguments[2] : 1;
+
+ let leftbarcr = leftbar.getBoundingClientRect();
+ let rightbarcr = rightbar.getBoundingClientRect();
+ let topbarcr = topbar.getBoundingClientRect();
+
+ let xtrans = direction * (-leftbarcr.width);
+ let ytrans = direction * (-topbarcr.height);
+ x += xtrans;
+ y += ytrans;
+
+ return [x, y];
+}
+
+function scrollboxToBrowserXY(browserView, x, y) {
+ [x, y] = scrollboxToViewportXY(x, y);
+ return [browserView.viewportToBrowser(x),
+ browserView.viewportToBrowser(y)];
+}
+
+function scrollboxToViewportRect(rect) {
+ let leftbarcr = leftbar.getBoundingClientRect();
+ let topbarcr = topbar.getBoundingClientRect();
+
+ let xtrans = -leftbarcr.width;
+ let ytrans = -topbarcr.height;
+
+ rect.translate(xtrans, ytrans);
+
+ return rect;
+}
+
+function getScrollboxPosition() {
+ return [scrollbox.positionX, scrollbox.positionY];
+}
+
+function getContentScrollValues(browser) {
+ let cwu = getBrowserDOMWindowUtils(browser);
+ let scrollX = {};
+ let scrollY = {};
+ cwu.getScrollXY(false, scrollX, scrollY);
+
+ return [scrollX.value, scrollY.value];
+}
+
+function getBrowserDOMWindowUtils(browser) {
+ return browser.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+}
+
+function getBrowserClientRect(browser, el) {
+ let [scrollX, scrollY] = getContentScrollValues(browser);
+ let r = el.getBoundingClientRect();
+
+ return new wsRect(r.left + scrollX,
+ r.top + scrollY,
+ r.width, r.height);
+}
+
+function scrollToElement(browser, el) {
+ var elRect = getPagePosition(browser, el);
+ bv.browserToViewportRect(elRect);
+ elRect.round();
+ this.scrollTo(elRect.x, elRect.y);
+}
+
+function zoomToElement(aElement) {
+ const margin = 15;
+
+ let elRect = getBrowserClientRect(browser, aElement);
+ let elWidth = elRect.width;
+ let vrWidth = bv.visibleRect.width;
+ /* Try to set zoom-level such that once zoomed element is as wide
+ * as the visible rect */
+ let zoomLevel = vrtWidth / (elWidth + (2 * margin));
+
+ bv.beginBatchOperation();
+
+ bv.setZoomLevel(zoomLevel);
+
+ /* If zoomLevel ends up clamped to less than asked for, calculate
+ * how many more screen pixels will fit horizontally in addition to
+ * element's width. This ensures that more of the webpage is
+ * showing instead of the navbar. Bug 480595. */
+ let xpadding = Math.max(margin, vrWidth - bv.browserToViewport(elWidth));
+
+ // XXX TODO these arguments are wrong, we still have to transform the coordinates
+ // from viewport to scrollbox before sending them to scrollTo
+ this.scrollTo(Math.floor(Math.max(bv.browserToViewport(elRect.x) - xpadding, 0)),
+ Math.floor(Math.max(bv.browserToViewport(elRect.y) - margin, 0)));
+
+ bv.commitBatchOperation();
+}
+
+function zoomFromElement(browser, aElement) {
+ let elRect = getBrowserClientRect(browser, aElement);
+
+ bv.beginBatchOperation();
+
+ // pan to the element
+ // don't bother with x since we're zooming all the way out
+ bv.zoomToPage();
+
+ // XXX have this center the element on the page
+ // XXX TODO these arguments are wrong, we still have to transform the coordinates
+ // from viewport to scrollbox before sending them to scrollTo
+ this.scrollTo(0, Math.floor(Math.max(0, bv.browserToViewport(elRect.y))));
+
+ bv.commitBatchOperation();
+}
+
+/**
+ * Retrieve the content element for a given point in client coordinates
+ * (relative to the top left corner of the chrome window).
+ */
+function elementFromPoint(browser, browserView, x, y) {
+ [x, y] = scrollboxToBrowserXY(browserView, x, y);
+ let cwu = getBrowserDOMWindowUtils(browser);
+ return cwu.elementFromPoint(x, y,
+ true, /* ignore root scroll frame*/
+ false); /* don't flush layout */
+}
+
+/* ensures that a given content element is visible */
+function ensureElementIsVisible(browser, aElement) {
+ let elRect = getBrowserClientRect(browser, aElement);
+
+ bv.browserToViewportRect(elRect);
+
+ let curRect = bv.visibleRect;
+ let newx = curRect.x;
+ let newy = curRect.y;
+
+ if (elRect.x < curRect.x || elRect.width > curRect.width) {
+ newx = elRect.x;
+ } else if (elRect.x + elRect.width > curRect.x + curRect.width) {
+ newx = elRect.x - curRect.width + elRect.width;
+ }
+
+ if (elRect.y < curRect.y || elRect.height > curRect.height) {
+ newy = elRect.y;
+ } else if (elRect.y + elRect.height > curRect.y + curRect.height) {
+ newy = elRect.y - curRect.height + elRect.height;
+ }
+
+ // XXX TODO these arguments are wrong, we still have to transform the coordinates
+ // from viewport to scrollbox before sending them to scrollTo
+ this.scrollTo(newx, newy);
+}
+
+// this is a mehful way of getting the visible rect in scrollbox coordinates
+// that we use in this here lab environment and hopefully nowhere in real fennec
+function getVisibleRect() {
+ let w = window.innerWidth;
+ let h = window.innerHeight;
+
+ let [x, y] = getScrollboxPosition();
+
+ return new wsRect(x, y, w, h);
+}
+
+]]>
+</script>
+
+<scrollbox id="scrollbox" style="-moz-box-orient: vertical; overflow: scroll;" flex="1">
+ <hbox id="top_urlbar" style="background-color: pink"><textbox flex="1"/></hbox>
+ <hbox style="position: relative">
+ <vbox id="left_sidebar" class="sidebar" style="background-color: red"><button label="left sidebar"/></vbox>
+ <box>
+ <html:div id="tile_container" style="position: relative; width: 800px; height: 480px; overflow: -moz-hidden-unscrollable;"/>
+ </box>
+ <vbox id="right_sidebar" class="sidebar" style="background-color: blue"><button label="right sidebar"/></vbox>
+ </hbox>
+</scrollbox>
+
+ <box>
+ <html:div style="position: relative; overflow: hidden; max-width: 0px; max-height: 0px; visibility: hidden;">
+ <html:div id="browsers" style="position: absolute;">
+ <!-- <browser id="googlenews" src="http://www.webhamster.com/" type="content" remote="true" style="width: 1024px; height: 614px"/> -->
+ <iframe id="googlenews" src="http://news.google.com/" type="content" remote="true" style="width: 1024px; height: 614px"/>
+ </html:div>
+ </html:div>
+ </box>
+
+</window>
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/main.xul b/toolkit/content/tests/fennec-tile-testapp/chrome/content/main.xul new file mode 100644 index 0000000000..f829b3f4a1 --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/main.xul @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<window id="main" title="My App" width="300" height="300" +xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <caption label="Hello World"/> +</window> diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/overlay.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/overlay.js new file mode 100644 index 0000000000..8dd09af00d --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/overlay.js @@ -0,0 +1,15 @@ +var tile = { + onLoad: function() { + // initialization code + this.initialized = true; + this.strings = document.getElementById("tile-strings"); + }, + onMenuItemCommand: function(e) { + var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + promptService.alert(window, this.strings.getString("helloMessageTitle"), + this.strings.getString("helloMessage")); + }, + +}; +window.addEventListener("load", function(e) { tile.onLoad(e); }, false); diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.dtd b/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.dtd new file mode 100644 index 0000000000..8cffbce359 --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.dtd @@ -0,0 +1 @@ +<!ENTITY tile.label "Your localized menuitem"> diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.properties b/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.properties new file mode 100644 index 0000000000..72062a4f0b --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.properties @@ -0,0 +1,3 @@ +helloMessage=Hello World! +helloMessageTitle=Hello +prefMessage=Int Pref Value: %d diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/skin/overlay.css b/toolkit/content/tests/fennec-tile-testapp/chrome/skin/overlay.css new file mode 100644 index 0000000000..98718057f4 --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/chrome/skin/overlay.css @@ -0,0 +1,5 @@ +/* This is just an example. You shouldn't do this. */ +#tile-hello +{ + color: red ! important; +} diff --git a/toolkit/content/tests/fennec-tile-testapp/defaults/preferences/prefs.js b/toolkit/content/tests/fennec-tile-testapp/defaults/preferences/prefs.js new file mode 100644 index 0000000000..823196f8bf --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/defaults/preferences/prefs.js @@ -0,0 +1,2 @@ +pref("toolkit.defaultChromeURI", "chrome://tile/content/foo.xul"); +pref("browser.dom.window.dump.enabled", true); diff --git a/toolkit/content/tests/fennec-tile-testapp/install.rdf b/toolkit/content/tests/fennec-tile-testapp/install.rdf new file mode 100644 index 0000000000..e80fb845cc --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/install.rdf @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>tile@roy</em:id> + <em:name>tile</em:name> + <em:version>1.0</em:version> + <em:creator>Roy</em:creator> + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!-- firefox --> + <em:minVersion>1.5</em:minVersion> + <em:maxVersion>3.5.*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/toolkit/content/tests/fennec-tile-testapp/logread.py b/toolkit/content/tests/fennec-tile-testapp/logread.py new file mode 100644 index 0000000000..afa1fa524b --- /dev/null +++ b/toolkit/content/tests/fennec-tile-testapp/logread.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +import re, sys + +interesting_re = re.compile("(js_Execute|CallHook) ([^ ]+) ([^ ]+ )?([^ ]+ms)") +class Entry: + def __init__(self, kind, depth, file, linenum, func, timetaken): + self.kind = kind + self.depth = depth + self.file = file + self.linenum = linenum + self.func = func + self.timetaken = timetaken + self.calls = 0 + self.duration = 0 + + def __str__(self): + return " ".join(map(str,[self.kind, self.depth, self.file, self.linenum, self.func, self.timetaken])) + + def id(self): + if self.kind == "js_Execute": + return self.file + else: + if self.file and self.linenum: + strout = "%s:%d" % (self.file, self.linenum) + if self.func: + strout = "%s %s" % (self.func, strout) + return strout + elif self.func: + return self.func + else: + print("No clue what my id is:"+self) + + def call(self, timetaken): + self.calls += 1 + self.duration += timetaken + +def parse_line(line): + m = interesting_re.search(line) + if not m: + return None + + ms_index = line.find("ms") + depth = m.start() - ms_index - 3 + kind = m.group(1) + func = None + file = None + linenum = None + if kind == "CallHook": + func = m.group(2) + file = m.group(3) + colpos = file.rfind(":") + (file,linenum) = file[:colpos], file[colpos+1:-1] + if linenum == "0": + linenum = None + else: + linenum = int(linenum) + offset = 1 + else: + file = m.group(3) + + timetaken = None + try: + timetaken = float(m.group(4)[:-2]) + except: + return None + return Entry(kind, depth, file, linenum, func, timetaken) + +def compare(x,y): + diff = x[1].calls - y[1].calls + if diff == 0: + return int(x[1].duration - y[1].duration) + elif diff > 0: + return 1 + elif diff < 0: + return -1 + +def frequency(ls): + dict = {} + for item in ls: + id = item.id() + stat = None + if not id in dict: + stat = dict[id] = item + else: + stat = dict[id] + stat.call(item.timetaken) + + ls = dict.items() + ls.sort(compare) + ls = filter(lambda (_,item): item.duration > 20, ls) +# ls = filter(lambda (_,item): item.file and item.file.find("browser.js") != -1 and item.linenum <= 1223 and item.linenum >1067, ls) + for key, item in ls: + print(item.calls,key, str(item.duration)+"ms") + +def go(): + file = sys.argv[1] + + ls = filter(lambda x: x != None, map(parse_line, open(file).readlines())) + + frequency(ls) + print ls[0] + +go() + diff --git a/toolkit/content/tests/mochitest/mochitest.ini b/toolkit/content/tests/mochitest/mochitest.ini new file mode 100644 index 0000000000..1ef68c022d --- /dev/null +++ b/toolkit/content/tests/mochitest/mochitest.ini @@ -0,0 +1,5 @@ +[test_autocomplete_change_after_focus.html] +skip-if = toolkit == "android" +[test_mousecapture.xhtml] +skip-if = toolkit == "android" + diff --git a/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html b/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html new file mode 100644 index 0000000000..540eeacf4b --- /dev/null +++ b/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=998893 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 998893 - Ensure that input.value changes affect autocomplete</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + /** Test for Bug 998893 **/ + add_task(function* waitForFocus() { + yield new Promise(resolve => SimpleTest.waitForFocus(resolve)); + }); + + add_task(function* setup() { + yield new Promise(resolve => { + let chromeScript = SpecialPowers.loadChromeScript(function() { + const {FormHistory} = Components.utils.import("resource://gre/modules/FormHistory.jsm", null); + FormHistory.update([ + { op : "bump", fieldname: "field1", value: "Default text option" }, + { op : "bump", fieldname: "field1", value: "New value option" }, + ], { + handleCompletion: function() { + sendAsyncMessage("Test:Resume"); + }, + }); + }); + + chromeScript.addMessageListener("Test:Resume", function resumeListener() { + chromeScript.removeMessageListener("Test:Resume", resumeListener); + chromeScript.destroy(); + resolve(); + }); + }); + }); + + add_task(function* runTest() { + let promisePopupShown = new Promise(resolve => { + let chromeScript = SpecialPowers.loadChromeScript(function() { + Components.utils.import("resource://gre/modules/Services.jsm"); + let window = Services.wm.getMostRecentWindow("navigator:browser"); + let popup = window.document.getElementById("PopupAutoComplete"); + popup.addEventListener("popupshown", function popupShown() { + popup.removeEventListener("popupshown", popupShown); + sendAsyncMessage("Test:Resume"); + }); + }); + + chromeScript.addMessageListener("Test:Resume", function resumeListener() { + chromeScript.removeMessageListener("Test:Resume", resumeListener); + chromeScript.destroy(); + resolve(); + }); + }); + + let field = document.getElementById("field1"); + + let promiseFieldFocus = new Promise(resolve => { + field.addEventListener("focus", function onFocus() { + info("field focused"); + field.value = "New value"; + sendKey("DOWN"); + resolve(); + }); + }); + + let handleEnterPromise = new Promise(resolve => { + function handleEnter(evt) { + if (evt.keyCode != KeyEvent.DOM_VK_RETURN) { + return; + } + info("RETURN received for phase: " + evt.eventPhase); + is(evt.target.value, "New value option", "Check that the correct autocomplete entry was used"); + resolve(); + } + + field.addEventListener("keypress", handleEnter, true); + }); + + field.focus(); + + yield promiseFieldFocus; + + yield promisePopupShown; + + sendKey("DOWN"); + sendKey("RETURN"); + sendKey("RETURN"); + + yield handleEnterPromise; + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998893">Mozilla Bug 998893</a> +<p id="display"><input id="field1" value="Default text"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/mochitest/test_mousecapture.xhtml b/toolkit/content/tests/mochitest/test_mousecapture.xhtml new file mode 100644 index 0000000000..d4ae945bb3 --- /dev/null +++ b/toolkit/content/tests/mochitest/test_mousecapture.xhtml @@ -0,0 +1,340 @@ +<?xml version="1.0"?> +<!DOCTYPE HTML> +<html xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Mouse Capture Tests</title> + <link rel="stylesheet" href="chrome://global/skin/" type="text/css"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body id="body" xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"/><div id="content" style="display: none"/><pre id="test"/> + +<script><![CDATA[ + +SimpleTest.expectAssertions(6, 12); + +SimpleTest.waitForExplicitFinish(); + +var captureRetargetMode = false; +var cachedMouseDown = null; +var previousWidth = 0, originalWidth = 0; +var loadInWindow = false; + +function splitterCallback(adjustment) +{ + var newWidth = Number($("leftbox").width); // getBoundingClientRect().width; + var expectedWidth = previousWidth + adjustment; + if (expectedWidth > $("splitterbox").getBoundingClientRect().width) + expectedWidth = $("splitterbox").getBoundingClientRect().width - $("splitter").getBoundingClientRect().width; + is(newWidth, expectedWidth, "splitter left box size (" + adjustment + ")"); + previousWidth = newWidth; +} + +function selectionCallback(adjustment) +{ + if (adjustment == 4000) { + is(frames[0].getSelection().toString(), "This is some text", "selection after drag (" + adjustment + ")"); + ok(frames[0].scrollY > 40, "selection caused scroll down (" + adjustment + ")"); + } + else { + if (adjustment == 0) { + is(frames[0].getSelection().toString(), ".", "selection after drag (" + adjustment + ")"); + } + is(frames[0].scrollY, 0, "selection scrollY (" + adjustment + ")"); + } +} + +function framesetCallback(adjustment) +{ + var newWidth = frames[1].frames[0].document.documentElement.clientWidth; + var expectedWidth = originalWidth + adjustment; + if (adjustment == 0) + expectedWidth = originalWidth - 12; + else if (expectedWidth >= 4000) + expectedWidth = originalWidth * 2 - 2; + + ok(Math.abs(newWidth - expectedWidth) <= 1, "frameset after drag (" + adjustment + "), new width " + newWidth + ", expected " + expectedWidth); +} + +var otherWindow = null; + +function selectionScrollCheck() +{ + var element = otherWindow.document.documentElement; + + var count = 0; + function selectionScrollDone() { + // wait for 6 scroll events to occur + if (count++ < 6) + return; + + otherWindow.removeEventListener("scroll", selectionScrollDone, false); + + var selectedText = otherWindow.getSelection().toString().replace(/\r/g, ""); + is(selectedText, "One\n\nTwo", "text is selected"); + + // should have scrolled 20 pixels from the mousemove above and at least 6 + // extra 20-pixel increments from the selection scroll timer. "At least 6" + // because we waited for 6 scroll events but multiple scrolls could get + // coalesced into a single scroll event, and paints could be delayed when + // the window loads when the compositor is busy. As a result, we have no + // real guarantees about the upper bound here, and as the upper bound is + // not important for what we're testing here, we don't check it. + var scrollY = otherWindow.scrollY; + info(`Scrolled ${scrollY} pixels`); + ok(scrollY >= 140, "selection scroll position after timer is at least 140"); + ok((scrollY % 20) == 0, "selection scroll position after timer is multiple of 20"); + + synthesizeMouse(element, 4, otherWindow.innerHeight + 25, { type: "mouseup" }, otherWindow); + disableNonTestMouseEvents(false); + otherWindow.close(); + + if (loadInWindow) { + SimpleTest.finish(); + } + else { + // now try again, but open the page in a new window + loadInWindow = true; + synthesizeMouse(document.getElementById("custom"), 2, 2, { type: "mousedown" }); + + // check to ensure that selection dragging scrolls the right scrollable area + otherWindow = window.open("data:text/html,<html><p>One</p><p style='margin-top: 200px;'>Two</p><p style='margin-top: 4000px'>This is some text</p></html>", "_blank", "width=200,height=200,scrollbars=yes"); + SimpleTest.waitForFocus(selectionScrollCheck, otherWindow); + } + } + + SimpleTest.executeSoon(function () { + disableNonTestMouseEvents(true); + synthesizeMouse(element, 2, 2, { type: "mousedown" }, otherWindow); + synthesizeMouse(element, 100, otherWindow.innerHeight + 20, { type: "mousemove" }, otherWindow); + otherWindow.addEventListener("scroll", selectionScrollDone, false); + }); +} + +function runTests() +{ + previousWidth = $("leftbox").getBoundingClientRect().width; + runCaptureTest($("splitter"), splitterCallback); + + var custom = document.getElementById("custom"); + runCaptureTest(custom); + + synthesizeMouseExpectEvent($("rightbox"), 2, 2, { type: "mousemove" }, + $("rightbox"), "mousemove", "setCapture and releaseCapture"); + + custom.setCapture(); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "setCapture fails on non mousedown"); + + var custom2 = document.getElementById("custom2"); + synthesizeMouse(custom2, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "document.releaseCapture releases capture"); + + var custom3 = document.getElementById("custom3"); + synthesizeMouse(custom3, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "element.releaseCapture releases capture"); + + var custom4 = document.getElementById("custom4"); + synthesizeMouse(custom4, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + custom4, "mousemove", "element.releaseCapture during mousemove before releaseCapture"); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "element.releaseCapture during mousemove after releaseCapture"); + + var custom5 = document.getElementById("custom5"); + runCaptureTest(custom5); + captureRetargetMode = true; + runCaptureTest(custom5); + captureRetargetMode = false; + + var custom6 = document.getElementById("custom6"); + synthesizeMouse(custom6, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "setCapture only works on elements in documents"); + synthesizeMouse(custom6, 2, 2, { type: "mouseup" }); + + // test that mousedown on an image with setCapture followed by a big enough + // mouse move does not start a drag (bug 517737) + var image = document.getElementById("image"); + image.scrollIntoView(); + synthesizeMouse(image, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + image, "mousemove", "setCapture works on images"); + synthesizeMouse(image, 2, 2, { type: "mouseup" }); + + window.scroll(0, 0); + + // save scroll + var scrollX = parent ? parent.scrollX : 0; + var scrollY = parent ? parent.scrollY : 0; + + var b = frames[0].document.getElementById("b"); +// runCaptureTest(b, selectionCallback); + + // restore scroll + if (parent) parent.scroll(scrollX, scrollY); + +// frames[0].getSelection().collapseToStart(); + + var body = frames[0].document.body; + var fixed = frames[0].document.getElementById("fixed"); + function captureOnBody() { body.setCapture() } + body.addEventListener("mousedown", captureOnBody, true); + synthesizeMouse(body, 8, 8, { type: "mousedown" }, frames[0]); + body.removeEventListener("mousedown", captureOnBody, true); + synthesizeMouseExpectEvent(fixed, 2, 2, { type: "mousemove" }, + fixed, "mousemove", "setCapture on body retargets to root node", frames[0]); + synthesizeMouse(body, 8, 8, { type: "mouseup" }, frames[0]); + + previousWidth = frames[1].frames[0].document.documentElement.clientWidth; + originalWidth = previousWidth; + runCaptureTest(frames[1].document.documentElement.lastChild, framesetCallback); + + // ensure that clicking on an element where the frame disappears doesn't crash + synthesizeMouse(frames[2].document.getElementById("input"), 8, 8, { type: "mousedown" }, frames[2]); + synthesizeMouse(frames[2].document.getElementById("input"), 8, 8, { type: "mouseup" }, frames[2]); + + var select = document.getElementById("select"); + select.scrollIntoView(); + + synthesizeMouse(document.getElementById("option3"), 2, 2, { type: "mousedown" }); + synthesizeMouse(document.getElementById("option3"), 2, 1000, { type: "mousemove" }); + is(select.selectedIndex, 2, "scroll select"); + synthesizeMouse(document.getElementById("select"), 2, 2, { type: "mouseup" }); + window.scroll(0, 0); + + synthesizeMouse(custom, 2, 2, { type: "mousedown" }); + + // check to ensure that selection dragging scrolls the right scrollable area. + // This should open the page in a new tab. + + var topPos = window.innerHeight; + otherWindow = window.open("data:text/html,<html><p>One</p><p style='margin-top: " + topPos + "'>Two</p><p style='margin-top: 4000px'>This is some text</p></html>", "_blank"); + SimpleTest.waitForFocus(selectionScrollCheck, otherWindow); +} + +function runCaptureTest(element, callback) +{ + var expectedTarget = null; + + var win = element.ownerDocument.defaultView; + + function mouseMoved(event) { + is(event.originalTarget, expectedTarget, + expectedTarget.id + " target for point " + event.clientX + "," + event.clientY); + } + win.addEventListener("mousemove", mouseMoved, false); + + expectedTarget = element; + + var basepoint = element.localName == "frameset" ? 50 : 2; + synthesizeMouse(element, basepoint, basepoint, { type: "mousedown" }, win); + + // in setCapture(true) mode, all events should fire on custom5. In + // setCapture(false) mode, events can fire at a descendant + if (expectedTarget == $("custom5") && !captureRetargetMode) + expectedTarget = $("custom5spacer"); + + // releaseCapture should do nothing for an element which isn't capturing + $("splitterbox").releaseCapture(); + + synthesizeMouse(element, basepoint + 2, basepoint + 2, { type: "mousemove" }, win); + if (callback) + callback(2); + + if (expectedTarget == $("custom5spacer") && !captureRetargetMode) + expectedTarget = $("custom5inner"); + + if (element.id == "b") { + var tooltip = document.getElementById("tooltip"); + tooltip.openPopup(); + tooltip.hidePopup(); + } + + synthesizeMouse(element, basepoint + 25, basepoint + 25, { type: "mousemove" }, win); + if (callback) + callback(25); + + expectedTarget = element.localName == "b" ? win.document.documentElement : element; + synthesizeMouse(element, basepoint + 4000, basepoint + 4000, { type: "mousemove" }, win); + if (callback) + callback(4000); + synthesizeMouse(element, basepoint - 12, basepoint - 12, { type: "mousemove" }, win); + if (callback) + callback(-12); + + expectedTarget = element.localName == "frameset" ? element : win.document.documentElement; + synthesizeMouse(element, basepoint + 30, basepoint + 30, { type: "mouseup" }, win); + synthesizeMouse(win.document.documentElement, 2, 2, { type: "mousemove" }, win); + if (callback) + callback(0); + + win.removeEventListener("mousemove", mouseMoved, false); +} + +SimpleTest.waitForFocus(runTests); + +]]> +</script> + +<xul:vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" align="start"> + <tooltip id="tooltip"> + <label value="Test"/> + </tooltip> + + <hbox id="splitterbox" style="margin-top: 5px;" onmousedown="this.setCapture()"> + <hbox id="leftbox" width="100" flex="1"/> + <splitter id="splitter" height="5"/> + <hbox id="rightbox" width="100" flex="1"/> + </hbox> + + <vbox id="custom" width="10" height="10" onmousedown="this.setCapture(); cachedMouseDown = event;"/> + <vbox id="custom2" width="10" height="10" onmousedown="this.setCapture(); document.releaseCapture();"/> + <vbox id="custom3" width="10" height="10" onmousedown="this.setCapture(); this.releaseCapture();"/> + <vbox id="custom4" width="10" height="10" onmousedown="this.setCapture();" + onmousemove="this.releaseCapture();"/> + <hbox id="custom5" width="40" height="40" + onmousedown="this.setCapture(captureRetargetMode);"> + <spacer id="custom5spacer" width="5"/> + <hbox id="custom5inner" width="35" height="35"/> + </hbox> + <vbox id="custom6" width="10" height="10" + onmousedown="document.createElement('hbox').setCapture();"/> +</xul:vbox> + + <iframe width="100" height="100" + src="data:text/html,%3Cbody style%3D'font-size%3A 40pt%3B'%3E.%3Cb id%3D'b'%3EThis%3C/b%3E is some text%3Cdiv id='fixed' style='position: fixed; left: 55px; top: 5px; width: 10px; height: 10px'%3E.%3C/div%3E%3C/body%3E"/> + + <iframe width="100" height="100" + src="data:text/html,%3Cframeset cols='50%, 50%'%3E%3Cframe src='about:blank'%3E%3Cframe src='about:blank'%3E%3C/frameset%3E"/> + + <iframe width="100" height="100" + src="data:text/html,%3Cinput id='input' onfocus='this.style.display = "none"' style='float: left;'>"/> + + <select id="select" xmlns="http://www.w3.org/1999/xhtml" size="4"> + <option id="option1">One</option> + <option id="option2">Two</option> + <option id="option3">Three</option> + <option id="option4">Four</option> + <option id="option5">Five</option> + <option id="option6">Six</option> + <option id="option7">Seven</option> + <option id="option8">Eight</option> + <option id="option9">Nine</option> + <option id="option10">Ten</option> + </select> + + <img id="image" xmlns="http://www.w3.org/1999/xhtml" + onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + ondragstart="ok(false, 'should not get a drag when a setCapture is active');" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC"/> + +</body> + +</html> + diff --git a/toolkit/content/tests/moz.build b/toolkit/content/tests/moz.build new file mode 100644 index 0000000000..e540fa11f7 --- /dev/null +++ b/toolkit/content/tests/moz.build @@ -0,0 +1,19 @@ +# -*- 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/. + +XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini'] + +BROWSER_CHROME_MANIFESTS += ['browser/browser.ini'] + +MOCHITEST_CHROME_MANIFESTS += [ + 'chrome/chrome.ini', + 'widgets/chrome.ini', +] + +MOCHITEST_MANIFESTS += [ + 'mochitest/mochitest.ini', + 'widgets/mochitest.ini', +] diff --git a/toolkit/content/tests/reftests/bug-442419-progressmeter-max-ref.xul b/toolkit/content/tests/reftests/bug-442419-progressmeter-max-ref.xul new file mode 100644 index 0000000000..a034b6e671 --- /dev/null +++ b/toolkit/content/tests/reftests/bug-442419-progressmeter-max-ref.xul @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <progressmeter value="50"/> <!-- default is max = 100 --> +</window> + diff --git a/toolkit/content/tests/reftests/bug-442419-progressmeter-max.xul b/toolkit/content/tests/reftests/bug-442419-progressmeter-max.xul new file mode 100644 index 0000000000..6225964064 --- /dev/null +++ b/toolkit/content/tests/reftests/bug-442419-progressmeter-max.xul @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <progressmeter max="198" value="99"/> <!-- 50% --> +</window> + diff --git a/toolkit/content/tests/reftests/reftest-stylo.list b/toolkit/content/tests/reftests/reftest-stylo.list new file mode 100644 index 0000000000..77caaabfcb --- /dev/null +++ b/toolkit/content/tests/reftests/reftest-stylo.list @@ -0,0 +1,6 @@ +# DO NOT EDIT! This is a auto-generated temporary list for Stylo testing +skip-if(B2G&&browserIsRemote) random-if(cocoaWidget) == bug-442419-progressmeter-max.xul bug-442419-progressmeter-max.xul +# fails most of the time on Mac because progress meter animates +# Bug 974780 +skip-if(B2G&&browserIsRemote) == textbox-multiline-default-value.xul textbox-multiline-default-value.xul +# Bug 974780 diff --git a/toolkit/content/tests/reftests/reftest.list b/toolkit/content/tests/reftests/reftest.list new file mode 100644 index 0000000000..a37a9722a5 --- /dev/null +++ b/toolkit/content/tests/reftests/reftest.list @@ -0,0 +1,2 @@ +random-if(cocoaWidget) == bug-442419-progressmeter-max.xul bug-442419-progressmeter-max-ref.xul # fails most of the time on Mac because progress meter animates +!= textbox-multiline-default-value.xul textbox-multiline-empty.xul diff --git a/toolkit/content/tests/reftests/textbox-multiline-default-value.xul b/toolkit/content/tests/reftests/textbox-multiline-default-value.xul new file mode 100644 index 0000000000..31a5bc556f --- /dev/null +++ b/toolkit/content/tests/reftests/textbox-multiline-default-value.xul @@ -0,0 +1,5 @@ +<?xml version='1.0'?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="test textbox multiline"> + <textbox multiline='true' value='foobar'></textbox> +</window> diff --git a/toolkit/content/tests/reftests/textbox-multiline-empty.xul b/toolkit/content/tests/reftests/textbox-multiline-empty.xul new file mode 100644 index 0000000000..c48f2c988f --- /dev/null +++ b/toolkit/content/tests/reftests/textbox-multiline-empty.xul @@ -0,0 +1,5 @@ +<?xml version='1.0'?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="test textbox multiline"> + <textbox multiline='true'></textbox> +</window> diff --git a/toolkit/content/tests/unit/.eslintrc.js b/toolkit/content/tests/unit/.eslintrc.js new file mode 100644 index 0000000000..fee088c179 --- /dev/null +++ b/toolkit/content/tests/unit/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/content/tests/unit/test_contentAreaUtils.js b/toolkit/content/tests/unit/test_contentAreaUtils.js new file mode 100644 index 0000000000..970e779ce8 --- /dev/null +++ b/toolkit/content/tests/unit/test_contentAreaUtils.js @@ -0,0 +1,80 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; + +function loadUtilsScript() { + var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + loader.loadSubScript("chrome://global/content/contentAreaUtils.js"); +} + +function test_urlSecurityCheck() { + var nullPrincipal = Cc["@mozilla.org/nullprincipal;1"]. + createInstance(Ci.nsIPrincipal); + + const HTTP_URI = "http://www.mozilla.org/"; + const CHROME_URI = "chrome://browser/content/browser.xul"; + const DISALLOW_INHERIT_PRINCIPAL = + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; + + try { + urlSecurityCheck(makeURI(HTTP_URI), nullPrincipal, + DISALLOW_INHERIT_PRINCIPAL); + } + catch (ex) { + do_throw("urlSecurityCheck should not throw when linking to a http uri with a null principal"); + } + + // urlSecurityCheck also supports passing the url as a string + try { + urlSecurityCheck(HTTP_URI, nullPrincipal, + DISALLOW_INHERIT_PRINCIPAL); + } + catch (ex) { + do_throw("urlSecurityCheck failed to handle the http URI as a string (uri spec)"); + } + + let shouldThrow = true; + try { + urlSecurityCheck(CHROME_URI, nullPrincipal, + DISALLOW_INHERIT_PRINCIPAL); + } + catch (ex) { + shouldThrow = false; + } + if (shouldThrow) + do_throw("urlSecurityCheck should throw when linking to a chrome uri with a null principal"); +} + +function test_stringBundle() { + // This test verifies that the elements that can be used as file picker title + // keys in the save* functions are actually present in the string bundle. + // These keys are part of the contentAreaUtils.js public API. + var validFilePickerTitleKeys = [ + "SaveImageTitle", + "SaveVideoTitle", + "SaveAudioTitle", + "SaveLinkTitle", + ]; + + for (let filePickerTitleKey of validFilePickerTitleKeys) { + // Just check that the string exists + try { + ContentAreaUtils.stringBundle.GetStringFromName(filePickerTitleKey); + } catch (e) { + do_throw("Error accessing file picker title key: " + filePickerTitleKey); + } + } +} + +function run_test() +{ + loadUtilsScript(); + test_urlSecurityCheck(); + test_stringBundle(); +} diff --git a/toolkit/content/tests/unit/xpcshell.ini b/toolkit/content/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..33a0383bd4 --- /dev/null +++ b/toolkit/content/tests/unit/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +head = +tail = + +[test_contentAreaUtils.js] diff --git a/toolkit/content/tests/widgets/.eslintrc.js b/toolkit/content/tests/widgets/.eslintrc.js new file mode 100644 index 0000000000..e149193751 --- /dev/null +++ b/toolkit/content/tests/widgets/.eslintrc.js @@ -0,0 +1,8 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/mochitest.eslintrc.js", + "../../../../testing/mochitest/chrome.eslintrc.js" + ] +}; diff --git a/toolkit/content/tests/widgets/audio.ogg b/toolkit/content/tests/widgets/audio.ogg Binary files differnew file mode 100644 index 0000000000..a553c23e73 --- /dev/null +++ b/toolkit/content/tests/widgets/audio.ogg diff --git a/toolkit/content/tests/widgets/audio.wav b/toolkit/content/tests/widgets/audio.wav Binary files differnew file mode 100644 index 0000000000..c6fd5cb869 --- /dev/null +++ b/toolkit/content/tests/widgets/audio.wav diff --git a/toolkit/content/tests/widgets/chrome.ini b/toolkit/content/tests/widgets/chrome.ini new file mode 100644 index 0000000000..841b86c0fc --- /dev/null +++ b/toolkit/content/tests/widgets/chrome.ini @@ -0,0 +1,20 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + tree_shared.js + popup_shared.js + window_menubar.xul + seek_with_sound.ogg + +[test_contextmenu_nested.xul] +skip-if = os == 'linux' # Bug 1116215 +[test_contextmenu_menugroup.xul] +skip-if = os == 'linux' # Bug 1115088 +[test_editor_currentURI.xul] +[test_menubar.xul] +skip-if = os == 'mac' +[test_popupanchor.xul] +skip-if = os == 'android' +[test_popupreflows.xul] +[test_tree_column_reorder.xul] +[test_videocontrols_onclickplay.html] diff --git a/toolkit/content/tests/widgets/head.js b/toolkit/content/tests/widgets/head.js new file mode 100644 index 0000000000..c2ae0c7ae1 --- /dev/null +++ b/toolkit/content/tests/widgets/head.js @@ -0,0 +1,23 @@ +"use strict"; + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} diff --git a/toolkit/content/tests/widgets/mochitest.ini b/toolkit/content/tests/widgets/mochitest.ini new file mode 100644 index 0000000000..abc77c80ba --- /dev/null +++ b/toolkit/content/tests/widgets/mochitest.ini @@ -0,0 +1,40 @@ +[DEFAULT] +support-files = + audio.wav + audio.ogg + seek_with_sound.ogg + head.js + tree_shared.js + videocontrols_direction-1-ref.html + videocontrols_direction-1a.html + videocontrols_direction-1b.html + videocontrols_direction-1c.html + videocontrols_direction-1d.html + videocontrols_direction-1e.html + videocontrols_direction-2-ref.html + videocontrols_direction-2a.html + videocontrols_direction-2b.html + videocontrols_direction-2c.html + videocontrols_direction-2d.html + videocontrols_direction-2e.html + videocontrols_direction_test.js + videomask.css + +[test_audiocontrols_dimensions.html] +skip-if = toolkit == 'android' +[test_mousecapture_area.html] +[test_videocontrols.html] +tags = fullscreen +skip-if = toolkit == 'android' #TIMED_OUT +[test_videocontrols_vtt.html] +skip-if = toolkit == 'android' +[test_videocontrols_iframe_fullscreen.html] +[test_videocontrols_audio.html] +[test_videocontrols_audio_direction.html] +[test_videocontrols_jsdisabled.html] +skip-if = toolkit == 'android' # bug 1272646 +[test_videocontrols_standalone.html] +skip-if = true # bug 1075573, bug 1262130 +[test_videocontrols_video_direction.html] +skip-if = os == 'win' +[test_bug898940.html] diff --git a/toolkit/content/tests/widgets/popup_shared.js b/toolkit/content/tests/widgets/popup_shared.js new file mode 100644 index 0000000000..49735c5ad8 --- /dev/null +++ b/toolkit/content/tests/widgets/popup_shared.js @@ -0,0 +1,424 @@ +/* + * This script is used for menu and popup tests. Call startPopupTests to start + * the tests, passing an array of tests as an argument. Each test is an object + * with the following properties: + * testname - name of the test + * test - function to call to perform the test + * events - a list of events that are expected to be fired in sequence + * as a result of calling the 'test' function. This list should be + * an array of strings of the form "eventtype targetid" where + * 'eventtype' is the event type and 'targetid' is the id of + * target of the event. This function will be passed two + * arguments, the testname and the step argument. + * Alternatively, events may be a function which returns the array + * of events. This can be used when the events vary per platform. + * result - function to call after all the events have fired to check + * for additional results. May be null. This function will be + * passed two arguments, the testname and the step argument. + * steps - optional array of values. The test will be repeated for + * each step, passing each successive value within the array to + * the test and result functions + * autohide - if set, should be set to the id of a popup to hide after + * the test is complete. This is a convenience for some tests. + * condition - an optional function which, if it returns false, causes the + * test to be skipped. + * end - used for debugging. Set to true to stop the tests after running + * this one. + */ + +const menuactiveAttribute = "_moz-menuactive"; + +var gPopupTests = null; +var gTestIndex = -1; +var gTestStepIndex = 0; +var gTestEventIndex = 0; +var gAutoHide = false; +var gExpectedEventDetails = null; +var gExpectedTriggerNode = null; +var gWindowUtils; +var gPopupWidth = -1, gPopupHeight = -1; + +function startPopupTests(tests) +{ + document.addEventListener("popupshowing", eventOccurred, false); + document.addEventListener("popupshown", eventOccurred, false); + document.addEventListener("popuphiding", eventOccurred, false); + document.addEventListener("popuphidden", eventOccurred, false); + document.addEventListener("command", eventOccurred, false); + document.addEventListener("DOMMenuItemActive", eventOccurred, false); + document.addEventListener("DOMMenuItemInactive", eventOccurred, false); + document.addEventListener("DOMMenuInactive", eventOccurred, false); + document.addEventListener("DOMMenuBarActive", eventOccurred, false); + document.addEventListener("DOMMenuBarInactive", eventOccurred, false); + + gPopupTests = tests; + gWindowUtils = SpecialPowers.getDOMWindowUtils(window); + + goNext(); +} + +function finish() +{ + if (window.opener) { + window.close(); + window.opener.SimpleTest.finish(); + return; + } + SimpleTest.finish(); + return; +} + +function ok(condition, message) { + if (window.opener) + window.opener.SimpleTest.ok(condition, message); + else + SimpleTest.ok(condition, message); +} + +function is(left, right, message) { + if (window.opener) + window.opener.SimpleTest.is(left, right, message); + else + SimpleTest.is(left, right, message); +} + +function disableNonTestMouse(aDisable) { + gWindowUtils.disableNonTestMouseEvents(aDisable); +} + +function eventOccurred(event) +{ + if (gPopupTests.length <= gTestIndex) { + ok(false, "Extra " + event.type + " event fired"); + return; + } + + var test = gPopupTests[gTestIndex]; + if ("autohide" in test && gAutoHide) { + if (event.type == "DOMMenuInactive") { + gAutoHide = false; + setTimeout(goNextStep, 0); + } + return; + } + + var events = test.events; + if (typeof events == "function") + events = events(); + if (events) { + if (events.length <= gTestEventIndex) { + ok(false, "Extra " + event.type + " event fired for " + event.target.id + + " " +gPopupTests[gTestIndex].testname); + return; + } + + var eventitem = events[gTestEventIndex].split(" "); + var matches; + if (eventitem[1] == "#tooltip") { + is(event.originalTarget.localName, "tooltip", + test.testname + " event.originalTarget.localName is 'tooltip'"); + is(event.originalTarget.getAttribute("default"), "true", + test.testname + " event.originalTarget default attribute is 'true'"); + matches = event.originalTarget.localName == "tooltip" && + event.originalTarget.getAttribute("default") == "true"; + } else { + is(event.type, eventitem[0], + test.testname + " event type " + event.type + " fired"); + is(event.target.id, eventitem[1], + test.testname + " event target ID " + event.target.id); + matches = eventitem[0] == event.type && eventitem[1] == event.target.id; + } + + var modifiersMask = eventitem[2]; + if (modifiersMask) { + var m = ""; + m += event.altKey ? '1' : '0'; + m += event.ctrlKey ? '1' : '0'; + m += event.shiftKey ? '1' : '0'; + m += event.metaKey ? '1' : '0'; + is(m, modifiersMask, test.testname + " modifiers mask matches"); + } + + var expectedState; + switch (event.type) { + case "popupshowing": expectedState = "showing"; break; + case "popupshown": expectedState = "open"; break; + case "popuphiding": expectedState = "hiding"; break; + case "popuphidden": expectedState = "closed"; break; + } + + if (gExpectedTriggerNode && event.type == "popupshowing") { + if (gExpectedTriggerNode == "notset") // check against null instead + gExpectedTriggerNode = null; + + is(event.originalTarget.triggerNode, gExpectedTriggerNode, test.testname + " popupshowing triggerNode"); + var isTooltip = (event.target.localName == "tooltip"); + is(document.popupNode, isTooltip ? null : gExpectedTriggerNode, + test.testname + " popupshowing document.popupNode"); + is(document.tooltipNode, isTooltip ? gExpectedTriggerNode : null, + test.testname + " popupshowing document.tooltipNode"); + } + + if (expectedState) + is(event.originalTarget.state, expectedState, + test.testname + " " + event.type + " state"); + + if (matches) { + gTestEventIndex++ + if (events.length <= gTestEventIndex) + setTimeout(checkResult, 0); + } + } +} + +function checkResult() +{ + var step = null; + var test = gPopupTests[gTestIndex]; + if ("steps" in test) + step = test.steps[gTestStepIndex]; + + if ("result" in test) + test.result(test.testname, step); + + if ("autohide" in test) { + gAutoHide = true; + document.getElementById(test.autohide).hidePopup(); + return; + } + + goNextStep(); +} + +function goNextStep() +{ + gTestEventIndex = 0; + + var step = null; + var test = gPopupTests[gTestIndex]; + if ("steps" in test) { + gTestStepIndex++; + step = test.steps[gTestStepIndex]; + if (gTestStepIndex < test.steps.length) { + test.test(test.testname, step); + return; + } + } + + goNext(); +} + +function goNext() +{ + // We want to continue after the next animation frame so that + // we're in a stable state and don't get spurious mouse events at unexpected targets. + window.requestAnimationFrame( + function() { + setTimeout(goNextStepSync, 0); + } + ); +} + +function goNextStepSync() +{ + if (gTestIndex >= 0 && "end" in gPopupTests[gTestIndex] && gPopupTests[gTestIndex].end) { + finish(); + return; + } + + gTestIndex++; + gTestStepIndex = 0; + if (gTestIndex < gPopupTests.length) { + var test = gPopupTests[gTestIndex]; + // Set the location hash so it's easy to see which test is running + document.location.hash = test.testname; + + // skip the test if the condition returns false + if ("condition" in test && !test.condition()) { + goNext(); + return; + } + + // start with the first step if there are any + var step = null; + if ("steps" in test) + step = test.steps[gTestStepIndex]; + + test.test(test.testname, step); + + // no events to check for so just check the result + if (!("events" in test)) + checkResult(); + } + else { + finish(); + } +} + +function openMenu(menu) +{ + if ("open" in menu) { + menu.open = true; + } + else { + var bo = menu.boxObject; + if (bo instanceof MenuBoxObject) + bo.openMenu(true); + else + synthesizeMouse(menu, 4, 4, { }); + } +} + +function closeMenu(menu, popup) +{ + if ("open" in menu) { + menu.open = false; + } + else { + var bo = menu.boxObject; + if (bo instanceof MenuBoxObject) + bo.openMenu(false); + else + popup.hidePopup(); + } +} + +function checkActive(popup, id, testname) +{ + var activeok = true; + var children = popup.childNodes; + for (var c = 0; c < children.length; c++) { + var child = children[c]; + if ((id == child.id && child.getAttribute(menuactiveAttribute) != "true") || + (id != child.id && child.hasAttribute(menuactiveAttribute) != "")) { + activeok = false; + break; + } + } + ok(activeok, testname + " item " + (id ? id : "none") + " active"); +} + +function checkOpen(menuid, testname) +{ + var menu = document.getElementById(menuid); + if ("open" in menu) + ok(menu.open, testname + " " + menuid + " menu is open"); + else if (menu.boxObject instanceof MenuBoxObject) + ok(menu.getAttribute("open") == "true", testname + " " + menuid + " menu is open"); +} + +function checkClosed(menuid, testname) +{ + var menu = document.getElementById(menuid); + if ("open" in menu) + ok(!menu.open, testname + " " + menuid + " menu is open"); + else if (menu.boxObject instanceof MenuBoxObject) + ok(!menu.hasAttribute("open"), testname + " " + menuid + " menu is closed"); +} + +function convertPosition(anchor, align) +{ + if (anchor == "topleft" && align == "topleft") return "overlap"; + if (anchor == "topleft" && align == "topright") return "start_before"; + if (anchor == "topleft" && align == "bottomleft") return "before_start"; + if (anchor == "topright" && align == "topleft") return "end_before"; + if (anchor == "topright" && align == "bottomright") return "before_end"; + if (anchor == "bottomleft" && align == "bottomright") return "start_after"; + if (anchor == "bottomleft" && align == "topleft") return "after_start"; + if (anchor == "bottomright" && align == "bottomleft") return "end_after"; + if (anchor == "bottomright" && align == "topright") return "after_end"; + return ""; +} + +/* + * When checking position of the bottom or right edge of the popup's rect, + * use this instead of strict equality check of rounded values, + * because we snap the top/left edges to pixel boundaries, + * which can shift the bottom/right up to 0.5px from its "ideal" location, + * and could cause it to round differently. (See bug 622507.) + */ +function isWithinHalfPixel(a, b) +{ + return Math.abs(a - b) <= 0.5; +} + +function compareEdge(anchor, popup, edge, offsetX, offsetY, testname) +{ + testname += " " + edge; + + checkOpen(anchor.id, testname); + + var anchorrect = anchor.getBoundingClientRect(); + var popuprect = popup.getBoundingClientRect(); + var check1 = false, check2 = false; + + if (gPopupWidth == -1) { + ok((Math.round(popuprect.right) - Math.round(popuprect.left)) && + (Math.round(popuprect.bottom) - Math.round(popuprect.top)), + testname + " size"); + } + else { + is(Math.round(popuprect.width), gPopupWidth, testname + " width"); + is(Math.round(popuprect.height), gPopupHeight, testname + " height"); + } + + var spaceIdx = edge.indexOf(" "); + if (spaceIdx > 0) { + let cornerX, cornerY; + let [position, align] = edge.split(" "); + switch (position) { + case "topleft": cornerX = anchorrect.left; cornerY = anchorrect.top; break; + case "topcenter": cornerX = anchorrect.left + anchorrect.width / 2; cornerY = anchorrect.top; break; + case "topright": cornerX = anchorrect.right; cornerY = anchorrect.top; break; + case "leftcenter": cornerX = anchorrect.left; cornerY = anchorrect.top + anchorrect.height / 2; break; + case "rightcenter": cornerX = anchorrect.right; cornerY = anchorrect.top + anchorrect.height / 2; break; + case "bottomleft": cornerX = anchorrect.left; cornerY = anchorrect.bottom; break; + case "bottomcenter": cornerX = anchorrect.left + anchorrect.width / 2; cornerY = anchorrect.bottom; break; + case "bottomright": cornerX = anchorrect.right; cornerY = anchorrect.bottom; break; + } + + switch (align) { + case "topleft": cornerX += offsetX; cornerY += offsetY; break; + case "topright": cornerX += -popuprect.width + offsetX; cornerY += offsetY; break; + case "bottomleft": cornerX += offsetX; cornerY += -popuprect.height + offsetY; break; + case "bottomright": cornerX += -popuprect.width + offsetX; cornerY += -popuprect.height + offsetY; break; + } + + is(Math.round(popuprect.left), Math.round(cornerX), testname + " x position"); + is(Math.round(popuprect.top), Math.round(cornerY), testname + " y position"); + return; + } + + if (edge == "after_pointer") { + is(Math.round(popuprect.left), Math.round(anchorrect.left) + offsetX, testname + " x position"); + is(Math.round(popuprect.top), Math.round(anchorrect.top) + offsetY + 21, testname + " y position"); + return; + } + + if (edge == "overlap") { + ok(Math.round(anchorrect.left) + offsetY == Math.round(popuprect.left) && + Math.round(anchorrect.top) + offsetY == Math.round(popuprect.top), + testname + " position"); + return; + } + + if (edge.indexOf("before") == 0) + check1 = isWithinHalfPixel(anchorrect.top + offsetY, popuprect.bottom); + else if (edge.indexOf("after") == 0) + check1 = (Math.round(anchorrect.bottom) + offsetY == Math.round(popuprect.top)); + else if (edge.indexOf("start") == 0) + check1 = isWithinHalfPixel(anchorrect.left + offsetX, popuprect.right); + else if (edge.indexOf("end") == 0) + check1 = (Math.round(anchorrect.right) + offsetX == Math.round(popuprect.left)); + + if (0 < edge.indexOf("before")) + check2 = (Math.round(anchorrect.top) + offsetY == Math.round(popuprect.top)); + else if (0 < edge.indexOf("after")) + check2 = isWithinHalfPixel(anchorrect.bottom + offsetY, popuprect.bottom); + else if (0 < edge.indexOf("start")) + check2 = (Math.round(anchorrect.left) + offsetX == Math.round(popuprect.left)); + else if (0 < edge.indexOf("end")) + check2 = isWithinHalfPixel(anchorrect.right + offsetX, popuprect.right); + + ok(check1 && check2, testname + " position"); +} diff --git a/toolkit/content/tests/widgets/seek_with_sound.ogg b/toolkit/content/tests/widgets/seek_with_sound.ogg Binary files differnew file mode 100644 index 0000000000..c86d9946bd --- /dev/null +++ b/toolkit/content/tests/widgets/seek_with_sound.ogg diff --git a/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html b/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html new file mode 100644 index 0000000000..0f295cce9b --- /dev/null +++ b/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Audio controls test</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> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls preload="auto"></audio> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + function loadedmetadata(event) { + is(event.type, "loadedmetadata", "checking event type"); + is(audio.clientHeight, 28, "checking height of audio element"); + + SimpleTest.finish(); + } + + var audio = document.getElementById("audio"); + + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startTest); + function startTest() { + // Kick off test once audio has loaded. + audio.addEventListener("loadedmetadata", loadedmetadata, false); + audio.src = "audio.wav"; + } + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_bug898940.html b/toolkit/content/tests/widgets/test_bug898940.html new file mode 100644 index 0000000000..10a6a80d93 --- /dev/null +++ b/toolkit/content/tests/widgets/test_bug898940.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that an audio element that's already playing when controls are attached displays the controls</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> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls src="audio.ogg"></audio> +</div> + +<pre id="test"> +<script class="testbody"> + var audio = document.getElementById("audio"); + audio.play(); + audio.ontimeupdate = function doTest() { + ok(audio.getBoundingClientRect().height > 0, + "checking audio element height is greater than zero"); + audio.ontimeupdate = null; + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_contextmenu_menugroup.xul b/toolkit/content/tests/widgets/test_contextmenu_menugroup.xul new file mode 100644 index 0000000000..594c0264d6 --- /dev/null +++ b/toolkit/content/tests/widgets/test_contextmenu_menugroup.xul @@ -0,0 +1,102 @@ +<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Context menugroup Tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <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>
+ <script type="application/javascript" src="popup_shared.js"></script>
+
+<menupopup id="context">
+ <menugroup>
+ <menuitem id="a"/>
+ <menuitem id="b"/>
+ </menugroup>
+ <menuitem id="c" label="c"/>
+ <menugroup/>
+</menupopup>
+
+<button label="Check"/>
+
+<vbox id="popuparea" popup="context" width="20" height="20"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+var gMenuPopup = $("context");
+ok(gMenuPopup, "Got the reference to the context menu");
+
+var popupTests = [
+{
+ testname: "one-down-key",
+ condition: function() { return (navigator.platform.indexOf("Mac") == -1); },
+ events: [ "popupshowing context", "popupshown context", "DOMMenuItemActive a" ],
+ test: function () {
+ synthesizeMouse($("popuparea"), 4, 4, {});
+ synthesizeKey("VK_DOWN", {});
+ },
+ result: function (testname) {
+ checkActive(gMenuPopup, "a", testname);
+ }
+},
+{
+ testname: "two-down-keys",
+ condition: function() { return (navigator.platform.indexOf("Mac") == -1); },
+ events: [ "DOMMenuItemInactive a", "DOMMenuItemActive b" ],
+ test: () => synthesizeKey("VK_DOWN", {}),
+ result: function (testname) {
+ checkActive(gMenuPopup, "b", testname);
+ }
+},
+{
+ testname: "three-down-keys",
+ condition: function() { return (navigator.platform.indexOf("Mac") == -1); },
+ events: [ "DOMMenuItemInactive b", "DOMMenuItemActive c" ],
+ test: () => synthesizeKey("VK_DOWN", {}),
+ result: function (testname) {
+ checkActive(gMenuPopup, "c", testname);
+ }
+},
+{
+ testname: "three-down-keys-one-up-key",
+ condition: function() { return (navigator.platform.indexOf("Mac") == -1); },
+ events: [ "DOMMenuItemInactive c", "DOMMenuItemActive b" ],
+ test: () => synthesizeKey("VK_UP", {}),
+ result: function (testname) {
+ checkActive(gMenuPopup, "b", testname);
+ }
+},
+{
+ testname: "three-down-keys-two-up-keys",
+ condition: function() { return (navigator.platform.indexOf("Mac") == -1); },
+ events: [ "DOMMenuItemInactive b", "DOMMenuItemActive a" ],
+ test: () => synthesizeKey("VK_UP", {}),
+ result: function (testname) {
+ checkActive(gMenuPopup, "a", testname);
+ }
+},
+{
+ testname: "three-down-keys-three-up-key",
+ condition: function() { return (navigator.platform.indexOf("Mac") == -1); },
+ events: [ "DOMMenuItemInactive a", "DOMMenuItemActive c" ],
+ test: () => synthesizeKey("VK_UP", {}),
+ result: function (testname) {
+ checkActive(gMenuPopup, "c", testname);
+ }
+},
+];
+
+SimpleTest.waitForFocus(function runTest() {
+ startPopupTests(popupTests);
+});
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body>
+
+</window>
diff --git a/toolkit/content/tests/widgets/test_contextmenu_nested.xul b/toolkit/content/tests/widgets/test_contextmenu_nested.xul new file mode 100644 index 0000000000..9eb42a1eda --- /dev/null +++ b/toolkit/content/tests/widgets/test_contextmenu_nested.xul @@ -0,0 +1,138 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Nested Context Menu Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <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> + <script type="application/javascript" src="popup_shared.js"></script> + +<menupopup id="outercontext"> + <menuitem label="Context One"/> + <menu id="outercontextmenu" label="Sub"> + <menupopup id="innercontext"> + <menuitem id="innercontextmenu" label="Sub Context One"/> + </menupopup> + </menu> +</menupopup> + +<menupopup id="outermain"> + <menuitem label="One"/> + <menu id="outermenu" label="Sub"> + <menupopup id="innermain"> + <menuitem id="innermenu" label="Sub One" context="outercontext"/> + </menupopup> + </menu> +</menupopup> + +<button label="Check"/> + +<vbox id="popuparea" popup="outermain" width="20" height="20"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var popupTests = [ +{ + testname: "open outer popup", + events: [ "popupshowing outermain", "popupshown outermain" ], + test: () => synthesizeMouse($("popuparea"), 4, 4, {}), + result: function (testname) { + is($("outermain").triggerNode, $("popuparea"), testname); + is(document.popupNode, $("popuparea"), testname + " document.popupNode"); + } +}, +{ + testname: "open inner popup", + events: [ "DOMMenuItemActive outermenu", "popupshowing innermain", "popupshown innermain" ], + test: function () { + synthesizeMouse($("outermenu"), 4, 4, { type: "mousemove" }); + synthesizeMouse($("outermenu"), 2, 2, { type: "mousemove" }); + }, + result: function (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is(document.popupNode, $("popuparea"), testname + " document.popupNode"); + } +}, +{ + testname: "open outer context", + condition: function() { return (navigator.platform.indexOf("Mac") == -1); }, + events: [ "popupshowing outercontext", "popupshown outercontext" ], + test: () => synthesizeMouse($("innermenu"), 4, 4, { type: "contextmenu", button: 2 }), + result: function (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, $("innermenu"), testname + " outer context"); + is(document.popupNode, $("innermenu"), testname + " document.popupNode"); + } +}, +{ + testname: "open inner context", + condition: function() { return (navigator.platform.indexOf("Mac") == -1); }, + events: [ "DOMMenuItemActive outercontextmenu", "popupshowing innercontext", "popupshown innercontext" ], + test: function () { + synthesizeMouse($("outercontextmenu"), 4, 4, { type: "mousemove" }); + setTimeout(function() { + synthesizeMouse($("outercontextmenu"), 2, 2, { type: "mousemove" }); + }, 1000); + }, + result: function (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, $("innermenu"), testname + " outer context"); + is($("innercontext").triggerNode, $("innermenu"), testname + " inner context"); + is(document.popupNode, $("innermenu"), testname + " document.popupNode"); + } +}, +{ + testname: "close context", + condition: function() { return (navigator.platform.indexOf("Mac") == -1); }, + events: [ "popuphiding innercontext", "popuphidden innercontext", + "popuphiding outercontext", "popuphidden outercontext", + "DOMMenuInactive innercontext", + "DOMMenuItemInactive outercontextmenu", "DOMMenuItemInactive outercontextmenu", + "DOMMenuInactive outercontext" ], + test: () => $("outercontext").hidePopup(), + result: function (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is($("innercontext").triggerNode, null, testname + " inner context"); + is(document.popupNode, $("popuparea"), testname + " document.popupNode"); + } +}, +{ + testname: "hide menus", + events: [ "popuphiding innermain", "popuphidden innermain", + "popuphiding outermain", "popuphidden outermain", + "DOMMenuInactive innermain", + "DOMMenuItemInactive outermenu", "DOMMenuItemInactive outermenu", + "DOMMenuInactive outermain" ], + + test: () => $("outermain").hidePopup(), + result: function (testname) { + is($("outermain").triggerNode, null, testname + " outer"); + is($("innermain").triggerNode, null, testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is($("innercontext").triggerNode, null, testname + " inner context"); + is(document.popupNode, null, testname + " document.popupNode"); + } +} +]; + +SimpleTest.waitForFocus(function runTest() { + return startPopupTests(popupTests); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/widgets/test_editor_currentURI.xul b/toolkit/content/tests/widgets/test_editor_currentURI.xul new file mode 100644 index 0000000000..20ab3af7cf --- /dev/null +++ b/toolkit/content/tests/widgets/test_editor_currentURI.xul @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Editor currentURI Tests" onload="runTest();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + editortype="html" + style="width: 400px; height: 100px;"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + var editor = document.getElementById("editor"); + // Check that currentURI is a property of editor. + var result = "currentURI" in editor; + is(result, true, "currentURI is a property of editor"); + is(editor.currentURI.spec, "about:blank", "currentURI.spec is about:blank"); + SimpleTest.finish(); + } +]]> +</script> +</window> diff --git a/toolkit/content/tests/widgets/test_menubar.xul b/toolkit/content/tests/widgets/test_menubar.xul new file mode 100644 index 0000000000..7aa15fb2ac --- /dev/null +++ b/toolkit/content/tests/widgets/test_menubar.xul @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menubar Popup Tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <title>Menubar Popup Tests</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_menubar.xul", "_blank", "width=600,height=600"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_mousecapture_area.html b/toolkit/content/tests/widgets/test_mousecapture_area.html new file mode 100644 index 0000000000..532f41a5ac --- /dev/null +++ b/toolkit/content/tests/widgets/test_mousecapture_area.html @@ -0,0 +1,109 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Mouse capture on area elements tests</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> +<p id="display"></p> + +<div id="content"> + <!-- The border="0" on the images is needed so that when we use + synthesizeMouse we don't accidentally target the border of the image and + miss the area because synthesizeMouse gets the rect of the primary frame + of the target (the area), which is the image due to bug 135040, which + includes the border, but the events targetted at the border aren't + targeted at the area. --> + + <!-- 20x20 of red --> + <img id="image" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#Map"/> + + <map name="Map"> + <!-- area over the whole image --> + <area id="area" onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + shape="poly" coords="0,0, 0,20, 20,20, 20,0" href="javascript:void(0);"/> + </map> + + + <!-- 20x20 of red --> + <img id="img1" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#sharedMap"/> + + <!-- 20x20 of red --> + <img id="img2" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#sharedMap"/> + + <map name="sharedMap"> + <!-- area over the whole image --> + <area id="sharedarea" onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + shape="poly" coords="0,0, 0,20, 20,20, 20,0" href="javascript:void(0);"/> + </map> + + + <div id="otherelement" style="width: 100px; height: 100px;"></div> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.expectAssertions(3); + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + // XXX We send a useless click to each image to force it to setup its image + // map, because flushing layout won't do it. Hopefully bug 135040 will make + // this not suck. + synthesizeMouse($("image"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("image"), 5, 5, { type: "mouseup" }); + synthesizeMouse($("img1"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("img1"), 5, 5, { type: "mouseup" }); + synthesizeMouse($("img2"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("img2"), 5, 5, { type: "mouseup" }); + + + // test that setCapture works on an area element (bug 517737) + var area = document.getElementById("area"); + synthesizeMouse(area, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + area, "mousemove", "setCapture works on areas"); + synthesizeMouse(area, 5, 5, { type: "mouseup" }); + + // test that setCapture works on an area element when it is part of an image + // map that is used by two images + + var img1 = document.getElementById("img1"); + var sharedarea = document.getElementById("sharedarea"); + // synthesizeMouse just sends the event by coordinates, so this is really a click on the area + synthesizeMouse(img1, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + sharedarea, "mousemove", "setCapture works on areas with multiple images"); + synthesizeMouse(img1, 5, 5, { type: "mouseup" }); + + var img2 = document.getElementById("img2"); + // synthesizeMouse just sends the event by coordinates, so this is really a click on the area + synthesizeMouse(img2, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + sharedarea, "mousemove", "setCapture works on areas with multiple images"); + synthesizeMouse(img2, 5, 5, { type: "mouseup" }); + + // Bug 862673 - nuke all content so assertions in this test are attributed to + // this test rather than the one which happens to follow. + var content = document.getElementById("content"); + content.parentNode.removeChild(content); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_popupanchor.xul b/toolkit/content/tests/widgets/test_popupanchor.xul new file mode 100644 index 0000000000..814d9272f3 --- /dev/null +++ b/toolkit/content/tests/widgets/test_popupanchor.xul @@ -0,0 +1,430 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Anchor Tests" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <panel id="testPanel" + type="arrow" + animate="false" + noautohide="true"> + </panel> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +<![CDATA[ +var anchor, panel, arrow; + +function is_close(got, exp, msg) { + // on some platforms we see differences of a fraction of a pixel - so + // allow any difference of < 1 pixels as being OK. + ok(Math.abs(got - exp) < 1, msg + ": " + got + " should be equal(-ish) to " + exp); +} + +function isArrowPositionedOn(side, offset) { + var arrowRect = arrow.getBoundingClientRect(); + var arrowMidX = (arrowRect.left + arrowRect.right) / 2; + var arrowMidY = (arrowRect.top + arrowRect.bottom) / 2; + var panelRect = panel.getBoundingClientRect(); + var panelMidX = (panelRect.left + panelRect.right) / 2; + var panelMidY = (panelRect.top + panelRect.bottom) / 2; + // First check the "flip" of the panel is correct. If we are expecting the + // arrow to be pointing to the left side of the anchor, the arrow must + // also be on the left side of the panel (and vice-versa) + // XXX - on OSX, the arrow seems to always be exactly in the center, hence + // the 'equals' sign in the "<=" and ">=" comparisons. NFI why though... + switch (side) { + case "left": + ok(arrowMidX <= panelMidX, "arrow should be on the left of the panel"); + break; + case "right": + ok(arrowMidX >= panelMidX, "arrow should be on the right of the panel"); + break; + case "top": + ok(arrowMidY <= panelMidY, "arrow should be on the top of the panel"); + break; + case "bottom": + ok(arrowMidY >= panelMidY, "arrow should be on the bottom of the panel"); + break; + default: + ok(false, "invalid position " + where); + break; + } + // Now check the arrow really is pointing where we expect. The middle of + // the arrow should be pointing exactly to the left (or right) side of the + // anchor rect, +- any offsets. + if (offset === null) // special case - explicit 'null' means 'don't check offset' + return; + offset = offset || 0; // no param means no offset expected. + var anchorRect = anchor.getBoundingClientRect(); + var anchorPos = anchorRect[side]; + switch (side) { + case "left": + case "right": + is_close(arrowMidX - anchorPos, offset, "arrow should be " + offset + "px from " + side + " side of anchor"); + is_close(panelRect.top, anchorRect.bottom, "top of panel should be at bottom of anchor"); + break; + case "top": + case "bottom": + is_close(arrowMidY - anchorPos, offset, "arrow should be " + offset + "px from " + side + " side of anchor"); + is_close(panelRect.right, anchorRect.left, "right of panel should be left of anchor"); + break; + default: + ok(false, "unknown side " + side); + break; + } +} + +function openSlidingPopup(position, callback) { + panel.setAttribute("flip", "slide"); + _openPopup(position, callback); +} + +function openPopup(position, callback) { + panel.setAttribute("flip", "both"); + _openPopup(position, callback); +} + +function waitForPopupPositioned(actionFn, callback) +{ + panel.addEventListener("popuppositioned", function listener() { + panel.removeEventListener("popuppositioned", listener, false); + callback(); + }, false); + actionFn(); +} + +function _openPopup(position, callback) { + // this is very ugly: the panel CSS sets the arrow's list-style-image based + // on the 'side' attribute. If the setting of the 'side' attribute causes + // the image to change, we may get the popupshown event before the new + // image has loaded - which causes the size of the arrow to be incorrect + // for a brief moment - right when we are measuring it! + // So we work around this in 2 steps: + // * Force the 'side' attribute to a value which causes the CSS to not + // specify an image - then when the popup gets shown, the correct image + // is set, causing a load() event on the image element. + // * Listen to *both* popupshown and the image load event. When both have + // fired (the order is indeterminate) we start the test. + panel.setAttribute("side", "noside"); + var numEvents = 0; + function onEvent() { + if (++numEvents == 2) // after both panel 'popupshown' and image 'load' + callback(); + }; + panel.addEventListener("popupshown", function popupshown() { + panel.removeEventListener("popupshown", popupshown); + onEvent(); + }); + arrow.addEventListener("load", function imageload() { + arrow.removeEventListener("load", imageload); + onEvent(); + }); + panel.openPopup(anchor, position); +} + +var tests = [ + // A panel with the anchor after_end - the anchor should not move on resize + ['simpleResizeHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + isArrowPositionedOn("right"); + var origPanelRect = panel.getBoundingClientRect(); + panel.sizeTo(100, 100); + isArrowPositionedOn("right"); // should not have flipped, so still "right" + panel.sizeTo(origPanelRect.width, origPanelRect.height); + isArrowPositionedOn("right"); // should not have flipped, so still "right" + next(); + }); + }], + + ['simpleResizeVertical', 'middle', function(next) { + openPopup("start_after", function() { + isArrowPositionedOn("bottom"); + var origPanelRect = panel.getBoundingClientRect(); + panel.sizeTo(100, 100); + isArrowPositionedOn("bottom"); // should not have flipped + panel.sizeTo(origPanelRect.width, origPanelRect.height); + isArrowPositionedOn("bottom"); // should not have flipped + next(); + }); + }], + + ['flippingResizeHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + isArrowPositionedOn("right"); + panel.sizeTo(anchor.getBoundingClientRect().left + 50, 50); + isArrowPositionedOn("left"); // check it flipped and has zero offset. + next(); + }); + }], + + ['flippingResizeVertical', 'middle', function(next) { + openPopup("start_after", function() { + isArrowPositionedOn("bottom"); + panel.sizeTo(50, anchor.getBoundingClientRect().top + 50); + isArrowPositionedOn("top"); // check it flipped and has zero offset. + next(); + }); + }], + + ['simpleMoveToAnchorHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + isArrowPositionedOn("right"); + panel.moveToAnchor(anchor, "after_end", 20, 0); + // the anchor and the panel should have moved 20px right without flipping. + isArrowPositionedOn("right", 20); + panel.moveToAnchor(anchor, "after_end", -20, 0); + // the anchor and the panel should have moved 20px left without flipping. + isArrowPositionedOn("right", -20); + next(); + }); + }], + + ['simpleMoveToAnchorVertical', 'middle', function(next) { + openPopup("start_after", function() { + isArrowPositionedOn("bottom"); + panel.moveToAnchor(anchor, "start_after", 0, 20); + // the anchor and the panel should have moved 20px down without flipping. + isArrowPositionedOn("bottom", 20); + panel.moveToAnchor(anchor, "start_after", 0, -20); + // the anchor and the panel should have moved 20px up without flipping. + isArrowPositionedOn("bottom", -20); + next(); + }); + }], + + // Do a moveToAnchor that causes the panel to flip horizontally + ['flippingMoveToAnchorHorizontal', 'middle', function(next) { + var anchorRight = anchor.getBoundingClientRect().right; + // Size the panel such that it only just fits from the left-hand side of + // the window to the right of the anchor - thus, it will fit when + // anchored to the right-hand side of the anchor. + panel.sizeTo(anchorRight - 10, 100); + openPopup("after_end", function() { + isArrowPositionedOn("right"); + // Ask for it to be anchored 1/2 way between the left edge of the window + // and the anchor right - it can't fit with the panel on the left/arrow + // on the right, so it must flip (arrow on the left, panel on the right) + var offset = Math.floor(-anchorRight / 2); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "after_end", offset, 0), + () => { + isArrowPositionedOn("left", offset); // should have flipped and have the offset. + // resize back to original and move to a zero offset - it should flip back. + + panel.sizeTo(anchorRight - 10, 100); + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "after_end", 0, 0), + () => { + isArrowPositionedOn("right"); // should have flipped back and no offset + next(); + }); + }); + }); + }], + + // Do a moveToAnchor that causes the panel to flip vertically + ['flippingMoveToAnchorVertical', 'middle', function(next) { + var anchorBottom = anchor.getBoundingClientRect().bottom; + // See comments above in flippingMoveToAnchorHorizontal, but read + // "top/bottom" instead of "left/right" + panel.sizeTo(100, anchorBottom - 10); + openPopup("start_after", function() { + isArrowPositionedOn("bottom"); + var offset = Math.floor(-anchorBottom / 2); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "start_after", 0, offset), + () => { + isArrowPositionedOn("top", offset); + panel.sizeTo(100, anchorBottom - 10); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "start_after", 0, 0), + () => { + isArrowPositionedOn("bottom"); + next(); + }); + }); + }); + }], + + ['veryWidePanel-after_end', 'middle', function(next) { + openSlidingPopup("after_end", function() { + var origArrowRect = arrow.getBoundingClientRect(); + // Now move it such that the arrow can't be at either end of the panel but + // instead somewhere in the middle as that is the only way things fit, + // meaning the arrow should "slide" down the panel. + panel.sizeTo(window.innerWidth - 10, 60); + is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested.") + // the arrow should not have moved. + var curArrowRect = arrow.getBoundingClientRect(); + is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved"); + is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down"); + next(); + }); + }], + + ['veryWidePanel-before_start', 'middle', function(next) { + openSlidingPopup("before_start", function() { + var origArrowRect = arrow.getBoundingClientRect(); + // Now size it such that the arrow can't be at either end of the panel but + // instead somewhere in the middle as that is the only way things fit. + panel.sizeTo(window.innerWidth - 10, 60); + is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested") + // the arrow should not have moved. + var curArrowRect = arrow.getBoundingClientRect(); + is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved"); + is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down"); + next(); + }); + }], + + ['veryTallPanel-start_after', 'middle', function(next) { + openSlidingPopup("start_after", function() { + var origArrowRect = arrow.getBoundingClientRect(); + // Now move it such that the arrow can't be at either end of the panel but + // instead somewhere in the middle as that is the only way things fit, + // meaning the arrow should "slide" down the panel. + panel.sizeTo(100, window.innerHeight - 10); + is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested.") + // the arrow should not have moved. + var curArrowRect = arrow.getBoundingClientRect(); + is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved"); + is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down"); + next(); + }); + }], + + ['veryTallPanel-start_before', 'middle', function(next) { + openSlidingPopup("start_before", function() { + var origArrowRect = arrow.getBoundingClientRect(); + // Now size it such that the arrow can't be at either end of the panel but + // instead somewhere in the middle as that is the only way things fit. + panel.sizeTo(100, window.innerHeight - 10); + is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested") + // the arrow should not have moved. + var curArrowRect = arrow.getBoundingClientRect(); + is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved"); + is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down"); + next(); + }); + }], + + // Tests against the anchor at the right-hand side of the window + ['afterend', 'right', function(next) { + openPopup("after_end", function() { + // when we request too far to the right/bottom, the panel gets shrunk + // and moved. The amount it is shrunk by is how far it is moved. + var panelRect = panel.getBoundingClientRect(); + // panel was requested 100px wide - calc offset based on actual width. + var offset = panelRect.width - 100; + isArrowPositionedOn("right", offset); + next(); + }); + }], + + ['after_start', 'right', function(next) { + openPopup("after_start", function() { + // See above - we are still too far to the right, but the anchor is + // on the other side. + var panelRect = panel.getBoundingClientRect(); + var offset = panelRect.width - 100; + isArrowPositionedOn("right", offset); + next(); + }); + }], + + // Tests against the anchor at the left-hand side of the window + ['after_start', 'left', function(next) { + openPopup("after_start", function() { + var panelRect = panel.getBoundingClientRect(); + is(panelRect.left, 0, "panel remains within the screen"); + // not sure how to determine the offset here, so given we have checked + // the panel is as left as possible while still being inside the window, + // we just don't check the offset. + isArrowPositionedOn("left", null); + next(); + }); + }], +] + +function runTests() { + function runNextTest() { + let result = tests.shift(); + if (!result) { + // out of tests + panel.hidePopup(); + SimpleTest.finish(); + return; + } + let [name, anchorPos, test] = result; + SimpleTest.info("sub-test " + anchorPos + "." + name + " starting"); + // first arrange for the anchor to be where the test requires it. + panel.hidePopup(); + panel.sizeTo(100, 50); + // hide all the anchors here, then later we make one of them visible. + document.getElementById("anchor-left-wrapper").style.display = "none"; + document.getElementById("anchor-middle-wrapper").style.display = "none"; + document.getElementById("anchor-right-wrapper").style.display = "none"; + switch(anchorPos) { + case 'middle': + anchor = document.getElementById("anchor-middle"); + document.getElementById("anchor-middle-wrapper").style.display = "block"; + break; + case 'left': + anchor = document.getElementById("anchor-left"); + document.getElementById("anchor-left-wrapper").style.display = "block"; + break; + case 'right': + anchor = document.getElementById("anchor-right"); + document.getElementById("anchor-right-wrapper").style.display = "block"; + break; + default: + SimpleTest.ok(false, "Bad anchorPos: " + anchorPos); + runNextTest(); + return; + } + try { + test(runNextTest); + } catch (ex) { + SimpleTest.ok(false, "sub-test " + anchorPos + "." + name + " failed: " + ex.toString() + "\n" + ex.stack); + runNextTest(); + } + } + runNextTest(); +} + +SimpleTest.waitForExplicitFinish(); + +addEventListener("load", function() { + // anchor is set by the test runner above + panel = document.getElementById("testPanel"); + + arrow = SpecialPowers.wrap(document).getAnonymousElementByAttribute(panel, "anonid", "arrow"); + runTests(); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<!-- Our tests assume at least 100px around the anchor on all sides, else the + panel may flip when we don't expect it to +--> +<div id="anchor-middle-wrapper" style="margin: 100px 100px 100px 100px;"> + <p>The anchor --> <span id="anchor-middle">v</span> <--</p> +</div> +<div id="anchor-left-wrapper" style="text-align: left; display: none;"> + <p><span id="anchor-left">v</span> <-- The anchor;</p> +</div> +<div id="anchor-right-wrapper" style="text-align: right; display: none;"> + <p>The anchor --> <span id="anchor-right">v</span></p> +</div> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_popupreflows.xul b/toolkit/content/tests/widgets/test_popupreflows.xul new file mode 100644 index 0000000000..6969f77672 --- /dev/null +++ b/toolkit/content/tests/widgets/test_popupreflows.xul @@ -0,0 +1,111 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Reflow Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <panel id="testPanel" + type="arrow" + noautohide="true"> + </panel> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +<![CDATA[ +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +let panel, anchor, arrow; + +// A reflow observer - it just remembers the stack trace of all sync reflows +// done by the panel. +let observer = { + reflows: [], + reflow: function (start, end) { + // Ignore reflows triggered by native code + // (Reflows from native code only have an empty stack after the first frame) + var path = (new Error().stack).split("\n").slice(1).join(""); + if (path === "") { + return; + } + + this.reflows.push(new Error().stack); + }, + + reflowInterruptible: function (start, end) { + // We're not interested in interruptible reflows. Why, you ask? Because + // we've simply cargo-culted this test from browser_tabopen_reflows.js! + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver, + Ci.nsISupportsWeakReference]) +}; + +// A test utility that counts the reflows caused by a test function. If the +// count of reflows isn't what is expected, it causes a test failure and logs +// the stack trace of all seen reflows. +function countReflows(testfn, expected) { + let deferred = Promise.defer(); + observer.reflows = []; + let docShell = panel.ownerDocument.defaultView + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsIDocShell); + docShell.addWeakReflowObserver(observer); + testfn().then(() => { + docShell.removeWeakReflowObserver(observer); + SimpleTest.is(observer.reflows.length, expected, "correct number of reflows"); + if (observer.reflows.length != expected) { + SimpleTest.info("stack traces of reflows:\n" + observer.reflows.join("\n") + "\n"); + } + deferred.resolve(); + }); + return deferred.promise +} + +function openPopup() { + let deferred = Promise.defer(); + panel.addEventListener("popupshown", function popupshown() { + panel.removeEventListener("popupshown", popupshown); + deferred.resolve(); + }); + panel.openPopup(anchor, "before_start"); + return deferred.promise +} + +// ******************** +// The actual tests... +// We only have one atm - simply open a popup. +// +function testSimplePanel() { + return openPopup(); +} + +// ******************** +// The test harness... +// +SimpleTest.waitForExplicitFinish(); + +addEventListener("load", function() { + anchor = document.getElementById("anchor"); + panel = document.getElementById("testPanel"); + arrow = document.getAnonymousElementByAttribute(panel, "anonid", "arrow"); + + // Cancel the arrow panel slide-in transition (bug 767133) - we are only + // testing reflows in the core panel implementation and not reflows that may + // or may not be caused by transitioning.... + arrow.style.transition = "none"; + + // and off we go... + countReflows(testSimplePanel, 1).then(SimpleTest.finish); +}); +]]> +</script> +<body xmlns="http://www.w3.org/1999/xhtml"> + <p>The anchor --> <span id="anchor">v</span> <--</p> +</body> +</window> diff --git a/toolkit/content/tests/widgets/test_tree_column_reorder.xul b/toolkit/content/tests/widgets/test_tree_column_reorder.xul new file mode 100644 index 0000000000..5315fee439 --- /dev/null +++ b/toolkit/content/tests/widgets/test_tree_column_reorder.xul @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- + XUL Widget Test for reordering tree columns + --> +<window title="Tree" width="500" height="600" + onload="setTimeout(testtag_tree_column_reorder, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script src="tree_shared.js"/> + +<tree id="tree-column-reorder" rows="1" enableColumnDrag="true"> + <treecols> + <treecol id="col_0" label="col_0" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_1" label="col_1" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_2" label="col_2" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_3" label="col_3" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_4" label="col_4" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_5" label="col_5" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_6" label="col_6" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_7" label="col_7" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_8" label="col_8" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_9" label="col_9" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_10" label="col_10" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_11" label="col_11" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_12" label="col_12" flex="1"/> + </treecols> + <treechildren id="treechildren-column-reorder"> + <treeitem> + <treerow> + <treecell label="col_0"/> + <treecell label="col_1"/> + <treecell label="col_2"/> + <treecell label="col_3"/> + <treecell label="col_4"/> + <treecell label="col_5"/> + <treecell label="col_6"/> + <treecell label="col_7"/> + <treecell label="col_8"/> + <treecell label="col_9"/> + <treecell label="col_10"/> + <treecell label="col_11"/> + <treecell label="col_12"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> + diff --git a/toolkit/content/tests/widgets/test_videocontrols.html b/toolkit/content/tests/widgets/test_videocontrols.html new file mode 100644 index 0000000000..191aaef580 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols.html @@ -0,0 +1,411 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</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> +<p id="display"></p> + +<div id="content"> + <video width="320" height="240" id="video" controls mozNoDynamicControls preload="auto"></video> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/* + * Positions of the UI elements, relative to the upper-left corner of the + * <video> box. + */ +const videoWidth = 320; +const videoHeight = 240; +const videoDuration = 3.8329999446868896; + +const playButtonWidth = 28; +const playButtonHeight = 28; +const muteButtonWidth = 33; +const muteButtonHeight = 28; +const durationWidth = 34; +const fullscreenButtonWidth = 28; +const fullscreenButtonHeight = 28; +const volumeSliderWidth = 32; +const scrubberWidth = videoWidth - playButtonWidth - durationWidth - muteButtonWidth - volumeSliderWidth - fullscreenButtonWidth; +const scrubberHeight = 28; + +// Play button is on the bottom-left +const playButtonCenterX = 0 + Math.round(playButtonWidth / 2); +const playButtonCenterY = videoHeight - Math.round(playButtonHeight / 2); +// Mute button is on the bottom-right before the full screen button and volume slider +const muteButtonCenterX = videoWidth - Math.round(muteButtonWidth / 2) - volumeSliderWidth - fullscreenButtonWidth; +const muteButtonCenterY = videoHeight - Math.round(muteButtonHeight / 2); +// Fullscreen button is on the bottom-right at the far end +const fullscreenButtonCenterX = videoWidth - Math.round(fullscreenButtonWidth / 2); +const fullscreenButtonCenterY = videoHeight - Math.round(fullscreenButtonHeight / 2); +// Scrubber bar is between the play and mute buttons. We don't need it's +// X center, just the offset of its box. +const scrubberOffsetX = 0 + playButtonWidth; +const scrubberCenterY = videoHeight - Math.round(scrubberHeight / 2); + +var testnum = 1; +var video = document.getElementById("video"); + +const domUtil = SpecialPowers.Cc["@mozilla.org/inspector/dom-utils;1"] + .getService(SpecialPowers.Ci.inIDOMUtils); + +function getButtonByAttribute(aName, aValue) { + var kids = domUtil.getChildrenForNode(video, true); + var videocontrols = kids[1]; + return SpecialPowers.wrap(document) + .getAnonymousElementByAttribute(videocontrols, aName, aValue); +} + +function isMuteButtonMuted() { + var muteButton = getButtonByAttribute('class', 'muteButton'); + return muteButton.getAttribute('muted') === 'true'; +} + +function isVolumeSliderShowingCorrectVolume(expectedVolume) { + var volumeButton = getButtonByAttribute('anonid', 'volumeForeground'); + let expectedPaddingRight = (1 - expectedVolume) * volumeSliderWidth + "px"; + is(volumeButton.style.paddingRight, expectedPaddingRight, + "volume slider should match expected volume"); +} + +function forceReframe() { + // Setting display then getting the bounding rect to force a frame + // reconstruction on the video element. + video.style.display = "block"; + video.getBoundingClientRect(); + video.style.display = ""; + video.getBoundingClientRect(); +} + +function runTest(event) { + ok(true, "----- test #" + testnum + " -----"); + + switch (testnum) { + /* + * Check operation of play/pause/mute/unmute buttons. + */ + case 1: + // Check initial state upon load + is(event.type, "canplaythrough", "checking event type"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + // Click the play button + SimpleTest.executeSoon(() => { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, { }); + }); + break; + + case 2: + is(event.type, "play", "checking event type"); + is(video.paused, false, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + // Click the pause button + SimpleTest.executeSoon(() => { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, { }); + }); + break; + + case 3: + is(event.type, "pause", "checking event type"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + SimpleTest.executeSoon(() => { + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, { }); // Mute. + }); + break; + + case 4: + is(event.type, "volumechange", "checking event type"); + is(video.paused, true, "checking video play state"); + is(video.muted, true, "checking video mute state"); + + SimpleTest.executeSoon(() => { + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, { }); // Unmute. + }); + break; + + /* + * Bug 470596: Make sure that having CSS border or padding doesn't + * break the controls (though it should move them) + */ + case 5: + is(event.type, "volumechange", "checking event type"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + video.style.border = "medium solid purple"; + video.style.borderWidth = "30px 40px 50px 60px"; + video.style.padding = "10px 20px 30px 40px"; + // totals: top: 40px, right: 60px, bottom: 80px, left: 100px + + // Click the play button + SimpleTest.executeSoon(() => { + synthesizeMouse(video, 100 + playButtonCenterX, 40 + playButtonCenterY, { }); + }); + break; + + case 6: + is(event.type, "play", "checking event type"); + is(video.paused, false, "checking video play state"); + is(video.muted, false, "checking video mute state"); + video.pause(); + break; + + case 7: + is(event.type, "pause", "checking event type"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + // Click the mute button + SimpleTest.executeSoon(() => { + synthesizeMouse(video, 100 + muteButtonCenterX, 40 + muteButtonCenterY, { }); + }); + break; + + case 8: + is(event.type, "volumechange", "checking event type"); + is(video.paused, true, "checking video play state"); + is(video.muted, true, "checking video mute state"); + // Clear the style set in test 5. + video.style.border = ""; + video.style.borderWidth = ""; + video.style.padding = ""; + + video.muted = false; + break; + + /* + * Previous tests have moved playback postion, reset it to 0. + */ + case 9: + is(event.type, "volumechange", "checking event type"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); + ok(true, "video position is at " + video.currentTime); + video.currentTime = 0.0; + break; + + case 10: + is(event.type, "seeking", "checking event type"); + ok(true, "video position is at " + video.currentTime); + break; + + /* + * Drag the slider's thumb to the halfway point with the mouse. + */ + case 11: + is(event.type, "seeked", "checking event type"); + ok(true, "video position is at " + video.currentTime); + // Bug 477434 -- sometimes we get 0.098999 here instead of 0! + // is(video.currentTime, 0.0, "checking playback position"); + + SimpleTest.executeSoon(() => { + var beginDragX = scrubberOffsetX; + var endDragX = scrubberOffsetX + (scrubberWidth / 2); + synthesizeMouse(video, beginDragX, scrubberCenterY, { type: "mousedown", button: 0 }); + synthesizeMouse(video, endDragX, scrubberCenterY, { type: "mousemove", button: 0 }); + synthesizeMouse(video, endDragX, scrubberCenterY, { type: "mouseup", button: 0 }); + }); + break; + + case 12: + is(event.type, "seeking", "checking event type"); + ok(true, "video position is at " + video.currentTime); + break; + + /* + * Click the slider at the 1/4 point with the mouse (jump backwards) + */ + case 13: + is(event.type, "seeked", "checking event type"); + ok(true, "video position is at " + video.currentTime); + var expectedTime = videoDuration / 2; + ok(Math.abs(video.currentTime - expectedTime) < 0.1, "checking expected playback position"); + + SimpleTest.executeSoon(() => { + synthesizeMouse(video, scrubberOffsetX + (scrubberWidth / 4), scrubberCenterY, { }); + }); + break; + + case 14: + is(event.type, "seeking", "checking event type"); + ok(true, "video position is at " + video.currentTime); + break; + + case 15: + is(event.type, "seeked", "checking event type"); + ok(true, "video position is at " + video.currentTime); + // The scrubber currently just jumps towards the nearest pageIncrement point, not + // precisely to the point clicked. So, expectedTime isn't (videoDuration / 4). + // We should end up at 1.733, but sometimes we end up at 1.498. I guess + // it's timing depending if the <scale> things it's click-and-hold, or a + // single click. So, just make sure we end up less that the previous + // position. + lastPosition = (videoDuration / 2) - 0.1; + ok(video.currentTime < lastPosition, "checking expected playback position"); + + // Set volume to 0.1 so one down arrow hit will decrease it to 0. + video.volume = 0.1; + break; + + // See bug 694696. + case 16: + is(event.type, "volumechange", "checking event type"); + is(video.volume, 0.1, "Volume should be set."); + ok(!video.muted, "Video is not muted."); + + video.focus(); + SimpleTest.executeSoon(() => synthesizeKey("VK_DOWN", {})); + break; + + case 17: + is(event.type, "volumechange", "checking event type"); + is(video.volume, 0, "Volume should be 0."); + ok(!video.muted, "Video is not muted."); + + SimpleTest.executeSoon(() => { + ok(isMuteButtonMuted(), "Mute button says it's muted"); + synthesizeKey("VK_UP", {}); + }); + break; + + case 18: + is(event.type, "volumechange", "checking event type"); + is(video.volume, 0.1, "Volume is increased."); + ok(!video.muted, "Video is not muted."); + + SimpleTest.executeSoon(() => { + ok(!isMuteButtonMuted(), "Mute button says it's not muted"); + synthesizeKey("VK_DOWN", {}); + }); + break; + + case 19: + is(event.type, "volumechange", "checking event type"); + is(video.volume, 0, "Volume should be 0."); + ok(!video.muted, "Video is not muted."); + + SimpleTest.executeSoon(() => { + ok(isMuteButtonMuted(), "Mute button says it's muted"); + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, { }); + }); + break; + + case 20: + is(event.type, "volumechange", "checking event type"); + is(video.volume, 0.5, "Volume should be 0.5."); + ok(!video.muted, "Video is not muted."); + + SimpleTest.executeSoon(() => synthesizeKey("VK_UP", {})); + break; + + case 21: + is(event.type, "volumechange", "checking event type"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(!video.muted, "Video is not muted."); + + SimpleTest.executeSoon(() => { + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, { }); + }); + break; + + case 22: + is(event.type, "volumechange", "checking event type"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(video.muted, "Video is muted."); + + SimpleTest.executeSoon(() => { + ok(isMuteButtonMuted(), "Mute button says it's muted"); + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, { }); + }); + break; + + case 23: + is(event.type, "volumechange", "checking event type"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(!video.muted, "Video is not muted."); + + SimpleTest.executeSoon(() => { + ok(!isMuteButtonMuted(), "Mute button says it's not muted"); + synthesizeMouse(video, fullscreenButtonCenterX, fullscreenButtonCenterY, { }); + }); + break; + + case 24: + is(event.type, "mozfullscreenchange", "checking event type"); + is(video.volume, 0.6, "Volume should still be 0.6"); + SimpleTest.executeSoon(function() { + isVolumeSliderShowingCorrectVolume(video.volume); + synthesizeKey("VK_ESCAPE", {}); + }); + break; + + case 25: + is(event.type, "mozfullscreenchange", "checking event type"); + is(video.volume, 0.6, "Volume should still be 0.6"); + SimpleTest.executeSoon(function() { + isVolumeSliderShowingCorrectVolume(video.volume); + forceReframe(); + video.focus(); + synthesizeKey("VK_DOWN", {}); + }); + break; + + case 26: + is(event.type, "volumechange", "checking event type"); + is(video.volume, 0.5, "Volume should be decreased by 0.1"); + SimpleTest.executeSoon(function() { + isVolumeSliderShowingCorrectVolume(video.volume); + SimpleTest.finish(); + }); + break; + + default: + throw "unexpected test #" + testnum + " w/ event " + event.type; + } + + testnum++; +} + + + +function canplaythroughevent(event) { + video.removeEventListener("canplaythrough", canplaythroughevent, false); + // Other events expected by the test. + video.addEventListener("play", runTest, false); + video.addEventListener("pause", runTest, false); + video.addEventListener("volumechange", runTest, false); + video.addEventListener("seeking", runTest, false); + video.addEventListener("seeked", runTest, false); + document.addEventListener("mozfullscreenchange", runTest, false); + // Begin the test. + runTest(event); +} + +function startMediaLoad() { + // Kick off test once video has loaded, in its canplaythrough event handler. + video.src = "seek_with_sound.ogg"; + video.addEventListener("canplaythrough", canplaythroughevent, false); +} + +function loadevent(event) { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startMediaLoad); +} + +window.addEventListener("load", loadevent, false); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_audio.html b/toolkit/content/tests/widgets/test_videocontrols_audio.html new file mode 100644 index 0000000000..7d1dc32e39 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_audio.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls with Audio file test</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> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="metadata"></video> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + + var domUtils = SpecialPowers.Cc["@mozilla.org/inspector/dom-utils;1"]. + getService(SpecialPowers.Ci.inIDOMUtils); + + function findElementByAttribute(element, aName, aValue) { + if (!('getAttribute' in element)) { + return false; + } + if (element.getAttribute(aName) === aValue) { + return element; + } + let children = domUtils.getChildrenForNode(element, true); + for (let child of children) { + var result = findElementByAttribute(child, aName, aValue); + if (result) { + return result; + } + } + return false; + } + + function loadedmetadata(event) { + SimpleTest.executeSoon(function() { + var controlBar = findElementByAttribute(video, "class", "controlBar"); + is(controlBar.getAttribute("fullscreen-unavailable"), "true", "Fullscreen button is hidden"); + SimpleTest.finish(); + }); + } + + var video = document.getElementById("video"); + + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startTest); + function startTest() { + // Kick off test once audio has loaded. + video.addEventListener("loadedmetadata", loadedmetadata, false); + video.src = "audio.ogg"; + } + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html b/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html new file mode 100644 index 0000000000..2512b997a0 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls directionality test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var tests = [ + {op: "==", test: "videocontrols_direction-2a.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2b.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2c.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2d.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2e.html", ref: "videocontrols_direction-2-ref.html"} +]; + +</script> +<script type="text/javascript" src="videocontrols_direction_test.js"></script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html new file mode 100644 index 0000000000..6391dcc1b7 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - iframe</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> +<p id="display"></p> + +<div id="content"> +<iframe id="ifr1"></iframe> +<iframe id="ifr2" allowfullscreen></iframe> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const domUtils = SpecialPowers.Cc["@mozilla.org/inspector/dom-utils;1"]. + getService(SpecialPowers.Ci.inIDOMUtils); + const iframe1 = SpecialPowers.wrap(document.getElementById("ifr1")); + const iframe2 = SpecialPowers.wrap(document.getElementById("ifr2")); + const testCases = []; + + function checkIframeFullscreenAvailable(ifr) { + const available = ifr.hasAttribute("allowfullscreen"); + let video; + + return () => new Promise(resolve => { + ifr.srcdoc = `<video id="video" controls preload="auto"></video>`; + ifr.addEventListener("load", resolve, false); + }).then(() => new Promise(resolve => { + video = ifr.contentDocument.getElementById("video"); + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", resolve, false); + })).then(() => new Promise(resolve => { + const videoControl = domUtils.getChildrenForNode(video, true)[1]; + const controlBar = video.ownerDocument.getAnonymousElementByAttribute( + videoControl, "class", "controlBar"); + + is(controlBar.getAttribute("fullscreen-unavailable") == "true", !available, "The controlbar should have an attribute marking whether fullscreen is available that corresponds to if the iframe has the allowfullscreen attribute."); + resolve(); + })); + } + + function start() { + testCases.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function load() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, start); + } + + testCases.push(checkIframeFullscreenAvailable(iframe1)); + testCases.push(checkIframeFullscreenAvailable(iframe2)); + testCases.push(SimpleTest.finish); + + window.addEventListener("load", load, false); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html new file mode 100644 index 0000000000..f57cda0636 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</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> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function runTest(event) { + info(true, "----- test #" + testnum + " -----"); + + switch (testnum) { + case 1: + is(event.type, "timeupdate", "checking event type"); + is(video.paused, false, "checking video play state"); + video.removeEventListener("timeupdate", runTest); + + // Click to toggle play/pause (now pausing) + synthesizeMouseAtCenter(video, {}, win); + break; + + case 2: + is(event.type, "pause", "checking event type"); + is(video.paused, true, "checking video play state"); + win.close(); + + SimpleTest.finish(); + break; + + default: + ok(false, "unexpected test #" + testnum + " w/ event " + event.type); + throw "unexpected test #" + testnum + " w/ event " + event.type; + } + + testnum++; +} + +SpecialPowers.pushPrefEnv({"set": [["javascript.enabled", false]]}, startTest); + +var testnum = 1; + +var video; +function loadevent(event) { + is(win["testExpando"], undefined, "expando shouldn't exist because js is disabled"); + video = win.document.querySelector("video"); + // Other events expected by the test. + video.addEventListener("timeupdate", runTest, false); + video.addEventListener("pause", runTest, false); +} + +var win; +function startTest() { + var videoURL = new URL("seek_with_sound.ogg", document.documentURI).href; + var url = "data:text/html,<video src=" + videoURL + " controls autoplay=true></video><script>window.testExpando = true;</scr" + "ipt>"; + + win = window.open(url); + win.addEventListener("load", loadevent, false); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html new file mode 100644 index 0000000000..d681b31587 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls mozNoDynamicControls preload="auto"></video> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +var video = document.getElementById("video"); + +function startMediaLoad() { + // Kick off test once video has loaded, in its canplaythrough event handler. + video.src = "seek_with_sound.ogg"; + video.addEventListener("canplaythrough", runTest, false); +} + +function loadevent(event) { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startMediaLoad); +} + +window.addEventListener("load", loadevent, false); + +function runTest() { + video.addEventListener("click", function() { + this.play(); + }); + ok(video.paused, "video should be paused initially"); + + new Promise(resolve => { + let timeupdates = 0; + video.addEventListener("timeupdate", function timeupdate() { + ok(!video.paused, "video should not get paused after clicking in middle"); + + if (++timeupdates == 3) { + video.removeEventListener("timeupdate", timeupdate); + resolve(); + } + }); + + synthesizeMouseAtCenter(video, {}, window); + }).then(function() { + new Promise(resolve => { + video.addEventListener("pause", function onpause() { + setTimeout(() => { + ok(video.paused, "video should still be paused 200ms after pause request"); + // When the video reaches the end of playback it is automatically paused. + // Check during the pause event that the video has not reachd the end + // of playback. + ok(!video.ended, "video should not have paused due to playback ending"); + resolve(); + }, 200); + }); + + synthesizeMouse(video, 10, video.clientHeight - 10, {}, window); + }).then(SimpleTest.finish); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_standalone.html b/toolkit/content/tests/widgets/test_videocontrols_standalone.html new file mode 100644 index 0000000000..8d1ce89844 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_standalone.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const videoWidth = 320; +const videoHeight = 240; + +function getMediaElement(aWindow) { + return aWindow.document.getElementsByTagName("video")[0]; +} + +var popup = window.open("seek_with_sound.ogg"); +popup.addEventListener("load", function onLoad() { + popup.removeEventListener("load", onLoad); + var video = getMediaElement(popup); + if (!video.paused) + runTestVideo(video); + else { + video.addEventListener("play", function onPlay() { + video.removeEventListener("play", onPlay); + runTestVideo(video); + }); + } +}); + +function runTestVideo(aVideo) { + var condition = function() { + var boundingRect = aVideo.getBoundingClientRect(); + return boundingRect.width == videoWidth && + boundingRect.height == videoHeight; + }; + waitForCondition(condition, function() { + var boundingRect = aVideo.getBoundingClientRect(); + is(boundingRect.width, videoWidth, "Width of the video should match expectation"); + is(boundingRect.height, videoHeight, "Height of video should match expectation"); + popup.close(); + runTestAudioPre(); + }, "The media element should eventually be resized to match the intrinsic size of the video."); +} + +function runTestAudioPre() { + popup = window.open("audio.ogg"); + popup.addEventListener("load", function onLoad() { + popup.removeEventListener("load", onLoad); + var audio = getMediaElement(popup); + if (!audio.paused) + runTestAudio(audio); + else { + audio.addEventListener("play", function onPlay() { + audio.removeEventListener("play", onPlay); + runTestAudio(audio); + }) + } + }) +} + +function runTestAudio(aAudio) { + info("User agent (help diagnose bug #943556): " + navigator.userAgent); + var isAndroid = navigator.userAgent.includes("Android"); + var expectedHeight = isAndroid ? 103 : 28; + var condition = function () { + var boundingRect = aAudio.getBoundingClientRect(); + return boundingRect.height == expectedHeight; + }; + waitForCondition(condition, function () { + var boundingRect = aAudio.getBoundingClientRect(); + is(boundingRect.height, expectedHeight, + "Height of audio element should be " + expectedHeight + ", which is equal to the controls bar."); + popup.close(); + SimpleTest.finish(); + }, "The media element should eventually be resized to match the height of the audio controls."); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_video_direction.html b/toolkit/content/tests/widgets/test_videocontrols_video_direction.html new file mode 100644 index 0000000000..54e0d5e722 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_video_direction.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls directionality test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var tests = [ + {op: "==", test: "videocontrols_direction-1a.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1b.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1c.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1d.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1e.html", ref: "videocontrols_direction-1-ref.html"}, +]; + +</script> +<script type="text/javascript" src="videocontrols_direction_test.js"></script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_vtt.html b/toolkit/content/tests/widgets/test_videocontrols_vtt.html new file mode 100644 index 0000000000..27052b770e --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_vtt.html @@ -0,0 +1,133 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - VTT</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> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const domUtils = SpecialPowers.Cc["@mozilla.org/inspector/dom-utils;1"]. + getService(SpecialPowers.Ci.inIDOMUtils); + const video = document.getElementById("video"); + const ccBtn = getElementByAttribute("class", "closedCaptionButton"); + const ttList = getElementByAttribute("class", "textTrackList"); + const testCases = []; + + testCases.push(() => new Promise(resolve => { + is(ccBtn.getAttribute("hidden"), "true", "CC button should hide"); + + resolve(); + })); + + testCases.push(() => new Promise(resolve => { + video.addTextTrack("descriptions", "English", "en"); + video.addTextTrack("chapters", "English", "en"); + video.addTextTrack("metadata", "English", "en"); + + SimpleTest.executeSoon(() => { + is(ccBtn.getAttribute("hidden"), "true", "CC button should hide if no supported tracks provided"); + + resolve(); + }); + })); + + testCases.push(() => new Promise(resolve => { + const sub = video.addTextTrack("subtitles", "English", "en"); + sub.mode = "disabled"; + + SimpleTest.executeSoon(() => { + is(ccBtn.getAttribute("hidden"), "", "CC button should show"); + is(ccBtn.getAttribute("enabled"), "", "CC button should be disabled"); + + resolve(); + }); + })); + + testCases.push(() => new Promise(resolve => { + const subtitle = video.addTextTrack("subtitles", "English", "en"); + subtitle.mode = "showing"; + + SimpleTest.executeSoon(() => { + is(ccBtn.getAttribute("enabled"), "true", "CC button should be enabled"); + subtitle.mode = "disabled"; + + resolve(); + }); + })); + + testCases.push(() => new Promise(resolve => { + const caption = video.addTextTrack("captions", "English", "en"); + caption.mode = "showing"; + + SimpleTest.executeSoon(() => { + is(ccBtn.getAttribute("enabled"), "true", "CC button should be enabled"); + + resolve(); + }); + })); + + testCases.push(() => new Promise(resolve => { + synthesizeMouseAtCenter(ccBtn, {}); + + SimpleTest.executeSoon(() => { + is(ttList.hasAttribute("hidden"), false, "Texttrack menu should show up"); + is(ttList.lastChild.getAttribute("on"), "true", "The last added item should be highlighted"); + + resolve(); + }); + })); + + testCases.push(() => new Promise(resolve => { + const tt = ttList.children[1]; + + isnot(tt.getAttribute("on"), "true", "Item should be off before click"); + synthesizeMouseAtCenter(tt, {}); + + SimpleTest.executeSoon(() => { + is(tt.getAttribute("on"), "true", "Selected item should be enabled"); + is(ttList.getAttribute("hidden"), "true", "Should hide texttrack menu once clicked on an item"); + + resolve(); + }); + })); + + function executeTestCases(tasks) { + return tasks.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function getElementByAttribute(aName, aValue) { + const videoControl = domUtils.getChildrenForNode(video, true)[1]; + + return SpecialPowers.wrap(document) + .getAnonymousElementByAttribute(videoControl, aName, aValue); + } + + function loadedmetadata() { + executeTestCases(testCases).then(SimpleTest.finish); + } + + function startMediaLoad() { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", loadedmetadata, false); + } + + function loadevent() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startMediaLoad); + } + + window.addEventListener("load", loadevent, false); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/tree_shared.js b/toolkit/content/tests/widgets/tree_shared.js new file mode 100644 index 0000000000..b157bdf564 --- /dev/null +++ b/toolkit/content/tests/widgets/tree_shared.js @@ -0,0 +1,1405 @@ +var columns_simpletree = +[ + { name: "name", label: "Name", key: true, properties: "one two" }, + { name: "address", label: "Address" } +]; + +var columns_hiertree = +[ + { name: "name", label: "Name", primary: true, key: true, properties: "one two" }, + { name: "address", label: "Address" }, + { name: "planet", label: "Planet" }, + { name: "gender", label: "Gender", cycler: true } +]; + +// XXXndeakin still to add some tests for: +// cycler columns, checkbox cells, progressmeter cells + +// this test function expects a tree to have 8 rows in it when it isn't +// expanded. The tree should only display four rows at a time. If editable, +// the cell at row 1 and column 0 must be editable, and the cell at row 2 and +// column 1 must not be editable. +function testtag_tree(treeid, treerowinfoid, seltype, columnstype, testid) +{ + // Stop keystrokes that aren't handled by the tree from leaking out and + // scrolling the main Mochitests window! + function preventDefault(event) { + event.preventDefault(); + } + document.addEventListener("keypress", preventDefault, false); + + var multiple = (seltype == "multiple"); + + var tree = document.getElementById(treeid); + var treerowinfo = document.getElementById(treerowinfoid); + var rowInfo; + if (testid =="tree view") + rowInfo = getCustomTreeViewCellInfo(); + else + rowInfo = convertDOMtoTreeRowInfo(treerowinfo, 0, { value: -1 }); + var columnInfo = (columnstype == "simple") ? columns_simpletree : columns_hiertree; + + is(tree.view.selection.currentColumn, null, testid + " initial currentColumn"); + is(tree.selType, seltype == "multiple" ? "" : seltype, testid + " seltype"); + + // note: the functions below should be in this order due to changes made in later tests + + // select the first column in cell selection mode so that the selection + // functions can be tested + if (seltype == "cell") + tree.view.selection.currentColumn = tree.columns[0]; + + testtag_tree_columns(tree, columnInfo, testid); + testtag_tree_TreeSelection(tree, testid, multiple); + testtag_tree_TreeSelection_UI(tree, testid, multiple); + if (seltype == "cell") + testtag_tree_TreeSelection_UI_cell(tree, testid, rowInfo); + + testtag_tree_TreeView(tree, testid, rowInfo); + + is(tree.editable, false, "tree should not be editable"); + // currently, the editable flag means that tree editing cannot be invoked + // by the user. However, editing can still be started with a script. + is(tree.editingRow, -1, testid + " initial editingRow"); + is(tree.editingColumn, null, testid + " initial editingColumn"); + + testtag_tree_UI_editing(tree, testid, rowInfo); + + is(tree.editable, false, "tree should not be editable after testtag_tree_UI_editing"); + // currently, the editable flag means that tree editing cannot be invoked + // by the user. However, editing can still be started with a script. + is(tree.editingRow, -1, testid + " initial editingRow (continued)"); + is(tree.editingColumn, null, testid + " initial editingColumn (continued)"); + + var ecolumn = tree.columns[0]; + ok(!tree.startEditing(1, ecolumn), "non-editable trees shouldn't start editing"); + is(tree.editingRow, -1, testid + " failed startEditing shouldn't set editingRow"); + is(tree.editingColumn, null, testid + " failed startEditing shouldn't set editingColumn"); + + tree.editable = true; + + ok(tree.startEditing(1, ecolumn), "startEditing should have returned true"); + is(tree.editingRow, 1, testid + " startEditing editingRow"); + is(tree.editingColumn, ecolumn, testid + " startEditing editingColumn"); + is(tree.getAttribute("editing"), "true", testid + " startEditing editing attribute"); + + tree.stopEditing(true); + is(tree.editingRow, -1, testid + " stopEditing editingRow"); + is(tree.editingColumn, null, testid + " stopEditing editingColumn"); + is(tree.hasAttribute("editing"), false, testid + " stopEditing editing attribute"); + + tree.startEditing(-1, ecolumn); + is(tree.editingRow == -1 && tree.editingColumn == null, true, testid + " startEditing -1 editingRow"); + tree.startEditing(15, ecolumn); + is(tree.editingRow == -1 && tree.editingColumn == null, true, testid + " startEditing 15 editingRow"); + tree.startEditing(1, null); + is(tree.editingRow == -1 && tree.editingColumn == null, true, testid + " startEditing null column editingRow"); + tree.startEditing(2, tree.columns[1]); + is(tree.editingRow == -1 && tree.editingColumn == null, true, testid + " startEditing non editable cell editingRow"); + + tree.startEditing(1, ecolumn); + var inputField = tree.inputField; + is(inputField instanceof Components.interfaces.nsIDOMXULTextBoxElement, true, testid + "inputField"); + inputField.value = "Changed Value"; + tree.stopEditing(true); + is(tree.view.getCellText(1, ecolumn), "Changed Value", testid + "edit cell accept"); + + // this cell can be edited, but stopEditing(false) means don't accept the change. + tree.startEditing(1, ecolumn); + inputField.value = "Second Value"; + tree.stopEditing(false); + is(tree.view.getCellText(1, ecolumn), "Changed Value", testid + "edit cell no accept"); + + tree.editable = false; + + // do the sorting tests last as it will cause the rows to rearrange + // skip them for the custom tree view + if (testid !="tree view") + testtag_tree_TreeView_rows_sort(tree, testid, rowInfo); + + testtag_tree_wheel(tree); + + document.removeEventListener("keypress", preventDefault, false); + + SimpleTest.finish(); +} + +function testtag_tree_columns(tree, expectedColumns, testid) +{ + testid += " "; + + var columns = tree.columns; + + is(columns instanceof TreeColumns, true, testid + "columns is a TreeColumns"); + is(columns.count, expectedColumns.length, testid + "TreeColumns count"); + is(columns.length, expectedColumns.length, testid + "TreeColumns length"); + + var treecols = tree.getElementsByTagName("treecols")[0]; + var treecol = treecols.getElementsByTagName("treecol"); + + var x = 0; + var primary = null, sorted = null, key = null; + for (var c = 0; c < expectedColumns.length; c++) { + var adjtestid = testid + " column " + c + " "; + var column = columns[c]; + var expectedColumn = expectedColumns[c]; + is(columns.getColumnAt(c), column, adjtestid + "getColumnAt"); + is(columns.getNamedColumn(expectedColumn.name), column, adjtestid + "getNamedColumn"); + is(columns.getColumnFor(treecol[c]), column, adjtestid + "getColumnFor"); + if (expectedColumn.primary) + primary = column; + if (expectedColumn.sorted) + sorted = column; + if (expectedColumn.key) + key = column; + + // XXXndeakin on Windows and Linux, some columns are one pixel to the + // left of where they should be. Could just be a rounding issue. + var adj = 1; + is(column.x + adj >= x, true, adjtestid + "position is after last column " + + column.x + "," + column.width + "," + x); + is(column.width > 0, true, adjtestid + "width is greater than 0"); + x = column.x + column.width; + + // now check the TreeColumn properties + is(column instanceof TreeColumn, true, adjtestid + "is a TreeColumn"); + is(column.element, treecol[c], adjtestid + "element is treecol"); + is(column.columns, columns, adjtestid + "columns is TreeColumns"); + is(column.id, expectedColumn.name, adjtestid + "name"); + is(column.index, c, adjtestid + "index"); + is(column.primary, primary == column, adjtestid + "column is primary"); + + is(column.cycler, "cycler" in expectedColumn && expectedColumn.cycler, + adjtestid + "column is cycler"); + is(column.selectable, true, adjtestid + "column is selectable"); + is(column.editable, "editable" in expectedColumn && expectedColumn.editable, + adjtestid + "column is editable"); + + is(column.type, "type" in expectedColumn ? expectedColumn.type : 1, adjtestid + "type"); + + is(column.getPrevious(), c > 0 ? columns[c - 1] : null, adjtestid + "getPrevious"); + is(column.getNext(), c < columns.length - 1 ? columns[c + 1] : null, adjtestid + "getNext"); + + // check the view's getColumnProperties method + var properties = tree.view.getColumnProperties(column); + var expectedProperties = expectedColumn.properties; + is(properties, expectedProperties ? expectedProperties : "", adjtestid + "getColumnProperties"); + } + + is(columns.getFirstColumn(), columns[0], testid + "getFirstColumn"); + is(columns.getLastColumn(), columns[columns.length - 1], testid + "getLastColumn"); + is(columns.getPrimaryColumn(), primary, testid + "getPrimaryColumn"); + is(columns.getSortedColumn(), sorted, testid + "getSortedColumn"); + is(columns.getKeyColumn(), key, testid + "getKeyColumn"); + + is(columns.getColumnAt(-1), null, testid + "getColumnAt under"); + is(columns.getColumnAt(columns.length), null, testid + "getColumnAt over"); + is(columns.getNamedColumn(""), null, testid + "getNamedColumn null"); + is(columns.getNamedColumn("unknown"), null, testid + "getNamedColumn unknown"); + is(columns.getColumnFor(null), null, testid + "getColumnFor null"); + is(columns.getColumnFor(tree), null, testid + "getColumnFor other"); +} + +function testtag_tree_TreeSelection(tree, testid, multiple) +{ + testid += " selection "; + + var selection = tree.view.selection; + is(selection instanceof Components.interfaces.nsITreeSelection, true, + testid + "selection is a TreeSelection"); + is(selection.single, !multiple, testid + "single"); + + testtag_tree_TreeSelection_State(tree, testid + "initial", -1, []); + is(selection.shiftSelectPivot, -1, testid + "initial shiftSelectPivot"); + + selection.currentIndex = 2; + testtag_tree_TreeSelection_State(tree, testid + "set currentIndex", 2, []); + tree.currentIndex = 3; + testtag_tree_TreeSelection_State(tree, testid + "set tree.currentIndex", 3, []); + + // test the select() method, which should deselect all rows and select + // a single row + selection.select(1); + testtag_tree_TreeSelection_State(tree, testid + "select 1", 1, [1]); + selection.select(3); + testtag_tree_TreeSelection_State(tree, testid + "select 2", 3, [3]); + selection.select(3); + testtag_tree_TreeSelection_State(tree, testid + "select same", 3, [3]); + + selection.currentIndex = 1; + testtag_tree_TreeSelection_State(tree, testid + "set currentIndex with single selection", 1, [3]); + + tree.currentIndex = 2; + testtag_tree_TreeSelection_State(tree, testid + "set tree.currentIndex with single selection", 2, [3]); + + // check the toggleSelect method. In single selection mode, it only toggles on when + // there isn't currently a selection. + selection.toggleSelect(2); + testtag_tree_TreeSelection_State(tree, testid + "toggleSelect 1", 2, multiple ? [2, 3] : [3]); + selection.toggleSelect(2); + selection.toggleSelect(3); + testtag_tree_TreeSelection_State(tree, testid + "toggleSelect 2", 3, []); + + // the current index doesn't change after a selectAll, so it should still be set to 1 + // selectAll has no effect on single selection trees + selection.currentIndex = 1; + selection.selectAll(); + testtag_tree_TreeSelection_State(tree, testid + "selectAll 1", 1, multiple ? [0, 1, 2, 3, 4, 5, 6, 7] : []); + selection.toggleSelect(2); + testtag_tree_TreeSelection_State(tree, testid + "toggleSelect after selectAll", 2, + multiple ? [0, 1, 3, 4, 5, 6, 7] : [2]); + selection.clearSelection(); + testtag_tree_TreeSelection_State(tree, testid + "clearSelection", 2, []); + selection.toggleSelect(3); + selection.toggleSelect(1); + if (multiple) { + selection.selectAll(); + testtag_tree_TreeSelection_State(tree, testid + "selectAll 2", 1, [0, 1, 2, 3, 4, 5, 6, 7]); + } + selection.currentIndex = 2; + selection.clearSelection(); + testtag_tree_TreeSelection_State(tree, testid + "clearSelection after selectAll", 2, []); + + // XXXndeakin invertSelection isn't implemented + // selection.invertSelection(); + + is(selection.shiftSelectPivot, -1, testid + "shiftSelectPivot set to -1"); + + // rangedSelect and clearRange set the currentIndex to the endIndex. The + // shiftSelectPivot property will be set to startIndex. + selection.rangedSelect(1, 3, false); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect no augment", + multiple ? 3 : 2, multiple ? [1, 2, 3] : []); + is(selection.shiftSelectPivot, multiple ? 1 : -1, + testid + "shiftSelectPivot after rangedSelect no augment"); + if (multiple) { + selection.select(1); + selection.rangedSelect(0, 2, true); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment", 2, [0, 1, 2]); + is(selection.shiftSelectPivot, 0, testid + "shiftSelectPivot after rangedSelect augment"); + + selection.clearRange(1, 3); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment", 3, [0]); + + // check that rangedSelect can take a start value higher than end + selection.rangedSelect(3, 1, false); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect reverse", 1, [1, 2, 3]); + is(selection.shiftSelectPivot, 3, testid + "shiftSelectPivot after rangedSelect reverse"); + + // check that setting the current index doesn't change the selection + selection.currentIndex = 0; + testtag_tree_TreeSelection_State(tree, testid + "currentIndex with range selection", 0, [1, 2, 3]); + } + + // both values of rangedSelect may be the same + selection.rangedSelect(2, 2, false); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect one row", 2, [2]); + is(selection.shiftSelectPivot, 2, testid + "shiftSelectPivot after selecting one row"); + + if (multiple) { + selection.rangedSelect(2, 3, true); + + // a start index of -1 means from the last point + selection.rangedSelect(-1, 0, true); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect -1 existing selection", 0, [0, 1, 2, 3]); + is(selection.shiftSelectPivot, 2, testid + "shiftSelectPivot after -1 existing selection"); + + selection.currentIndex = 2; + selection.rangedSelect(-1, 0, false); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect -1 from currentIndex", 0, [0, 1, 2]); + is(selection.shiftSelectPivot, 2, testid + "shiftSelectPivot -1 from currentIndex"); + } + + // XXXndeakin need to test out of range values but these don't work properly +/* + selection.select(-1); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment -1", -1, []); + + selection.select(8); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment 8", 3, [0]); +*/ +} + +function testtag_tree_TreeSelection_UI(tree, testid, multiple) +{ + testid += " selection UI "; + + var selection = tree.view.selection; + selection.clearSelection(); + selection.currentIndex = 0; + tree.focus(); + + var keydownFired = 0; + var keypressFired = 0; + function keydownListener(event) + { + keydownFired++; + } + function keypressListener(event) { + keypressFired++; + } + + // check that cursor up and down keys navigate up and down + // select event fires after a delay so don't expect it. The reason it fires after a delay + // is so that cursor navigation allows quicking skimming over a set of items without + // actually firing events in-between, improving performance. The select event will only + // be fired on the row where the cursor stops. + window.addEventListener("keydown", keydownListener, false); + window.addEventListener("keypress", keypressListener, false); + + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down"); + testtag_tree_TreeSelection_State(tree, testid + "key down", 1, [1], 0); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up"); + testtag_tree_TreeSelection_State(tree, testid + "key up", 0, [0], 0); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up at start"); + testtag_tree_TreeSelection_State(tree, testid + "key up at start", 0, [0], 0); + + // pressing down while the last row is selected should not fire a select event, + // as the selection won't have changed. Also the view is not scrolled in this case. + selection.select(7); + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down at end"); + testtag_tree_TreeSelection_State(tree, testid + "key down at end", 7, [7], 0); + + // pressing keys while at the edge of the visible rows should scroll the list + tree.treeBoxObject.scrollToRow(4); + selection.select(4); + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up with scroll"); + is(tree.treeBoxObject.getFirstVisibleRow(), 3, testid + "key up with scroll"); + + tree.treeBoxObject.scrollToRow(0); + selection.select(3); + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down with scroll"); + is(tree.treeBoxObject.getFirstVisibleRow(), 1, testid + "key down with scroll"); + + // accel key and cursor movement adjust currentIndex but should not change + // the selection. In single selection mode, the selection will not change, + // but instead will just scroll up or down a line + tree.treeBoxObject.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent("VK_DOWN", { accelKey: true }, tree, "!select", "key down with accel"); + testtag_tree_TreeSelection_State(tree, testid + "key down with accel", multiple ? 2 : 1, [1]); + if (!multiple) + is(tree.treeBoxObject.getFirstVisibleRow(), 1, testid + "key down with accel and scroll"); + + tree.treeBoxObject.scrollToRow(4); + selection.select(4); + synthesizeKeyExpectEvent("VK_UP", { accelKey: true }, tree, "!select", "key up with accel"); + testtag_tree_TreeSelection_State(tree, testid + "key up with accel", multiple ? 3 : 4, [4]); + if (!multiple) + is(tree.treeBoxObject.getFirstVisibleRow(), 3, testid + "key up with accel and scroll"); + + // do this three times, one for each state of pageUpOrDownMovesSelection, + // and then once with the accel key pressed + for (let t = 0; t < 3; t++) { + let testidmod = ""; + if (t == 2) + testidmod = " with accel" + else if (t == 1) + testidmod = " rev"; + var keymod = (t == 2) ? { accelKey: true } : { }; + + var moveselection = tree.pageUpOrDownMovesSelection; + if (t == 2) + moveselection = !moveselection; + + tree.treeBoxObject.scrollToRow(4); + selection.currentIndex = 6; + selection.select(6); + var expected = moveselection ? 4 : 6; + synthesizeKeyExpectEvent("VK_PAGE_UP", keymod, tree, "!select", "key page up"); + testtag_tree_TreeSelection_State(tree, testid + "key page up" + testidmod, + expected, [expected], moveselection ? 4 : 0); + + expected = moveselection ? 0 : 6; + synthesizeKeyExpectEvent("VK_PAGE_UP", keymod, tree, "!select", "key page up again"); + testtag_tree_TreeSelection_State(tree, testid + "key page up again" + testidmod, + expected, [expected], 0); + + expected = moveselection ? 0 : 6; + synthesizeKeyExpectEvent("VK_PAGE_UP", keymod, tree, "!select", "key page up at start"); + testtag_tree_TreeSelection_State(tree, testid + "key page up at start" + testidmod, + expected, [expected], 0); + + tree.treeBoxObject.scrollToRow(0); + selection.currentIndex = 1; + selection.select(1); + expected = moveselection ? 3 : 1; + synthesizeKeyExpectEvent("VK_PAGE_DOWN", keymod, tree, "!select", "key page down"); + testtag_tree_TreeSelection_State(tree, testid + "key page down" + testidmod, + expected, [expected], moveselection ? 0 : 4); + + expected = moveselection ? 7 : 1; + synthesizeKeyExpectEvent("VK_PAGE_DOWN", keymod, tree, "!select", "key page down again"); + testtag_tree_TreeSelection_State(tree, testid + "key page down again" + testidmod, + expected, [expected], 4); + + expected = moveselection ? 7 : 1; + synthesizeKeyExpectEvent("VK_PAGE_DOWN", keymod, tree, "!select", "key page down at start"); + testtag_tree_TreeSelection_State(tree, testid + "key page down at start" + testidmod, + expected, [expected], 4); + + if (t < 2) + tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; + } + + tree.treeBoxObject.scrollToRow(4); + selection.select(6); + synthesizeKeyExpectEvent("VK_HOME", {}, tree, "!select", "key home"); + testtag_tree_TreeSelection_State(tree, testid + "key home", 0, [0], 0); + + tree.treeBoxObject.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent("VK_END", {}, tree, "!select", "key end"); + testtag_tree_TreeSelection_State(tree, testid + "key end", 7, [7], 4); + + // in single selection mode, the selection doesn't change in this case + tree.treeBoxObject.scrollToRow(4); + selection.select(6); + synthesizeKeyExpectEvent("VK_HOME", { accelKey: true }, tree, "!select", "key home with accel"); + testtag_tree_TreeSelection_State(tree, testid + "key home with accel", multiple ? 0 : 6, [6], 0); + + tree.treeBoxObject.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent("VK_END", { accelKey: true }, tree, "!select", "key end with accel"); + testtag_tree_TreeSelection_State(tree, testid + "key end with accel", multiple ? 7 : 1, [1], 4); + + // next, test cursor navigation with selection. Here the select event will be fired + selection.select(1); + var eventExpected = multiple ? "select" : "!select"; + synthesizeKeyExpectEvent("VK_DOWN", { shiftKey: true }, tree, eventExpected, "key shift down to select"); + testtag_tree_TreeSelection_State(tree, testid + "key shift down to select", + multiple ? 2 : 1, multiple ? [1, 2] : [1]); + is(selection.shiftSelectPivot, multiple ? 1 : -1, + testid + "key shift down to select shiftSelectPivot"); + synthesizeKeyExpectEvent("VK_UP", { shiftKey: true }, tree, eventExpected, "key shift up to unselect"); + testtag_tree_TreeSelection_State(tree, testid + "key shift up to unselect", 1, [1]); + is(selection.shiftSelectPivot, multiple ? 1 : -1, + testid + "key shift up to unselect shiftSelectPivot"); + if (multiple) { + synthesizeKeyExpectEvent("VK_UP", { shiftKey: true }, tree, "select", "key shift up to select"); + testtag_tree_TreeSelection_State(tree, testid + "key shift up to select", 0, [0, 1]); + is(selection.shiftSelectPivot, 1, testid + "key shift up to select shiftSelectPivot"); + synthesizeKeyExpectEvent("VK_DOWN", { shiftKey: true }, tree, "select", "key shift down to unselect"); + testtag_tree_TreeSelection_State(tree, testid + "key shift down to unselect", 1, [1]); + is(selection.shiftSelectPivot, 1, testid + "key shift down to unselect shiftSelectPivot"); + } + + // do this twice, one for each state of pageUpOrDownMovesSelection, however + // when selecting with the shift key, pageUpOrDownMovesSelection is ignored + // and the selection always changes + var lastidx = tree.view.rowCount - 1; + for (let t = 0; t < 2; t++) { + let testidmod = (t == 0) ? "" : " rev"; + + // If the top or bottom visible row is the current row, pressing shift and + // page down / page up selects one page up or one page down. Otherwise, the + // selection is made to the top or bottom of the visible area. + tree.treeBoxObject.scrollToRow(lastidx - 3); + selection.currentIndex = 6; + selection.select(6); + synthesizeKeyExpectEvent("VK_PAGE_UP", { shiftKey: true }, tree, eventExpected, "key shift page up"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page up" + testidmod, + multiple ? 4 : 6, multiple ? [4, 5, 6] : [6]); + if (multiple) { + synthesizeKeyExpectEvent("VK_PAGE_UP", { shiftKey: true }, tree, "select", "key shift page up again"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page up again" + testidmod, + 0, [0, 1, 2, 3, 4, 5, 6]); + // no change in the selection, so no select event should be fired + synthesizeKeyExpectEvent("VK_PAGE_UP", { shiftKey: true }, tree, "!select", "key shift page up at start"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page up at start" + testidmod, + 0, [0, 1, 2, 3, 4, 5, 6]); + // deselect by paging down again + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { shiftKey: true }, tree, "select", "key shift page down deselect"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page down deselect" + testidmod, + 3, [3, 4, 5, 6]); + } + + tree.treeBoxObject.scrollToRow(1); + selection.currentIndex = 2; + selection.select(2); + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { shiftKey: true }, tree, eventExpected, "key shift page down"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page down" + testidmod, + multiple ? 4 : 2, multiple ? [2, 3, 4] : [2]); + if (multiple) { + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { shiftKey: true }, tree, "select", "key shift page down again"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page down again" + testidmod, + 7, [2, 3, 4, 5, 6, 7]); + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { shiftKey: true }, tree, "!select", "key shift page down at start"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page down at start" + testidmod, + 7, [2, 3, 4, 5, 6, 7]); + synthesizeKeyExpectEvent("VK_PAGE_UP", { shiftKey: true }, tree, "select", "key shift page up deselect"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page up deselect" + testidmod, + 4, [2, 3, 4]); + } + + // test when page down / page up is pressed when the view is scrolled such + // that the selection is not visible + if (multiple) { + tree.treeBoxObject.scrollToRow(3); + selection.currentIndex = 1; + selection.select(1); + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { shiftKey: true }, tree, eventExpected, + "key shift page down with view scrolled down"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page down with view scrolled down" + testidmod, + 6, [1, 2, 3, 4, 5, 6], 3); + + tree.treeBoxObject.scrollToRow(2); + selection.currentIndex = 6; + selection.select(6); + synthesizeKeyExpectEvent("VK_PAGE_UP", { shiftKey: true }, tree, eventExpected, + "key shift page up with view scrolled up"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page up with view scrolled up" + testidmod, + 2, [2, 3, 4, 5, 6], 2); + + tree.treeBoxObject.scrollToRow(2); + selection.currentIndex = 0; + selection.select(0); + // don't expect the select event, as the selection won't have changed + synthesizeKeyExpectEvent("VK_PAGE_UP", { shiftKey: true }, tree, "!select", + "key shift page up at start with view scrolled down"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page up at start with view scrolled down" + testidmod, + 0, [0], 0); + + tree.treeBoxObject.scrollToRow(0); + selection.currentIndex = 7; + selection.select(7); + // don't expect the select event, as the selection won't have changed + synthesizeKeyExpectEvent("VK_PAGE_DOWN", { shiftKey: true }, tree, "!select", + "key shift page down at end with view scrolled up"); + testtag_tree_TreeSelection_State(tree, testid + "key shift page down at end with view scrolled up" + testidmod, + 7, [7], 4); + } + + tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; + } + + tree.treeBoxObject.scrollToRow(4); + selection.select(5); + synthesizeKeyExpectEvent("VK_HOME", { shiftKey: true }, tree, eventExpected, "key shift home"); + testtag_tree_TreeSelection_State(tree, testid + "key shift home", + multiple ? 0 : 5, multiple ? [0, 1, 2, 3, 4, 5] : [5], multiple ? 0 : 4); + + tree.treeBoxObject.scrollToRow(0); + selection.select(3); + synthesizeKeyExpectEvent("VK_END", { shiftKey: true }, tree, eventExpected, "key shift end"); + testtag_tree_TreeSelection_State(tree, testid + "key shift end", + multiple ? 7 : 3, multiple ? [3, 4, 5, 6, 7] : [3], multiple ? 4 : 0); + + // pressing space selects a row, pressing accel + space unselects a row + selection.select(2); + selection.currentIndex = 4; + synthesizeKeyExpectEvent(" ", {}, tree, "select", "key space on"); + // in single selection mode, space shouldn't do anything + testtag_tree_TreeSelection_State(tree, testid + "key space on", 4, multiple ? [2, 4] : [2]); + + if (multiple) { + synthesizeKeyExpectEvent(" ", { accelKey: true }, tree, "select", "key space off"); + testtag_tree_TreeSelection_State(tree, testid + "key space off", 4, [2]); + } + + // check that clicking on a row selects it + tree.treeBoxObject.scrollToRow(0); + selection.select(2); + selection.currentIndex = 2; + if (0) { // XXXndeakin disable these tests for now + mouseOnCell(tree, 1, tree.columns[1], "mouse on row"); + testtag_tree_TreeSelection_State(tree, testid + "mouse on row", 1, [1], 0, + tree.selType == "cell" ? tree.columns[1] : null); + } + + // restore the scroll position to the start of the page + sendKey("HOME"); + + window.removeEventListener("keydown", keydownListener, false); + window.removeEventListener("keypress", keypressListener, false); + is(keydownFired, multiple ? 63 : 40, "keydown event wasn't fired properly"); + is(keypressFired, multiple ? 2 : 1, "keypress event wasn't fired properly"); +} + +function testtag_tree_UI_editing(tree, testid, rowInfo) +{ + testid += " editing UI "; + + // check editing UI + var ecolumn = tree.columns[0]; + var rowIndex = 2; + var inputField = tree.inputField; + + // temporary make the tree editable to test mouse double click + var wasEditable = tree.editable; + if (!wasEditable) + tree.editable = true; + + // if this is a container save its current open status + var row = rowInfo.rows[rowIndex]; + var wasOpen = null; + if (tree.view.isContainer(row)) + wasOpen = tree.view.isContainerOpen(row); + + // Test whether a keystroke can enter text entry, and another can exit. + if (tree.selType == "cell") + { + tree.stopEditing(false); + ok(!tree.editingColumn, "Should not be editing tree cell now"); + tree.view.selection.currentColumn = ecolumn; + tree.currentIndex = rowIndex; + + const isMac = (navigator.platform.indexOf("Mac") >= 0); + const StartEditingKey = isMac ? "RETURN" : "F2"; + sendKey(StartEditingKey); + is(tree.editingColumn, ecolumn, "Should be editing tree cell now"); + sendKey("ESCAPE"); + ok(!tree.editingColumn, "Should not be editing tree cell now"); + is(tree.currentIndex, rowIndex, "Current index should not have changed"); + is(tree.view.selection.currentColumn, ecolumn, "Current column should not have changed"); + } + + mouseDblClickOnCell(tree, rowIndex, ecolumn, testid + "edit on double click"); + is(tree.editingColumn, ecolumn, testid + "editing column"); + is(tree.editingRow, rowIndex, testid + "editing row"); + + // ensure that we don't expand an expandable container on edit + if (wasOpen != null) + is(tree.view.isContainerOpen(row), wasOpen, testid + "opened container node on edit"); + + // ensure to restore editable attribute + if (!wasEditable) + tree.editable = false; + + var ci = tree.currentIndex; + + // cursor navigation should not change the selection while editing + var testKey = function(key) { + synthesizeKeyExpectEvent(key, {}, tree, "!select", "key " + key + " with editing"); + is(tree.editingRow == rowIndex && tree.editingColumn == ecolumn && tree.currentIndex == ci, + true, testid + "key " + key + " while editing"); + } + + testKey("VK_DOWN"); + testKey("VK_UP"); + testKey("VK_PAGE_DOWN"); + testKey("VK_PAGE_UP"); + testKey("VK_HOME"); + testKey("VK_END"); + + // XXXndeakin figure out how to send characters to the textbox + // inputField.inputField.focus() + // synthesizeKeyExpectEvent(inputField.inputField, "b", null, ""); + // tree.stopEditing(true); + // is(tree.view.getCellText(0, ecolumn), "b", testid + "edit cell"); + + // Restore initial state. + tree.stopEditing(false); +} + +function testtag_tree_TreeSelection_UI_cell(tree, testid, rowInfo) +{ + testid += " selection UI cell "; + + var columns = tree.columns; + var firstcolumn = columns[0]; + var secondcolumn = columns[1]; + var lastcolumn = columns[columns.length - 1]; + var secondlastcolumn = columns[columns.length - 2]; + var selection = tree.view.selection; + + selection.clearSelection(); + selection.currentIndex = -1; + selection.currentColumn = firstcolumn; + is(selection.currentColumn, firstcolumn, testid + " first currentColumn"); + + // no selection yet so nothing should happen when the left and right cursor keys are pressed + synthesizeKeyExpectEvent("VK_RIGHT", {}, tree, "!select", "key right no selection"); + testtag_tree_TreeSelection_State(tree, testid + "key right no selection", -1, [], null, firstcolumn); + + selection.currentColumn = secondcolumn; + synthesizeKeyExpectEvent("VK_LEFT", {}, tree, "!select", "key left no selection"); + testtag_tree_TreeSelection_State(tree, testid + "key left no selection", -1, [], null, secondcolumn); + + selection.select(2); + selection.currentIndex = 2; + if (0) { // XXXndeakin disable these tests for now + mouseOnCell(tree, 1, secondlastcolumn, "mouse on cell"); + testtag_tree_TreeSelection_State(tree, testid + "mouse on cell", 1, [1], null, secondlastcolumn); + } + + tree.focus(); + + // selection is set, so it should move when the left and right cursor keys are pressed + tree.treeBoxObject.scrollToRow(0); + selection.select(1); + selection.currentIndex = 1; + selection.currentColumn = secondcolumn; + synthesizeKeyExpectEvent("VK_LEFT", {}, tree, "!select", "key left in second column"); + testtag_tree_TreeSelection_State(tree, testid + "key left in second column", 1, [1], 0, firstcolumn); + + synthesizeKeyExpectEvent("VK_LEFT", {}, tree, "!select", "key left in first column"); + testtag_tree_TreeSelection_State(tree, testid + "key left in first column", 1, [1], 0, firstcolumn); + + selection.currentColumn = secondlastcolumn; + synthesizeKeyExpectEvent("VK_RIGHT", {}, tree, "!select", "key right in second last column"); + testtag_tree_TreeSelection_State(tree, testid + "key right in second last column", 1, [1], 0, lastcolumn); + + synthesizeKeyExpectEvent("VK_RIGHT", {}, tree, "!select", "key right in last column"); + testtag_tree_TreeSelection_State(tree, testid + "key right in last column", 1, [1], 0, lastcolumn); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up in second row"); + testtag_tree_TreeSelection_State(tree, testid + "key up in second row", 0, [0], 0, lastcolumn); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up in first row"); + testtag_tree_TreeSelection_State(tree, testid + "key up in first row", 0, [0], 0, lastcolumn); + + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down in first row"); + testtag_tree_TreeSelection_State(tree, testid + "key down in first row", 1, [1], 0, lastcolumn); + + var lastidx = tree.view.rowCount - 1; + tree.treeBoxObject.scrollToRow(lastidx - 3); + selection.select(lastidx); + selection.currentIndex = lastidx; + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down in last row"); + testtag_tree_TreeSelection_State(tree, testid + "key down in last row", lastidx, [lastidx], lastidx - 3, lastcolumn); + + synthesizeKeyExpectEvent("VK_HOME", {}, tree, "!select", "key home"); + testtag_tree_TreeSelection_State(tree, testid + "key home", 0, [0], 0, lastcolumn); + + synthesizeKeyExpectEvent("VK_END", {}, tree, "!select", "key end"); + testtag_tree_TreeSelection_State(tree, testid + "key end", lastidx, [lastidx], lastidx - 3, lastcolumn); + + for (var t = 0; t < 2; t++) { + var testidmod = (t == 0) ? "" : " rev"; + + // scroll to the end, subtract 3 because we want lastidx to appear + // at the end of view + tree.treeBoxObject.scrollToRow(lastidx - 3); + selection.select(lastidx); + selection.currentIndex = lastidx; + var expectedrow = tree.pageUpOrDownMovesSelection ? lastidx - 3 : lastidx; + synthesizeKeyExpectEvent("VK_PAGE_UP", {}, tree, "!select", "key page up"); + testtag_tree_TreeSelection_State(tree, testid + "key page up" + testidmod, + expectedrow, [expectedrow], + tree.pageUpOrDownMovesSelection ? lastidx - 3 : 0, lastcolumn); + + tree.treeBoxObject.scrollToRow(1); + selection.select(1); + selection.currentIndex = 1; + expectedrow = tree.pageUpOrDownMovesSelection ? 4 : 1; + synthesizeKeyExpectEvent("VK_PAGE_DOWN", {}, tree, "!select", "key page down"); + testtag_tree_TreeSelection_State(tree, testid + "key page down" + testidmod, + expectedrow, [expectedrow], + tree.pageUpOrDownMovesSelection ? 1 : lastidx - 3, lastcolumn); + + tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; + } + + // now check navigation when there is unselctable column + secondcolumn.element.setAttribute("selectable", "false"); + secondcolumn.invalidate(); + is(secondcolumn.selectable, false, testid + "set selectable attribute"); + + if (columns.length >= 3) { + selection.select(3); + selection.currentIndex = 3; + // check whether unselectable columns are skipped over + selection.currentColumn = firstcolumn; + synthesizeKeyExpectEvent("VK_RIGHT", {}, tree, "!select", "key right unselectable column"); + testtag_tree_TreeSelection_State(tree, testid + "key right unselectable column", + 3, [3], null, secondcolumn.getNext()); + + synthesizeKeyExpectEvent("VK_LEFT", {}, tree, "!select", "key left unselectable column"); + testtag_tree_TreeSelection_State(tree, testid + "key left unselectable column", + 3, [3], null, firstcolumn); + } + + secondcolumn.element.removeAttribute("selectable"); + secondcolumn.invalidate(); + is(secondcolumn.selectable, true, testid + "clear selectable attribute"); + + // check to ensure that navigation isn't allowed if the first column is not selectable + selection.currentColumn = secondcolumn; + firstcolumn.element.setAttribute("selectable", "false"); + firstcolumn.invalidate(); + synthesizeKeyExpectEvent("VK_LEFT", {}, tree, "!select", "key left unselectable first column"); + testtag_tree_TreeSelection_State(tree, testid + "key left unselectable first column", + 3, [3], null, secondcolumn); + firstcolumn.element.removeAttribute("selectable"); + firstcolumn.invalidate(); + + // check to ensure that navigation isn't allowed if the last column is not selectable + selection.currentColumn = secondlastcolumn; + lastcolumn.element.setAttribute("selectable", "false"); + lastcolumn.invalidate(); + synthesizeKeyExpectEvent("VK_RIGHT", {}, tree, "!select", "key right unselectable last column"); + testtag_tree_TreeSelection_State(tree, testid + "key right unselectable last column", + 3, [3], null, secondlastcolumn); + lastcolumn.element.removeAttribute("selectable"); + lastcolumn.invalidate(); + + // now check for cells with selectable false + if (!rowInfo.rows[4].cells[1].selectable && columns.length >= 3) { + // check whether unselectable cells are skipped over + selection.select(4); + selection.currentIndex = 4; + + selection.currentColumn = firstcolumn; + synthesizeKeyExpectEvent("VK_RIGHT", {}, tree, "!select", "key right unselectable cell"); + testtag_tree_TreeSelection_State(tree, testid + "key right unselectable cell", + 4, [4], null, secondcolumn.getNext()); + + synthesizeKeyExpectEvent("VK_LEFT", {}, tree, "!select", "key left unselectable cell"); + testtag_tree_TreeSelection_State(tree, testid + "key left unselectable cell", + 4, [4], null, firstcolumn); + + tree.treeBoxObject.scrollToRow(1); + selection.select(3); + selection.currentIndex = 3; + selection.currentColumn = secondcolumn; + + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down unselectable cell"); + testtag_tree_TreeSelection_State(tree, testid + "key down unselectable cell", + 5, [5], 2, secondcolumn); + + tree.treeBoxObject.scrollToRow(4); + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up unselectable cell"); + testtag_tree_TreeSelection_State(tree, testid + "key up unselectable cell", + 3, [3], 3, secondcolumn); + } + + // restore the scroll position to the start of the page + sendKey("HOME"); +} + +function testtag_tree_TreeView(tree, testid, rowInfo) +{ + testid += " view "; + + var columns = tree.columns; + var view = tree.view; + + is(view instanceof Components.interfaces.nsITreeView, true, testid + "view is a TreeView"); + is(view.rowCount, rowInfo.rows.length, testid + "rowCount"); + + testtag_tree_TreeView_rows(tree, testid, rowInfo, 0); + + // note that this will only work for content trees currently + view.setCellText(0, columns[1], "Changed Value"); + is(view.getCellText(0, columns[1]), "Changed Value", "setCellText"); + + view.setCellValue(1, columns[0], "Another Changed Value"); + is(view.getCellValue(1, columns[0]), "Another Changed Value", "setCellText"); +} + +function testtag_tree_TreeView_rows(tree, testid, rowInfo, startRow) +{ + var r; + var columns = tree.columns; + var view = tree.view; + var length = rowInfo.rows.length; + + // methods to test along with the functions which determine the expected value + var checkRowMethods = + { + isContainer: function(row) { return row.container }, + isContainerOpen: function(row) { return false }, + isContainerEmpty: function(row) { return (row.children != null && row.children.rows.length == 0) }, + isSeparator: function(row) { return row.separator }, + getRowProperties: function(row) { return row.properties }, + getLevel: function(row) { return row.level }, + getParentIndex: function(row) { return row.parent }, + hasNextSibling: function(row) { return r < startRow + length - 1; } + }; + + var checkCellMethods = + { + getCellText: function(row, cell) { return cell.label }, + getCellValue: function(row, cell) { return cell.value }, + getCellProperties: function(row, cell) { return cell.properties }, + isEditable: function(row, cell) { return cell.editable }, + isSelectable: function(row, cell) { return cell.selectable }, + getImageSrc: function(row, cell) { return cell.image }, + getProgressMode: function(row, cell) { return cell.mode } + }; + + var failedMethods = { }; + var checkMethod, actual, expected; + var containerInfo = null; + var toggleOpenStateOK = true; + + for (r = startRow; r < length; r++) { + var row = rowInfo.rows[r]; + for (var c = 0; c < row.cells.length; c++) { + var cell = row.cells[c]; + + for (checkMethod in checkCellMethods) { + expected = checkCellMethods[checkMethod](row, cell); + actual = view[checkMethod](r, columns[c]); + if (actual !== expected) { + failedMethods[checkMethod] = true; + is(actual, expected, testid + "row " + r + " column " + c + " " + checkMethod + " is incorrect"); + } + } + } + + // compare row properties + for (checkMethod in checkRowMethods) { + expected = checkRowMethods[checkMethod](row, r); + if (checkMethod == "hasNextSibling") { + actual = view[checkMethod](r, r); + } + else { + actual = view[checkMethod](r); + } + if (actual !== expected) { + failedMethods[checkMethod] = true; + is(actual, expected, testid + "row " + r + " " + checkMethod + " is incorrect"); + } + } +/* + // open and recurse into containers + if (row.container) { + view.toggleOpenState(r); + if (!view.isContainerOpen(r)) { + toggleOpenStateOK = false; + is(view.isContainerOpen(r), true, testid + "row " + r + " toggleOpenState open"); + } + testtag_tree_TreeView_rows(tree, testid + "container " + r + " ", row.children, r + 1); + view.toggleOpenState(r); + if (view.isContainerOpen(r)) { + toggleOpenStateOK = false; + is(view.isContainerOpen(r), false, testid + "row " + r + " toggleOpenState close"); + } + } +*/ + } + + for (var failedMethod in failedMethods) { + if (failedMethod in checkRowMethods) + delete checkRowMethods[failedMethod]; + if (failedMethod in checkCellMethods) + delete checkCellMethods[failedMethod]; + } + + for (checkMethod in checkRowMethods) + is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); + for (checkMethod in checkCellMethods) + is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); + if (toggleOpenStateOK) + is("toggleOpenState ok", "toggleOpenState ok", testid + "toggleOpenState"); +} + +function testtag_tree_TreeView_rows_sort(tree, testid, rowInfo) +{ + // check if cycleHeader sorts the columns + var columnIndex = 0; + var view = tree.view; + var column = tree.columns[columnIndex]; + var columnElement = column.element; + var sortkey = columnElement.getAttribute("sort"); + if (sortkey) { + view.cycleHeader(column); + is(tree.getAttribute("sort"), sortkey, "cycleHeader sort"); + is(tree.getAttribute("sortDirection"), "ascending", "cycleHeader sortDirection ascending"); + is(columnElement.getAttribute("sortDirection"), "ascending", "cycleHeader column sortDirection"); + is(columnElement.getAttribute("sortActive"), "true", "cycleHeader column sortActive"); + view.cycleHeader(column); + is(tree.getAttribute("sortDirection"), "descending", "cycleHeader sortDirection descending"); + is(columnElement.getAttribute("sortDirection"), "descending", "cycleHeader column sortDirection descending"); + view.cycleHeader(column); + is(tree.getAttribute("sortDirection"), "", "cycleHeader sortDirection natural"); + is(columnElement.getAttribute("sortDirection"), "", "cycleHeader column sortDirection natural"); + // XXXndeakin content view isSorted needs to be tested + } + + // Check that clicking on column header sorts the column. + var columns = getSortedColumnArray(tree); + is(columnElement.getAttribute("sortDirection"), "", + "cycleHeader column sortDirection"); + + // Click once on column header and check sorting has cycled once. + mouseClickOnColumnHeader(columns, columnIndex, 0, 1); + is(columnElement.getAttribute("sortDirection"), "ascending", + "single click cycleHeader column sortDirection ascending"); + + // Now simulate a double click. + mouseClickOnColumnHeader(columns, columnIndex, 0, 2); + if (navigator.platform.indexOf("Win") == 0) { + // Windows cycles only once on double click. + is(columnElement.getAttribute("sortDirection"), "descending", + "double click cycleHeader column sortDirection descending"); + // 1 single clicks should restore natural sorting. + mouseClickOnColumnHeader(columns, columnIndex, 0, 1); + } + + // Check we have gone back to natural sorting. + is(columnElement.getAttribute("sortDirection"), "", + "cycleHeader column sortDirection"); + + columnElement.setAttribute("sorthints", "twostate"); + view.cycleHeader(column); + is(tree.getAttribute("sortDirection"), "ascending", "cycleHeader sortDirection ascending twostate"); + view.cycleHeader(column); + is(tree.getAttribute("sortDirection"), "descending", "cycleHeader sortDirection ascending twostate"); + view.cycleHeader(column); + is(tree.getAttribute("sortDirection"), "ascending", "cycleHeader sortDirection ascending twostate again"); + columnElement.removeAttribute("sorthints"); + view.cycleHeader(column); + view.cycleHeader(column); + + is(columnElement.getAttribute("sortDirection"), "", + "cycleHeader column sortDirection reset"); +} + +// checks if the current and selected rows are correct +// current is the index of the current row +// selected is an array of the indicies of the selected rows +// column is the selected column +// viewidx is the row that should be visible at the top of the tree +function testtag_tree_TreeSelection_State(tree, testid, current, selected, viewidx, column) +{ + var selection = tree.view.selection; + + if (!column) + column = (tree.selType == "cell") ? tree.columns[0] : null; + + is(selection.count, selected.length, testid + " count"); + is(tree.currentIndex, current, testid + " currentIndex"); + is(selection.currentIndex, current, testid + " TreeSelection currentIndex"); + is(selection.currentColumn, column, testid + " currentColumn"); + if (viewidx !== null && viewidx !== undefined) + is(tree.treeBoxObject.getFirstVisibleRow(), viewidx, testid + " first visible row"); + + var actualSelected = []; + var count = tree.view.rowCount; + for (var s = 0; s < count; s++) { + if (selection.isSelected(s)) + actualSelected.push(s); + } + + is(compareArrays(selected, actualSelected), true, testid + " selection [" + selected + "]"); + + actualSelected = []; + var rangecount = selection.getRangeCount(); + for (var r = 0; r < rangecount; r++) { + var start = {}, end = {}; + selection.getRangeAt(r, start, end); + for (var rs = start.value; rs <= end.value; rs++) + actualSelected.push(rs); + } + + is(compareArrays(selected, actualSelected), true, testid + " range selection [" + selected + "]"); +} + +function testtag_tree_column_reorder() +{ + // Make sure the tree is scrolled into the view, otherwise the test will + // fail + var testframe = window.parent.document.getElementById("testframe"); + if (testframe) { + testframe.scrollIntoView(); + } + + var tree = document.getElementById("tree-column-reorder"); + var numColumns = tree.columns.count; + + var reference = []; + for (let i = 0; i < numColumns; i++) { + reference.push("col_" + i); + } + + // Drag the first column to each position + for (let i = 0; i < numColumns - 1; i++) { + synthesizeColumnDrag(tree, i, i + 1, true); + arrayMove(reference, i, i + 1, true); + checkColumns(tree, reference, "drag first column right"); + } + + // And back + for (let i = numColumns - 1; i >= 1; i--) { + synthesizeColumnDrag(tree, i, i - 1, false); + arrayMove(reference, i, i - 1, false); + checkColumns(tree, reference, "drag last column left"); + } + + // Drag each column one column left + for (let i = 1; i < numColumns; i++) { + synthesizeColumnDrag(tree, i, i - 1, false); + arrayMove(reference, i, i - 1, false); + checkColumns(tree, reference, "drag each column left"); + } + + // And back + for (let i = numColumns - 2; i >= 0; i--) { + synthesizeColumnDrag(tree, i, i + 1, true); + arrayMove(reference, i, i + 1, true); + checkColumns(tree, reference, "drag each column right"); + } + + // Drag each column 5 to the right + for (let i = 0; i < numColumns - 5; i++) { + synthesizeColumnDrag(tree, i, i + 5, true); + arrayMove(reference, i, i + 5, true); + checkColumns(tree, reference, "drag each column 5 to the right"); + } + + // And to the left + for (let i = numColumns - 6; i >= 5; i--) { + synthesizeColumnDrag(tree, i, i - 5, false); + arrayMove(reference, i, i - 5, false); + checkColumns(tree, reference, "drag each column 5 to the left"); + } + + // Test that moving a column after itself does not move anything + synthesizeColumnDrag(tree, 0, 0, true); + checkColumns(tree, reference, "drag to itself"); + is(document.treecolDragging, null, "drag to itself completed"); + + // XXX roc should this be here??? + SimpleTest.finish(); +} + +function testtag_tree_wheel(aTree) +{ + const deltaModes = [ + WheelEvent.DOM_DELTA_PIXEL, // 0 + WheelEvent.DOM_DELTA_LINE, // 1 + WheelEvent.DOM_DELTA_PAGE // 2 + ]; + function helper(aStart, aDelta, aIntDelta, aDeltaMode) + { + aTree.treeBoxObject.scrollToRow(aStart); + var expected; + if (!aIntDelta) { + expected = aStart; + } + else if (aDeltaMode != WheelEvent.DOM_DELTA_PAGE) { + expected = aStart + aIntDelta; + } + else if (aIntDelta > 0) { + expected = aStart + aTree.treeBoxObject.getPageLength(); + } + else { + expected = aStart - aTree.treeBoxObject.getPageLength(); + } + + if (expected < 0) { + expected = 0; + } + if (expected > aTree.view.rowCount - aTree.treeBoxObject.getPageLength()) { + expected = aTree.view.rowCount - aTree.treeBoxObject.getPageLength(); + } + synthesizeWheel(aTree.body, 1, 1, + { deltaMode: aDeltaMode, deltaY: aDelta, + lineOrPageDeltaY: aIntDelta }); + is(aTree.treeBoxObject.getFirstVisibleRow(), expected, + "testtag_tree_wheel: vertical, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + aIntDelta + + " aDeltaMode " + aDeltaMode); + + aTree.treeBoxObject.scrollToRow(aStart); + // Check that horizontal scrolling has no effect + synthesizeWheel(aTree.body, 1, 1, + { deltaMode: aDeltaMode, deltaX: aDelta, + lineOrPageDeltaX: aIntDelta }); + is(aTree.treeBoxObject.getFirstVisibleRow(), aStart, + "testtag_tree_wheel: horizontal, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + aIntDelta + + " aDeltaMode " + aDeltaMode); + } + + var defaultPrevented = 0; + + function wheelListener(event) { + defaultPrevented++; + } + window.addEventListener("wheel", wheelListener, false); + + deltaModes.forEach(function(aDeltaMode) { + var delta = (aDeltaMode == WheelEvent.DOM_DELTA_PIXEL) ? 5.0 : 0.3; + helper(2, -delta, 0, aDeltaMode); + helper(2, -delta, -1, aDeltaMode); + helper(2, delta, 0, aDeltaMode); + helper(2, delta, 1, aDeltaMode); + helper(2, -2 * delta, 0, aDeltaMode); + helper(2, -2 * delta, -1, aDeltaMode); + helper(2, 2 * delta, 0, aDeltaMode); + helper(2, 2 * delta, 1, aDeltaMode); + }); + + window.removeEventListener("wheel", wheelListener, false); + is(defaultPrevented, 48, "wheel event default prevented"); +} + +function synthesizeColumnDrag(aTree, aMouseDownColumnNumber, aMouseUpColumnNumber, aAfter) +{ + var columns = getSortedColumnArray(aTree); + + var down = columns[aMouseDownColumnNumber].element; + var up = columns[aMouseUpColumnNumber].element; + + // Target the initial mousedown in the middle of the column header so we + // avoid the extra hit test space given to the splitter + var columnWidth = down.boxObject.width; + var splitterHitWidth = columnWidth / 2; + synthesizeMouse(down, splitterHitWidth, 3, { type: "mousedown"}); + + var offsetX = 0; + if (aAfter) { + offsetX = columnWidth; + } + + if (aMouseUpColumnNumber > aMouseDownColumnNumber) { + for (let i = aMouseDownColumnNumber; i <= aMouseUpColumnNumber; i++) { + let move = columns[i].element; + synthesizeMouse(move, offsetX, 3, { type: "mousemove"}); + } + } + else { + for (let i = aMouseDownColumnNumber; i >= aMouseUpColumnNumber; i--) { + let move = columns[i].element; + synthesizeMouse(move, offsetX, 3, { type: "mousemove"}); + } + } + + synthesizeMouse(up, offsetX, 3, { type: "mouseup"}); +} + +function arrayMove(aArray, aFrom, aTo, aAfter) +{ + var o = aArray.splice(aFrom, 1)[0]; + if (aTo > aFrom) { + aTo--; + } + + if (aAfter) { + aTo++; + } + + aArray.splice(aTo, 0, o); +} + +function getSortedColumnArray(aTree) +{ + var columns = aTree.columns; + var array = []; + for (let i = 0; i < columns.length; i++) { + array.push(columns.getColumnAt(i)); + } + + array.sort(function(a, b) { + var o1 = parseInt(a.element.getAttribute("ordinal")); + var o2 = parseInt(b.element.getAttribute("ordinal")); + return o1 - o2; + }); + return array; +} + +function checkColumns(aTree, aReference, aMessage) +{ + var columns = getSortedColumnArray(aTree); + var ids = []; + columns.forEach(function(e) { + ids.push(e.element.id); + }); + is(compareArrays(ids, aReference), true, aMessage); +} + +function mouseOnCell(tree, row, column, testname) +{ + var rect = tree.boxObject.getCoordsForCellItem(row, column, "text"); + + synthesizeMouseExpectEvent(tree.body, rect.x, rect.y, {}, tree, "select", testname); +} + +function mouseClickOnColumnHeader(aColumns, aColumnIndex, aButton, aClickCount) +{ + var columnHeader = aColumns[aColumnIndex].element; + var columnHeaderRect = columnHeader.getBoundingClientRect(); + var columnWidth = columnHeaderRect.right - columnHeaderRect.left; + // For multiple click we send separate click events, with increasing + // clickCount. This simulates the common behavior of multiple clicks. + for (let i = 1; i <= aClickCount; i++) { + // Target the middle of the column header. + synthesizeMouse(columnHeader, columnWidth / 2, 3, + { button: aButton, + clickCount: i }, null); + } +} + +function mouseDblClickOnCell(tree, row, column, testname) +{ + // select the row we will edit + var selection = tree.view.selection; + selection.select(row); + tree.treeBoxObject.ensureRowIsVisible(row); + + // get cell coordinates + var rect = tree.treeBoxObject.getCoordsForCellItem(row, column, "text"); + + synthesizeMouse(tree.body, rect.x, rect.y, { clickCount: 2 }, null); +} + +function compareArrays(arr1, arr2) +{ + if (arr1.length != arr2.length) + return false; + + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] != arr2[i]) + return false; + } + + return true; +} + +function convertProperties(arr) +{ + var results = []; + var count = arr.Count(); + for (let i = 0; i < count; i++) + results.push(arr.GetElementAt(i).QueryInterface(Components.interfaces.nsIAtom).toString()); + + results.sort(); + return results.join(" "); +} + +function convertDOMtoTreeRowInfo(treechildren, level, rowidx) +{ + var obj = { rows: [] }; + + var parentidx = rowidx.value; + + treechildren = treechildren.childNodes; + for (var r = 0; r < treechildren.length; r++) { + rowidx.value++; + + var treeitem = treechildren[r]; + if (treeitem.hasChildNodes()) { + var treerow = treeitem.firstChild; + var cellInfo = []; + for (var c = 0; c < treerow.childNodes.length; c++) { + var cell = treerow.childNodes[c]; + cellInfo.push({ label: "" + cell.getAttribute("label"), + value: cell.getAttribute("value"), + properties: cell.getAttribute("properties"), + editable: cell.getAttribute("editable") != "false", + selectable: cell.getAttribute("selectable") != "false", + image: cell.getAttribute("src"), + mode: cell.hasAttribute("mode") ? parseInt(cell.getAttribute("mode")) : 3 }); + } + + var descendants = treeitem.lastChild; + var children = (treerow == descendants) ? null : + convertDOMtoTreeRowInfo(descendants, level + 1, rowidx); + obj.rows.push({ cells: cellInfo, + properties: treerow.getAttribute("properties"), + container: treeitem.getAttribute("container") == "true", + separator: treeitem.localName == "treeseparator", + children: children, + level: level, + parent: parentidx }); + } + } + + return obj; +} diff --git a/toolkit/content/tests/widgets/video.ogg b/toolkit/content/tests/widgets/video.ogg Binary files differnew file mode 100644 index 0000000000..ac7ece3519 --- /dev/null +++ b/toolkit/content/tests/widgets/video.ogg diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html b/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html new file mode 100644 index 0000000000..1f7e76a7d0 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1a.html b/toolkit/content/tests/widgets/videocontrols_direction-1a.html new file mode 100644 index 0000000000..a4d3546294 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1a.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html dir="rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1b.html b/toolkit/content/tests/widgets/videocontrols_direction-1b.html new file mode 100644 index 0000000000..a14b11d5ff --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1b.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html style="direction: rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1c.html b/toolkit/content/tests/widgets/videocontrols_direction-1c.html new file mode 100644 index 0000000000..0885ebd893 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1c.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="direction: rtl"> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1d.html b/toolkit/content/tests/widgets/videocontrols_direction-1d.html new file mode 100644 index 0000000000..a39accec72 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1d.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav" dir="rtl"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1e.html b/toolkit/content/tests/widgets/videocontrols_direction-1e.html new file mode 100644 index 0000000000..25e7c2c1f9 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1e.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav" style="direction: rtl;"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html b/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html new file mode 100644 index 0000000000..630177883c --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2a.html b/toolkit/content/tests/widgets/videocontrols_direction-2a.html new file mode 100644 index 0000000000..2e40cdc1a7 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2a.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html dir="rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2b.html b/toolkit/content/tests/widgets/videocontrols_direction-2b.html new file mode 100644 index 0000000000..2e4dadb6ff --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2b.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html style="direction: rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2c.html b/toolkit/content/tests/widgets/videocontrols_direction-2c.html new file mode 100644 index 0000000000..a43b03e8f9 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2c.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="direction: rtl"> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2d.html b/toolkit/content/tests/widgets/videocontrols_direction-2d.html new file mode 100644 index 0000000000..52d56f1ccd --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2d.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav" dir="rtl"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2e.html b/toolkit/content/tests/widgets/videocontrols_direction-2e.html new file mode 100644 index 0000000000..58bc30e2b3 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2e.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav" style="direction: rtl;"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction_test.js b/toolkit/content/tests/widgets/videocontrols_direction_test.js new file mode 100644 index 0000000000..8ad76c0644 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction_test.js @@ -0,0 +1,90 @@ +var RemoteCanvas = function(url, id) { + this.url = url; + this.id = id; + this.snapshot = null; +}; + +RemoteCanvas.CANVAS_WIDTH = 200; +RemoteCanvas.CANVAS_HEIGHT = 200; + +RemoteCanvas.prototype.compare = function(otherCanvas, expected) { + return compareSnapshots(this.snapshot, otherCanvas.snapshot, expected)[0]; +} + +RemoteCanvas.prototype.load = function(callback) { + var iframe = document.createElement("iframe"); + iframe.id = this.id + "-iframe"; + iframe.width = RemoteCanvas.CANVAS_WIDTH + "px"; + iframe.height = RemoteCanvas.CANVAS_HEIGHT + "px"; + iframe.src = this.url; + var me = this; + iframe.addEventListener("load", function() { + info("iframe loaded"); + var m = iframe.contentDocument.getElementById("av"); + m.addEventListener("suspend", function(aEvent) { + m.removeEventListener("suspend", arguments.callee, false); + setTimeout(function() { + me.remotePageLoaded(callback); + }, 0); + }, false); + m.src = m.getAttribute("source"); + }, false); + window.document.body.appendChild(iframe); +}; + +RemoteCanvas.prototype.remotePageLoaded = function(callback) { + var ldrFrame = document.getElementById(this.id + "-iframe"); + this.snapshot = snapshotWindow(ldrFrame.contentWindow); + this.snapshot.id = this.id + "-canvas"; + window.document.body.appendChild(this.snapshot); + callback(this); +}; + +RemoteCanvas.prototype.cleanup = function() { + var iframe = document.getElementById(this.id + "-iframe"); + iframe.parentNode.removeChild(iframe); + var canvas = document.getElementById(this.id + "-canvas"); + canvas.parentNode.removeChild(canvas); +}; + +function runTest(index) { + var canvases = []; + function testCallback(canvas) { + canvases.push(canvas); + + if (canvases.length == 2) { // when both canvases are loaded + var expectedEqual = currentTest.op == "=="; + var result = canvases[0].compare(canvases[1], expectedEqual); + ok(result, "Rendering of reftest " + currentTest.test + " should " + + (expectedEqual ? "not " : "") + "be different to the reference"); + + if (result) { + canvases[0].cleanup(); + canvases[1].cleanup(); + } + else { + info("Snapshot of canvas 1: " + canvases[0].snapshot.toDataURL()); + info("Snapshot of canvas 2: " + canvases[1].snapshot.toDataURL()); + } + + if (index < tests.length - 1) + runTest(index + 1); + else + SimpleTest.finish(); + } + } + + var currentTest = tests[index]; + var testCanvas = new RemoteCanvas(currentTest.test, "test-" + index); + testCanvas.load(testCallback); + + var refCanvas = new RemoteCanvas(currentTest.ref, "ref-" + index); + refCanvas.load(testCallback); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); + +window.addEventListener("load", function() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, function() { runTest(0); }); +}, true); diff --git a/toolkit/content/tests/widgets/videomask.css b/toolkit/content/tests/widgets/videomask.css new file mode 100644 index 0000000000..066d441388 --- /dev/null +++ b/toolkit/content/tests/widgets/videomask.css @@ -0,0 +1,23 @@ +html, body { + margin: 0; + padding: 0; +} + +audio, video { + width: 140px; + height: 100px; + background-color: black; +} + +/** + * Create a mask for the video direction tests which covers up the throbber. + */ +#mask { + position: absolute; + z-index: 3; + width: 140px; + height: 72px; + background-color: green; + top: 0; + right: 0; +} diff --git a/toolkit/content/tests/widgets/window_menubar.xul b/toolkit/content/tests/widgets/window_menubar.xul new file mode 100644 index 0000000000..b7669e0b37 --- /dev/null +++ b/toolkit/content/tests/widgets/window_menubar.xul @@ -0,0 +1,820 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<!-- the condition in the focus event handler is because pressing Tab + unfocuses and refocuses the window on Windows --> + +<window title="Popup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<!-- + Need to investigate these tests a bit more. Some of the accessibility events + are firing multiple times or in different orders in different circumstances. + Note that this was also the case before bug 279703. + --> + +<hbox style="margin-left: 275px; margin-top: 275px;"> +<menubar id="menubar"> + <menu id="filemenu" label="File" accesskey="F"> + <menupopup id="filepopup"> + <menuitem id="item1" label="Open" accesskey="O"/> + <menuitem id="item2" label="Save" accesskey="S"/> + <menuitem id="item3" label="Close" accesskey="C"/> + </menupopup> + </menu> + <menu id="secretmenu" label="Secret Menu" accesskey="S" disabled="true"> + <menupopup> + <menuitem label="Secret Command" accesskey="S"/> + </menupopup> + </menu> + <menu id="editmenu" label="Edit" accesskey="E"> + <menupopup id="editpopup"> + <menuitem id="cut" label="Cut" accesskey="t" disabled="true"/> + <menuitem id="copy" label="Copy" accesskey="C"/> + <menuitem id="paste" label="Paste" accesskey="P"/> + </menupopup> + </menu> + <menu id="viewmenu" label="View" accesskey="V"> + <menupopup id="viewpopup"> + <menu id="toolbar" label="Toolbar" accesskey="T"> + <menupopup id="toolbarpopup"> + <menuitem id="navigation" label="Navigation" accesskey="N" disabled="true"/> + <menuitem label="Bookmarks" accesskey="B" disabled="true"/> + </menupopup> + </menu> + <menuitem label="Status Bar" accesskey="S"/> + <menu label="Sidebar" accesskey="d"> + <menupopup> + <menuitem label="Bookmarks" accesskey="B"/> + <menuitem label="History" accesskey="H"/> + </menupopup> + </menu> + </menupopup> + </menu> + <menu id="helpmenu" label="Help" accesskey="H"> + <menupopup id="helppopup" > + <label value="Unselectable"/> + <menuitem id="contents" label="Contents" accesskey="C"/> + <menuitem label="More Info" accesskey="I"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem label="Another Menu"/> + <menuitem id="one" label="One"/> + <menu id="only" label="Only Menu"> + <menupopup> + <menuitem label="Test Submenu"/> + </menupopup> + </menu> + <menuitem label="Second Menu"/> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="third" label="Third Menu"/> + <menuitem label="One Other Menu"/> + <label value="Unselectable"/> + <menuitem id="about" label="About" accesskey="A"/> + </menupopup> + </menu> +</menubar> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +window.opener.SimpleTest.waitForFocus(function () { + gFilePopup = document.getElementById("filepopup"); + var filemenu = document.getElementById("filemenu"); + filemenu.focus(); + is(filemenu.openedWithKey, false, "initial openedWithKey"); + startPopupTests(popupTests); +}, window); + +// On Linux, the first menu opens when F10 is pressed, but on other platforms +// the menubar is focused but no menu is opened. This means that different events +// fire. +function pressF10Events() +{ + return navigator.platform.indexOf("Linux") >= 0 ? + [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu", "popupshowing filepopup", "DOMMenuItemActive item1", "popupshown filepopup"] : + [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ]; +} + +function closeAfterF10Events(extraInactive) +{ + if (navigator.platform.indexOf("Linux") >= 0) { + var events = [ "popuphiding filepopup", "popuphidden filepopup", "DOMMenuItemInactive item1", + "DOMMenuInactive filepopup", "DOMMenuBarInactive menubar", + "DOMMenuItemInactive filemenu" ]; + if (extraInactive) + events.push("DOMMenuItemInactive filemenu"); + return events; + } + + return [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu" ]; +} + +var popupTests = [ +{ + testname: "press on menu", + events: [ "popupshowing filepopup", "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", "popupshown filepopup" ], + test: function() { synthesizeMouse(document.getElementById("filemenu"), 8, 8, { }); }, + result: function (testname) { + checkActive(gFilePopup, "", testname); + checkOpen("filemenu", testname); + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // check that pressing cursor down while there is no selection + // highlights the first item + testname: "cursor down no selection", + events: [ "DOMMenuItemActive item1" ], + test: function() { sendKey("DOWN"); }, + result: function(testname) { checkActive(gFilePopup, "item1", testname); } +}, +{ + // check that pressing cursor up wraps and highlights the last item + testname: "cursor up wrap", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive item3" ], + test: function() { sendKey("UP"); }, + result: function(testname) { checkActive(gFilePopup, "item3", testname); } +}, +{ + // check that pressing cursor down wraps and highlights the first item + testname: "cursor down wrap", + events: [ "DOMMenuItemInactive item3", "DOMMenuItemActive item1" ], + test: function() { sendKey("DOWN"); }, + result: function(testname) { checkActive(gFilePopup, "item1", testname); } +}, +{ + // check that pressing cursor down highlights the second item + testname: "cursor down", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive item2" ], + test: function() { sendKey("DOWN"); }, + result: function(testname) { checkActive(gFilePopup, "item2", testname); } +}, +{ + // check that pressing cursor up highlights the second item + testname: "cursor up", + events: [ "DOMMenuItemInactive item2", "DOMMenuItemActive item1" ], + test: function() { sendKey("UP"); }, + result: function(testname) { checkActive(gFilePopup, "item1", testname); } +}, + +{ + // cursor right should skip the disabled menu and move to the edit menu + testname: "cursor right skip disabled", + events: function() { + var elist = [ + // the file menu gets deactivated, the file menu gets hidden, then + // the edit menu is activated + "DOMMenuItemInactive filemenu", "DOMMenuItemActive editmenu", + "popuphiding filepopup", "popuphidden filepopup", + // the popupshowing event gets fired when showing the edit menu. + // The item from the file menu doesn't get deactivated until the + // next item needs to be selected + "popupshowing editpopup", "DOMMenuItemInactive item1", + // not sure why the menu inactivated event is firing so late + "DOMMenuInactive filepopup" + ]; + // finally, the first item is activated and popupshown is fired. + // On Windows, don't skip disabled items. + if (navigator.platform.indexOf("Win") == 0) + elist.push("DOMMenuItemActive cut"); + else + elist.push("DOMMenuItemActive copy"); + elist.push("popupshown editpopup"); + return elist; + }, + test: function() { sendKey("RIGHT"); }, + result: function(testname) { + var expected = (navigator.platform.indexOf("Win") == 0) ? "cut" : "copy"; + checkActive(document.getElementById("editpopup"), expected, testname); + checkClosed("filemenu", testname); + checkOpen("editmenu", testname); + is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // on Windows, a disabled item is selected, so pressing RETURN should close + // the menu but not fire a command event + testname: "enter on disabled", + events: function() { + if (navigator.platform.indexOf("Win") == 0) + return [ "popuphiding editpopup", "popuphidden editpopup", + "DOMMenuItemInactive cut", "DOMMenuInactive editpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive editmenu", "DOMMenuItemInactive editmenu" ]; + else + return [ "DOMMenuItemInactive copy", "DOMMenuInactive editpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive editmenu", "DOMMenuItemInactive editmenu", + "command copy", "popuphiding editpopup", "popuphidden editpopup", + "DOMMenuItemInactive copy" ]; + }, + test: function() { sendKey("RETURN"); }, + result: function(testname) { + checkClosed("editmenu", testname); + is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // pressing Alt + a key should open the corresponding menu + testname: "open with accelerator", + events: function() { + return [ "DOMMenuBarActive menubar", + "popupshowing viewpopup", "DOMMenuItemActive viewmenu", + "DOMMenuItemActive toolbar", "popupshown viewpopup" ]; + }, + test: function() { synthesizeKey("V", { altKey: true }); }, + result: function(testname) { + checkOpen("viewmenu", testname); + is(document.getElementById("viewmenu").openedWithKey, true, testname + " openedWithKey"); + } +}, +{ + // open the submenu with the cursor right key + testname: "open submenu with cursor right", + events: function() { + // on Windows, the disabled 'navigation' item can stll be highlihted + if (navigator.platform.indexOf("Win") == 0) + return [ "popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + else + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test: function() { sendKey("RIGHT"); }, + result: function(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + } +}, +{ + // close the submenu with the cursor left key + testname: "close submenu with cursor left", + events: function() { + if (navigator.platform.indexOf("Win") == 0) + return [ "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "DOMMenuItemInactive navigation", "DOMMenuInactive toolbarpopup", + "DOMMenuItemActive toolbar" ]; + else + return [ "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "DOMMenuInactive toolbarpopup", + "DOMMenuItemActive toolbar" ]; + }, + test: function() { sendKey("LEFT"); }, + result: function(testname) { + checkOpen("viewmenu", testname); + checkClosed("toolbar", testname); + } +}, +{ + // open the submenu with the enter key + testname: "open submenu with enter", + events: function() { + // on Windows, the disabled 'navigation' item can stll be highlighted + if (navigator.platform.indexOf("Win") == 0) + return [ "popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + else + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test: function() { sendKey("RETURN"); }, + result: function(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + }, +}, +{ + // close the submenu with the escape key + testname: "close submenu with escape", + events: function() { + if (navigator.platform.indexOf("Win") == 0) + return [ "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "DOMMenuItemInactive navigation", "DOMMenuInactive toolbarpopup", + "DOMMenuItemActive toolbar" ]; + else + return [ "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "DOMMenuInactive toolbarpopup", + "DOMMenuItemActive toolbar" ]; + }, + test: function() { sendKey("ESCAPE"); }, + result: function(testname) { + checkOpen("viewmenu", testname); + checkClosed("toolbar", testname); + }, +}, +{ + // open the submenu with the enter key again + testname: "open submenu with enter again", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: function() { + // on Windows, the disabled 'navigation' item can stll be highlighted + if (navigator.platform.indexOf("Win") == 0) + return [ "popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + else + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test: function() { sendKey("RETURN"); }, + result: function(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + }, +}, +{ + // while a submenu is open, switch to the next toplevel menu with the cursor right key + testname: "while a submenu is open, switch to the next menu with the cursor right", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuItemInactive viewmenu", "DOMMenuItemActive helpmenu", + "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "popuphiding viewpopup", "popuphidden viewpopup", + "popupshowing helppopup", "DOMMenuItemInactive navigation", + "DOMMenuInactive toolbarpopup", "DOMMenuItemInactive toolbar", + "DOMMenuInactive viewpopup", "DOMMenuItemActive contents", + "popupshown helppopup" ], + test: function() { sendKey("RIGHT"); }, + result: function(testname) { + checkOpen("helpmenu", testname); + checkClosed("toolbar", testname); + checkClosed("viewmenu", testname); + } +}, +{ + // close the main menu with the escape key + testname: "close menubar menu with escape", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "popuphiding helppopup", "popuphidden helppopup", + "DOMMenuItemInactive contents", "DOMMenuInactive helppopup", + "DOMMenuBarInactive menubar", "DOMMenuItemInactive helpmenu" ], + test: function() { sendKey("ESCAPE"); }, + result: function(testname) { checkClosed("viewmenu", testname); }, +}, +{ + // close the main menu with the escape key + testname: "close menubar menu with escape", + condition: function() { return (navigator.platform.indexOf("Win") != 0) }, + events: [ "popuphiding viewpopup", "popuphidden viewpopup", + "DOMMenuItemInactive toolbar", "DOMMenuInactive viewpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive viewmenu" ], + test: function() { sendKey("ESCAPE"); }, + result: function(testname) { checkClosed("viewmenu", testname); }, +}, +{ + // Pressing Alt should highlight the first menu but not open it, + // but it should be ignored if the alt keydown event is consumed. + testname: "alt shouldn't activate menubar if keydown event is consumed", + test: function() { + document.addEventListener("keydown", function (aEvent) { + document.removeEventListener("keydown", arguments.callee, true); + aEvent.preventDefault(); + }, true); + sendKey("ALT"); + }, + result: function(testname) { + ok(!document.getElementById("filemenu").openedWithKey, testname); + checkClosed("filemenu", testname); + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it, + // but it should be ignored if the alt keyup event is consumed. + testname: "alt shouldn't activate menubar if keyup event is consumed", + test: function() { + document.addEventListener("keyup", function (aEvent) { + document.removeEventListener("keyup", arguments.callee, true); + aEvent.preventDefault(); + }, true); + sendKey("ALT"); + }, + result: function(testname) { + ok(!document.getElementById("filemenu").openedWithKey, testname); + checkClosed("filemenu", testname); + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it. + testname: "alt to activate menubar", + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test: function() { sendKey("ALT"); }, + result: function(testname) { + is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey"); + checkClosed("filemenu", testname); + }, +}, +{ + // pressing cursor left should select the previous menu but not open it + testname: "cursor left on active menubar", + events: [ "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu" ], + test: function() { sendKey("LEFT"); }, + result: function(testname) { checkClosed("helpmenu", testname); }, +}, +{ + // pressing cursor right should select the previous menu but not open it + testname: "cursor right on active menubar", + events: [ "DOMMenuItemInactive helpmenu", "DOMMenuItemActive filemenu" ], + test: function() { sendKey("RIGHT"); }, + result: function(testname) { checkClosed("filemenu", testname); }, +}, +{ + // pressing a character should act as an accelerator and open the menu + testname: "accelerator on active menubar", + events: [ "popupshowing helppopup", + "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu", + "DOMMenuItemActive contents", "popupshown helppopup" ], + test: function() { sendChar("h"); }, + result: function(testname) { + checkOpen("helpmenu", testname); + is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey"); + }, +}, +{ + // check that pressing cursor up skips non menuitems + testname: "cursor up wrap", + events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive about" ], + test: function() { sendKey("UP"); }, + result: function(testname) { } +}, +{ + // check that pressing cursor down skips non menuitems + testname: "cursor down wrap", + events: [ "DOMMenuItemInactive about", "DOMMenuItemActive contents" ], + test: function() { sendKey("DOWN"); }, + result: function(testname) { } +}, +{ + // check that pressing a menuitem's accelerator selects it + testname: "menuitem accelerator", + events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", "DOMMenuInactive helppopup", + "DOMMenuBarInactive menubar", "DOMMenuItemInactive helpmenu", + "DOMMenuItemInactive helpmenu", + "command amenu", "popuphiding helppopup", "popuphidden helppopup", + "DOMMenuItemInactive amenu", + ], + test: function() { sendChar("m"); }, + result: function(testname) { checkClosed("helpmenu", testname); } +}, +{ + // pressing F10 should highlight the first menu. On Linux, the menu is opened. + testname: "F10 to activate menubar", + events: pressF10Events(), + test: function() { sendKey("F10"); }, + result: function(testname) { + is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey"); + if (navigator.platform.indexOf("Linux") >= 0) + checkOpen("filemenu", testname); + else + checkClosed("filemenu", testname); + }, +}, +{ + // pressing cursor left then down should open a menu + testname: "cursor down on menu", + events: (navigator.platform.indexOf("Linux") >= 0) ? + [ "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu", + // This is in a different order than the + // "accelerator on active menubar" because menus opened from a + // shortcut key are fired asynchronously + "popuphiding filepopup", "popuphidden filepopup", + "popupshowing helppopup", "DOMMenuItemInactive item1", + "DOMMenuItemActive item2", "DOMMenuItemInactive item2", + "DOMMenuInactive filepopup", "DOMMenuItemActive contents", + "popupshown helppopup" ] : + [ "popupshowing helppopup", "DOMMenuItemInactive filemenu", + "DOMMenuItemActive helpmenu", + // This is in a different order than the + // "accelerator on active menubar" because menus opened from a + // shortcut key are fired asynchronously + "DOMMenuItemActive contents", "popupshown helppopup" ], + test: function() { sendKey("LEFT"); sendKey("DOWN"); }, + result: function(testname) { + is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey"); + } +}, +{ + // pressing a letter that doesn't correspond to an accelerator. The menu + // should not close because there is more than one item corresponding to + // that letter + testname: "menuitem with no accelerator", + events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive one" ], + test: function() { sendChar("o"); }, + result: function(testname) { checkOpen("helpmenu", testname); } +}, +{ + // pressing the letter again should select the next one that starts with + // that letter + testname: "menuitem with no accelerator again", + events: [ "DOMMenuItemInactive one", "DOMMenuItemActive only" ], + test: function() { sendChar("o"); }, + result: function(testname) { + // 'only' is a menu but it should not be open + checkOpen("helpmenu", testname); + checkClosed("only", testname); + } +}, +{ + // pressing the letter again when the next item is disabled should still + // select the disabled item + testname: "menuitem with no accelerator disabled", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuItemInactive only", "DOMMenuItemActive other" ], + test: function() { sendChar("o"); }, + result: function(testname) { } +}, +{ + // when only one menuitem starting with that letter exists, it should be + // selected and the menu closed + testname: "menuitem with no accelerator single", + events: function() { + var elist = [ "DOMMenuItemInactive other", "DOMMenuItemActive third", + "DOMMenuItemInactive third", "DOMMenuInactive helppopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive helpmenu", + "DOMMenuItemInactive helpmenu", + "command third", "popuphiding helppopup", + "popuphidden helppopup", "DOMMenuItemInactive third", + ]; + if (navigator.platform.indexOf("Win") == -1) + elist[0] = "DOMMenuItemInactive only"; + return elist; + }, + test: function() { sendChar("t"); }, + result: function(testname) { checkClosed("helpmenu", testname); } +}, +{ + // pressing F10 should highlight the first menu but not open it + testname: "F10 to activate menubar again", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test: function() { sendKey("F10"); }, + result: function(testname) { checkClosed("filemenu", testname); }, +}, +{ + // pressing an accelerator for a disabled item should deactivate the menubar + testname: "accelerator for disabled menu", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + test: function() { sendChar("s"); }, + result: function(testname) { + checkClosed("secretmenu", testname); + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + }, +}, +{ + testname: "press on disabled menu", + test: function() { + synthesizeMouse(document.getElementById("secretmenu"), 8, 8, { }); + }, + result: function (testname) { + checkClosed("secretmenu", testname); + } +}, +{ + testname: "press on second menu with shift", + events: [ "popupshowing editpopup", "DOMMenuBarActive menubar", + "DOMMenuItemActive editmenu", "popupshown editpopup" ], + test: function() { + synthesizeMouse(document.getElementById("editmenu"), 8, 8, { shiftKey : true }); + }, + result: function (testname) { + checkOpen("editmenu", testname); + checkActive(document.getElementById("menubar"), "editmenu", testname); + } +}, +{ + testname: "press on disabled menuitem", + test: function() { + synthesizeMouse(document.getElementById("cut"), 8, 8, { }); + }, + result: function (testname) { + checkOpen("editmenu", testname); + } +}, +{ + testname: "press on menuitem", + events: [ "DOMMenuInactive editpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive editmenu", + "DOMMenuItemInactive editmenu", + "command copy", "popuphiding editpopup", "popuphidden editpopup", + "DOMMenuItemInactive copy", + ], + test: function() { + synthesizeMouse(document.getElementById("copy"), 8, 8, { }); + }, + result: function (testname) { + checkClosed("editmenu", testname); + } +}, +{ + // this test ensures that the menu can still be opened by clicking after selecting + // a menuitem from the menu. See bug 399350. + testname: "press on menu after menuitem selected", + events: [ "popupshowing editpopup", "DOMMenuBarActive menubar", + "DOMMenuItemActive editmenu", "popupshown editpopup" ], + test: function() { synthesizeMouse(document.getElementById("editmenu"), 8, 8, { }); }, + result: function (testname) { + checkActive(document.getElementById("editpopup"), "", testname); + checkOpen("editmenu", testname); + } +}, +{ // try selecting a different command + testname: "press on menuitem again", + events: [ "DOMMenuInactive editpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive editmenu", + "DOMMenuItemInactive editmenu", + "command paste", "popuphiding editpopup", "popuphidden editpopup", + "DOMMenuItemInactive paste", + ], + test: function() { + synthesizeMouse(document.getElementById("paste"), 8, 8, { }); + }, + result: function (testname) { + checkClosed("editmenu", testname); + } +}, +{ + testname: "F10 to activate menubar for tab deactivation", + events: pressF10Events(), + test: function() { sendKey("F10"); }, +}, +{ + testname: "Deactivate menubar with tab key", + events: closeAfterF10Events(true), + test: function() { sendKey("TAB"); }, + result: function(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for escape deactivation", + events: pressF10Events(), + test: function() { sendKey("F10"); }, +}, +{ + testname: "Deactivate menubar with escape key", + events: closeAfterF10Events(false), + test: function() { sendKey("ESCAPE"); }, + result: function(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for f10 deactivation", + events: pressF10Events(), + test: function() { sendKey("F10"); }, +}, +{ + testname: "Deactivate menubar with f10 key", + events: closeAfterF10Events(true), + test: function() { sendKey("F10"); }, + result: function(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for alt deactivation", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test: function() { sendKey("F10"); }, +}, +{ + testname: "Deactivate menubar with alt key", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu" ], + test: function() { sendKey("ALT"); }, + result: function(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "Don't activate menubar with mousedown during alt key auto-repeat", + test: function() { + synthesizeKey("VK_ALT", { type: "keydown" }); + synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mousedown", altKey: true }); + synthesizeKey("VK_ALT", { type: "keydown" }); + synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mouseup", altKey: true }); + synthesizeKey("VK_ALT", { type: "keydown" }); + synthesizeKey("VK_ALT", { type: "keyup" }); + }, + result: function (testname) { + checkActive(document.getElementById("menubar"), "", testname); + } +}, + +{ + testname: "Open menu and press alt key by itself - open menu", + events: [ "DOMMenuBarActive menubar", + "popupshowing filepopup", "DOMMenuItemActive filemenu", + "DOMMenuItemActive item1", "popupshown filepopup" ], + test: function() { synthesizeKey("F", { altKey: true }); }, + result: function (testname) { + checkOpen("filemenu", testname); + } +}, +{ + testname: "Open menu and press alt key by itself - close menu", + events: [ "popuphiding filepopup", "popuphidden filepopup", + "DOMMenuItemInactive item1", "DOMMenuInactive filepopup", + "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu", + "DOMMenuItemInactive filemenu" ], + test: function() { + synthesizeKey("VK_ALT", { }); + }, + result: function (testname) { + checkClosed("filemenu", testname); + } +}, + +// Fllowing 4 tests are a test of bug 616797, don't insert any new tests +// between them. +{ + testname: "Open file menu by accelerator", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: function() { + return [ "DOMMenuBarActive menubar", "popupshowing filepopup", + "DOMMenuItemActive filemenu", "DOMMenuItemActive item1", + "popupshown filepopup" ]; + }, + test: function() { + synthesizeKey("VK_ALT", { type: "keydown" }); + synthesizeKey("F", { altKey: true }); + synthesizeKey("VK_ALT", { type: "keyup" }); + } +}, +{ + testname: "Close file menu by click at outside of popup menu", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: function() { + return [ "popuphiding filepopup", "popuphidden filepopup", + "DOMMenuItemInactive item1", "DOMMenuInactive filepopup", + "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu", + "DOMMenuItemInactive filemenu" ]; + }, + test: function() { + // XXX hidePopup() causes DOMMenuItemInactive event to be fired twice. + document.getElementById("filepopup").hidePopup(); + } +}, +{ + testname: "Alt keydown set focus the menubar", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: function() { + return [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ]; + }, + test: function() { + sendKey("ALT"); + }, + result: function (testname) { + checkClosed("filemenu", testname); + } +}, +{ + testname: "unset focus the menubar", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: function() { + return [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu" ]; + }, + test: function() { + sendKey("ALT"); + } +}, + +// bug 625151 +{ + testname: "Alt key state before deactivating the window shouldn't prevent " + + "next Alt key handling", + condition: function() { return (navigator.platform.indexOf("Win") == 0) }, + events: function() { + return [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ]; + }, + test: function() { + synthesizeKey("VK_ALT", { type: "keydown" }); + synthesizeKey("VK_TAB", { type: "keydown" }); // cancels the Alt key + var thisWindow = window; + var newWindow = + window.open("data:text/html,", "_blank", "width=100,height=100"); + newWindow.addEventListener("focus", function () { + newWindow.removeEventListener("focus", arguments.callee, false); + thisWindow.addEventListener("focus", function () { + thisWindow.removeEventListener("focus", arguments.callee, false); + setTimeout(function () { + sendKey("ALT", thisWindow); + }, 0); + }, false); + newWindow.close(); + thisWindow.focus(); + }, false); + } +} + +]; + +]]> +</script> + +</window> diff --git a/toolkit/content/textbox.css b/toolkit/content/textbox.css new file mode 100644 index 0000000000..a16b4fd436 --- /dev/null +++ b/toolkit/content/textbox.css @@ -0,0 +1,35 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ +@namespace html url("http://www.w3.org/1999/xhtml"); /* namespace for HTML elements */ + +html|*.textbox-input { + -moz-appearance: none !important; + text-align: inherit; + text-shadow: inherit; + box-sizing: border-box; + -moz-box-flex: 1; +} + +html|*.textbox-textarea { + -moz-appearance: none !important; + text-shadow: inherit; + box-sizing: border-box; + -moz-box-flex: 1; +} + +/* +html|*.textbox-input::placeholder, +html|*.textbox-textarea::placeholder { + text-align: left; + direction: ltr; +} + +html|*.textbox-input::placeholder:-moz-locale-dir(rtl), +html|*.textbox-textarea::placeholder:-moz-locale-dir(rtl) { + text-align: right; + direction: rtl; +} +*/ diff --git a/toolkit/content/timepicker.xhtml b/toolkit/content/timepicker.xhtml new file mode 100644 index 0000000000..1396223f1b --- /dev/null +++ b/toolkit/content/timepicker.xhtml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> + <title>Time Picker</title> + <link rel="stylesheet" href="chrome://global/skin/timepicker.css"/> + <script type="application/javascript" src="chrome://global/content/bindings/timekeeper.js"></script> + <script type="application/javascript" src="chrome://global/content/bindings/spinner.js"></script> + <script type="application/javascript" src="chrome://global/content/bindings/timepicker.js"></script> +</head> +<body> + <div id="time-picker"></div> + <template id="spinner-template"> + <div class="spinner-container"> + <button class="up"/> + <div class="spinner"></div> + <button class="down"/> + </div> + </template> + <script type="application/javascript"> + // We need to hide the scroll bar but maintain its scrolling + // capability, so using |overflow: hidden| is not an option. + // Instead, we are inserting a user agent stylesheet that is + // capable of selecting scrollbars, and do |display: none|. + var domWinUtls = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindowUtils); + domWinUtls.loadSheetUsingURIString('data:text/css,@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); scrollbar { display: none; }', domWinUtls.AGENT_SHEET); + // Create a TimePicker instance and prepare to be + // initialized by the "TimePickerInit" event from timepicker.xml + new TimePicker(document.getElementById("time-picker")); + </script> +</body> +</html>
\ No newline at end of file diff --git a/toolkit/content/treeUtils.js b/toolkit/content/treeUtils.js new file mode 100644 index 0000000000..b4d1fb17d9 --- /dev/null +++ b/toolkit/content/treeUtils.js @@ -0,0 +1,78 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +var gTreeUtils = { + deleteAll: function (aTree, aView, aItems, aDeletedItems) + { + for (var i = 0; i < aItems.length; ++i) + aDeletedItems.push(aItems[i]); + aItems.splice(0, aItems.length); + var oldCount = aView.rowCount; + aView._rowCount = 0; + aTree.treeBoxObject.rowCountChanged(0, -oldCount); + }, + + deleteSelectedItems: function (aTree, aView, aItems, aDeletedItems) + { + var selection = aTree.view.selection; + selection.selectEventsSuppressed = true; + + var rc = selection.getRangeCount(); + for (var i = 0; i < rc; ++i) { + var min = { }; var max = { }; + selection.getRangeAt(i, min, max); + for (let j = min.value; j <= max.value; ++j) { + aDeletedItems.push(aItems[j]); + aItems[j] = null; + } + } + + var nextSelection = 0; + for (i = 0; i < aItems.length; ++i) { + if (!aItems[i]) { + let j = i; + while (j < aItems.length && !aItems[j]) + ++j; + aItems.splice(i, j - i); + nextSelection = j < aView.rowCount ? j - 1 : j - 2; + aView._rowCount -= j - i; + aTree.treeBoxObject.rowCountChanged(i, i - j); + } + } + + if (aItems.length) { + selection.select(nextSelection); + aTree.treeBoxObject.ensureRowIsVisible(nextSelection); + aTree.focus(); + } + selection.selectEventsSuppressed = false; + }, + + sort: function (aTree, aView, aDataSet, aColumn, aComparator, + aLastSortColumn, aLastSortAscending) + { + var ascending = (aColumn == aLastSortColumn) ? !aLastSortAscending : true; + if (aDataSet.length == 0) + return ascending; + + var numericSort = !isNaN(aDataSet[0][aColumn]); + var sortFunction = null; + if (aComparator) { + sortFunction = function (a, b) { return aComparator(a[aColumn], b[aColumn]); }; + } + aDataSet.sort(sortFunction); + if (!ascending) + aDataSet.reverse(); + + aTree.view.selection.clearSelection(); + aTree.view.selection.select(0); + aTree.treeBoxObject.invalidate(); + aTree.treeBoxObject.ensureRowIsVisible(0); + + return ascending; + } +}; + diff --git a/toolkit/content/viewZoomOverlay.js b/toolkit/content/viewZoomOverlay.js new file mode 100644 index 0000000000..66e054437c --- /dev/null +++ b/toolkit/content/viewZoomOverlay.js @@ -0,0 +1,117 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +/** Document Zoom Management Code + * + * To use this, you'll need to have a getBrowser() function or use the methods + * that accept a browser to be modified. + **/ + +var ZoomManager = { + get _prefBranch() { + delete this._prefBranch; + return this._prefBranch = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + }, + + get MIN() { + delete this.MIN; + return this.MIN = this._prefBranch.getIntPref("zoom.minPercent") / 100; + }, + + get MAX() { + delete this.MAX; + return this.MAX = this._prefBranch.getIntPref("zoom.maxPercent") / 100; + }, + + get useFullZoom() { + return this._prefBranch.getBoolPref("browser.zoom.full"); + }, + + set useFullZoom(aVal) { + this._prefBranch.setBoolPref("browser.zoom.full", aVal); + return aVal; + }, + + get zoom() { + return this.getZoomForBrowser(getBrowser()); + }, + + getZoomForBrowser: function ZoomManager_getZoomForBrowser(aBrowser) { + let zoom = (this.useFullZoom || aBrowser.isSyntheticDocument) + ? aBrowser.fullZoom : aBrowser.textZoom; + // Round to remove any floating-point error. + return Number(zoom.toFixed(2)); + }, + + set zoom(aVal) { + this.setZoomForBrowser(getBrowser(), aVal); + return aVal; + }, + + setZoomForBrowser: function ZoomManager_setZoomForBrowser(aBrowser, aVal) { + if (aVal < this.MIN || aVal > this.MAX) + throw Components.results.NS_ERROR_INVALID_ARG; + + if (this.useFullZoom || aBrowser.isSyntheticDocument) { + aBrowser.textZoom = 1; + aBrowser.fullZoom = aVal; + } else { + aBrowser.textZoom = aVal; + aBrowser.fullZoom = 1; + } + }, + + get zoomValues() { + var zoomValues = this._prefBranch.getCharPref("toolkit.zoomManager.zoomValues") + .split(",").map(parseFloat); + zoomValues.sort((a, b) => a - b); + + while (zoomValues[0] < this.MIN) + zoomValues.shift(); + + while (zoomValues[zoomValues.length - 1] > this.MAX) + zoomValues.pop(); + + delete this.zoomValues; + return this.zoomValues = zoomValues; + }, + + enlarge: function ZoomManager_enlarge() { + var i = this.zoomValues.indexOf(this.snap(this.zoom)) + 1; + if (i < this.zoomValues.length) + this.zoom = this.zoomValues[i]; + }, + + reduce: function ZoomManager_reduce() { + var i = this.zoomValues.indexOf(this.snap(this.zoom)) - 1; + if (i >= 0) + this.zoom = this.zoomValues[i]; + }, + + reset: function ZoomManager_reset() { + this.zoom = 1; + }, + + toggleZoom: function ZoomManager_toggleZoom() { + var zoomLevel = this.zoom; + + this.useFullZoom = !this.useFullZoom; + this.zoom = zoomLevel; + }, + + snap: function ZoomManager_snap(aVal) { + var values = this.zoomValues; + for (var i = 0; i < values.length; i++) { + if (values[i] >= aVal) { + if (i > 0 && aVal - values[i - 1] < values[i] - aVal) + i--; + return values[i]; + } + } + return values[i - 1]; + } +}; diff --git a/toolkit/content/widgets/autocomplete.xml b/toolkit/content/widgets/autocomplete.xml new file mode 100644 index 0000000000..da2bf678d4 --- /dev/null +++ b/toolkit/content/widgets/autocomplete.xml @@ -0,0 +1,2515 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<bindings id="autocompleteBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="autocomplete" role="xul:combobox" + extends="chrome://global/content/bindings/textbox.xml#textbox"> + <resources> + <stylesheet src="chrome://global/content/autocomplete.css"/> + <stylesheet src="chrome://global/skin/autocomplete.css"/> + </resources> + + <content sizetopopup="pref"> + <xul:hbox class="autocomplete-textbox-container" flex="1" xbl:inherits="focused"> + <children includes="image|deck|stack|box"> + <xul:image class="autocomplete-icon" allowevents="true"/> + </children> + + <xul:hbox anonid="textbox-input-box" class="textbox-input-box" flex="1" xbl:inherits="tooltiptext=inputtooltiptext"> + <children/> + <html:input anonid="input" class="autocomplete-textbox textbox-input" + allowevents="true" + xbl:inherits="tooltiptext=inputtooltiptext,value,type=inputtype,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint"/> + </xul:hbox> + <children includes="hbox"/> + </xul:hbox> + + <xul:dropmarker anonid="historydropmarker" class="autocomplete-history-dropmarker" + allowevents="true" + xbl:inherits="open,enablehistory,parentfocused=focused"/> + + <xul:popupset anonid="popupset" class="autocomplete-result-popupset"/> + + <children includes="toolbarbutton"/> + </content> + + <implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement"> + <field name="mController">null</field> + <field name="mSearchNames">null</field> + <field name="mIgnoreInput">false</field> + + <field name="_searchBeginHandler">null</field> + <field name="_searchCompleteHandler">null</field> + <field name="_textEnteredHandler">null</field> + <field name="_textRevertedHandler">null</field> + + <constructor><![CDATA[ + this.mController = Components.classes["@mozilla.org/autocomplete/controller;1"]. + getService(Components.interfaces.nsIAutoCompleteController); + + this._searchBeginHandler = this.initEventHandler("searchbegin"); + this._searchCompleteHandler = this.initEventHandler("searchcomplete"); + this._textEnteredHandler = this.initEventHandler("textentered"); + this._textRevertedHandler = this.initEventHandler("textreverted"); + + // For security reasons delay searches on pasted values. + this.inputField.controllers.insertControllerAt(0, this._pasteController); + ]]></constructor> + + <destructor><![CDATA[ + this.inputField.controllers.removeController(this._pasteController); + ]]></destructor> + + <!-- =================== nsIAutoCompleteInput =================== --> + + <field name="_popup">null</field> + <property name="popup" readonly="true"> + <getter><![CDATA[ + // Memoize the result in a field rather than replacing this property, + // so that it can be reset along with the binding. + if (this._popup) { + return this._popup; + } + + let popup = null; + let popupId = this.getAttribute("autocompletepopup"); + if (popupId) { + popup = document.getElementById(popupId); + } + if (!popup) { + popup = document.createElement("panel"); + popup.setAttribute("type", "autocomplete"); + popup.setAttribute("noautofocus", "true"); + + let popupset = document.getAnonymousElementByAttribute(this, "anonid", "popupset"); + popupset.appendChild(popup); + } + popup.mInput = this; + + return this._popup = popup; + ]]></getter> + </property> + + <property name="controller" onget="return this.mController;" readonly="true"/> + + <property name="popupOpen" + onget="return this.popup.popupOpen;" + onset="if (val) this.openPopup(); else this.closePopup();"/> + + <property name="disableAutoComplete" + onset="this.setAttribute('disableautocomplete', val); return val;" + onget="return this.getAttribute('disableautocomplete') == 'true';"/> + + <property name="completeDefaultIndex" + onset="this.setAttribute('completedefaultindex', val); return val;" + onget="return this.getAttribute('completedefaultindex') == 'true';"/> + + <property name="completeSelectedIndex" + onset="this.setAttribute('completeselectedindex', val); return val;" + onget="return this.getAttribute('completeselectedindex') == 'true';"/> + + <property name="forceComplete" + onset="this.setAttribute('forcecomplete', val); return val;" + onget="return this.getAttribute('forcecomplete') == 'true';"/> + + <property name="minResultsForPopup" + onset="this.setAttribute('minresultsforpopup', val); return val;" + onget="var m = parseInt(this.getAttribute('minresultsforpopup')); return isNaN(m) ? 1 : m;"/> + + <property name="showCommentColumn" + onset="this.setAttribute('showcommentcolumn', val); return val;" + onget="return this.getAttribute('showcommentcolumn') == 'true';"/> + + <property name="showImageColumn" + onset="this.setAttribute('showimagecolumn', val); return val;" + onget="return this.getAttribute('showimagecolumn') == 'true';"/> + + <property name="timeout" + onset="this.setAttribute('timeout', val); return val;"> + <getter><![CDATA[ + // For security reasons delay searches on pasted values. + if (this._valueIsPasted) { + let t = parseInt(this.getAttribute('pastetimeout')); + return isNaN(t) ? 1000 : t; + } + + let t = parseInt(this.getAttribute('timeout')); + return isNaN(t) ? 50 : t; + ]]></getter> + </property> + + <property name="searchParam" + onget="return this.getAttribute('autocompletesearchparam') || '';" + onset="this.setAttribute('autocompletesearchparam', val); return val;"/> + + <property name="searchCount" readonly="true" + onget="this.initSearchNames(); return this.mSearchNames.length;"/> + + <field name="shrinkDelay" readonly="true"> + parseInt(this.getAttribute("shrinkdelay")) || 0 + </field> + + <property name="PrivateBrowsingUtils" readonly="true"> + <getter><![CDATA[ + let module = {}; + Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", module); + Object.defineProperty(this, "PrivateBrowsingUtils", { + configurable: true, + enumerable: true, + writable: true, + value: module.PrivateBrowsingUtils + }); + return module.PrivateBrowsingUtils; + ]]></getter> + </property> + + <property name="inPrivateContext" readonly="true" + onget="return this.PrivateBrowsingUtils.isWindowPrivate(window);"/> + + <property name="noRollupOnCaretMove" readonly="true" + onget="return this.popup.getAttribute('norolluponanchor') == 'true'"/> + + <!-- This is the maximum number of drop-down rows we get when we + hit the drop marker beside fields that have it (like the URLbar).--> + <field name="maxDropMarkerRows" readonly="true">14</field> + + <method name="getSearchAt"> + <parameter name="aIndex"/> + <body><![CDATA[ + this.initSearchNames(); + return this.mSearchNames[aIndex]; + ]]></body> + </method> + + <method name="setTextValueWithReason"> + <parameter name="aValue"/> + <parameter name="aReason"/> + <body><![CDATA[ + if (aReason == Components.interfaces.nsIAutoCompleteInput + .TEXTVALUE_REASON_COMPLETEDEFAULT) { + this._disableTrim = true; + } + this.textValue = aValue; + this._disableTrim = false; + ]]></body> + </method> + + <property name="textValue"> + <getter><![CDATA[ + if (typeof this.onBeforeTextValueGet == "function") { + let result = this.onBeforeTextValueGet(); + if (result) { + return result.value; + } + } + return this.value; + ]]></getter> + <setter><![CDATA[ + if (typeof this.onBeforeTextValueSet == "function") + val = this.onBeforeTextValueSet(val); + + this.value = val; + + // Completing a result should simulate the user typing the result, so + // fire an input event. + let evt = document.createEvent("UIEvents"); + evt.initUIEvent("input", true, false, window, 0); + this.mIgnoreInput = true; + this.dispatchEvent(evt); + this.mIgnoreInput = false; + + return this.value; + ]]></setter> + </property> + + <method name="selectTextRange"> + <parameter name="aStartIndex"/> + <parameter name="aEndIndex"/> + <body><![CDATA[ + this.inputField.setSelectionRange(aStartIndex, aEndIndex); + ]]></body> + </method> + + <method name="onSearchBegin"> + <body><![CDATA[ + if (this.popup && typeof this.popup.onSearchBegin == "function") + this.popup.onSearchBegin(); + if (this._searchBeginHandler) + this._searchBeginHandler(); + ]]></body> + </method> + + <method name="onSearchComplete"> + <body><![CDATA[ + if (this.mController.matchCount == 0) + this.setAttribute("nomatch", "true"); + else + this.removeAttribute("nomatch"); + + if (this.ignoreBlurWhileSearching && !this.focused) { + this.handleEnter(); + this.detachController(); + } + + if (this._searchCompleteHandler) + this._searchCompleteHandler(); + ]]></body> + </method> + + <method name="onTextEntered"> + <parameter name="event"/> + <body><![CDATA[ + let rv = false; + if (this._textEnteredHandler) { + rv = this._textEnteredHandler(event); + } + return rv; + ]]></body> + </method> + + <method name="onTextReverted"> + <body><![CDATA[ + if (this._textRevertedHandler) + return this._textRevertedHandler(); + return false; + ]]></body> + </method> + + <!-- =================== nsIDOMXULMenuListElement =================== --> + + <property name="editable" readonly="true" + onget="return true;" /> + + <property name="crop" + onset="this.setAttribute('crop',val); return val;" + onget="return this.getAttribute('crop');"/> + + <property name="open" + onget="return this.getAttribute('open') == 'true';"> + <setter><![CDATA[ + if (val) + this.showHistoryPopup(); + else + this.closePopup(); + ]]></setter> + </property> + + <!-- =================== PUBLIC MEMBERS =================== --> + + <field name="valueIsTyped">false</field> + <field name="_disableTrim">false</field> + <property name="value"> + <getter><![CDATA[ + if (typeof this.onBeforeValueGet == "function") { + var result = this.onBeforeValueGet(); + if (result) + return result.value; + } + return this.inputField.value; + ]]></getter> + <setter><![CDATA[ + this.mIgnoreInput = true; + + if (typeof this.onBeforeValueSet == "function") + val = this.onBeforeValueSet(val); + + if (typeof this.trimValue == "function" && !this._disableTrim) + val = this.trimValue(val); + + this.valueIsTyped = false; + this.inputField.value = val; + + if (typeof this.formatValue == "function") + this.formatValue(); + + this.mIgnoreInput = false; + var event = document.createEvent('Events'); + event.initEvent('ValueChange', true, true); + this.inputField.dispatchEvent(event); + return val; + ]]></setter> + </property> + + <property name="focused" readonly="true" + onget="return this.getAttribute('focused') == 'true';"/> + + <!-- maximum number of rows to display at a time --> + <property name="maxRows" + onset="this.setAttribute('maxrows', val); return val;" + onget="return parseInt(this.getAttribute('maxrows')) || 0;"/> + + <!-- option to allow scrolling through the list via the tab key, rather than + tab moving focus out of the textbox --> + <property name="tabScrolling" + onset="this.setAttribute('tabscrolling', val); return val;" + onget="return this.getAttribute('tabscrolling') == 'true';"/> + + <!-- option to completely ignore any blur events while searches are + still going on. --> + <property name="ignoreBlurWhileSearching" + onset="this.setAttribute('ignoreblurwhilesearching', val); return val;" + onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/> + + <!-- disable key navigation handling in the popup results --> + <property name="disableKeyNavigation" + onset="this.setAttribute('disablekeynavigation', val); return val;" + onget="return this.getAttribute('disablekeynavigation') == 'true';"/> + + <!-- option to highlight entries that don't have any matches --> + <property name="highlightNonMatches" + onset="this.setAttribute('highlightnonmatches', val); return val;" + onget="return this.getAttribute('highlightnonmatches') == 'true';"/> + + <!-- =================== PRIVATE MEMBERS =================== --> + + <!-- ::::::::::::: autocomplete controller ::::::::::::: --> + + <method name="attachController"> + <body><![CDATA[ + this.mController.input = this; + ]]></body> + </method> + + <method name="detachController"> + <body><![CDATA[ + if (this.mController.input == this) + this.mController.input = null; + ]]></body> + </method> + + <!-- ::::::::::::: popup opening ::::::::::::: --> + + <method name="openPopup"> + <body><![CDATA[ + if (this.focused) + this.popup.openAutocompletePopup(this, this); + ]]></body> + </method> + + <method name="closePopup"> + <body><![CDATA[ + this.popup.closePopup(); + ]]></body> + </method> + + <method name="showHistoryPopup"> + <body><![CDATA[ + // history dropmarker pushed state + function cleanup(popup) { + popup.removeEventListener("popupshowing", onShow, false); + } + function onShow(event) { + var popup = event.target, input = popup.input; + cleanup(popup); + input.setAttribute("open", "true"); + function onHide() { + input.removeAttribute("open"); + popup.removeEventListener("popuphiding", onHide, false); + } + popup.addEventListener("popuphiding", onHide, false); + } + this.popup.addEventListener("popupshowing", onShow, false); + setTimeout(cleanup, 1000, this.popup); + + // Store our "normal" maxRows on the popup, so that it can reset the + // value when the popup is hidden. + this.popup._normalMaxRows = this.maxRows; + + // Increase our maxRows temporarily, since we want the dropdown to + // be bigger in this case. The popup's popupshowing/popuphiding + // handlers will take care of resetting this. + this.maxRows = this.maxDropMarkerRows; + + // Ensure that we have focus. + if (!this.focused) + this.focus(); + this.attachController(); + this.mController.startSearch(""); + ]]></body> + </method> + + <method name="toggleHistoryPopup"> + <body><![CDATA[ + // If this method is called on the same event tick as the popup gets + // hidden, do nothing to avoid re-opening the popup when the drop + // marker is clicked while the popup is still open. + if (!this.popup.isPopupHidingTick && !this.popup.popupOpen) + this.showHistoryPopup(); + else + this.closePopup(); + ]]></body> + </method> + + <!-- ::::::::::::: event dispatching ::::::::::::: --> + + <method name="initEventHandler"> + <parameter name="aEventType"/> + <body><![CDATA[ + let handlerString = this.getAttribute("on" + aEventType); + if (handlerString) { + return (new Function("eventType", "param", handlerString)).bind(this, aEventType); + } + return null; + ]]></body> + </method> + + <!-- ::::::::::::: key handling ::::::::::::: --> + + <field name="_selectionDetails">null</field> + <method name="onKeyPress"> + <parameter name="aEvent"/> + <body><![CDATA[ + return this.handleKeyPress(aEvent); + ]]></body> + </method> + + <method name="handleKeyPress"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.target.localName != "textbox") + return true; // Let child buttons of autocomplete take input + + // XXXpch this is so bogus... + if (aEvent.defaultPrevented) + return false; + + var cancel = false; + + // Catch any keys that could potentially move the caret. Ctrl can be + // used in combination with these keys on Windows and Linux; and Alt + // can be used on OS X, so make sure the unused one isn't used. + let metaKey = /Mac/.test(navigator.platform) ? aEvent.ctrlKey : aEvent.altKey; + if (!this.disableKeyNavigation && !metaKey) { + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_LEFT: + case KeyEvent.DOM_VK_RIGHT: + case KeyEvent.DOM_VK_HOME: + cancel = this.mController.handleKeyNavigation(aEvent.keyCode); + break; + } + } + + // Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt) + if (!this.disableKeyNavigation && !aEvent.ctrlKey && !aEvent.altKey) { + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_TAB: + if (this.tabScrolling && this.popup.popupOpen) + cancel = this.mController.handleKeyNavigation(aEvent.shiftKey ? + KeyEvent.DOM_VK_UP : + KeyEvent.DOM_VK_DOWN); + else if (this.forceComplete && this.mController.matchCount >= 1) + this.mController.handleTab(); + break; + case KeyEvent.DOM_VK_UP: + case KeyEvent.DOM_VK_DOWN: + case KeyEvent.DOM_VK_PAGE_UP: + case KeyEvent.DOM_VK_PAGE_DOWN: + cancel = this.mController.handleKeyNavigation(aEvent.keyCode); + break; + } + } + + // Handle keys we know aren't part of a shortcut, even with Alt or + // Ctrl. + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_ESCAPE: + cancel = this.mController.handleEscape(); + break; + case KeyEvent.DOM_VK_RETURN: + if (/Mac/.test(navigator.platform)) { + // Prevent the default action, since it will beep on Mac + if (aEvent.metaKey) + aEvent.preventDefault(); + } + if (this.mController.selection) { + this._selectionDetails = { + index: this.mController.selection.currentIndex, + kind: "key" + }; + } + cancel = this.handleEnter(aEvent); + break; + case KeyEvent.DOM_VK_DELETE: + if (/Mac/.test(navigator.platform) && !aEvent.shiftKey) { + break; + } + cancel = this.handleDelete(); + break; + case KeyEvent.DOM_VK_BACK_SPACE: + if (/Mac/.test(navigator.platform) && aEvent.shiftKey) { + cancel = this.handleDelete(); + } + break; + case KeyEvent.DOM_VK_DOWN: + case KeyEvent.DOM_VK_UP: + if (aEvent.altKey) + this.toggleHistoryPopup(); + break; + case KeyEvent.DOM_VK_F4: + if (!/Mac/.test(navigator.platform)) { + this.toggleHistoryPopup(); + } + break; + } + + if (cancel) { + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + + return true; + ]]></body> + </method> + + <method name="handleEnter"> + <parameter name="event"/> + <body><![CDATA[ + return this.mController.handleEnter(false, event || null); + ]]></body> + </method> + + <method name="handleDelete"> + <body><![CDATA[ + return this.mController.handleDelete(); + ]]></body> + </method> + + <!-- ::::::::::::: miscellaneous ::::::::::::: --> + + <method name="initSearchNames"> + <body><![CDATA[ + if (!this.mSearchNames) { + var names = this.getAttribute("autocompletesearch"); + if (!names) + this.mSearchNames = []; + else + this.mSearchNames = names.split(" "); + } + ]]></body> + </method> + + <method name="_focus"> + <!-- doesn't reset this.mController --> + <body><![CDATA[ + this._dontBlur = true; + this.focus(); + this._dontBlur = false; + ]]></body> + </method> + + <method name="resetActionType"> + <body><![CDATA[ + if (this.mIgnoreInput) + return; + this.removeAttribute("actiontype"); + ]]></body> + </method> + + <field name="_valueIsPasted">false</field> + <field name="_pasteController"><![CDATA[ + ({ + _autocomplete: this, + _kGlobalClipboard: Components.interfaces.nsIClipboard.kGlobalClipboard, + supportsCommand: aCommand => aCommand == "cmd_paste", + doCommand: function(aCommand) { + this._autocomplete._valueIsPasted = true; + this._autocomplete.editor.paste(this._kGlobalClipboard); + this._autocomplete._valueIsPasted = false; + }, + isCommandEnabled: function(aCommand) { + return this._autocomplete.editor.isSelectionEditable && + this._autocomplete.editor.canPaste(this._kGlobalClipboard); + }, + onEvent: function() {} + }) + ]]></field> + + <method name="onInput"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!this.mIgnoreInput && this.mController.input == this) { + this.valueIsTyped = true; + this.mController.handleText(); + } + this.resetActionType(); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="input"><![CDATA[ + this.onInput(event); + ]]></handler> + + <handler event="keypress" phase="capturing" + action="return this.onKeyPress(event);"/> + + <handler event="compositionstart" phase="capturing" + action="if (this.mController.input == this) this.mController.handleStartComposition();"/> + + <handler event="compositionend" phase="capturing" + action="if (this.mController.input == this) this.mController.handleEndComposition();"/> + + <handler event="focus" phase="capturing"><![CDATA[ + this.attachController(); + if (window.gBrowser && window.gBrowser.selectedBrowser.hasAttribute("usercontextid")) { + this.userContextId = parseInt(window.gBrowser.selectedBrowser.getAttribute("usercontextid")); + } else { + this.userContextId = 0; + } + ]]></handler> + + <handler event="blur" phase="capturing"><![CDATA[ + if (!this._dontBlur) { + if (this.forceComplete && this.mController.matchCount >= 1) { + // mousemove sets selected index. Don't blindly use that selected + // index in this blur handler since if the popup is open you can + // easily "select" another match just by moving the mouse over it. + let filledVal = this.value.replace(/.+ >> /, "").toLowerCase(); + let selectedVal = null; + if (this.popup.selectedIndex >= 0) { + selectedVal = this.mController.getFinalCompleteValueAt( + this.popup.selectedIndex); + } + if (selectedVal && filledVal != selectedVal.toLowerCase()) { + for (let i = 0; i < this.mController.matchCount; i++) { + let matchVal = this.mController.getFinalCompleteValueAt(i); + if (matchVal.toLowerCase() == filledVal) { + this.popup.selectedIndex = i; + break; + } + } + } + this.mController.handleEnter(false); + } + if (!this.ignoreBlurWhileSearching) + this.detachController(); + } + ]]></handler> + </handlers> + </binding> + + <binding id="autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup"> + <resources> + <stylesheet src="chrome://global/content/autocomplete.css"/> + <stylesheet src="chrome://global/skin/tree.css"/> + <stylesheet src="chrome://global/skin/autocomplete.css"/> + </resources> + + <content ignorekeys="true" level="top" consumeoutsideclicks="never"> + <xul:tree anonid="tree" class="autocomplete-tree plain" hidecolumnpicker="true" flex="1" seltype="single"> + <xul:treecols anonid="treecols"> + <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/> + </xul:treecols> + <xul:treechildren class="autocomplete-treebody"/> + </xul:tree> + </content> + + <implementation> + <field name="mShowCommentColumn">false</field> + <field name="mShowImageColumn">false</field> + + <property name="showCommentColumn" + onget="return this.mShowCommentColumn;"> + <setter> + <![CDATA[ + if (!val && this.mShowCommentColumn) { + // reset the flex on the value column and remove the comment column + document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 1); + this.removeColumn("treecolAutoCompleteComment"); + } else if (val && !this.mShowCommentColumn) { + // reset the flex on the value column and add the comment column + document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 2); + this.addColumn({id: "treecolAutoCompleteComment", flex: 1}); + } + this.mShowCommentColumn = val; + return val; + ]]> + </setter> + </property> + + <property name="showImageColumn" + onget="return this.mShowImageColumn;"> + <setter> + <![CDATA[ + if (!val && this.mShowImageColumn) { + // remove the image column + this.removeColumn("treecolAutoCompleteImage"); + } else if (val && !this.mShowImageColumn) { + // add the image column + this.addColumn({id: "treecolAutoCompleteImage", flex: 1}); + } + this.mShowImageColumn = val; + return val; + ]]> + </setter> + </property> + + + <method name="addColumn"> + <parameter name="aAttrs"/> + <body> + <![CDATA[ + var col = document.createElement("treecol"); + col.setAttribute("class", "autocomplete-treecol"); + for (var name in aAttrs) + col.setAttribute(name, aAttrs[name]); + this.treecols.appendChild(col); + return col; + ]]> + </body> + </method> + + <method name="removeColumn"> + <parameter name="aColId"/> + <body> + <![CDATA[ + return this.treecols.removeChild(document.getElementById(aColId)); + ]]> + </body> + </method> + + <property name="selectedIndex" + onget="return this.tree.currentIndex;"> + <setter> + <![CDATA[ + this.tree.view.selection.select(val); + if (this.tree.treeBoxObject.height > 0) + this.tree.treeBoxObject.ensureRowIsVisible(val < 0 ? 0 : val); + // Fire select event on xul:tree so that accessibility API + // support layer can fire appropriate accessibility events. + var event = document.createEvent('Events'); + event.initEvent("select", true, true); + this.tree.dispatchEvent(event); + return val; + ]]></setter> + </property> + + <method name="adjustHeight"> + <body> + <![CDATA[ + // detect the desired height of the tree + var bx = this.tree.treeBoxObject; + var view = this.tree.view; + if (!view) + return; + var rows = this.maxRows; + if (!view.rowCount || (rows && view.rowCount < rows)) + rows = view.rowCount; + + var height = rows * bx.rowHeight; + + if (height == 0) { + this.tree.setAttribute("collapsed", "true"); + } else { + if (this.tree.hasAttribute("collapsed")) + this.tree.removeAttribute("collapsed"); + + this.tree.setAttribute("height", height); + } + this.tree.setAttribute("hidescrollbar", view.rowCount <= rows); + ]]> + </body> + </method> + + <method name="openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body><![CDATA[ + // until we have "baseBinding", (see bug #373652) this allows + // us to override openAutocompletePopup(), but still call + // the method on the base class + this._openAutocompletePopup(aInput, aElement); + ]]></body> + </method> + + <method name="_openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body><![CDATA[ + if (!this.mPopupOpen) { + this.mInput = aInput; + this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView); + this.invalidate(); + + this.showCommentColumn = this.mInput.showCommentColumn; + this.showImageColumn = this.mInput.showImageColumn; + + var rect = aElement.getBoundingClientRect(); + var nav = aElement.ownerDocument.defaultView.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation); + var docShell = nav.QueryInterface(Components.interfaces.nsIDocShell); + var docViewer = docShell.contentViewer; + var width = (rect.right - rect.left) * docViewer.fullZoom; + this.setAttribute("width", width > 100 ? width : 100); + + // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840 + var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction; + this.style.direction = popupDirection; + + this.openPopup(aElement, "after_start", 0, 0, false, false); + } + ]]></body> + </method> + + <method name="invalidate"> + <body><![CDATA[ + this.adjustHeight(); + this.tree.treeBoxObject.invalidate(); + ]]></body> + </method> + + <method name="selectBy"> + <parameter name="aReverse"/> + <parameter name="aPage"/> + <body><![CDATA[ + try { + var amount = aPage ? 5 : 1; + this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.tree.view.rowCount-1); + if (this.selectedIndex == -1) { + this.input._focus(); + } + } catch (ex) { + // do nothing - occasionally timer-related js errors happen here + // e.g. "this.selectedIndex has no properties", when you type fast and hit a + // navigation key before this popup has opened + } + ]]></body> + </method> + + <!-- =================== PUBLIC MEMBERS =================== --> + + <field name="tree"> + document.getAnonymousElementByAttribute(this, "anonid", "tree"); + </field> + + <field name="treecols"> + document.getAnonymousElementByAttribute(this, "anonid", "treecols"); + </field> + + <property name="view" + onget="return this.mView;"> + <setter><![CDATA[ + // We must do this by hand because the tree binding may not be ready yet + this.mView = val; + this.tree.boxObject.view = val; + ]]></setter> + </property> + + </implementation> + </binding> + + <binding id="autocomplete-base-popup" role="none" +extends="chrome://global/content/bindings/popup.xml#popup"> + <implementation implements="nsIAutoCompletePopup"> + <field name="mInput">null</field> + <field name="mPopupOpen">false</field> + <field name="mIsPopupHidingTick">false</field> + + <!-- =================== nsIAutoCompletePopup =================== --> + + <property name="input" readonly="true" + onget="return this.mInput"/> + + <property name="overrideValue" readonly="true" + onget="return null;"/> + + <property name="popupOpen" readonly="true" + onget="return this.mPopupOpen;"/> + + <property name="isPopupHidingTick" readonly="true" + onget="return this.mIsPopupHidingTick;"/> + + <method name="closePopup"> + <body> + <![CDATA[ + if (this.mPopupOpen) { + this.hidePopup(); + this.removeAttribute("width"); + } + ]]> + </body> + </method> + + <!-- This is the default number of rows that we give the autocomplete + popup when the textbox doesn't have a "maxrows" attribute + for us to use. --> + <field name="defaultMaxRows" readonly="true">6</field> + + <!-- In some cases (e.g. when the input's dropmarker button is clicked), + the input wants to display a popup with more rows. In that case, it + should increase its maxRows property and store the "normal" maxRows + in this field. When the popup is hidden, we restore the input's + maxRows to the value stored in this field. + + This field is set to -1 between uses so that we can tell when it's + been set by the input and when we need to set it in the popupshowing + handler. --> + <field name="_normalMaxRows">-1</field> + + <property name="maxRows" readonly="true"> + <getter> + <![CDATA[ + return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows; + ]]> + </getter> + </property> + + <method name="getNextIndex"> + <parameter name="aReverse"/> + <parameter name="aAmount"/> + <parameter name="aIndex"/> + <parameter name="aMaxRow"/> + <body><![CDATA[ + if (aMaxRow < 0) + return -1; + + var newIdx = aIndex + (aReverse?-1:1)*aAmount; + if (aReverse && aIndex == -1 || newIdx > aMaxRow && aIndex != aMaxRow) + newIdx = aMaxRow; + else if (!aReverse && aIndex == -1 || newIdx < 0 && aIndex != 0) + newIdx = 0; + + if (newIdx < 0 && aIndex == 0 || newIdx > aMaxRow && aIndex == aMaxRow) + aIndex = -1; + else + aIndex = newIdx; + + return aIndex; + ]]></body> + </method> + + <method name="onPopupClick"> + <parameter name="aEvent"/> + <body><![CDATA[ + var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); + controller.handleEnter(true, aEvent); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="popupshowing"><![CDATA[ + // If normalMaxRows wasn't already set by the input, then set it here + // so that we restore the correct number when the popup is hidden. + + // Null-check this.mInput; see bug 1017914 + if (this._normalMaxRows < 0 && this.mInput) { + this._normalMaxRows = this.mInput.maxRows; + } + + // Set an attribute for styling the popup based on the input. + let inputID = ""; + if (this.mInput && this.mInput.ownerDocument && + this.mInput.ownerDocument.documentURIObject.schemeIs("chrome")) { + inputID = this.mInput.id; + // Take care of elements with no id that are inside xbl bindings + if (!inputID) { + let bindingParent = this.mInput.ownerDocument.getBindingParent(this.mInput); + if (bindingParent) { + inputID = bindingParent.id; + } + } + } + this.setAttribute("autocompleteinput", inputID); + + this.mPopupOpen = true; + ]]></handler> + + <handler event="popuphiding"><![CDATA[ + var isListActive = true; + if (this.selectedIndex == -1) + isListActive = false; + var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); + controller.stopSearch(); + + this.removeAttribute("autocompleteinput"); + this.mPopupOpen = false; + + // Prevent opening popup from historydropmarker mousedown handler + // on the same event tick the popup is hidden by the same mousedown + // event. + this.mIsPopupHidingTick = true; + setTimeout(() => { + this.mIsPopupHidingTick = false; + }, 0); + + // Reset the maxRows property to the cached "normal" value, and reset + // _normalMaxRows so that we can detect whether it was set by the input + // when the popupshowing handler runs. + + // Null-check this.mInput; see bug 1017914 + if (this.mInput) + this.mInput.maxRows = this._normalMaxRows; + this._normalMaxRows = -1; + // If the list was being navigated and then closed, make sure + // we fire accessible focus event back to textbox + + // Null-check this.mInput; see bug 1017914 + if (isListActive && this.mInput) { + this.mInput.mIgnoreFocus = true; + this.mInput._focus(); + this.mInput.mIgnoreFocus = false; + } + ]]></handler> + </handlers> + </binding> + + <binding id="autocomplete-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup"> + <resources> + <stylesheet src="chrome://global/content/autocomplete.css"/> + <stylesheet src="chrome://global/skin/autocomplete.css"/> + </resources> + + <content ignorekeys="true" level="top" consumeoutsideclicks="never"> + <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" flex="1"/> + <xul:hbox> + <children/> + </xul:hbox> + </content> + + <implementation implements="nsIAutoCompletePopup"> + <field name="_currentIndex">0</field> + <field name="_rlbAnimated">false</field> + + <!-- =================== nsIAutoCompletePopup =================== --> + + <property name="selectedIndex" + onget="return this.richlistbox.selectedIndex;"> + <setter> + <![CDATA[ + this.richlistbox.selectedIndex = val; + + // when clearing the selection (val == -1, so selectedItem will be + // null), we want to scroll back to the top. see bug #406194 + this.richlistbox.ensureElementIsVisible( + this.richlistbox.selectedItem || this.richlistbox.firstChild); + + return val; + ]]> + </setter> + </property> + + <method name="onSearchBegin"> + <body><![CDATA[ + this.richlistbox.mouseSelectedIndex = -1; + + if (typeof this._onSearchBegin == "function") { + this._onSearchBegin(); + } + ]]></body> + </method> + + <method name="openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body> + <![CDATA[ + // until we have "baseBinding", (see bug #373652) this allows + // us to override openAutocompletePopup(), but still call + // the method on the base class + this._openAutocompletePopup(aInput, aElement); + ]]> + </body> + </method> + + <method name="_openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body> + <![CDATA[ + if (!this.mPopupOpen) { + // It's possible that the panel is hidden initially + // to avoid impacting startup / new window performance + aInput.popup.hidden = false; + + this.mInput = aInput; + // clear any previous selection, see bugs 400671 and 488357 + this.selectedIndex = -1; + + var width = aElement.getBoundingClientRect().width; + this.setAttribute("width", width > 100 ? width : 100); + // invalidate() depends on the width attribute + this._invalidate(); + + this.openPopup(aElement, "after_start", 0, 0, false, false); + } + ]]> + </body> + </method> + + <method name="invalidate"> + <parameter name="reason"/> + <body> + <![CDATA[ + // Don't bother doing work if we're not even showing + if (!this.mPopupOpen) + return; + + this._invalidate(reason); + ]]> + </body> + </method> + + <method name="_invalidate"> + <parameter name="reason"/> + <body> + <![CDATA[ + // collapsed if no matches + this.richlistbox.collapsed = (this._matchCount == 0); + + // Update the richlistbox height. + if (this._adjustHeightTimeout) { + clearTimeout(this._adjustHeightTimeout); + } + if (this._shrinkTimeout) { + clearTimeout(this._shrinkTimeout); + } + + if (this.mPopupOpen) { + delete this._adjustHeightOnPopupShown; + this._adjustHeightTimeout = setTimeout(() => this.adjustHeight(), 0); + } else { + this._adjustHeightOnPopupShown = true; + } + + this._currentIndex = 0; + if (this._appendResultTimeout) { + clearTimeout(this._appendResultTimeout); + } + this._appendCurrentResult(reason); + ]]> + </body> + </method> + + <property name="maxResults" readonly="true"> + <getter> + <![CDATA[ + // this is how many richlistitems will be kept around + // (note, this getter may be overridden) + return 20; + ]]> + </getter> + </property> + + <property name="_matchCount" readonly="true"> + <getter> + <![CDATA[ + return Math.min(this.mInput.controller.matchCount, this.maxResults); + ]]> + </getter> + </property> + + <method name="_collapseUnusedItems"> + <body> + <![CDATA[ + let existingItemsCount = this.richlistbox.childNodes.length; + for (let i = this._matchCount; i < existingItemsCount; ++i) { + this.richlistbox.childNodes[i].collapsed = true; + } + ]]> + </body> + </method> + + <method name="adjustHeight"> + <body> + <![CDATA[ + // Figure out how many rows to show + let rows = this.richlistbox.childNodes; + let numRows = Math.min(this._matchCount, this.maxRows, rows.length); + + this.removeAttribute("height"); + + // Default the height to 0 if we have no rows to show + let height = 0; + if (numRows) { + let firstRowRect = rows[0].getBoundingClientRect(); + if (this._rlbPadding == undefined) { + let style = window.getComputedStyle(this.richlistbox); + + let transition = style.transitionProperty; + this._rlbAnimated = transition && transition != "none"; + + let paddingTop = parseInt(style.paddingTop) || 0; + let paddingBottom = parseInt(style.paddingBottom) || 0; + this._rlbPadding = paddingTop + paddingBottom; + } + + if (numRows > this.maxRows) { + // Set a fixed max-height to avoid flicker when growing the panel. + let lastVisibleRowRect = rows[this.maxRows - 1].getBoundingClientRect(); + let visibleHeight = lastVisibleRowRect.bottom - firstRowRect.top; + this.richlistbox.style.maxHeight = + visibleHeight + this._rlbPadding + "px"; + } + + // The class `forceHandleUnderflow` is for the item might need to + // handle OverUnderflow or Overflow when the height of an item will + // be changed dynamically. + for (let i = 0; i < numRows; i++) { + if (rows[i].classList.contains("forceHandleUnderflow")) { + rows[i].handleOverUnderflow(); + } + } + + let lastRowRect = rows[numRows - 1].getBoundingClientRect(); + // Calculate the height to have the first row to last row shown + height = lastRowRect.bottom - firstRowRect.top + + this._rlbPadding; + } + + let animate = this._rlbAnimated && + this.getAttribute("dontanimate") != "true"; + let currentHeight = this.richlistbox.getBoundingClientRect().height; + if (height > currentHeight) { + // Grow immediately. + if (animate) { + this.richlistbox.removeAttribute("height"); + this.richlistbox.style.height = height + "px"; + } else { + this.richlistbox.style.removeProperty("height"); + this.richlistbox.height = height; + } + } else { + // Delay shrinking to avoid flicker. + this._shrinkTimeout = setTimeout(() => { + this._collapseUnusedItems(); + if (animate) { + this.richlistbox.removeAttribute("height"); + this.richlistbox.style.height = height + "px"; + } else { + this.richlistbox.style.removeProperty("height"); + this.richlistbox.height = height; + } + }, this.mInput.shrinkDelay); + } + ]]> + </body> + </method> + + <method name="_appendCurrentResult"> + <parameter name="invalidateReason"/> + <body> + <![CDATA[ + var controller = this.mInput.controller; + var matchCount = this._matchCount; + var existingItemsCount = this.richlistbox.childNodes.length; + + // Process maxRows per chunk to improve performance and user experience + for (let i = 0; i < this.maxRows; i++) { + if (this._currentIndex >= matchCount) + break; + + var item; + + // trim the leading/trailing whitespace + var trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, ""); + + let url = controller.getValueAt(this._currentIndex); + + if (this._currentIndex < existingItemsCount) { + // re-use the existing item + item = this.richlistbox.childNodes[this._currentIndex]; + item.setAttribute("dir", this.style.direction); + + // Completely reuse the existing richlistitem for invalidation + // due to new results, but only when: the item is the same, *OR* + // we are about to replace the currently mouse-selected item, to + // avoid surprising the user. + let iface = Components.interfaces.nsIAutoCompletePopup; + if (item.getAttribute("text") == trimmedSearchString && + invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT && + (item.getAttribute("url") == url || + this.richlistbox.mouseSelectedIndex === this._currentIndex)) { + // Additionally, if the item is a searchengine action, then it + // should only be reused if the engine name is the same as the + // popup's override engine name, if any. + let action = item._parseActionUrl(url); + if (!action || + action.type != "searchengine" || + !this.overrideSearchEngineName || + action.params.engineName == this.overrideSearchEngineName) { + item.collapsed = false; + // Call adjustSiteIconStart only after setting collapsed= + // false. The calculations it does may be wrong otherwise. + item.adjustSiteIconStart(this._siteIconStart); + // The popup may have changed size between now and the last + // time the item was shown, so always handle over/underflow. + item.handleOverUnderflow(); + this._currentIndex++; + continue; + } + } + } + else { + // need to create a new item + item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem"); + item.setAttribute("dir", this.style.direction); + } + + // set these attributes before we set the class + // so that we can use them from the constructor + let iconURI = controller.getImageAt(this._currentIndex); + item.setAttribute("image", iconURI); + item.setAttribute("url", url); + item.setAttribute("title", controller.getCommentAt(this._currentIndex)); + item.setAttribute("originaltype", controller.getStyleAt(this._currentIndex)); + item.setAttribute("text", trimmedSearchString); + + if (this._currentIndex < existingItemsCount) { + // re-use the existing item + item._adjustAcItem(); + item.collapsed = false; + } + else { + // set the class at the end so we can use the attributes + // in the xbl constructor + item.className = "autocomplete-richlistitem"; + this.richlistbox.appendChild(item); + } + + // The binding may have not been applied yet. + setTimeout(() => { + let changed = item.adjustSiteIconStart(this._siteIconStart); + if (changed) { + item.handleOverUnderflow(); + } + }, 0); + + this._currentIndex++; + } + + if (typeof this.onResultsAdded == "function") + this.onResultsAdded(); + + if (this._currentIndex < matchCount) { + // yield after each batch of items so that typing the url bar is + // responsive + this._appendResultTimeout = setTimeout(() => this._appendCurrentResult(), 0); + } + ]]> + </body> + </method> + + <!-- The x-coordinate relative to the leading edge of the window of the + items' site icons (favicons). --> + <property name="siteIconStart" + onget="return this._siteIconStart;"> + <setter> + <![CDATA[ + if (val != this._siteIconStart) { + this._siteIconStart = val; + for (let item of this.richlistbox.childNodes) { + let changed = item.adjustSiteIconStart(val); + if (changed) { + item.handleOverUnderflow(); + } + } + } + return val; + ]]> + </setter> + </property> + + <property name="overflowPadding" + onget="return Number(this.getAttribute('overflowpadding'))" + readonly="true" /> + + <method name="selectBy"> + <parameter name="aReverse"/> + <parameter name="aPage"/> + <body> + <![CDATA[ + try { + var amount = aPage ? 5 : 1; + + // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount + this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this._matchCount - 1); + if (this.selectedIndex == -1) { + this.input._focus(); + } + } catch (ex) { + // do nothing - occasionally timer-related js errors happen here + // e.g. "this.selectedIndex has no properties", when you type fast and hit a + // navigation key before this popup has opened + } + ]]> + </body> + </method> + + <field name="richlistbox"> + document.getAnonymousElementByAttribute(this, "anonid", "richlistbox"); + </field> + + <property name="view" + onget="return this.mInput.controller;" + onset="return val;"/> + + </implementation> + <handlers> + <handler event="popupshown"> + <![CDATA[ + if (this._adjustHeightOnPopupShown) { + delete this._adjustHeightOnPopupShown; + this.adjustHeight(); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="autocomplete-richlistitem-insecure-field" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem"> + <content align="center" + onoverflow="this._onOverflow();" + onunderflow="this._onUnderflow();"> + <xul:image anonid="type-icon" + class="ac-type-icon" + xbl:inherits="selected,current,type"/> + <xul:image anonid="site-icon" + class="ac-site-icon" + xbl:inherits="src=image,selected,type"/> + <xul:vbox class="ac-title" + align="left" + xbl:inherits=""> + <xul:description class="ac-text-overflow-container"> + <xul:description anonid="title-text" + class="ac-title-text" + xbl:inherits="selected"/> + </xul:description> + </xul:vbox> + <xul:hbox anonid="tags" + class="ac-tags" + align="center" + xbl:inherits="selected"> + <xul:description class="ac-text-overflow-container"> + <xul:description anonid="tags-text" + class="ac-tags-text" + xbl:inherits="selected"/> + </xul:description> + </xul:hbox> + <xul:hbox anonid="separator" + class="ac-separator" + align="center" + xbl:inherits="selected,actiontype,type"> + <xul:description class="ac-separator-text">—</xul:description> + </xul:hbox> + <xul:hbox class="ac-url" + align="center" + xbl:inherits="selected,actiontype"> + <xul:description class="ac-text-overflow-container"> + <xul:description anonid="url-text" + class="ac-url-text" + xbl:inherits="selected"/> + </xul:description> + </xul:hbox> + <xul:hbox class="ac-action" + align="center" + xbl:inherits="selected,actiontype"> + <xul:description class="ac-text-overflow-container"> + <xul:description anonid="action-text" + class="ac-action-text" + xbl:inherits="selected"/> + </xul:description> + </xul:hbox> + </content> + + <handlers> + <handler event="click" button="0"><![CDATA[ + let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); + window.openUILinkIn(baseURL + "insecure-password", "tab", { + relatedToCurrent: true, + }); + ]]></handler> + </handlers> + + <implementation> + <constructor><![CDATA[ + // Unlike other autocomplete items, the height of the insecure warning + // increases by wrapping. So "forceHandleUnderflow" is for container to + // recalculate an item's height and width. + this.classList.add("forceHandleUnderflow"); + ]]></constructor> + + <property name="_learnMoreString"> + <getter><![CDATA[ + if (!this.__learnMoreString) { + this.__learnMoreString = + Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties"). + GetStringFromName("insecureFieldWarningLearnMore"); + } + return this.__learnMoreString; + ]]></getter> + </property> + + <method name="_getSearchTokens"> + <parameter name="aSearch"/> + <body> + <![CDATA[ + return [this._learnMoreString.toLowerCase()]; + ]]> + </body> + </method> + + </implementation> + </binding> + + <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + + <content align="center" + onoverflow="this._onOverflow();" + onunderflow="this._onUnderflow();"> + <xul:image anonid="type-icon" + class="ac-type-icon" + xbl:inherits="selected,current,type"/> + <xul:image anonid="site-icon" + class="ac-site-icon" + xbl:inherits="src=image,selected,type"/> + <xul:hbox class="ac-title" + align="center" + xbl:inherits="selected"> + <xul:description class="ac-text-overflow-container"> + <xul:description anonid="title-text" + class="ac-title-text" + xbl:inherits="selected"/> + </xul:description> + </xul:hbox> + <xul:hbox anonid="tags" + class="ac-tags" + align="center" + xbl:inherits="selected"> + <xul:description class="ac-text-overflow-container"> + <xul:description anonid="tags-text" + class="ac-tags-text" + xbl:inherits="selected"/> + </xul:description> + </xul:hbox> + <xul:hbox anonid="separator" + class="ac-separator" + align="center" + xbl:inherits="selected,actiontype,type"> + <xul:description class="ac-separator-text">—</xul:description> + </xul:hbox> + <xul:hbox class="ac-url" + align="center" + xbl:inherits="selected,actiontype"> + <xul:description class="ac-text-overflow-container"> + <xul:description anonid="url-text" + class="ac-url-text" + xbl:inherits="selected"/> + </xul:description> + </xul:hbox> + <xul:hbox class="ac-action" + align="center" + xbl:inherits="selected,actiontype"> + <xul:description class="ac-text-overflow-container"> + <xul:description anonid="action-text" + class="ac-action-text" + xbl:inherits="selected"/> + </xul:description> + </xul:hbox> + </content> + + <implementation implements="nsIDOMXULSelectControlItemElement"> + <constructor> + <![CDATA[ + this._typeIcon = document.getAnonymousElementByAttribute( + this, "anonid", "type-icon" + ); + this._siteIcon = document.getAnonymousElementByAttribute( + this, "anonid", "site-icon" + ); + this._titleText = document.getAnonymousElementByAttribute( + this, "anonid", "title-text" + ); + this._tags = document.getAnonymousElementByAttribute( + this, "anonid", "tags" + ); + this._tagsText = document.getAnonymousElementByAttribute( + this, "anonid", "tags-text" + ); + this._separator = document.getAnonymousElementByAttribute( + this, "anonid", "separator" + ); + this._urlText = document.getAnonymousElementByAttribute( + this, "anonid", "url-text" + ); + this._actionText = document.getAnonymousElementByAttribute( + this, "anonid", "action-text" + ); + this._adjustAcItem(); + ]]> + </constructor> + + <property name="label" readonly="true"> + <getter> + <![CDATA[ + // This property is a string that is read aloud by screen readers, + // so it must not contain anything that should not be user-facing. + + let parts = [ + this.getAttribute("title"), + this.getAttribute("displayurl"), + ]; + let label = parts.filter(str => str).join(" ") + + // allow consumers that have extended popups to override + // the label values for the richlistitems + let panel = this.parentNode.parentNode; + if (panel.createResultLabel) { + return panel.createResultLabel(this, label); + } + + return label; + ]]> + </getter> + </property> + + <property name="_stringBundle"> + <getter><![CDATA[ + if (!this.__stringBundle) { + this.__stringBundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties"); + } + return this.__stringBundle; + ]]></getter> + </property> + + <field name="_boundaryCutoff">null</field> + + <property name="boundaryCutoff" readonly="true"> + <getter> + <![CDATA[ + if (!this._boundaryCutoff) { + this._boundaryCutoff = + Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch). + getIntPref("toolkit.autocomplete.richBoundaryCutoff"); + } + return this._boundaryCutoff; + ]]> + </getter> + </property> + + <field name="_inOverflow">false</field> + + <method name="_onOverflow"> + <body> + <![CDATA[ + this._inOverflow = true; + this._handleOverflow(); + ]]> + </body> + </method> + + <method name="_onUnderflow"> + <body> + <![CDATA[ + this._inOverflow = false; + this._handleOverflow(); + ]]> + </body> + </method> + + <method name="_getBoundaryIndices"> + <parameter name="aText"/> + <parameter name="aSearchTokens"/> + <body> + <![CDATA[ + // Short circuit for empty search ([""] == "") + if (aSearchTokens == "") + return [0, aText.length]; + + // Find which regions of text match the search terms + let regions = []; + for (let search of Array.prototype.slice.call(aSearchTokens)) { + let matchIndex = -1; + let searchLen = search.length; + + // Find all matches of the search terms, but stop early for perf + let lowerText = aText.substr(0, this.boundaryCutoff).toLowerCase(); + while ((matchIndex = lowerText.indexOf(search, matchIndex + 1)) >= 0) { + regions.push([matchIndex, matchIndex + searchLen]); + } + } + + // Sort the regions by start position then end position + regions = regions.sort((a, b) => { + let start = a[0] - b[0]; + return (start == 0) ? a[1] - b[1] : start; + }); + + // Generate the boundary indices from each region + let start = 0; + let end = 0; + let boundaries = []; + let len = regions.length; + for (let i = 0; i < len; i++) { + // We have a new boundary if the start of the next is past the end + let region = regions[i]; + if (region[0] > end) { + // First index is the beginning of match + boundaries.push(start); + // Second index is the beginning of non-match + boundaries.push(end); + + // Track the new region now that we've stored the previous one + start = region[0]; + } + + // Push back the end index for the current or new region + end = Math.max(end, region[1]); + } + + // Add the last region + boundaries.push(start); + boundaries.push(end); + + // Put on the end boundary if necessary + if (end < aText.length) + boundaries.push(aText.length); + + // Skip the first item because it's always 0 + return boundaries.slice(1); + ]]> + </body> + </method> + + <method name="_getSearchTokens"> + <parameter name="aSearch"/> + <body> + <![CDATA[ + let search = aSearch.toLowerCase(); + return search.split(/\s+/); + ]]> + </body> + </method> + + <method name="_setUpDescription"> + <parameter name="aDescriptionElement"/> + <parameter name="aText"/> + <parameter name="aNoEmphasis"/> + <body> + <![CDATA[ + // Get rid of all previous text + if (!aDescriptionElement) { + return; + } + while (aDescriptionElement.hasChildNodes()) + aDescriptionElement.removeChild(aDescriptionElement.firstChild); + + // If aNoEmphasis is specified, don't add any emphasis + if (aNoEmphasis) { + aDescriptionElement.appendChild(document.createTextNode(aText)); + return; + } + + // Get the indices that separate match and non-match text + let search = this.getAttribute("text"); + let tokens = this._getSearchTokens(search); + let indices = this._getBoundaryIndices(aText, tokens); + + this._appendDescriptionSpans(indices, aText, aDescriptionElement, + aDescriptionElement); + ]]> + </body> + </method> + + <method name="_appendDescriptionSpans"> + <parameter name="indices"/> + <parameter name="text"/> + <parameter name="spansParentElement"/> + <parameter name="descriptionElement"/> + <body> + <![CDATA[ + let next; + let start = 0; + let len = indices.length; + // Even indexed boundaries are matches, so skip the 0th if it's empty + for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) { + next = indices[i]; + let spanText = text.substr(start, next - start); + start = next; + + if (i % 2 == 0) { + // Emphasize the text for even indices + let span = spansParentElement.appendChild( + document.createElementNS("http://www.w3.org/1999/xhtml", "span")); + this._setUpEmphasisSpan(span, descriptionElement); + span.textContent = spanText; + } else { + // Otherwise, it's plain text + spansParentElement.appendChild(document.createTextNode(spanText)); + } + } + ]]> + </body> + </method> + + <method name="_setUpTags"> + <parameter name="tags"/> + <body> + <![CDATA[ + while (this._tagsText.hasChildNodes()) { + this._tagsText.firstChild.remove(); + } + + let anyTagsMatch = false; + + // Include only tags that match the search string. + for (let tag of tags) { + // Check if the tag matches the search string. + let search = this.getAttribute("text"); + let tokens = this._getSearchTokens(search); + let indices = this._getBoundaryIndices(tag, tokens); + + if (indices.length == 2 && + indices[0] == 0 && + indices[1] == tag.length) { + // The tag doesn't match the search string, so don't include it. + continue; + } + + anyTagsMatch = true; + + let tagSpan = + document.createElementNS("http://www.w3.org/1999/xhtml", "span"); + tagSpan.classList.add("ac-tag"); + this._tagsText.appendChild(tagSpan); + + this._appendDescriptionSpans(indices, tag, tagSpan, this._tagsText); + } + + return anyTagsMatch; + ]]> + </body> + </method> + + <method name="_setUpEmphasisSpan"> + <parameter name="aSpan"/> + <parameter name="aDescriptionElement"/> + <body> + <![CDATA[ + aSpan.classList.add("ac-emphasize-text"); + switch (aDescriptionElement) { + case this._titleText: + aSpan.classList.add("ac-emphasize-text-title"); + break; + case this._tagsText: + aSpan.classList.add("ac-emphasize-text-tag"); + break; + case this._urlText: + aSpan.classList.add("ac-emphasize-text-url"); + break; + case this._actionText: + aSpan.classList.add("ac-emphasize-text-action"); + break; + } + ]]> + </body> + </method> + + <!-- + This will generate an array of emphasis pairs for use with + _setUpEmphasisedSections(). Each pair is a tuple (array) that + represents a block of text - containing the text of that block, and a + boolean for whether that block should have an emphasis styling applied + to it. + + These pairs are generated by parsing a localised string (aSourceString) + with parameters, in the format that is used by + nsIStringBundle.formatStringFromName(): + + "textA %1$S textB textC %2$S" + + Or: + + "textA %S" + + Where "%1$S", "%2$S", and "%S" are intended to be replaced by provided + replacement strings. These are specified an array of tuples + (aReplacements), each containing the replacement text and a boolean for + whether that text should have an emphasis styling applied. This is used + as a 1-based array - ie, "%1$S" is replaced by the item in the first + index of aReplacements, "%2$S" by the second, etc. "%S" will always + match the first index. + --> + <method name="_generateEmphasisPairs"> + <parameter name="aSourceString"/> + <parameter name="aReplacements"/> + <body> + <![CDATA[ + let pairs = []; + + // Split on %S, %1$S, %2$S, etc. ie: + // "textA %S" + // becomes ["textA ", "%S"] + // "textA %1$S textB textC %2$S" + // becomes ["textA ", "%1$S", " textB textC ", "%2$S"] + let parts = aSourceString.split(/(%(?:[0-9]+\$)?S)/); + + for (let part of parts) { + // The above regex will actually give us an empty string at the + // end - we don't want that, as we don't want to later generate an + // empty text node for it. + if (part.length === 0) + continue; + + // Determine if this token is a replacement token or a normal text + // token. If it is a replacement token, we want to extract the + // numerical number. However, we still want to match on "$S". + let match = part.match(/^%(?:([0-9]+)\$)?S$/); + + if (match) { + // "%S" doesn't have a numerical number in it, but will always + // be assumed to be 1. Furthermore, the input string specifies + // these with a 1-based index, but we want a 0-based index. + let index = (match[1] || 1) - 1; + + if (index >= 0 && index < aReplacements.length) { + pairs.push([...aReplacements[index]]); + } + } else { + pairs.push([part]); + } + } + + return pairs; + ]]> + </body> + </method> + + <!-- + _setUpEmphasisedSections() has the same use as _setUpDescription, + except instead of taking a string and highlighting given tokens, it takes + an array of pairs generated by _generateEmphasisPairs(). This allows + control over emphasising based on specific blocks of text, rather than + search for substrings. + --> + <method name="_setUpEmphasisedSections"> + <parameter name="aDescriptionElement"/> + <parameter name="aTextPairs"/> + <body> + <![CDATA[ + // Get rid of all previous text + while (aDescriptionElement.hasChildNodes()) + aDescriptionElement.firstChild.remove(); + + for (let [text, emphasise] of aTextPairs) { + if (emphasise) { + let span = aDescriptionElement.appendChild( + document.createElementNS("http://www.w3.org/1999/xhtml", "span")); + span.textContent = text; + switch (emphasise) { + case "match": + this._setUpEmphasisSpan(span, aDescriptionElement); + break; + } + } else { + aDescriptionElement.appendChild(document.createTextNode(text)); + } + } + ]]> + </body> + </method> + + <field name="_textToSubURI">null</field> + <method name="_unescapeUrl"> + <parameter name="url"/> + <body> + <![CDATA[ + if (!this._textToSubURI) { + this._textToSubURI = + Components.classes["@mozilla.org/intl/texttosuburi;1"] + .getService(Components.interfaces.nsITextToSubURI); + } + return this._textToSubURI.unEscapeURIForUI("UTF-8", url); + ]]> + </body> + </method> + + <method name="_adjustAcItem"> + <body> + <![CDATA[ + let popup = this.parentNode.parentNode; + if (!popup.popupOpen) { + // Removing the max-width and resetting it later when overflow is + // handled is jarring when the item is visible, so skip this when + // the popup is open. + this._removeMaxWidths(); + } + + let title = this.getAttribute("title"); + let titleLooksLikeUrl = false; + + let displayUrl; + let originalUrl = this.getAttribute("url"); + let emphasiseUrl = true; + + let type = this.getAttribute("originaltype"); + let types = new Set(type.split(/\s+/)); + let initialTypes = new Set(types); + // Remove types that should ultimately not be in the `type` string. + types.delete("action"); + types.delete("autofill"); + types.delete("heuristic"); + type = [...types][0] || ""; + + let action; + + if (initialTypes.has("autofill")) { + // Treat autofills as visiturl actions. + action = { + type: "visiturl", + params: { + url: originalUrl, + }, + }; + } + + this.removeAttribute("actiontype"); + this.classList.remove("overridable-action"); + + // If the type includes an action, set up the item appropriately. + if (initialTypes.has("action") || action) { + action = action || this._parseActionUrl(originalUrl); + this.setAttribute("actiontype", action.type); + + if (action.type == "switchtab") { + this.classList.add("overridable-action"); + displayUrl = this._unescapeUrl(action.params.url); + let desc = this._stringBundle.GetStringFromName("switchToTab2"); + this._setUpDescription(this._actionText, desc, true); + } else if (action.type == "remotetab") { + displayUrl = this._unescapeUrl(action.params.url); + let desc = action.params.deviceName; + this._setUpDescription(this._actionText, desc, true); + } else if (action.type == "searchengine") { + emphasiseUrl = false; + + // The order here is not localizable, we default to appending + // "- Search with Engine" to the search string, to be able to + // properly generate emphasis pairs. That said, no localization + // changed the order while it was possible, so doesn't look like + // there's a strong need for that. + let {engineName, searchSuggestion, searchQuery} = action.params; + + // Override the engine name if the popup defines an override. + let override = popup.overrideSearchEngineName; + if (override && override != engineName) { + engineName = override; + action.params.engineName = override; + let newURL = + PlacesUtils.mozActionURI(action.type, action.params); + this.setAttribute("url", newURL); + } + + let engineStr = + this._stringBundle.formatStringFromName("searchWithEngine", + [engineName], 1); + this._setUpDescription(this._actionText, engineStr, true); + + // Make the title by generating an array of pairs and its + // corresponding interpolation string (e.g., "%1$S") to pass to + // _generateEmphasisPairs. + let pairs; + if (searchSuggestion) { + // Check if the search query appears in the suggestion. It may + // not. If it does, then emphasize the query in the suggestion + // and otherwise just include the suggestion without emphasis. + let idx = searchSuggestion.indexOf(searchQuery); + if (idx >= 0) { + pairs = [ + [searchSuggestion.substring(0, idx), ""], + [searchQuery, "match"], + [searchSuggestion.substring(idx + searchQuery.length), ""], + ]; + } else { + pairs = [ + [searchSuggestion, ""], + ]; + } + } else { + pairs = [ + [searchQuery, ""], + ]; + } + let interpStr = pairs.map((pair, i) => `%${i + 1}$S`).join(""); + title = this._generateEmphasisPairs(interpStr, pairs); + + // If this is a default search match, we remove the image so we + // can style it ourselves with a generic search icon. + // We don't do this when matching an aliased search engine, + // because the icon helps with recognising which engine will be + // used (when using the default engine, we don't need that + // recognition). + if (!action.params.alias && !initialTypes.has("favicon")) { + this.removeAttribute("image"); + } + } else if (action.type == "visiturl") { + emphasiseUrl = false; + displayUrl = this._unescapeUrl(action.params.url); + title = displayUrl; + titleLooksLikeUrl = true; + let visitStr = this._stringBundle.GetStringFromName("visit"); + this._setUpDescription(this._actionText, visitStr, true); + } else if (action.type == "extension") { + let content = action.params.content; + displayUrl = content; + this._setUpDescription(this._actionText, content, true); + } + } + + if (!displayUrl) { + let input = popup.input; + let url = typeof(input.trimValue) == "function" ? + input.trimValue(originalUrl) : + originalUrl; + displayUrl = this._unescapeUrl(url); + } + // For performance reasons we may want to limit the displayUrl size. + if (popup.textRunsMaxLen) { + displayUrl = displayUrl.substr(0, popup.textRunsMaxLen); + } + this.setAttribute("displayurl", displayUrl); + + // Show the domain as the title if we don't have a title. + if (!title) { + title = displayUrl; + titleLooksLikeUrl = true; + try { + let uri = Services.io.newURI(originalUrl, null, null); + // Not all valid URLs have a domain. + if (uri.host) + title = uri.host; + } catch (e) {} + } + + this._tags.setAttribute("empty", "true"); + + if (type == "tag" || type == "bookmark-tag") { + // The title is separated from the tags by an endash + let tags; + [, title, tags] = title.match(/^(.+) \u2013 (.+)$/); + + // Each tag is split by a comma in an undefined order, so sort it + let sortedTags = tags.split(/\s*,\s*/).sort((a, b) => { + return a.localeCompare(a); + }); + + let anyTagsMatch = this._setUpTags(sortedTags); + if (anyTagsMatch) { + this._tags.removeAttribute("empty"); + } + if (type == "bookmark-tag") { + type = "bookmark"; + } + } else if (type == "keyword") { + // Note that this is a moz-action with action.type == keyword. + emphasiseUrl = false; + let keywordArg = this.getAttribute("text").replace(/^[^\s]+\s*/, ""); + if (!keywordArg) { + // Treat keyword searches without arguments as visiturl actions. + type = "visiturl"; + this.setAttribute("actiontype", "visiturl"); + let visitStr = this._stringBundle.GetStringFromName("visit"); + this._setUpDescription(this._actionText, visitStr, true); + } else { + let pairs = [[title, ""], [keywordArg, "match"]]; + let interpStr = + this._stringBundle.GetStringFromName("bookmarkKeywordSearch"); + title = this._generateEmphasisPairs(interpStr, pairs); + // The action box will be visible since this is a moz-action, but + // we want it to appear as if it were not visible, so set its text + // to the empty string. + this._setUpDescription(this._actionText, "", false); + } + } + + this.setAttribute("type", type); + + if (titleLooksLikeUrl) { + this._titleText.setAttribute("lookslikeurl", "true"); + } else { + this._titleText.removeAttribute("lookslikeurl"); + } + + if (Array.isArray(title)) { + // For performance reasons we may want to limit the title size. + if (popup.textRunsMaxLen) { + title = title.map(t => t.substr(0, popup.textRunsMaxLen)); + } + this._setUpEmphasisedSections(this._titleText, title); + } else { + // For performance reasons we may want to limit the title size. + if (popup.textRunsMaxLen) { + title = title.substr(0, popup.textRunsMaxLen); + } + this._setUpDescription(this._titleText, title, false); + } + this._setUpDescription(this._urlText, displayUrl, !emphasiseUrl); + + if (this._inOverflow) { + this._handleOverflow(); + } + ]]> + </body> + </method> + + <method name="_removeMaxWidths"> + <body> + <![CDATA[ + this._titleText.style.removeProperty("max-width"); + this._tagsText.style.removeProperty("max-width"); + this._urlText.style.removeProperty("max-width"); + this._actionText.style.removeProperty("max-width"); + ]]> + </body> + </method> + + <!-- Sets the x-coordinate of the leading edge of the site icon (favicon) + relative the the leading edge of the window. + @param newStart The new x-coordinate, relative to the leading edge of + the window. Pass undefined to reset the icon's position to + whatever is specified in CSS. + @return True if the icon's position changed, false if not. --> + <method name="adjustSiteIconStart"> + <parameter name="newStart"/> + <body> + <![CDATA[ + if (typeof(newStart) != "number") { + this._typeIcon.style.removeProperty("margin-inline-start"); + return true; + } + let rect = this._siteIcon.getBoundingClientRect(); + let dir = this.getAttribute("dir"); + let delta = dir == "rtl" ? rect.right - newStart + : newStart - rect.left; + let px = this._typeIcon.style.marginInlineStart; + if (!px) { + // Allow margin-inline-start not to be specified in CSS initially. + let style = window.getComputedStyle(this._typeIcon); + px = dir == "rtl" ? style.marginRight : style.marginLeft; + } + let typeIconStart = Number(px.substr(0, px.length - 2)); + this._typeIcon.style.marginInlineStart = (typeIconStart + delta) + "px"; + return delta > 0; + ]]> + </body> + </method> + + <!-- This method truncates the displayed strings as necessary. --> + <method name="_handleOverflow"> + <body><![CDATA[ + let itemRect = this.parentNode.getBoundingClientRect(); + let titleRect = this._titleText.getBoundingClientRect(); + let tagsRect = this._tagsText.getBoundingClientRect(); + let separatorRect = this._separator.getBoundingClientRect(); + let urlRect = this._urlText.getBoundingClientRect(); + let actionRect = this._actionText.getBoundingClientRect(); + let separatorURLActionWidth = + separatorRect.width + Math.max(urlRect.width, actionRect.width); + + // Total width for the title and URL/action is the width of the item + // minus the start of the title text minus a little optional extra padding. + // This extra padding amount is basically arbitrary but keeps the text + // from getting too close to the popup's edge. + let dir = this.getAttribute("dir"); + let titleStart = dir == "rtl" ? itemRect.right - titleRect.right + : titleRect.left - itemRect.left; + + let popup = this.parentNode.parentNode; + let itemWidth = itemRect.width - titleStart - popup.overflowPadding; + + if (this._tags.hasAttribute("empty")) { + // The tags box is not displayed in this case. + tagsRect.width = 0; + } + + let titleTagsWidth = titleRect.width + tagsRect.width; + if (titleTagsWidth + separatorURLActionWidth > itemWidth) { + // Title + tags + URL/action overflows the item width. + + // The percentage of the item width allocated to the title and tags. + let titleTagsPct = 0.66; + + let titleTagsAvailable = itemWidth - separatorURLActionWidth; + let titleTagsMaxWidth = Math.max( + titleTagsAvailable, + itemWidth * titleTagsPct + ); + if (titleTagsWidth > titleTagsMaxWidth) { + // Title + tags overflows the max title + tags width. + + // The percentage of the title + tags width allocated to the + // title. + let titlePct = 0.33; + + let titleAvailable = titleTagsMaxWidth - tagsRect.width; + let titleMaxWidth = Math.max( + titleAvailable, + titleTagsMaxWidth * titlePct + ); + let tagsAvailable = titleTagsMaxWidth - titleRect.width; + let tagsMaxWidth = Math.max( + tagsAvailable, + titleTagsMaxWidth * (1 - titlePct) + ); + this._titleText.style.maxWidth = titleMaxWidth + "px"; + this._tagsText.style.maxWidth = tagsMaxWidth + "px"; + } + let urlActionMaxWidth = Math.max( + itemWidth - titleTagsWidth, + itemWidth * (1 - titleTagsPct) + ); + urlActionMaxWidth -= separatorRect.width; + this._urlText.style.maxWidth = urlActionMaxWidth + "px"; + this._actionText.style.maxWidth = urlActionMaxWidth + "px"; + } + ]]></body> + </method> + + <method name="handleOverUnderflow"> + <body> + <![CDATA[ + this._removeMaxWidths(); + this._handleOverflow(); + ]]> + </body> + </method> + + <method name="_parseActionUrl"> + <parameter name="aUrl"/> + <body><![CDATA[ + if (!aUrl.startsWith("moz-action:")) + return null; + + // URL is in the format moz-action:ACTION,PARAMS + // Where PARAMS is a JSON encoded object. + let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/); + + let action = { + type: type, + }; + + try { + action.params = JSON.parse(params); + for (let key in action.params) { + action.params[key] = decodeURIComponent(action.params[key]); + } + } catch (e) { + // If this failed, we assume that params is not a JSON object, and + // is instead just a flat string. This may happen for legacy + // search components. + action.params = { + url: params, + } + } + + return action; + ]]></body> + </method> + </implementation> + </binding> + + <binding id="autocomplete-tree" extends="chrome://global/content/bindings/tree.xml#tree"> + <content> + <children includes="treecols"/> + <xul:treerows class="autocomplete-treerows tree-rows" xbl:inherits="hidescrollbar" flex="1"> + <children/> + </xul:treerows> + </content> + </binding> + + <binding id="autocomplete-richlistbox" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox"> + <implementation> + <field name="mLastMoveTime">Date.now()</field> + <field name="mouseSelectedIndex">-1</field> + </implementation> + <handlers> + <handler event="mouseup"> + <![CDATA[ + // don't call onPopupClick for the scrollbar buttons, thumb, slider, etc. + let item = event.originalTarget; + while (item && item.localName != "richlistitem") { + item = item.parentNode; + } + + if (!item) + return; + + this.parentNode.onPopupClick(event); + ]]> + </handler> + + <handler event="mousemove"> + <![CDATA[ + if (Date.now() - this.mLastMoveTime > 30) { + let item = event.target; + while (item && item.localName != "richlistitem") { + item = item.parentNode; + } + + if (!item) + return; + + let index = this.getIndexOfItem(item); + if (index != this.selectedIndex) { + this.mouseSelectedIndex = this.selectedIndex = index; + } + + this.mLastMoveTime = Date.now(); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="autocomplete-treebody"> + <implementation> + <field name="mLastMoveTime">Date.now()</field> + </implementation> + + <handlers> + <handler event="mouseup" action="this.parentNode.parentNode.onPopupClick(event);"/> + + <handler event="mousedown"><![CDATA[ + var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY); + if (rc != this.parentNode.currentIndex) + this.parentNode.view.selection.select(rc); + ]]></handler> + + <handler event="mousemove"><![CDATA[ + if (Date.now() - this.mLastMoveTime > 30) { + var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY); + if (rc != this.parentNode.currentIndex) + this.parentNode.view.selection.select(rc); + this.mLastMoveTime = Date.now(); + } + ]]></handler> + </handlers> + </binding> + + <binding id="autocomplete-treerows"> + <content> + <xul:hbox flex="1" class="tree-bodybox"> + <children/> + </xul:hbox> + <xul:scrollbar xbl:inherits="collapsed=hidescrollbar" orient="vertical" class="tree-scrollbar"/> + </content> + </binding> + + <binding id="history-dropmarker" extends="chrome://global/content/bindings/general.xml#dropmarker"> + <handlers> + <handler event="mousedown" button="0"><![CDATA[ + document.getBindingParent(this).toggleHistoryPopup(); + ]]></handler> + </handlers> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/browser.xml b/toolkit/content/widgets/browser.xml new file mode 100644 index 0000000000..a5f37b62a1 --- /dev/null +++ b/toolkit/content/widgets/browser.xml @@ -0,0 +1,1571 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<bindings id="browserBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="browser" extends="xul:browser" role="outerdoc"> + <content clickthrough="never"> + <children/> + </content> + <implementation type="application/javascript" implements="nsIObserver, nsIDOMEventListener, nsIMessageListener, nsIBrowser"> + <property name="autoscrollEnabled"> + <getter> + <![CDATA[ + if (this.getAttribute("autoscroll") == "false") + return false; + + var enabled = true; + try { + enabled = this.mPrefs.getBoolPref("general.autoScroll"); + } + catch (ex) { + } + + return enabled; + ]]> + </getter> + </property> + + <property name="canGoBack" + onget="return this.webNavigation.canGoBack;" + readonly="true"/> + + <property name="canGoForward" + onget="return this.webNavigation.canGoForward;" + readonly="true"/> + + <method name="_wrapURIChangeCall"> + <parameter name="fn"/> + <body> + <![CDATA[ + if (!this.isRemoteBrowser) { + this.inLoadURI = true; + try { + fn(); + } finally { + this.inLoadURI = false; + } + } else { + fn(); + } + ]]> + </body> + </method> + + + <method name="goBack"> + <body> + <![CDATA[ + var webNavigation = this.webNavigation; + if (webNavigation.canGoBack) + this._wrapURIChangeCall(() => webNavigation.goBack()); + ]]> + </body> + </method> + + <method name="goForward"> + <body> + <![CDATA[ + var webNavigation = this.webNavigation; + if (webNavigation.canGoForward) + this._wrapURIChangeCall(() => webNavigation.goForward()); + ]]> + </body> + </method> + + <method name="reload"> + <body> + <![CDATA[ + const nsIWebNavigation = Components.interfaces.nsIWebNavigation; + const flags = nsIWebNavigation.LOAD_FLAGS_NONE; + this.reloadWithFlags(flags); + ]]> + </body> + </method> + + <method name="reloadWithFlags"> + <parameter name="aFlags"/> + <body> + <![CDATA[ + this.webNavigation.reload(aFlags); + ]]> + </body> + </method> + + <method name="stop"> + <body> + <![CDATA[ + const nsIWebNavigation = Components.interfaces.nsIWebNavigation; + const flags = nsIWebNavigation.STOP_ALL; + this.webNavigation.stop(flags); + ]]> + </body> + </method> + + <!-- throws exception for unknown schemes --> + <method name="loadURI"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <body> + <![CDATA[ + const nsIWebNavigation = Components.interfaces.nsIWebNavigation; + const flags = nsIWebNavigation.LOAD_FLAGS_NONE; + this._wrapURIChangeCall(() => + this.loadURIWithFlags(aURI, flags, aReferrerURI, aCharset)); + ]]> + </body> + </method> + + <!-- throws exception for unknown schemes --> + <method name="loadURIWithFlags"> + <parameter name="aURI"/> + <parameter name="aFlags"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <body> + <![CDATA[ + if (!aURI) + aURI = "about:blank"; + + var aReferrerPolicy = Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT; + + // Check for loadURIWithFlags(uri, { ... }); + var params = arguments[1]; + if (params && typeof(params) == "object") { + aFlags = params.flags; + aReferrerURI = params.referrerURI; + if ('referrerPolicy' in params) { + aReferrerPolicy = params.referrerPolicy; + } + aCharset = params.charset; + aPostData = params.postData; + } + + this._wrapURIChangeCall(() => + this.webNavigation.loadURIWithOptions( + aURI, aFlags, aReferrerURI, aReferrerPolicy, + aPostData, null, null)); + ]]> + </body> + </method> + + <method name="goHome"> + <body> + <![CDATA[ + try { + this.loadURI(this.homePage); + } + catch (e) { + } + ]]> + </body> + </method> + + <property name="homePage"> + <getter> + <![CDATA[ + var uri; + + if (this.hasAttribute("homepage")) + uri = this.getAttribute("homepage"); + else + uri = "http://www.mozilla.org/"; // widget pride + + return uri; + ]]> + </getter> + <setter> + <![CDATA[ + this.setAttribute("homepage", val); + return val; + ]]> + </setter> + </property> + + <method name="gotoIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + this._wrapURIChangeCall(() => this.webNavigation.gotoIndex(aIndex)); + ]]> + </body> + </method> + + <property name="currentURI" readonly="true"> + <getter><![CDATA[ + if (this.webNavigation) { + return this.webNavigation.currentURI; + } + return null; + ]]> + </getter> + </property> + + <!-- + Used by session restore to ensure that currentURI is set so + that switch-to-tab works before the tab is fully + restored. This function also invokes onLocationChanged + listeners in tabbrowser.xml. + --> + <method name="_setCurrentURI"> + <parameter name="aURI"/> + <body><![CDATA[ + this.docShell.setCurrentURI(aURI); + ]]></body> + </method> + + <property name="documentURI" + onget="return this.contentDocument.documentURIObject;" + readonly="true"/> + + <property name="documentContentType" + onget="return this.contentDocument ? this.contentDocument.contentType : null;" + readonly="true"/> + + <property name="preferences" + onget="return this.mPrefs.QueryInterface(Components.interfaces.nsIPrefService);" + readonly="true"/> + + <!-- + Weak reference to the related browser (see + nsIBrowser.getRelatedBrowser). + --> + <field name="_relatedBrowser">null</field> + <property name="relatedBrowser"> + <getter><![CDATA[ + return this._relatedBrowser && this._relatedBrowser.get(); + ]]></getter> + <setter><![CDATA[ + this._relatedBrowser = Cu.getWeakReference(val); + ]]></setter> + </property> + + <field name="_docShell">null</field> + + <property name="docShell" readonly="true"> + <getter><![CDATA[ + if (this._docShell) + return this._docShell; + + let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader; + if (!frameLoader) + return null; + this._docShell = frameLoader.docShell; + return this._docShell; + ]]></getter> + </property> + + <field name="_loadContext">null</field> + + <property name="loadContext" readonly="true"> + <getter><![CDATA[ + if (this._loadContext) + return this._loadContext; + + let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader; + if (!frameLoader) + return null; + this._loadContext = frameLoader.loadContext; + return this._loadContext; + ]]></getter> + </property> + + <property name="autoCompletePopup" + onget="return document.getElementById(this.getAttribute('autocompletepopup'))" + readonly="true"/> + + <property name="dateTimePicker" + onget="return document.getElementById(this.getAttribute('datetimepicker'))" + readonly="true"/> + + <property name="docShellIsActive"> + <getter> + <![CDATA[ + return this.docShell && this.docShell.isActive; + ]]> + </getter> + <setter> + <![CDATA[ + if (this.docShell) + return this.docShell.isActive = val; + return false; + ]]> + </setter> + </property> + + <method name="preserveLayers"> + <parameter name="preserve"/> + <body> + // Only useful for remote browsers. + </body> + </method> + + <method name="makePrerenderedBrowserActive"> + <body> + <![CDATA[ + let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader; + if (frameLoader) { + frameLoader.makePrerenderedLoaderActive(); + } + ]]> + </body> + </method> + + <property name="imageDocument" + readonly="true"> + <getter> + <![CDATA[ + var document = this.contentDocument; + if (!document || !(document instanceof Components.interfaces.nsIImageDocument)) + return null; + + try { + return {width: document.imageRequest.image.width, height: document.imageRequest.image.height }; + } catch (e) {} + return null; + ]]> + </getter> + </property> + + <property name="isRemoteBrowser" + onget="return (this.getAttribute('remote') == 'true');" + readonly="true"/> + + <property name="messageManager" + readonly="true"> + <getter> + <![CDATA[ + var owner = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner); + if (!owner.frameLoader) { + return null; + } + return owner.frameLoader.messageManager; + ]]> + </getter> + + </property> + + <field name="_webNavigation">null</field> + + <property name="webNavigation" + readonly="true"> + <getter> + <![CDATA[ + if (!this._webNavigation) { + if (!this.docShell) { + return null; + } + this._webNavigation = this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation); + } + return this._webNavigation; + ]]> + </getter> + </property> + + <field name="_webBrowserFind">null</field> + + <property name="webBrowserFind" + readonly="true"> + <getter> + <![CDATA[ + if (!this._webBrowserFind) + this._webBrowserFind = this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebBrowserFind); + return this._webBrowserFind; + ]]> + </getter> + </property> + + <method name="getTabBrowser"> + <body> + <![CDATA[ + var tabBrowser = this.parentNode; + while (tabBrowser && tabBrowser.localName != "tabbrowser") + tabBrowser = tabBrowser.parentNode; + return tabBrowser; + ]]> + </body> + </method> + + <field name="_finder">null</field> + + <property name="finder" readonly="true"> + <getter><![CDATA[ + if (!this._finder) { + if (!this.docShell) + return null; + + let Finder = Components.utils.import("resource://gre/modules/Finder.jsm", {}).Finder; + this._finder = new Finder(this.docShell); + } + return this._finder; + ]]></getter> + </property> + + <field name="_fastFind">null</field> + <property name="fastFind" readonly="true"> + <getter><![CDATA[ + if (!this._fastFind) { + if (!("@mozilla.org/typeaheadfind;1" in Components.classes)) + return null; + + var tabBrowser = this.getTabBrowser(); + if (tabBrowser && "fastFind" in tabBrowser) + return this._fastFind = tabBrowser.fastFind; + + if (!this.docShell) + return null; + + this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"] + .createInstance(Components.interfaces.nsITypeAheadFind); + this._fastFind.init(this.docShell); + } + return this._fastFind; + ]]></getter> + </property> + + <property name="outerWindowID" readonly="true"> + <getter><![CDATA[ + return this.contentWindow + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils) + .outerWindowID; + ]]></getter> + </property> + + <property name="innerWindowID" readonly="true"> + <getter><![CDATA[ + try { + return this.contentWindow + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils) + .currentInnerWindowID; + } catch (e) { + if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw e; + } + return null; + } + ]]></getter> + </property> + + <field name="_lastSearchString">null</field> + + <property name="webProgress" + readonly="true" + onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);"/> + + <field name="_contentWindow">null</field> + + <property name="contentWindow" + readonly="true" + onget="return this._contentWindow || (this._contentWindow = this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow));"/> + + <property name="contentWindowAsCPOW" + readonly="true" + onget="return this.contentWindow;"/> + + <property name="sessionHistory" + onget="return this.webNavigation.sessionHistory;" + readonly="true"/> + + <property name="markupDocumentViewer" + onget="return this.docShell.contentViewer;" + readonly="true"/> + + <property name="contentViewerEdit" + onget="return this.docShell.contentViewer.QueryInterface(Components.interfaces.nsIContentViewerEdit);" + readonly="true"/> + + <property name="contentViewerFile" + onget="return this.docShell.contentViewer.QueryInterface(Components.interfaces.nsIContentViewerFile);" + readonly="true"/> + + <property name="contentDocument" + onget="return this.webNavigation.document;" + readonly="true"/> + + <property name="contentDocumentAsCPOW" + onget="return this.contentDocument;" + readonly="true"/> + + <property name="contentTitle" + onget="return this.contentDocument.title;" + readonly="true"/> + + <property name="characterSet" + onget="return this.docShell.charset;"> + <setter><![CDATA[ + this.docShell.charset = val; + this.docShell.gatherCharsetMenuTelemetry(); + ]]></setter> + </property> + + <property name="mayEnableCharacterEncodingMenu" + onget="return this.docShell.mayEnableCharacterEncodingMenu;" + readonly="true"/> + + <property name="contentPrincipal" + onget="return this.contentDocument.nodePrincipal;" + readonly="true"/> + + <property name="showWindowResizer" + onset="if (val) this.setAttribute('showresizer', 'true'); + else this.removeAttribute('showresizer'); + return val;" + onget="return this.getAttribute('showresizer') == 'true';"/> + + <property name="manifestURI" + readonly="true"> + <getter><![CDATA[ + return this.contentDocument.documentElement && + this.contentDocument.documentElement.getAttribute("manifest"); + ]]></getter> + </property> + + <property name="fullZoom"> + <getter><![CDATA[ + return this.markupDocumentViewer.fullZoom; + ]]></getter> + <setter><![CDATA[ + this.markupDocumentViewer.fullZoom = val; + ]]></setter> + </property> + + <property name="textZoom"> + <getter><![CDATA[ + return this.markupDocumentViewer.textZoom; + ]]></getter> + <setter><![CDATA[ + this.markupDocumentViewer.textZoom = val; + ]]></setter> + </property> + + <property name="isSyntheticDocument"> + <getter><![CDATA[ + return this.contentDocument.mozSyntheticDocument; + ]]></getter> + </property> + + <property name="hasContentOpener"> + <getter><![CDATA[ + return !!this.contentWindow.opener; + ]]></getter> + </property> + + <field name="mPrefs" readonly="true"> + Components.classes['@mozilla.org/preferences-service;1'] + .getService(Components.interfaces.nsIPrefBranch); + </field> + + <field name="_mStrBundle">null</field> + + <property name="mStrBundle"> + <getter> + <![CDATA[ + if (!this._mStrBundle) { + // need to create string bundle manually instead of using <xul:stringbundle/> + // see bug 63370 for details + this._mStrBundle = Components.classes["@mozilla.org/intl/stringbundle;1"] + .getService(Components.interfaces.nsIStringBundleService) + .createBundle("chrome://global/locale/browser.properties"); + } + return this._mStrBundle; + ]]></getter> + </property> + + <method name="addProgressListener"> + <parameter name="aListener"/> + <parameter name="aNotifyMask"/> + <body> + <![CDATA[ + if (!aNotifyMask) { + aNotifyMask = Components.interfaces.nsIWebProgress.NOTIFY_ALL; + } + this.webProgress.addProgressListener(aListener, aNotifyMask); + ]]> + </body> + </method> + + <method name="removeProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + this.webProgress.removeProgressListener(aListener); + ]]> + </body> + </method> + + <method name="findChildShell"> + <parameter name="aDocShell"/> + <parameter name="aSoughtURI"/> + <body> + <![CDATA[ + if (aDocShell.QueryInterface(Components.interfaces.nsIWebNavigation) + .currentURI.spec == aSoughtURI.spec) + return aDocShell; + var node = aDocShell.QueryInterface( + Components.interfaces.nsIDocShellTreeItem); + for (var i = 0; i < node.childCount; ++i) { + var docShell = node.getChildAt(i); + docShell = this.findChildShell(docShell, aSoughtURI); + if (docShell) + return docShell; + } + return null; + ]]> + </body> + </method> + + <method name="onPageHide"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + // Delete the feeds cache if we're hiding the topmost page + // (as opposed to one of its iframes). + if (this.feeds && aEvent.target == this.contentDocument) + this.feeds = null; + if (!this.docShell || !this.fastFind) + return; + var tabBrowser = this.getTabBrowser(); + if (!tabBrowser || !("fastFind" in tabBrowser) || + tabBrowser.selectedBrowser == this) + this.fastFind.setDocShell(this.docShell); + ]]> + </body> + </method> + + <method name="updateBlockedPopups"> + <body> + <![CDATA[ + let event = document.createEvent("Events"); + event.initEvent("DOMUpdatePageReport", true, true); + this.dispatchEvent(event); + ]]> + </body> + </method> + + <method name="retrieveListOfBlockedPopups"> + <body> + <![CDATA[ + this.messageManager.sendAsyncMessage("PopupBlocking:GetBlockedPopupList", null); + return new Promise(resolve => { + let self = this; + this.messageManager.addMessageListener("PopupBlocking:ReplyGetBlockedPopupList", + function replyReceived(msg) { + self.messageManager.removeMessageListener("PopupBlocking:ReplyGetBlockedPopupList", + replyReceived); + resolve(msg.data.popupData); + } + ); + }); + ]]> + </body> + </method> + + <method name="unblockPopup"> + <parameter name="aPopupIndex"/> + <body><![CDATA[ + this.messageManager.sendAsyncMessage("PopupBlocking:UnblockPopup", + {index: aPopupIndex}); + ]]></body> + </method> + + <field name="blockedPopups">null</field> + + <!-- Obsolete name for blockedPopups. Used by android. --> + <property name="pageReport" + onget="return this.blockedPopups;" + readonly="true"/> + + <method name="audioPlaybackStarted"> + <body> + <![CDATA[ + let event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackStarted", true, false); + this.dispatchEvent(event); + if (this._audioBlocked) { + this._audioBlocked = false; + event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackBlockStopped", true, false); + this.dispatchEvent(event); + } + ]]> + </body> + </method> + + <method name="audioPlaybackStopped"> + <body> + <![CDATA[ + let event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackStopped", true, false); + this.dispatchEvent(event); + ]]> + </body> + </method> + + <method name="audioPlaybackBlocked"> + <body> + <![CDATA[ + this._audioBlocked = true; + let event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackBlockStarted", true, false); + this.dispatchEvent(event); + ]]> + </body> + </method> + + <field name="_audioMuted">false</field> + <property name="audioMuted" + onget="return this._audioMuted;" + readonly="true"/> + + + <field name="_audioBlocked">false</field> + <property name="audioBlocked" + onget="return this._audioBlocked;" + readonly="true"/> + + <method name="mute"> + <body> + <![CDATA[ + this._audioMuted = true; + this.messageManager.sendAsyncMessage("AudioPlayback", + {type: "mute"}); + ]]> + </body> + </method> + + <method name="unmute"> + <body> + <![CDATA[ + this._audioMuted = false; + this.messageManager.sendAsyncMessage("AudioPlayback", + {type: "unmute"}); + ]]> + </body> + </method> + + <method name="pauseMedia"> + <parameter name="disposable"/> + <body> + <![CDATA[ + let suspendedReason; + if (disposable) { + suspendedReason = "mediaControlPaused"; + } else { + suspendedReason = "lostAudioFocusTransiently"; + } + + this.messageManager.sendAsyncMessage("AudioPlayback", + {type: suspendedReason}); + ]]> + </body> + </method> + + <method name="stopMedia"> + <body> + <![CDATA[ + this.messageManager.sendAsyncMessage("AudioPlayback", + {type: "mediaControlStopped"}); + ]]> + </body> + </method> + + <method name="blockMedia"> + <body> + <![CDATA[ + this._audioBlocked = true; + this.messageManager.sendAsyncMessage("AudioPlayback", + {type: "blockInactivePageMedia"}); + ]]> + </body> + </method> + + <method name="resumeMedia"> + <body> + <![CDATA[ + this._audioBlocked = false; + this.messageManager.sendAsyncMessage("AudioPlayback", + {type: "resumeMedia"}); + let event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackBlockStopped", true, false); + this.dispatchEvent(event); + ]]> + </body> + </method> + + <property name="securityUI"> + <getter> + <![CDATA[ + if (!this.docShell.securityUI) { + const SECUREBROWSERUI_CONTRACTID = "@mozilla.org/secure_browser_ui;1"; + if (!this.hasAttribute("disablesecurity") && + SECUREBROWSERUI_CONTRACTID in Components.classes) { + var securityUI = Components.classes[SECUREBROWSERUI_CONTRACTID] + .createInstance(Components.interfaces.nsISecureBrowserUI); + securityUI.init(this.contentWindow); + } + } + + return this.docShell.securityUI; + ]]> + </getter> + <setter> + <![CDATA[ + this.docShell.securityUI = val; + ]]> + </setter> + </property> + + <!-- increases or decreases the browser's network priority --> + <method name="adjustPriority"> + <parameter name="adjustment"/> + <body><![CDATA[ + let loadGroup = this.webNavigation.QueryInterface(Components.interfaces.nsIDocumentLoader) + .loadGroup.QueryInterface(Components.interfaces.nsISupportsPriority); + loadGroup.adjustPriority(adjustment); + ]]></body> + </method> + + <!-- sets the browser's network priority to a discrete value --> + <method name="setPriority"> + <parameter name="priority"/> + <body><![CDATA[ + let loadGroup = this.webNavigation.QueryInterface(Components.interfaces.nsIDocumentLoader) + .loadGroup.QueryInterface(Components.interfaces.nsISupportsPriority); + loadGroup.priority = priority; + ]]></body> + </method> + + <field name="urlbarChangeTracker"> + ({ + _startedLoadSinceLastUserTyping: false, + + startedLoad() { + this._startedLoadSinceLastUserTyping = true; + }, + finishedLoad() { + this._startedLoadSinceLastUserTyping = false; + }, + userTyped() { + this._startedLoadSinceLastUserTyping = false; + }, + }) + </field> + + <method name="didStartLoadSinceLastUserTyping"> + <body><![CDATA[ + return !this.inLoadURI && + this.urlbarChangeTracker._startedLoadSinceLastUserTyping; + ]]></body> + </method> + + <field name="_userTypedValue"> + null + </field> + + <property name="userTypedValue" + onget="return this._userTypedValue;"> + <setter><![CDATA[ + this.urlbarChangeTracker.userTyped(); + this._userTypedValue = val; + return val; + ]]></setter> + </property> + + <field name="mFormFillAttached"> + false + </field> + + <field name="isShowingMessage"> + false + </field> + + <field name="droppedLinkHandler"> + null + </field> + + <field name="mIconURL">null</field> + + <!-- This is managed by the tabbrowser --> + <field name="lastURI">null</field> + + <field name="mDestroyed">false</field> + + <constructor> + <![CDATA[ + try { + // |webNavigation.sessionHistory| will have been set by the frame + // loader when creating the docShell as long as this xul:browser + // doesn't have the 'disablehistory' attribute set. + if (this.docShell && this.webNavigation.sessionHistory) { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.addObserver(this, "browser:purge-session-history", true); + + // enable global history if we weren't told otherwise + if (!this.hasAttribute("disableglobalhistory") && !this.isRemoteBrowser) { + try { + this.docShell.useGlobalHistory = true; + } catch (ex) { + // This can occur if the Places database is locked + Components.utils.reportError("Error enabling browser global history: " + ex); + } + } + } + } + catch (e) { + Components.utils.reportError(e); + } + try { + var securityUI = this.securityUI; + } + catch (e) { + } + + // XXX tabbrowser.xml sets "relatedBrowser" as a direct property on + // some browsers before they are put into a DOM (and get a binding). + // This hack makes sure that we hold a weak reference to the other + // browser (and go through the proper getter and setter). + if (this.hasOwnProperty("relatedBrowser")) { + var relatedBrowser = this.relatedBrowser; + delete this.relatedBrowser; + this.relatedBrowser = relatedBrowser; + } + + if (!this.isRemoteBrowser) { + this.addEventListener("pagehide", this.onPageHide, true); + } + + if (this.messageManager) { + this.messageManager.addMessageListener("PopupBlocking:UpdateBlockedPopups", this); + this.messageManager.addMessageListener("Autoscroll:Start", this); + this.messageManager.addMessageListener("Autoscroll:Cancel", this); + this.messageManager.addMessageListener("AudioPlayback:Start", this); + this.messageManager.addMessageListener("AudioPlayback:Stop", this); + this.messageManager.addMessageListener("AudioPlayback:Block", this); + } + ]]> + </constructor> + + <destructor> + <![CDATA[ + this.destroy(); + ]]> + </destructor> + + <!-- This is necessary because the destructor doesn't always get called when + we are removed from a tabbrowser. This will be explicitly called by tabbrowser. + + Note: this function is overriden in remote-browser.xml, so any clean-up that + also applies to browser.isRemoteBrowser = true must be duplicated there. --> + <method name="destroy"> + <body> + <![CDATA[ + if (this.mDestroyed) + return; + this.mDestroyed = true; + + if (this.docShell && this.webNavigation.sessionHistory) { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + try { + os.removeObserver(this, "browser:purge-session-history"); + } catch (ex) { + // It's not clear why this sometimes throws an exception. + } + } + + this._fastFind = null; + this._webBrowserFind = null; + + // The feeds cache can keep the document inside this browser alive. + this.feeds = null; + + this.lastURI = null; + + if (!this.isRemoteBrowser) { + this.removeEventListener("pagehide", this.onPageHide, true); + } + + if (this._autoScrollNeedsCleanup) { + // we polluted the global scope, so clean it up + this._autoScrollPopup.parentNode.removeChild(this._autoScrollPopup); + } + ]]> + </body> + </method> + + <!-- + We call this _receiveMessage (and alias receiveMessage to it) so that + bindings that inherit from this one can delegate to it. + --> + <method name="_receiveMessage"> + <parameter name="aMessage"/> + <body><![CDATA[ + let data = aMessage.data; + switch (aMessage.name) { + case "PopupBlocking:UpdateBlockedPopups": { + this.blockedPopups = { + length: data.count, + reported: !data.freshPopup, + }; + + this.updateBlockedPopups(); + break; + } + case "Autoscroll:Start": { + if (!this.autoscrollEnabled) { + return false; + } + this.startScroll(data.scrolldir, data.screenX, data.screenY); + return true; + } + case "Autoscroll:Cancel": + this._autoScrollPopup.hidePopup(); + break; + case "AudioPlayback:Start": + this.audioPlaybackStarted(); + break; + case "AudioPlayback:Stop": + this.audioPlaybackStopped(); + break; + case "AudioPlayback:Block": + this.audioPlaybackBlocked(); + break; + } + return undefined; + ]]></body> + </method> + + <method name="receiveMessage"> + <parameter name="aMessage"/> + <body><![CDATA[ + return this._receiveMessage(aMessage); + ]]></body> + </method> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aState"/> + <body> + <![CDATA[ + if (aTopic != "browser:purge-session-history") + return; + + this.purgeSessionHistory(); + ]]> + </body> + </method> + + <method name="purgeSessionHistory"> + <body> + <![CDATA[ + this.messageManager.sendAsyncMessage("Browser:PurgeSessionHistory"); + ]]> + </body> + </method> + + <method name="createAboutBlankContentViewer"> + <parameter name="aPrincipal"/> + <body> + <![CDATA[ + let principal = BrowserUtils.principalWithMatchingOA(aPrincipal, this.contentPrincipal); + this.docShell.createAboutBlankContentViewer(principal); + ]]> + </body> + </method> + + <field name="_AUTOSCROLL_SNAP">10</field> + <field name="_scrolling">false</field> + <field name="_startX">null</field> + <field name="_startY">null</field> + <field name="_autoScrollPopup">null</field> + <field name="_autoScrollNeedsCleanup">false</field> + + <method name="stopScroll"> + <body> + <![CDATA[ + if (this._scrolling) { + this._scrolling = false; + window.removeEventListener("mousemove", this, true); + window.removeEventListener("mousedown", this, true); + window.removeEventListener("mouseup", this, true); + window.removeEventListener("DOMMouseScroll", this, true); + window.removeEventListener("contextmenu", this, true); + window.removeEventListener("keydown", this, true); + window.removeEventListener("keypress", this, true); + window.removeEventListener("keyup", this, true); + this.messageManager.sendAsyncMessage("Autoscroll:Stop"); + } + ]]> + </body> + </method> + + <method name="_createAutoScrollPopup"> + <body> + <![CDATA[ + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var popup = document.createElementNS(XUL_NS, "panel"); + popup.className = "autoscroller"; + // We set this attribute on the element so that mousemove + // events can be handled by browser-content.js. + popup.setAttribute("mousethrough", "always"); + popup.setAttribute("rolluponmousewheel", "true"); + return popup; + ]]> + </body> + </method> + + <method name="startScroll"> + <parameter name="scrolldir"/> + <parameter name="screenX"/> + <parameter name="screenY"/> + <body><![CDATA[ + if (!this._autoScrollPopup) { + if (this.hasAttribute("autoscrollpopup")) { + // our creator provided a popup to share + this._autoScrollPopup = document.getElementById(this.getAttribute("autoscrollpopup")); + } + else { + // we weren't provided a popup; we have to use the global scope + this._autoScrollPopup = this._createAutoScrollPopup(); + document.documentElement.appendChild(this._autoScrollPopup); + this._autoScrollNeedsCleanup = true; + } + } + + // we need these attributes so themers don't need to create per-platform packages + if (screen.colorDepth > 8) { // need high color for transparency + // Exclude second-rate platforms + this._autoScrollPopup.setAttribute("transparent", !/BeOS|OS\/2/.test(navigator.appVersion)); + // Enable translucency on Windows and Mac + this._autoScrollPopup.setAttribute("translucent", /Win|Mac/.test(navigator.platform)); + } + + this._autoScrollPopup.setAttribute("noautofocus", "true"); + this._autoScrollPopup.setAttribute("scrolldir", scrolldir); + this._autoScrollPopup.addEventListener("popuphidden", this, true); + this._autoScrollPopup.showPopup(document.documentElement, + screenX, + screenY, + "popup", null, null); + this._ignoreMouseEvents = true; + this._scrolling = true; + this._startX = screenX; + this._startY = screenY; + + window.addEventListener("mousemove", this, true); + window.addEventListener("mousedown", this, true); + window.addEventListener("mouseup", this, true); + window.addEventListener("DOMMouseScroll", this, true); + window.addEventListener("contextmenu", this, true); + window.addEventListener("keydown", this, true); + window.addEventListener("keypress", this, true); + window.addEventListener("keyup", this, true); + ]]> + </body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (this._scrolling) { + switch (aEvent.type) { + case "mousemove": { + var x = aEvent.screenX - this._startX; + var y = aEvent.screenY - this._startY; + + if ((x > this._AUTOSCROLL_SNAP || x < -this._AUTOSCROLL_SNAP) || + (y > this._AUTOSCROLL_SNAP || y < -this._AUTOSCROLL_SNAP)) + this._ignoreMouseEvents = false; + break; + } + case "mouseup": + case "mousedown": + case "contextmenu": { + if (!this._ignoreMouseEvents) { + // Use a timeout to prevent the mousedown from opening the popup again. + // Ideally, we could use preventDefault here, but contenteditable + // and middlemouse paste don't interact well. See bug 1188536. + setTimeout(() => this._autoScrollPopup.hidePopup(), 0); + } + this._ignoreMouseEvents = false; + break; + } + case "DOMMouseScroll": { + this._autoScrollPopup.hidePopup(); + event.preventDefault(); + break; + } + case "popuphidden": { + this._autoScrollPopup.removeEventListener("popuphidden", this, true); + this.stopScroll(); + break; + } + case "keydown": { + if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { + // the escape key will be processed by + // nsXULPopupManager::KeyDown and the panel will be closed. + // So, don't consume the key event here. + break; + } + // don't break here. we need to eat keydown events. + } + case "keypress": + case "keyup": { + // All keyevents should be eaten here during autoscrolling. + aEvent.stopPropagation(); + aEvent.preventDefault(); + break; + } + } + } + ]]> + </body> + </method> + + <method name="closeBrowser"> + <body> + <![CDATA[ + // The request comes from a XPCOM component, we'd want to redirect + // the request to tabbrowser. + let tabbrowser = this.getTabBrowser(); + if (tabbrowser) { + let tab = tabbrowser.getTabForBrowser(this); + if (tab) { + tabbrowser.removeTab(tab); + return; + } + } + + throw new Error("Closing a browser which was not attached to a tabbrowser is unsupported."); + ]]> + </body> + </method> + + <method name="swapBrowsers"> + <parameter name="aOtherBrowser"/> + <body> + <![CDATA[ + // The request comes from a XPCOM component, we'd want to redirect + // the request to tabbrowser so tabbrowser will be setup correctly, + // and it will eventually call swapDocShells. + let tabbrowser = this.getTabBrowser(); + if (tabbrowser) { + let tab = tabbrowser.getTabForBrowser(this); + if (tab) { + tabbrowser.swapBrowsers(tab, aOtherBrowser); + return; + } + } + + // If we're not attached to a tabbrowser, just swap. + this.swapDocShells(aOtherBrowser); + ]]> + </body> + </method> + + <method name="swapDocShells"> + <parameter name="aOtherBrowser"/> + <body> + <![CDATA[ + if (this.isRemoteBrowser != aOtherBrowser.isRemoteBrowser) + throw new Error("Can only swap docshells between browsers in the same process."); + + // Give others a chance to swap state. + // IMPORTANT: Since a swapDocShells call does not swap the messageManager + // instances attached to a browser to aOtherBrowser, others + // will need to add the message listeners to the new + // messageManager. + // This is not a bug in swapDocShells or the FrameLoader, + // merely a design decision: If message managers were swapped, + // so that no new listeners were needed, the new + // aOtherBrowser.messageManager would have listeners pointing + // to the JS global of the current browser, which would rather + // easily create leaks while swapping. + // IMPORTANT2: When the current browser element is removed from DOM, + // which is quite common after a swpDocShells call, its + // frame loader is destroyed, and that destroys the relevant + // message manager, which will remove the listeners. + let event = new CustomEvent("SwapDocShells", {"detail": aOtherBrowser}); + this.dispatchEvent(event); + event = new CustomEvent("SwapDocShells", {"detail": this}); + aOtherBrowser.dispatchEvent(event); + + // We need to swap fields that are tied to our docshell or related to + // the loaded page + // Fields which are built as a result of notifactions (pageshow/hide, + // DOMLinkAdded/Removed, onStateChange) should not be swapped here, + // because these notifications are dispatched again once the docshells + // are swapped. + var fieldsToSwap = [ + "_docShell", + "_webBrowserFind", + "_contentWindow", + "_webNavigation" + ]; + + if (this.isRemoteBrowser) { + fieldsToSwap.push(...[ + "_remoteWebNavigation", + "_remoteWebNavigationImpl", + "_remoteWebProgressManager", + "_remoteWebProgress", + "_remoteFinder", + "_securityUI", + "_documentURI", + "_documentContentType", + "_contentTitle", + "_characterSet", + "_contentPrincipal", + "_imageDocument", + "_fullZoom", + "_textZoom", + "_isSyntheticDocument", + "_innerWindowID", + "_manifestURI", + ]); + } + + var ourFieldValues = {}; + var otherFieldValues = {}; + for (let field of fieldsToSwap) { + ourFieldValues[field] = this[field]; + otherFieldValues[field] = aOtherBrowser[field]; + } + + if (window.PopupNotifications) + PopupNotifications._swapBrowserNotifications(aOtherBrowser, this); + + try { + this.swapFrameLoaders(aOtherBrowser); + } catch (ex) { + // This may not be implemented for browser elements that are not + // attached to a BrowserDOMWindow. + } + + for (let field of fieldsToSwap) { + this[field] = otherFieldValues[field]; + aOtherBrowser[field] = ourFieldValues[field]; + } + + if (!this.isRemoteBrowser) { + // Null the current nsITypeAheadFind instances so that they're + // lazily re-created on access. We need to do this because they + // might have attached the wrong docShell. + this._fastFind = aOtherBrowser._fastFind = null; + } + else { + // Rewire the remote listeners + this._remoteWebNavigationImpl.swapBrowser(this); + aOtherBrowser._remoteWebNavigationImpl.swapBrowser(aOtherBrowser); + + if (this._remoteWebProgressManager && aOtherBrowser._remoteWebProgressManager) { + this._remoteWebProgressManager.swapBrowser(this); + aOtherBrowser._remoteWebProgressManager.swapBrowser(aOtherBrowser); + } + + if (this._remoteFinder) + this._remoteFinder.swapBrowser(this); + if (aOtherBrowser._remoteFinder) + aOtherBrowser._remoteFinder.swapBrowser(aOtherBrowser); + } + + event = new CustomEvent("EndSwapDocShells", {"detail": aOtherBrowser}); + this.dispatchEvent(event); + event = new CustomEvent("EndSwapDocShells", {"detail": this}); + aOtherBrowser.dispatchEvent(event); + ]]> + </body> + </method> + + <method name="getInPermitUnload"> + <parameter name="aCallback"/> + <body> + <![CDATA[ + if (!this.docShell || !this.docShell.contentViewer) { + aCallback(false); + return; + } + aCallback(this.docShell.contentViewer.inPermitUnload); + ]]> + </body> + </method> + + <method name="permitUnload"> + <body> + <![CDATA[ + if (!this.docShell || !this.docShell.contentViewer) { + return {permitUnload: true, timedOut: false}; + } + return {permitUnload: this.docShell.contentViewer.permitUnload(), timedOut: false}; + ]]> + </body> + </method> + + <method name="print"> + <parameter name="aOuterWindowID"/> + <parameter name="aPrintSettings"/> + <parameter name="aPrintProgressListener"/> + <body> + <![CDATA[ + var owner = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner); + if (!owner.frameLoader) { + throw Components.Exception("No frame loader.", + Components.results.NS_ERROR_FAILURE); + } + + owner.frameLoader.print(aOuterWindowID, aPrintSettings, + aPrintProgressListener); + ]]> + </body> + </method> + + <method name="dropLinks"> + <parameter name="aLinksCount"/> + <parameter name="aLinks"/> + <body><![CDATA[ + if (!this.droppedLinkHandler) { + return false; + } + let links = []; + for (let i = 0; i < aLinksCount; i += 3) { + links.push({ + url: aLinks[i], + name: aLinks[i + 1], + type: aLinks[i + 2], + }); + } + this.droppedLinkHandler(null, links); + return true; + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="keypress" keycode="VK_F7" group="system"> + <![CDATA[ + if (event.defaultPrevented || !event.isTrusted) + return; + + const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled"; + const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret"; + const kPrefCaretBrowsingOn = "accessibility.browsewithcaret"; + + var isEnabled = this.mPrefs.getBoolPref(kPrefShortcutEnabled); + if (!isEnabled) + return; + + // Toggle browse with caret mode + var browseWithCaretOn = false; + var warn = true; + + try { + warn = this.mPrefs.getBoolPref(kPrefWarnOnEnable); + } catch (ex) { + } + + try { + browseWithCaretOn = this.mPrefs.getBoolPref(kPrefCaretBrowsingOn); + } catch (ex) { + } + if (warn && !browseWithCaretOn) { + var checkValue = {value:false}; + var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + + var buttonPressed = promptService.confirmEx(window, + this.mStrBundle.GetStringFromName('browsewithcaret.checkWindowTitle'), + this.mStrBundle.GetStringFromName('browsewithcaret.checkLabel'), + // Make "No" the default: + promptService.STD_YES_NO_BUTTONS | promptService.BUTTON_POS_1_DEFAULT, + null, null, null, this.mStrBundle.GetStringFromName('browsewithcaret.checkMsg'), + checkValue); + if (buttonPressed != 0) { + if (checkValue.value) { + try { + this.mPrefs.setBoolPref(kPrefShortcutEnabled, false); + } catch (ex) { + } + } + return; + } + if (checkValue.value) { + try { + this.mPrefs.setBoolPref(kPrefWarnOnEnable, false); + } + catch (ex) { + } + } + } + + // Toggle the pref + try { + this.mPrefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn); + } catch (ex) { + } + ]]> + </handler> + <handler event="dragover" group="system"> + <![CDATA[ + if (!this.droppedLinkHandler || event.defaultPrevented) + return; + + // For drags that appear to be internal text (for example, tab drags), + // set the dropEffect to 'none'. This prevents the drop even if some + // other listener cancelled the event. + var types = event.dataTransfer.types; + if (types.includes("text/x-moz-text-internal") && !types.includes("text/plain")) { + event.dataTransfer.dropEffect = "none"; + event.stopPropagation(); + event.preventDefault(); + } + + // No need to handle "dragover" in e10s, since nsDocShellTreeOwner.cpp in the child process + // handles that case using "@mozilla.org/content/dropped-link-handler;1" service. + if (this.isRemoteBrowser) + return; + + let linkHandler = Components.classes["@mozilla.org/content/dropped-link-handler;1"]. + getService(Components.interfaces.nsIDroppedLinkHandler); + if (linkHandler.canDropLink(event, false)) + event.preventDefault(); + ]]> + </handler> + <handler event="drop" group="system"> + <![CDATA[ + // No need to handle "drop" in e10s, since nsDocShellTreeOwner.cpp in the child process + // handles that case using "@mozilla.org/content/dropped-link-handler;1" service. + if (!this.droppedLinkHandler || event.defaultPrevented || this.isRemoteBrowser) + return; + + let name = { }; + let linkHandler = Components.classes["@mozilla.org/content/dropped-link-handler;1"]. + getService(Components.interfaces.nsIDroppedLinkHandler); + try { + // Pass true to prevent the dropping of javascript:/data: URIs + var links = linkHandler.dropLinks(event, true); + } catch (ex) { + return; + } + + if (links.length) { + this.droppedLinkHandler(event, links); + } + ]]> + </handler> + </handlers> + + </binding> + +</bindings> diff --git a/toolkit/content/widgets/button.xml b/toolkit/content/widgets/button.xml new file mode 100644 index 0000000000..89d9d86c6d --- /dev/null +++ b/toolkit/content/widgets/button.xml @@ -0,0 +1,389 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="buttonBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="button-base" extends="chrome://global/content/bindings/general.xml#basetext" role="xul:button"> + <implementation implements="nsIDOMXULButtonElement"> + <property name="type" + onget="return this.getAttribute('type');" + onset="this.setAttribute('type', val); return val;"/> + + <property name="dlgType" + onget="return this.getAttribute('dlgtype');" + onset="this.setAttribute('dlgtype', val); return val;"/> + + <property name="group" + onget="return this.getAttribute('group');" + onset="this.setAttribute('group', val); return val;"/> + + <property name="open" onget="return this.hasAttribute('open');"> + <setter><![CDATA[ + if (this.boxObject instanceof MenuBoxObject) { + this.boxObject.openMenu(val); + } else if (val) { + // Fall back to just setting the attribute + this.setAttribute('open', 'true'); + } else { + this.removeAttribute('open'); + } + return val; + ]]></setter> + </property> + + <property name="checked" onget="return this.hasAttribute('checked');"> + <setter><![CDATA[ + if (this.type == "checkbox") { + this.checkState = val ? 1 : 0; + } else if (this.type == "radio" && val) { + var sibs = this.parentNode.getElementsByAttribute("group", this.group); + for (var i = 0; i < sibs.length; ++i) + sibs[i].removeAttribute("checked"); + } + + if (val) + this.setAttribute("checked", "true"); + else + this.removeAttribute("checked"); + + return val; + ]]></setter> + </property> + + <property name="checkState"> + <getter><![CDATA[ + var state = this.getAttribute("checkState"); + if (state == "") + return this.checked ? 1 : 0; + if (state == "0") + return 0; + if (state == "2") + return 2; + return 1; + ]]></getter> + <setter><![CDATA[ + this.setAttribute("checkState", val); + return val; + ]]></setter> + </property> + + <property name="autoCheck" + onget="return this.getAttribute('autoCheck') == 'true';" + onset="this.setAttribute('autoCheck', val); return val;"/> + + <method name ="filterButtons"> + <parameter name="node"/> + <body> + <![CDATA[ + // if the node isn't visible, don't descend into it. + var cs = node.ownerDocument.defaultView.getComputedStyle(node, null); + if (cs.visibility != "visible" || cs.display == "none") { + return NodeFilter.FILTER_REJECT; + } + // but it may be a popup element, in which case we look at "state"... + if (cs.display == "-moz-popup" && node.state != "open") { + return NodeFilter.FILTER_REJECT; + } + // OK - the node seems visible, so it is a candidate. + if (node.localName == "button" && node.accessKey && !node.disabled) + return NodeFilter.FILTER_ACCEPT; + return NodeFilter.FILTER_SKIP; + ]]> + </body> + </method> + + <method name="fireAccessKeyButton"> + <parameter name="aSubtree"/> + <parameter name="aAccessKeyLower"/> + <body> + <![CDATA[ + var iterator = aSubtree.ownerDocument.createTreeWalker(aSubtree, + NodeFilter.SHOW_ELEMENT, + this.filterButtons); + while (iterator.nextNode()) { + var test = iterator.currentNode; + if (test.accessKey.toLowerCase() == aAccessKeyLower && + !test.disabled && !test.collapsed && !test.hidden) { + test.focus(); + test.click(); + return true; + } + } + return false; + ]]> + </body> + </method> + + <method name="_handleClick"> + <body> + <![CDATA[ + if (!this.disabled && + (this.autoCheck || !this.hasAttribute("autoCheck"))) { + + if (this.type == "checkbox") { + this.checked = !this.checked; + } else if (this.type == "radio") { + this.checked = true; + } + } + ]]> + </body> + </method> + </implementation> + + <handlers> + <!-- While it would seem we could do this by handling oncommand, we can't + because any external oncommand handlers might get called before ours, + and then they would see the incorrect value of checked. Additionally + a command attribute would redirect the command events anyway.--> + <handler event="click" button="0" action="this._handleClick();"/> + <handler event="keypress" key=" "> + <![CDATA[ + this._handleClick(); + // Prevent page from scrolling on the space key. + event.preventDefault(); + ]]> + </handler> + + <handler event="keypress"> + <![CDATA[ + if (this.boxObject instanceof MenuBoxObject) { + if (this.open) + return; + } else { + if (event.keyCode == KeyEvent.DOM_VK_UP || + (event.keyCode == KeyEvent.DOM_VK_LEFT && + document.defaultView.getComputedStyle(this.parentNode, "") + .direction == "ltr") || + (event.keyCode == KeyEvent.DOM_VK_RIGHT && + document.defaultView.getComputedStyle(this.parentNode, "") + .direction == "rtl")) { + event.preventDefault(); + window.document.commandDispatcher.rewindFocus(); + return; + } + + if (event.keyCode == KeyEvent.DOM_VK_DOWN || + (event.keyCode == KeyEvent.DOM_VK_RIGHT && + document.defaultView.getComputedStyle(this.parentNode, "") + .direction == "ltr") || + (event.keyCode == KeyEvent.DOM_VK_LEFT && + document.defaultView.getComputedStyle(this.parentNode, "") + .direction == "rtl")) { + event.preventDefault(); + window.document.commandDispatcher.advanceFocus(); + return; + } + } + + if (event.keyCode || event.charCode <= 32 || event.altKey || + event.ctrlKey || event.metaKey) + return; // No printable char pressed, not a potential accesskey + + // Possible accesskey pressed + var charPressedLower = String.fromCharCode(event.charCode).toLowerCase(); + + // If the accesskey of the current button is pressed, just activate it + if (this.accessKey.toLowerCase() == charPressedLower) { + this.click(); + return; + } + + // Search for accesskey in the list of buttons for this doc and each subdoc + // Get the buttons for the main document and all sub-frames + for (var frameCount = -1; frameCount < window.top.frames.length; frameCount++) { + var doc = (frameCount == -1)? window.top.document: + window.top.frames[frameCount].document + if (this.fireAccessKeyButton(doc.documentElement, charPressedLower)) + return; + } + + // Test anonymous buttons + var dlg = window.top.document; + var buttonBox = dlg.getAnonymousElementByAttribute(dlg.documentElement, + "anonid", "buttons"); + if (buttonBox) + this.fireAccessKeyButton(buttonBox, charPressedLower); + ]]> + </handler> + </handlers> + </binding> + + <binding id="button" display="xul:button" + extends="chrome://global/content/bindings/button.xml#button-base"> + <resources> + <stylesheet src="chrome://global/skin/button.css"/> + </resources> + + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:hbox class="box-inherit button-box" xbl:inherits="align,dir,pack,orient" + align="center" pack="center" flex="1" anonid="button-box"> + <children> + <xul:image class="button-icon" xbl:inherits="src=image"/> + <xul:label class="button-text" xbl:inherits="value=label,accesskey,crop"/> + </children> + </xul:hbox> + </content> + </binding> + + <binding id="menu" display="xul:menu" + extends="chrome://global/content/bindings/button.xml#button"> + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:hbox class="box-inherit button-box" xbl:inherits="align,dir,pack,orient" + align="center" pack="center" flex="1"> + <children> + <xul:hbox class="box-inherit" xbl:inherits="align,dir,pack,orient" + align="center" pack="center" flex="1"> + <xul:image class="button-icon" xbl:inherits="src=image"/> + <xul:label class="button-text" xbl:inherits="value=label,accesskey,crop"/> + </xul:hbox> + <xul:dropmarker class="button-menu-dropmarker" xbl:inherits="open,disabled,label"/> + </children> + </xul:hbox> + </content> + + <handlers> + <handler event="keypress" keycode="VK_RETURN" action="this.open = true;"/> + <handler event="keypress" key=" "> + <![CDATA[ + this.open = true; + // Prevent page from scrolling on the space key. + event.preventDefault(); + ]]> + </handler> + </handlers> + </binding> + + <binding id="menu-button-base" + extends="chrome://global/content/bindings/button.xml#button-base"> + <implementation implements="nsIDOMEventListener"> + <constructor> + this.init(); + </constructor> + + <method name="init"> + <body> + <![CDATA[ + var btn = document.getAnonymousElementByAttribute(this, "anonid", "button"); + if (!btn) + throw "XBL binding for <button type=\"menu-button\"/> binding must contain an element with anonid=\"button\""; + + var menubuttonParent = this; + btn.addEventListener("mouseover", function() { + if (!this.disabled) + menubuttonParent.buttonover = true; + }, true); + btn.addEventListener("mouseout", function() { + menubuttonParent.buttonover = false; + }, true); + btn.addEventListener("mousedown", function() { + if (!this.disabled) { + menubuttonParent.buttondown = true; + document.addEventListener("mouseup", menubuttonParent, true); + } + }, true); + ]]> + </body> + </method> + + <property name="buttonover" onget="return this.getAttribute('buttonover');"> + <setter> + <![CDATA[ + var v = val || val == "true"; + if (!v && this.buttondown) { + this.buttondown = false; + this._pendingActive = true; + } + else if (this._pendingActive) { + this.buttondown = true; + this._pendingActive = false; + } + + if (v) + this.setAttribute("buttonover", "true"); + else + this.removeAttribute("buttonover"); + return val; + ]]> + </setter> + </property> + + <property name="buttondown" onget="return this.getAttribute('buttondown') == 'true';"> + <setter> + <![CDATA[ + if (val || val == "true") + this.setAttribute("buttondown", "true"); + else + this.removeAttribute("buttondown"); + return val; + ]]> + </setter> + </property> + + <field name="_pendingActive">false</field> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + this._pendingActive = false; + this.buttondown = false; + document.removeEventListener("mouseup", this, true); + ]]> + </body> + </method> + + </implementation> + + <handlers> + <handler event="keypress" keycode="VK_RETURN"> + if (event.originalTarget == this) + this.open = true; + </handler> + <handler event="keypress" key=" "> + if (event.originalTarget == this) { + this.open = true; + // Prevent page from scrolling on the space key. + event.preventDefault(); + } + </handler> + </handlers> + </binding> + + <binding id="menu-button" display="xul:menu" + extends="chrome://global/content/bindings/button.xml#menu-button-base"> + <resources> + <stylesheet src="chrome://global/skin/button.css"/> + </resources> + + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:button class="box-inherit button-menubutton-button" + anonid="button" flex="1" allowevents="true" + xbl:inherits="disabled,crop,image,label,accesskey,command, + buttonover,buttondown,align,dir,pack,orient"> + <children/> + </xul:button> + <xul:dropmarker class="button-menubutton-dropmarker" xbl:inherits="open,disabled,label"/> + </content> + </binding> + + <binding id="button-image" display="xul:button" + extends="chrome://global/content/bindings/button.xml#button"> + <content> + <xul:image class="button-image-icon" xbl:inherits="src=image"/> + </content> + </binding> + + <binding id="button-repeat" display="xul:autorepeatbutton" + extends="chrome://global/content/bindings/button.xml#button"/> + +</bindings> diff --git a/toolkit/content/widgets/checkbox.xml b/toolkit/content/widgets/checkbox.xml new file mode 100644 index 0000000000..c6a5babfd8 --- /dev/null +++ b/toolkit/content/widgets/checkbox.xml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="checkboxBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="checkbox" extends="chrome://global/content/bindings/checkbox.xml#checkbox-baseline"> + <resources> + <stylesheet src="chrome://global/skin/checkbox.css"/> + </resources> + </binding> + + <binding id="checkbox-baseline" role="xul:checkbox" + extends="chrome://global/content/bindings/general.xml#basetext"> + <content> + <xul:image class="checkbox-check" xbl:inherits="checked,disabled"/> + <xul:hbox class="checkbox-label-box" flex="1"> + <xul:image class="checkbox-icon" xbl:inherits="src"/> + <xul:label class="checkbox-label" xbl:inherits="xbl:text=label,accesskey,crop" flex="1"/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMXULCheckboxElement"> + <method name="setChecked"> + <parameter name="aValue"/> + <body> + <![CDATA[ + var change = (aValue != (this.getAttribute('checked') == 'true')); + if (aValue) + this.setAttribute('checked', 'true'); + else + this.removeAttribute('checked'); + if (change) { + var event = document.createEvent('Events'); + event.initEvent('CheckboxStateChange', true, true); + this.dispatchEvent(event); + } + return aValue; + ]]> + </body> + </method> + + <!-- public implementation --> + <property name="checked" onset="return this.setChecked(val);" + onget="return this.getAttribute('checked') == 'true';"/> + </implementation> + + <handlers> + <!-- While it would seem we could do this by handling oncommand, we need can't + because any external oncommand handlers might get called before ours, and + then they would see the incorrect value of checked. --> + <handler event="click" button="0" action="if (!this.disabled) this.checked = !this.checked;"/> + <handler event="keypress" key=" "> + <![CDATA[ + this.checked = !this.checked; + // Prevent page from scrolling on the space key. + event.preventDefault(); + ]]> + </handler> + </handlers> + </binding> + + <binding id="checkbox-with-spacing" + extends="chrome://global/content/bindings/checkbox.xml#checkbox"> + + <content> + <xul:hbox class="checkbox-spacer-box"> + <xul:image class="checkbox-check" xbl:inherits="checked,disabled"/> + </xul:hbox> + <xul:hbox class="checkbox-label-center-box" flex="1"> + <xul:hbox class="checkbox-label-box" flex="1"> + <xul:image class="checkbox-icon" xbl:inherits="src"/> + <xul:label class="checkbox-label" xbl:inherits="xbl:text=label,accesskey,crop" flex="1"/> + </xul:hbox> + </xul:hbox> + </content> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/colorpicker.xml b/toolkit/content/widgets/colorpicker.xml new file mode 100644 index 0000000000..30f8a63540 --- /dev/null +++ b/toolkit/content/widgets/colorpicker.xml @@ -0,0 +1,565 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="colorpickerBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="colorpicker" extends="chrome://global/content/bindings/general.xml#basecontrol"> + <resources> + <stylesheet src="chrome://global/skin/colorpicker.css"/> + </resources> + + <content> + <xul:vbox flex="1"> + + <xul:hbox> + <xul:image class="colorpickertile cp-light" color="#FFFFFF"/> + <xul:image class="colorpickertile cp-light" color="#FFCCCC"/> + <xul:image class="colorpickertile cp-light" color="#FFCC99"/> + <xul:image class="colorpickertile cp-light" color="#FFFF99"/> + <xul:image class="colorpickertile cp-light" color="#FFFFCC"/> + <xul:image class="colorpickertile cp-light" color="#99FF99"/> + <xul:image class="colorpickertile cp-light" color="#99FFFF"/> + <xul:image class="colorpickertile cp-light" color="#CCFFFF"/> + <xul:image class="colorpickertile cp-light" color="#CCCCFF"/> + <xul:image class="colorpickertile cp-light" color="#FFCCFF"/> + </xul:hbox> + <xul:hbox> + <xul:image class="colorpickertile" color="#CCCCCC"/> + <xul:image class="colorpickertile" color="#FF6666"/> + <xul:image class="colorpickertile" color="#FF9966"/> + <xul:image class="colorpickertile cp-light" color="#FFFF66"/> + <xul:image class="colorpickertile cp-light" color="#FFFF33"/> + <xul:image class="colorpickertile cp-light" color="#66FF99"/> + <xul:image class="colorpickertile cp-light" color="#33FFFF"/> + <xul:image class="colorpickertile cp-light" color="#66FFFF"/> + <xul:image class="colorpickertile" color="#9999FF"/> + <xul:image class="colorpickertile" color="#FF99FF"/> + </xul:hbox> + <xul:hbox> + <xul:image class="colorpickertile" color="#C0C0C0"/> + <xul:image class="colorpickertile" color="#FF0000"/> + <xul:image class="colorpickertile" color="#FF9900"/> + <xul:image class="colorpickertile" color="#FFCC66"/> + <xul:image class="colorpickertile cp-light" color="#FFFF00"/> + <xul:image class="colorpickertile cp-light" color="#33FF33"/> + <xul:image class="colorpickertile" color="#66CCCC"/> + <xul:image class="colorpickertile" color="#33CCFF"/> + <xul:image class="colorpickertile" color="#6666CC"/> + <xul:image class="colorpickertile" color="#CC66CC"/> + </xul:hbox> + <xul:hbox> + <xul:image class="colorpickertile" color="#999999"/> + <xul:image class="colorpickertile" color="#CC0000"/> + <xul:image class="colorpickertile" color="#FF6600"/> + <xul:image class="colorpickertile" color="#FFCC33"/> + <xul:image class="colorpickertile" color="#FFCC00"/> + <xul:image class="colorpickertile" color="#33CC00"/> + <xul:image class="colorpickertile" color="#00CCCC"/> + <xul:image class="colorpickertile" color="#3366FF"/> + <xul:image class="colorpickertile" color="#6633FF"/> + <xul:image class="colorpickertile" color="#CC33CC"/> + </xul:hbox> + <xul:hbox> + <xul:image class="colorpickertile" color="#666666"/> + <xul:image class="colorpickertile" color="#990000"/> + <xul:image class="colorpickertile" color="#CC6600"/> + <xul:image class="colorpickertile" color="#CC9933"/> + <xul:image class="colorpickertile" color="#999900"/> + <xul:image class="colorpickertile" color="#009900"/> + <xul:image class="colorpickertile" color="#339999"/> + <xul:image class="colorpickertile" color="#3333FF"/> + <xul:image class="colorpickertile" color="#6600CC"/> + <xul:image class="colorpickertile" color="#993399"/> + </xul:hbox> + <xul:hbox> + <xul:image class="colorpickertile" color="#333333"/> + <xul:image class="colorpickertile" color="#660000"/> + <xul:image class="colorpickertile" color="#993300"/> + <xul:image class="colorpickertile" color="#996633"/> + <xul:image class="colorpickertile" color="#666600"/> + <xul:image class="colorpickertile" color="#006600"/> + <xul:image class="colorpickertile" color="#336666"/> + <xul:image class="colorpickertile" color="#000099"/> + <xul:image class="colorpickertile" color="#333399"/> + <xul:image class="colorpickertile" color="#663366"/> + </xul:hbox> + <xul:hbox> + <xul:image class="colorpickertile" color="#000000"/> + <xul:image class="colorpickertile" color="#330000"/> + <xul:image class="colorpickertile" color="#663300"/> + <xul:image class="colorpickertile" color="#663333"/> + <xul:image class="colorpickertile" color="#333300"/> + <xul:image class="colorpickertile" color="#003300"/> + <xul:image class="colorpickertile" color="#003333"/> + <xul:image class="colorpickertile" color="#000066"/> + <xul:image class="colorpickertile" color="#330099"/> + <xul:image class="colorpickertile" color="#330033"/> + </xul:hbox> + </xul:vbox> + <!-- Something to take tab focus + <button style="border : 0px; width: 0px; height: 0px;"/> + --> + </content> + + <implementation implements="nsIDOMEventListener"> + <property name="color"> + <getter><![CDATA[ + return this.mSelectedCell ? this.mSelectedCell.getAttribute("color") : null; + ]]></getter> + <setter><![CDATA[ + if (!val) + return val; + var uppercaseVal = val.toUpperCase(); + // Translate standard HTML color strings: + if (uppercaseVal[0] != "#") { + switch (uppercaseVal) { + case "GREEN": + uppercaseVal = "#008000"; + break; + case "LIME": + uppercaseVal = "#00FF00"; + break; + case "OLIVE": + uppercaseVal = "#808000"; + break; + case "TEAL": + uppercaseVal = "#008080"; + break; + case "YELLOW": + uppercaseVal = "#FFFF00"; + break; + case "RED": + uppercaseVal = "#FF0000"; + break; + case "MAROON": + uppercaseVal = "#800000"; + break; + case "PURPLE": + uppercaseVal = "#800080"; + break; + case "FUCHSIA": + uppercaseVal = "#FF00FF"; + break; + case "NAVY": + uppercaseVal = "#000080"; + break; + case "BLUE": + uppercaseVal = "#0000FF"; + break; + case "AQUA": + uppercaseVal = "#00FFFF"; + break; + case "WHITE": + uppercaseVal = "#FFFFFF"; + break; + case "SILVER": + uppercaseVal = "#C0C0C0"; + break; + case "GRAY": + uppercaseVal = "#808080"; + break; + default: // BLACK + uppercaseVal = "#000000"; + break; + } + } + var cells = this.mBox.getElementsByAttribute("color", uppercaseVal); + if (cells.item(0)) { + this.selectCell(cells[0]); + this.hoverCell(this.mSelectedCell); + } + return val; + ]]></setter> + </property> + + <method name="initColor"> + <parameter name="aColor"/> + <body><![CDATA[ + // Use this to initialize color without + // triggering the "onselect" handler, + // which closes window when it's a popup + this.mDoOnSelect = false; + this.color = aColor; + this.mDoOnSelect = true; + ]]></body> + </method> + + <method name="initialize"> + <body><![CDATA[ + this.mSelectedCell = null; + this.mHoverCell = null; + this.mBox = document.getAnonymousNodes(this)[0]; + this.mIsPopup = false; + this.mDoOnSelect = true; + + let imageEls = this.mBox.querySelectorAll("image"); + // We set the background of the picker tiles here using images in + // order for the color to show up even when author colors are + // disabled or the user is using high contrast mode. + for (let el of imageEls) { + let dataURI = "data:image/svg+xml,<svg style='background-color: " + + encodeURIComponent(el.getAttribute("color")) + + "' xmlns='http://www.w3.org/2000/svg' />"; + el.setAttribute("src", dataURI); + } + + this.hoverCell(this.mBox.childNodes[0].childNodes[0]); + + // used to capture keydown at the document level + this.mPickerKeyDown = function(aEvent) + { + document._focusedPicker.pickerKeyDown(aEvent); + } + + ]]></body> + </method> + + <method name="_fireEvent"> + <parameter name="aTarget"/> + <parameter name="aEventName"/> + <body> + <![CDATA[ + try { + var event = document.createEvent("Events"); + event.initEvent(aEventName, true, true); + var cancel = !aTarget.dispatchEvent(event); + if (aTarget.hasAttribute("on" + aEventName)) { + var fn = new Function ("event", aTarget.getAttribute("on" + aEventName)); + var rv = fn.call(aTarget, event); + if (rv == false) + cancel = true; + } + return !cancel; + } + catch (e) { + Components.utils.reportError(e); + } + return false; + ]]> + </body> + </method> + + <method name="resetHover"> + <body><![CDATA[ + if (this.mHoverCell) + this.mHoverCell.removeAttribute("hover"); + ]]></body> + </method> + + <method name="getColIndex"> + <parameter name="aCell"/> + <body><![CDATA[ + var cell = aCell; + var idx; + for (idx = -1; cell; idx++) + cell = cell.previousSibling; + + return idx; + ]]></body> + </method> + + <method name="isColorCell"> + <parameter name="aCell"/> + <body><![CDATA[ + return aCell && aCell.hasAttribute("color"); + ]]></body> + </method> + + <method name="hoverLeft"> + <body><![CDATA[ + var cell = this.mHoverCell.previousSibling; + this.hoverCell(cell); + ]]></body> + </method> + + <method name="hoverRight"> + <body><![CDATA[ + var cell = this.mHoverCell.nextSibling; + this.hoverCell(cell); + ]]></body> + </method> + + <method name="hoverUp"> + <body><![CDATA[ + var row = this.mHoverCell.parentNode.previousSibling; + if (row) { + var colIdx = this.getColIndex(this.mHoverCell); + var cell = row.childNodes[colIdx]; + this.hoverCell(cell); + } + ]]></body> + </method> + + <method name="hoverDown"> + <body><![CDATA[ + var row = this.mHoverCell.parentNode.nextSibling; + if (row) { + var colIdx = this.getColIndex(this.mHoverCell); + var cell = row.childNodes[colIdx]; + this.hoverCell(cell); + } + ]]></body> + </method> + + <method name="hoverTo"> + <parameter name="aRow"/> + <parameter name="aCol"/> + + <body><![CDATA[ + var row = this.mBox.childNodes[aRow]; + if (!row) return; + var cell = row.childNodes[aCol]; + if (!cell) return; + this.hoverCell(cell); + ]]></body> + </method> + + <method name="hoverCell"> + <parameter name="aCell"/> + + <body><![CDATA[ + if (this.isColorCell(aCell)) { + this.resetHover(); + aCell.setAttribute("hover", "true"); + this.mHoverCell = aCell; + var event = document.createEvent('Events'); + event.initEvent('DOMMenuItemActive', true, true); + aCell.dispatchEvent(event); + } + ]]></body> + </method> + + <method name="selectHoverCell"> + <body><![CDATA[ + this.selectCell(this.mHoverCell); + ]]></body> + </method> + + <method name="selectCell"> + <parameter name="aCell"/> + + <body><![CDATA[ + if (this.isColorCell(aCell)) { + if (this.mSelectedCell) + this.mSelectedCell.removeAttribute("selected"); + + this.mSelectedCell = aCell; + aCell.setAttribute("selected", "true"); + + if (this.mDoOnSelect) + this._fireEvent(this, "select"); + } + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.keyCode) { + case 37: // left + this.hoverLeft(); + break; + case 38: // up + this.hoverUp(); + break; + case 39: // right + this.hoverRight(); + break; + case 40: // down + this.hoverDown(); + break; + case 13: // enter + case 32: // space + this.selectHoverCell(); + break; + } + ]]></body> + </method> + + <constructor><![CDATA[ + this.initialize(); + ]]></constructor> + + </implementation> + + <handlers> + <handler event="mouseover"><![CDATA[ + this.hoverCell(event.originalTarget); + ]]></handler> + + <handler event="click"><![CDATA[ + if (event.originalTarget.hasAttribute("color")) { + this.selectCell(event.originalTarget); + this.hoverCell(this.mSelectedCell); + } + ]]></handler> + + + <handler event="focus" phase="capturing"> + <![CDATA[ + if (!mIsPopup && this.getAttribute('focused') != 'true') { + this.setAttribute('focused', 'true'); + document.addEventListener("keydown", this, true); + if (this.mSelectedCell) + this.hoverCell(this.mSelectedCell); + } + ]]> + </handler> + + <handler event="blur" phase="capturing"> + <![CDATA[ + if (!mIsPopup && this.getAttribute('focused') == 'true') { + document.removeEventListener("keydown", this, true); + this.removeAttribute('focused'); + this.resetHover(); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="colorpicker-button" display="xul:menu" role="xul:colorpicker" + extends="chrome://global/content/bindings/general.xml#basecontrol"> + <resources> + <stylesheet src="chrome://global/skin/colorpicker.css"/> + </resources> + + <content> + <xul:image class="colorpicker-button-colorbox" anonid="colorbox" flex="1" xbl:inherits="disabled"/> + + <xul:panel class="colorpicker-button-menupopup" + anonid="colorpopup" noautofocus="true" level="top" + onmousedown="event.stopPropagation()" + onpopupshowing="this._colorPicker.onPopupShowing()" + onpopuphiding="this._colorPicker.onPopupHiding()" + onselect="this._colorPicker.pickerChange()"> + <xul:colorpicker xbl:inherits="palettename,disabled" allowevents="true" anonid="colorpicker"/> + </xul:panel> + </content> + + <implementation> + <property name="open" + onget="return this.getAttribute('open') == 'true'" + onset="this.showPopup();"/> + <property name="color"> + <getter><![CDATA[ + return this.getAttribute("color"); + ]]></getter> + <setter><![CDATA[ + this.mColorBox.setAttribute("src", + "data:image/svg+xml,<svg style='background-color: " + + encodeURIComponent(val) + + "' xmlns='http://www.w3.org/2000/svg' />"); + this.setAttribute("color", val); + return val; + ]]></setter> + </property> + + <method name="initialize"> + <body><![CDATA[ + this.mColorBox = document.getAnonymousElementByAttribute(this, "anonid", "colorbox"); + this.mColorBox.setAttribute("src", + "data:image/svg+xml,<svg style='background-color: " + + encodeURIComponent(this.color) + + "' xmlns='http://www.w3.org/2000/svg' />"); + + var popup = document.getAnonymousElementByAttribute(this, "anonid", "colorpopup") + popup._colorPicker = this; + + this.mPicker = document.getAnonymousElementByAttribute(this, "anonid", "colorpicker") + ]]></body> + </method> + + <method name="_fireEvent"> + <parameter name="aTarget"/> + <parameter name="aEventName"/> + <body> + <![CDATA[ + try { + var event = document.createEvent("Events"); + event.initEvent(aEventName, true, true); + var cancel = !aTarget.dispatchEvent(event); + if (aTarget.hasAttribute("on" + aEventName)) { + var fn = new Function ("event", aTarget.getAttribute("on" + aEventName)); + var rv = fn.call(aTarget, event); + if (rv == false) + cancel = true; + } + return !cancel; + } + catch (e) { + dump(e); + } + return false; + ]]> + </body> + </method> + + <method name="showPopup"> + <body><![CDATA[ + this.mPicker.parentNode.openPopup(this, "after_start", 0, 0, false, false); + ]]></body> + </method> + + <method name="hidePopup"> + <body><![CDATA[ + this.mPicker.parentNode.hidePopup(); + ]]></body> + </method> + + <method name="onPopupShowing"> + <body><![CDATA[ + if ("resetHover" in this.mPicker) + this.mPicker.resetHover(); + document.addEventListener("keydown", this.mPicker, true); + this.mPicker.mIsPopup = true; + // Initialize to current button's color + this.mPicker.initColor(this.color); + ]]></body> + </method> + + <method name="onPopupHiding"> + <body><![CDATA[ + // Removes the key listener + document.removeEventListener("keydown", this.mPicker, true); + this.mPicker.mIsPopup = false; + ]]></body> + </method> + + <method name="pickerChange"> + <body><![CDATA[ + this.color = this.mPicker.color; + setTimeout(function(aPopup) { aPopup.hidePopup() }, 1, this.mPicker.parentNode); + + this._fireEvent(this, "change"); + ]]></body> + </method> + + <constructor><![CDATA[ + this.initialize(); + ]]></constructor> + + </implementation> + + <handlers> + <handler event="keydown"><![CDATA[ + // open popup if key is space/up/left/right/down and popup is closed + if ( (event.keyCode == 32 || (event.keyCode > 36 && event.keyCode < 41)) && !this.open) + this.showPopup(); + else if ( (event.keyCode == 27) && this.open) + this.hidePopup(); + ]]></handler> + </handlers> + </binding> + + <binding id="colorpickertile" role="xul:colorpickertile"> + </binding> + +</bindings> + diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css new file mode 100644 index 0000000000..4a9593a697 --- /dev/null +++ b/toolkit/content/widgets/datetimebox.css @@ -0,0 +1,45 @@ +/* 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/. */ + +@namespace url("http://www.w3.org/1999/xhtml"); +@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +.datetime-input-box-wrapper { + -moz-appearance: none; + display: inline-flex; + cursor: default; + background-color: inherit; + color: inherit; +} + +.datetime-input { + -moz-appearance: none; + text-align: center; + padding: 0; + border: 0; + margin: 0; + ime-mode: disabled; +} + +.datetime-separator { + margin: 0 !important; +} + +.datetime-input[readonly], +.datetime-input[disabled] { + color: GrayText; + -moz-user-select: none; +} + +.datetime-reset-button { + background-image: url(chrome://global/skin/icons/input-clear.svg); + background-color: transparent; + background-repeat: no-repeat; + background-size: 12px, 12px; + border: none; + height: 12px; + width: 12px; + align-self: center; + justify-content: flex-end; +} diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml new file mode 100644 index 0000000000..05591e65ad --- /dev/null +++ b/toolkit/content/widgets/datetimebox.xml @@ -0,0 +1,807 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<bindings id="datetimeboxBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="time-input" + extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base"> + <resources> + <stylesheet src="chrome://global/content/textbox.css"/> + <stylesheet src="chrome://global/skin/textbox.css"/> + <stylesheet src="chrome://global/content/bindings/datetimebox.css"/> + </resources> + + <implementation> + <constructor> + <![CDATA[ + // TODO: Bug 1301312 - localization for input type=time input. + this.mHour12 = true; + this.mAMIndicator = "AM"; + this.mPMIndicator = "PM"; + this.mPlaceHolder = "--"; + this.mSeparatorText = ":"; + this.mMillisecSeparatorText = "."; + this.mMaxLength = 2; + this.mMillisecMaxLength = 3; + this.mDefaultStep = 60 * 1000; // in milliseconds + + this.mMinHourInHour12 = 1; + this.mMaxHourInHour12 = 12; + this.mMinMinute = 0; + this.mMaxMinute = 59; + this.mMinSecond = 0; + this.mMaxSecond = 59; + this.mMinMillisecond = 0; + this.mMaxMillisecond = 999; + + this.mHourPageUpDownInterval = 3; + this.mMinSecPageUpDownInterval = 10; + + this.mHourField = + document.getAnonymousElementByAttribute(this, "anonid", "input-one"); + this.mHourField.setAttribute("typeBuffer", ""); + this.mMinuteField = + document.getAnonymousElementByAttribute(this, "anonid", "input-two"); + this.mMinuteField.setAttribute("typeBuffer", ""); + this.mDayPeriodField = + document.getAnonymousElementByAttribute(this, "anonid", "input-three"); + this.mDayPeriodField.classList.remove("numeric"); + + this.mHourField.placeholder = this.mPlaceHolder; + this.mMinuteField.placeholder = this.mPlaceHolder; + this.mDayPeriodField.placeholder = this.mPlaceHolder; + + this.mHourField.setAttribute("min", this.mMinHourInHour12); + this.mHourField.setAttribute("max", this.mMaxHourInHour12); + this.mMinuteField.setAttribute("min", this.mMinMinute); + this.mMinuteField.setAttribute("max", this.mMaxMinute); + + this.mMinuteSeparator = + document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); + this.mMinuteSeparator.textContent = this.mSeparatorText; + this.mSpaceSeparator = + document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); + // space between time and am/pm field + this.mSpaceSeparator.textContent = " "; + + this.mSecondSeparator = null; + this.mSecondField = null; + this.mMillisecSeparator = null; + this.mMillisecField = null; + + if (this.mInputElement.value) { + this.setFieldsFromInputValue(); + } + ]]> + </constructor> + + <method name="insertSeparator"> + <parameter name="aSeparatorText"/> + <body> + <![CDATA[ + let container = this.mHourField.parentNode; + const HTML_NS = "http://www.w3.org/1999/xhtml"; + + let separator = document.createElementNS(HTML_NS, "span"); + separator.textContent = aSeparatorText; + separator.setAttribute("class", "datetime-separator"); + container.insertBefore(separator, this.mSpaceSeparator); + + return separator; + ]]> + </body> + </method> + + <method name="insertAdditionalField"> + <parameter name="aPlaceHolder"/> + <parameter name="aMin"/> + <parameter name="aMax"/> + <parameter name="aSize"/> + <parameter name="aMaxLength"/> + <body> + <![CDATA[ + let container = this.mHourField.parentNode; + const HTML_NS = "http://www.w3.org/1999/xhtml"; + + let field = document.createElementNS(HTML_NS, "input"); + field.classList.add("textbox-input", "datetime-input", "numeric"); + field.setAttribute("size", aSize); + field.setAttribute("maxlength", aMaxLength); + field.setAttribute("min", aMin); + field.setAttribute("max", aMax); + field.setAttribute("typeBuffer", ""); + field.disabled = this.mInputElement.disabled; + field.readOnly = this.mInputElement.readOnly; + field.tabIndex = this.mInputElement.tabIndex; + field.placeholder = aPlaceHolder; + container.insertBefore(field, this.mSpaceSeparator); + + return field; + ]]> + </body> + </method> + + <method name="setFieldsFromInputValue"> + <body> + <![CDATA[ + let value = this.mInputElement.value; + if (!value) { + this.clearInputFields(true); + return; + } + + this.log("setFieldsFromInputValue: " + value); + let [hour, minute, second] = value.split(':'); + + this.setFieldValue(this.mHourField, hour); + this.setFieldValue(this.mMinuteField, minute); + if (this.mHour12) { + this.mDayPeriodField.value = (hour >= this.mMaxHourInHour12) ? + this.mPMIndicator : this.mAMIndicator; + } + + if (!this.isEmpty(second)) { + let index = second.indexOf("."); + let millisecond; + if (index != -1) { + millisecond = second.substring(index + 1); + second = second.substring(0, index); + } + + if (!this.mSecondField) { + this.mSecondSeparator = this.insertSeparator(this.mSeparatorText); + this.mSecondField = this.insertAdditionalField(this.mPlaceHolder, + this.mMinSecond, this.mMaxSecond, this.mMaxLength, + this.mMaxLength); + } + this.setFieldValue(this.mSecondField, second); + + if (!this.isEmpty(millisecond)) { + if (!this.mMillisecField) { + this.mMillisecSeparator = this.insertSeparator( + this.mMillisecSeparatorText); + this.mMillisecField = this.insertAdditionalField( + this.mPlaceHolder, this.mMinMillisecond, this.mMaxMillisecond, + this.mMillisecMaxLength, this.mMillisecMaxLength); + } + this.setFieldValue(this.mMillisecField, millisecond); + } else if (this.mMillisecField) { + this.mMillisecField.remove(); + this.mMillisecField = null; + + this.mMillisecSeparator.remove(); + this.mMillisecSeparator = null; + } + } else { + if (this.mSecondField) { + this.mSecondField.remove(); + this.mSecondField = null; + + this.mSecondSeparator.remove(); + this.mSecondSeparator = null; + } + + if (this.mMillisecField) { + this.mMillisecField.remove(); + this.mMillisecField = null; + + this.mMillisecSeparator.remove(); + this.mMillisecSeparator = null; + } + } + this.notifyPicker(); + ]]> + </body> + </method> + + <method name="setInputValueFromFields"> + <body> + <![CDATA[ + if (this.isEmpty(this.mHourField.value) || + this.isEmpty(this.mMinuteField.value) || + (this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) || + (this.mSecondField && this.isEmpty(this.mSecondField.value)) || + (this.mMillisecField && this.isEmpty(this.mMillisecField.value))) { + // We still need to notify picker in case any of the field has + // changed. If we can set input element value, then notifyPicker + // will be called in setFieldsFromInputValue(). + this.notifyPicker(); + return; + } + + let hour = Number(this.mHourField.value); + if (this.mHour12) { + let dayPeriod = this.mDayPeriodField.value; + if (dayPeriod == this.mPMIndicator && + hour < this.mMaxHourInHour12) { + hour += this.mMaxHourInHour12; + } else if (dayPeriod == this.mAMIndicator && + hour == this.mMaxHourInHour12) { + hour = 0; + } + } + + hour = (hour < 10) ? ("0" + hour) : hour; + + let time = hour + ":" + this.mMinuteField.value; + if (this.mSecondField) { + time += ":" + this.mSecondField.value; + } + + if (this.mMillisecField) { + time += "." + this.mMillisecField.value; + } + + this.log("setInputValueFromFields: " + time); + this.mInputElement.setUserInput(time); + ]]> + </body> + </method> + + <method name="setFieldsFromPicker"> + <parameter name="aValue"/> + <body> + <![CDATA[ + let hour = aValue.hour; + let minute = aValue.minute; + this.log("setFieldsFromPicker: " + hour + ":" + minute); + + if (!this.isEmpty(hour)) { + this.setFieldValue(this.mHourField, hour); + if (this.mHour12) { + this.mDayPeriodField.value = + (hour >= this.mMaxHourInHour12) ? this.mPMIndicator + : this.mAMIndicator; + } + } + + if (!this.isEmpty(minute)) { + this.setFieldValue(this.mMinuteField, minute); + } + ]]> + </body> + </method> + + <method name="clearInputFields"> + <parameter name="aFromInputElement"/> + <body> + <![CDATA[ + this.log("clearInputFields"); + + if (this.isDisabled() || this.isReadonly()) { + return; + } + + if (this.mHourField && !this.mHourField.disabled && + !this.mHourField.readOnly) { + this.mHourField.value = ""; + } + + if (this.mMinuteField && !this.mMinuteField.disabled && + !this.mMinuteField.readOnly) { + this.mMinuteField.value = ""; + } + + if (this.mSecondField && !this.mSecondField.disabled && + !this.mSecondField.readOnly) { + this.mSecondField.value = ""; + } + + if (this.mMillisecField && !this.mMillisecField.disabled && + !this.mMillisecField.readOnly) { + this.mMillisecField.value = ""; + } + + if (this.mDayPeriodField && !this.mDayPeriodField.disabled && + !this.mDayPeriodField.readOnly) { + this.mDayPeriodField.value = ""; + } + + if (!aFromInputElement) { + this.mInputElement.setUserInput(""); + } + ]]> + </body> + </method> + + <method name="incrementFieldValue"> + <parameter name="aTargetField"/> + <parameter name="aTimes"/> + <body> + <![CDATA[ + let value; + + // Use current time if field is empty. + if (this.isEmpty(aTargetField.value)) { + let now = new Date(); + + if (aTargetField == this.mHourField) { + value = now.getHours() % this.mMaxHourInHour12 || + this.mMaxHourInHour12; + } else if (aTargetField == this.mMinuteField) { + value = now.getMinutes(); + } else if (aTargetField == this.mSecondField) { + value = now.getSeconds(); + } else if (aTargetField == this.mMillisecField) { + value = now.getMilliseconds(); + } else { + this.log("Field not supported in incrementFieldValue."); + return; + } + } else { + value = Number(aTargetField.value); + } + + let min = aTargetField.getAttribute("min"); + let max = aTargetField.getAttribute("max"); + + value += aTimes; + if (value > max) { + value -= (max - min + 1); + } else if (value < min) { + value += (max - min + 1); + } + this.setFieldValue(aTargetField, value); + aTargetField.select(); + ]]> + </body> + </method> + + <method name="handleKeyboardNav"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (this.isDisabled() || this.isReadonly()) { + return; + } + + let targetField = aEvent.originalTarget; + let key = aEvent.key; + + if (this.mDayPeriodField && + targetField == this.mDayPeriodField) { + // Home/End key does nothing on AM/PM field. + if (key == "Home" || key == "End") { + return; + } + + this.mDayPeriodField.value = + this.mDayPeriodField.value == this.mAMIndicator ? + this.mPMIndicator : this.mAMIndicator; + this.mDayPeriodField.select(); + this.setInputValueFromFields(); + return; + } + + switch (key) { + case "ArrowUp": + this.incrementFieldValue(targetField, 1); + break; + case "ArrowDown": + this.incrementFieldValue(targetField, -1); + break; + case "PageUp": + this.incrementFieldValue(targetField, + targetField == this.mHourField ? this.mHourPageUpDownInterval + : this.mMinSecPageUpDownInterval); + break; + case "PageDown": + this.incrementFieldValue(targetField, + targetField == this.mHourField ? (0 - this.mHourPageUpDownInterval) + : (0 - this.mMinSecPageUpDownInterval)); + break; + case "Home": + let min = targetField.getAttribute("min"); + this.setFieldValue(targetField, min); + targetField.select(); + break; + case "End": + let max = targetField.getAttribute("max"); + this.setFieldValue(targetField, max); + targetField.select(); + break; + } + this.setInputValueFromFields(); + ]]> + </body> + </method> + + <method name="handleKeypress"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (this.isDisabled() || this.isReadonly()) { + return; + } + + let targetField = aEvent.originalTarget; + let key = aEvent.key; + + if (this.mDayPeriodField && + targetField == this.mDayPeriodField) { + if (key == "a" || key == "A") { + this.mDayPeriodField.value = this.mAMIndicator; + this.mDayPeriodField.select(); + } else if (key == "p" || key == "P") { + this.mDayPeriodField.value = this.mPMIndicator; + this.mDayPeriodField.select(); + } + return; + } + + if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) { + let buffer = targetField.getAttribute("typeBuffer") || ""; + + buffer = buffer.concat(key); + this.setFieldValue(targetField, buffer); + targetField.select(); + + let n = Number(buffer); + let max = targetField.getAttribute("max"); + if (buffer.length >= targetField.maxLength || n * 10 > max) { + buffer = ""; + this.advanceToNextField(); + } + targetField.setAttribute("typeBuffer", buffer); + } + ]]> + </body> + </method> + + <method name="setFieldValue"> + <parameter name="aField"/> + <parameter name="aValue"/> + <body> + <![CDATA[ + let value = Number(aValue); + if (isNaN(value)) { + this.log("NaN on setFieldValue!"); + return; + } + + if (aField.maxLength == this.mMaxLength) { // For hour, minute and second + if (aField == this.mHourField && this.mHour12) { + value = (value > this.mMaxHourInHour12) ? + value - this.mMaxHourInHour12 : value; + if (aValue == "00") { + value = this.mMaxHourInHour12; + } + } + // prepend zero + if (value < 10) { + value = "0" + value; + } + } else if (aField.maxLength == this.mMillisecMaxLength) { + // prepend zeroes + if (value < 10) { + value = "00" + value; + } else if (value < 100) { + value = "0" + value; + } + } + + aField.value = value; + ]]> + </body> + </method> + + <method name="isValueAvailable"> + <body> + <![CDATA[ + // Picker only cares about hour:minute. + return !this.isEmpty(this.mHourField.value) || + !this.isEmpty(this.mMinuteField.value); + ]]> + </body> + </method> + + <method name="getCurrentValue"> + <body> + <![CDATA[ + let hour; + if (!this.isEmpty(this.mHourField.value)) { + hour = Number(this.mHourField.value); + if (this.mHour12) { + let dayPeriod = this.mDayPeriodField.value; + if (dayPeriod == this.mPMIndicator && + hour < this.mMaxHourInHour12) { + hour += this.mMaxHourInHour12; + } else if (dayPeriod == this.mAMIndicator && + hour == this.mMaxHourInHour12) { + hour = 0; + } + } + } + + let minute; + if (!this.isEmpty(this.mMinuteField.value)) { + minute = Number(this.mMinuteField.value); + } + + // Picker only needs hour/minute. + let time = { hour, minute }; + + this.log("getCurrentValue: " + JSON.stringify(time)); + return time; + ]]> + </body> + </method> + </implementation> + </binding> + + <binding id="datetime-input-base"> + <resources> + <stylesheet src="chrome://global/content/textbox.css"/> + <stylesheet src="chrome://global/skin/textbox.css"/> + <stylesheet src="chrome://global/content/bindings/datetimebox.css"/> + </resources> + + <content> + <html:div class="datetime-input-box-wrapper" + xbl:inherits="context,disabled,readonly"> + <html:span> + <html:input anonid="input-one" + class="textbox-input datetime-input numeric" + size="2" maxlength="2" + xbl:inherits="disabled,readonly,tabindex"/> + <html:span anonid="sep-first" class="datetime-separator"></html:span> + <html:input anonid="input-two" + class="textbox-input datetime-input numeric" + size="2" maxlength="2" + xbl:inherits="disabled,readonly,tabindex"/> + <html:span anonid="sep-second" class="datetime-separator"></html:span> + <html:input anonid="input-three" + class="textbox-input datetime-input numeric" + size="2" maxlength="2" + xbl:inherits="disabled,readonly,tabindex"/> + </html:span> + + <html:button class="datetime-reset-button" anoid="reset-button" + tabindex="-1" xbl:inherits="disabled" + onclick="document.getBindingParent(this).clearInputFields(false);"/> + </html:div> + </content> + + <implementation implements="nsIDateTimeInputArea"> + <constructor> + <![CDATA[ + this.DEBUG = false; + this.mInputElement = this.parentNode; + + this.mMin = this.mInputElement.min; + this.mMax = this.mInputElement.max; + this.mStep = this.mInputElement.step; + this.mIsPickerOpen = false; + ]]> + </constructor> + + <method name="log"> + <parameter name="aMsg"/> + <body> + <![CDATA[ + if (this.DEBUG) { + dump("[DateTimeBox] " + aMsg + "\n"); + } + ]]> + </body> + </method> + + <method name="focusInnerTextBox"> + <body> + <![CDATA[ + this.log("focusInnerTextBox"); + document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus(); + ]]> + </body> + </method> + + <method name="blurInnerTextBox"> + <body> + <![CDATA[ + this.log("blurInnerTextBox"); + if (this.mLastFocusedField) { + this.mLastFocusedField.blur(); + } + ]]> + </body> + </method> + + <method name="notifyInputElementValueChanged"> + <body> + <![CDATA[ + this.log("inputElementValueChanged"); + this.setFieldsFromInputValue(); + ]]> + </body> + </method> + + <method name="setValueFromPicker"> + <parameter name="aValue"/> + <body> + <![CDATA[ + this.setFieldsFromPicker(aValue); + ]]> + </body> + </method> + + <method name="advanceToNextField"> + <parameter name="aReverse"/> + <body> + <![CDATA[ + this.log("advanceToNextField"); + + let focusedInput = this.mLastFocusedField; + let next = aReverse ? focusedInput.previousElementSibling + : focusedInput.nextElementSibling; + if (!next && !aReverse) { + this.setInputValueFromFields(); + return; + } + + while (next) { + if (next.type == "text" && !next.disabled) { + next.focus(); + break; + } + next = aReverse ? next.previousElementSibling + : next.nextElementSibling; + } + ]]> + </body> + </method> + + <method name="setPickerState"> + <parameter name="aIsOpen"/> + <body> + <![CDATA[ + this.log("picker is now " + (aIsOpen ? "opened" : "closed")); + this.mIsPickerOpen = aIsOpen; + ]]> + </body> + </method> + + <method name="isEmpty"> + <parameter name="aValue"/> + <body> + return (aValue == undefined || 0 === aValue.length); + </body> + </method> + + <method name="clearInputFields"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + + <method name="setFieldsFromInputValue"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + + <method name="setInputValueFromFields"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + + <method name="setFieldsFromPicker"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + + <method name="handleKeypress"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + + <method name="handleKeyboardNav"> + <body> + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + </body> + </method> + + <method name="notifyPicker"> + <body> + <![CDATA[ + if (this.mIsPickerOpen && this.isValueAvailable()) { + this.mInputElement.updateDateTimePicker(this.getCurrentValue()); + } + ]]> + </body> + </method> + + <method name="isDisabled"> + <body> + <![CDATA[ + return this.hasAttribute("disabled"); + ]]> + </body> + </method> + + <method name="isReadonly"> + <body> + <![CDATA[ + return this.hasAttribute("readonly"); + ]]> + </body> + </method> + + </implementation> + + <handlers> + <handler event="focus"> + <![CDATA[ + this.log("focus on: " + event.originalTarget); + + let target = event.originalTarget; + if (target.type == "text") { + this.mLastFocusedField = target; + target.select(); + } + ]]> + </handler> + + <handler event="blur"> + <![CDATA[ + this.setInputValueFromFields(); + ]]> + </handler> + + <handler event="click"> + <![CDATA[ + // XXX: .originalTarget is not expected. + // When clicking on one of the inner text boxes, the .originalTarget is + // a HTMLDivElement and when clicking on the reset button, it's a + // HTMLButtonElement but it's not equal to our reset-button. + this.log("click on: " + event.originalTarget); + if (event.defaultPrevented || this.isDisabled() || this.isReadonly()) { + return; + } + + if (!(event.originalTarget instanceof HTMLButtonElement)) { + this.mInputElement.openDateTimePicker(this.getCurrentValue()); + } + ]]> + </handler> + + <handler event="keypress" phase="capturing"> + <![CDATA[ + let key = event.key; + this.log("keypress: " + key); + + if (key == "Backspace" || key == "Tab") { + return; + } + + if (key == "Enter" || key == " ") { + // Close picker on Enter and Space. + this.mInputElement.closeDateTimePicker(); + } + + if (key == "ArrowUp" || key == "ArrowDown" || + key == "PageUp" || key == "PageDown" || + key == "Home" || key == "End") { + this.handleKeyboardNav(event); + } else if (key == "ArrowRight" || key == "ArrowLeft") { + this.advanceToNextField((key == "ArrowRight" ? false : true)); + } else { + this.handleKeypress(event); + } + + event.preventDefault(); + ]]> + </handler> + </handlers> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/datetimepicker.xml b/toolkit/content/widgets/datetimepicker.xml new file mode 100644 index 0000000000..5f16f1ff0c --- /dev/null +++ b/toolkit/content/widgets/datetimepicker.xml @@ -0,0 +1,1301 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE bindings [ + <!ENTITY % datetimepickerDTD SYSTEM "chrome://global/locale/datetimepicker.dtd"> + %datetimepickerDTD; +]> + +<bindings id="timepickerBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="datetimepicker-base" + extends="chrome://global/content/bindings/general.xml#basecontrol"> + + <resources> + <stylesheet src="chrome://global/content/textbox.css"/> + <stylesheet src="chrome://global/skin/textbox.css"/> + <stylesheet src="chrome://global/skin/dropmarker.css"/> + <stylesheet src="chrome://global/skin/datetimepicker.css"/> + </resources> + + <content align="center"> + <xul:hbox class="datetimepicker-input-box" align="center" + xbl:inherits="context,disabled,readonly"> + <xul:hbox class="textbox-input-box datetimepicker-input-subbox" align="center"> + <html:input class="datetimepicker-input textbox-input" anonid="input-one" + size="2" maxlength="2" + xbl:inherits="disabled,readonly"/> + </xul:hbox> + <xul:label anonid="sep-first" class="datetimepicker-separator" value=":"/> + <xul:hbox class="textbox-input-box datetimepicker-input-subbox" align="center"> + <html:input class="datetimepicker-input textbox-input" anonid="input-two" + size="2" maxlength="2" + xbl:inherits="disabled,readonly"/> + </xul:hbox> + <xul:label anonid="sep-second" class="datetimepicker-separator" value=":"/> + <xul:hbox class="textbox-input-box datetimepicker-input-subbox" align="center"> + <html:input class="datetimepicker-input textbox-input" anonid="input-three" + size="2" maxlength="2" + xbl:inherits="disabled,readonly"/> + </xul:hbox> + <xul:hbox class="textbox-input-box datetimepicker-input-subbox" align="center"> + <html:input class="datetimepicker-input textbox-input" anonid="input-ampm" + size="2" maxlength="2" + xbl:inherits="disabled,readonly"/> + </xul:hbox> + </xul:hbox> + <xul:spinbuttons anonid="buttons" xbl:inherits="disabled" + onup="this.parentNode._increaseOrDecrease(1);" + ondown="this.parentNode._increaseOrDecrease(-1);"/> + </content> + + <implementation> + <field name="_dateValue">null</field> + <field name="_fieldOne"> + document.getAnonymousElementByAttribute(this, "anonid", "input-one"); + </field> + <field name="_fieldTwo"> + document.getAnonymousElementByAttribute(this, "anonid", "input-two"); + </field> + <field name="_fieldThree"> + document.getAnonymousElementByAttribute(this, "anonid", "input-three"); + </field> + <field name="_fieldAMPM"> + document.getAnonymousElementByAttribute(this, "anonid", "input-ampm"); + </field> + <field name="_separatorFirst"> + document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); + </field> + <field name="_separatorSecond"> + document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); + </field> + <field name="_lastFocusedField">null</field> + <field name="_hasEntry">true</field> + <field name="_valueEntered">false</field> + <field name="attachedControl">null</field> + + <property name="_currentField" readonly="true"> + <getter> + var focusedInput = document.activeElement; + if (focusedInput == this._fieldOne || + focusedInput == this._fieldTwo || + focusedInput == this._fieldThree || + focusedInput == this._fieldAMPM) + return focusedInput; + return this._lastFocusedField || this._fieldOne; + </getter> + </property> + + <property name="dateValue" onget="return new Date(this._dateValue);"> + <setter> + <![CDATA[ + if (!(val instanceof Date)) + throw "Invalid Date"; + + this._setValueNoSync(val); + if (this.attachedControl) + this.attachedControl._setValueNoSync(val); + return val; + ]]> + </setter> + </property> + + <property name="readOnly" onset="if (val) this.setAttribute('readonly', 'true'); + else this.removeAttribute('readonly'); return val;" + onget="return this.getAttribute('readonly') == 'true';"/> + + <method name="_fireEvent"> + <parameter name="aEventName"/> + <parameter name="aTarget"/> + <body> + var event = document.createEvent("Events"); + event.initEvent(aEventName, true, true); + return !aTarget.dispatchEvent(event); + </body> + </method> + + <method name="_setValueOnChange"> + <parameter name="aField"/> + <body> + <![CDATA[ + if (!this._hasEntry) + return; + + if (aField == this._fieldOne || + aField == this._fieldTwo || + aField == this._fieldThree) { + var value = Number(aField.value); + if (isNaN(value)) + value = 0; + + value = this._constrainValue(aField, value, true); + this._setFieldValue(aField, value); + } + ]]> + </body> + </method> + + <method name="_init"> + <body/> + </method> + + <constructor> + this._init(); + + var cval = this.getAttribute("value"); + if (cval) { + try { + this.value = cval; + return; + } catch (ex) { } + } + this.dateValue = new Date(); + </constructor> + + <destructor> + if (this.attachedControl) { + this.attachedControl.attachedControl = null; + this.attachedControl = null; + } + </destructor> + + </implementation> + + <handlers> + <handler event="focus" phase="capturing"> + <![CDATA[ + var target = event.originalTarget; + if (target == this._fieldOne || + target == this._fieldTwo || + target == this._fieldThree || + target == this._fieldAMPM) + this._lastFocusedField = target; + ]]> + </handler> + + <handler event="keypress"> + <![CDATA[ + if (this._hasEntry && event.charCode && + this._currentField != this._fieldAMPM && + ! (event.altKey || event.ctrlKey || event.metaKey) && + (event.charCode < 48 || event.charCode > 57)) + event.preventDefault(); + ]]> + </handler> + + <handler event="keypress" keycode="VK_UP"> + if (this._hasEntry) + this._increaseOrDecrease(1); + </handler> + <handler event="keypress" keycode="VK_DOWN"> + if (this._hasEntry) + this._increaseOrDecrease(-1); + </handler> + + <handler event="input"> + this._valueEntered = true; + </handler> + + <handler event="change"> + this._setValueOnChange(event.originalTarget); + </handler> + </handlers> + + </binding> + + <binding id="timepicker" + extends="chrome://global/content/bindings/datetimepicker.xml#datetimepicker-base"> + + <implementation> + <field name="is24HourClock">false</field> + <field name="hourLeadingZero">false</field> + <field name="minuteLeadingZero">true</field> + <field name="secondLeadingZero">true</field> + <field name="amIndicator">"AM"</field> + <field name="pmIndicator">"PM"</field> + + <field name="hourField">null</field> + <field name="minuteField">null</field> + <field name="secondField">null</field> + + <property name="value"> + <getter> + <![CDATA[ + var minute = this._dateValue.getMinutes(); + if (minute < 10) + minute = "0" + minute; + + var second = this._dateValue.getSeconds(); + if (second < 10) + second = "0" + second; + return this._dateValue.getHours() + ":" + minute + ":" + second; + ]]> + </getter> + <setter> + <![CDATA[ + var items = val.match(/^([0-9]{1,2})\:([0-9]{1,2})\:?([0-9]{1,2})?$/); + if (!items) + throw "Invalid Time"; + + var dt = this.dateValue; + dt.setHours(items[1]); + dt.setMinutes(items[2]); + dt.setSeconds(items[3] ? items[3] : 0); + this.dateValue = dt; + return val; + ]]> + </setter> + </property> + <property name="hour" onget="return this._dateValue.getHours();"> + <setter> + <![CDATA[ + var valnum = Number(val); + if (isNaN(valnum) || valnum < 0 || valnum > 23) + throw "Invalid Hour"; + this._setFieldValue(this.hourField, valnum); + return val; + ]]> + </setter> + </property> + <property name="minute" onget="return this._dateValue.getMinutes();"> + <setter> + <![CDATA[ + var valnum = Number(val); + if (isNaN(valnum) || valnum < 0 || valnum > 59) + throw "Invalid Minute"; + this._setFieldValue(this.minuteField, valnum); + return val; + ]]> + </setter> + </property> + <property name="second" onget="return this._dateValue.getSeconds();"> + <setter> + <![CDATA[ + var valnum = Number(val); + if (isNaN(valnum) || valnum < 0 || valnum > 59) + throw "Invalid Second"; + this._setFieldValue(this.secondField, valnum); + return val; + ]]> + </setter> + </property> + <property name="isPM"> + <getter> + <![CDATA[ + return (this.hour >= 12); + ]]> + </getter> + <setter> + <![CDATA[ + if (val) { + if (this.hour < 12) + this.hour += 12; + } + else if (this.hour >= 12) + this.hour -= 12; + return val; + ]]> + </setter> + </property> + <property name="hideSeconds"> + <getter> + return (this.getAttribute("hideseconds") == "true"); + </getter> + <setter> + if (val) + this.setAttribute("hideseconds", "true"); + else + this.removeAttribute("hideseconds"); + if (this.secondField) + this.secondField.parentNode.collapsed = val; + this._separatorSecond.collapsed = val; + return val; + </setter> + </property> + <property name="increment"> + <getter> + <![CDATA[ + var increment = this.getAttribute("increment"); + increment = Number(increment); + if (isNaN(increment) || increment <= 0 || increment >= 60) + return 1; + return increment; + ]]> + </getter> + <setter> + <![CDATA[ + if (typeof val == "number") + this.setAttribute("increment", val); + return val; + ]]> + </setter> + </property> + + <method name="_setValueNoSync"> + <parameter name="aValue"/> + <body> + <![CDATA[ + var dt = new Date(aValue); + if (!isNaN(dt)) { + this._dateValue = dt; + this.setAttribute("value", this.value); + this._updateUI(this.hourField, this.hour); + this._updateUI(this.minuteField, this.minute); + this._updateUI(this.secondField, this.second); + } + ]]> + </body> + </method> + <method name="_increaseOrDecrease"> + <parameter name="aDir"/> + <body> + <![CDATA[ + if (this.disabled || this.readOnly) + return; + + var field = this._currentField; + if (this._valueEntered) + this._setValueOnChange(field); + + if (field == this._fieldAMPM) { + this.isPM = !this.isPM; + this._fireEvent("change", this); + } + else { + var oldval; + var change = aDir; + if (field == this.hourField) { + oldval = this.hour; + } + else if (field == this.minuteField) { + oldval = this.minute; + change *= this.increment; + } + else if (field == this.secondField) { + oldval = this.second; + } + + var newval = this._constrainValue(field, oldval + change, false); + + if (field == this.hourField) + this.hour = newval; + else if (field == this.minuteField) + this.minute = newval; + else if (field == this.secondField) + this.second = newval; + + if (oldval != newval) + this._fireEvent("change", this); + } + field.select(); + ]]> + </body> + </method> + <method name="_setFieldValue"> + <parameter name="aField"/> + <parameter name="aValue"/> + <body> + <![CDATA[ + if (aField == this.hourField) + this._dateValue.setHours(aValue); + else if (aField == this.minuteField) + this._dateValue.setMinutes(aValue); + else if (aField == this.secondField) + this._dateValue.setSeconds(aValue); + + this.setAttribute("value", this.value); + this._updateUI(aField, aValue); + + if (this.attachedControl) + this.attachedControl._setValueNoSync(this._dateValue); + ]]> + </body> + </method> + <method name="_updateUI"> + <parameter name="aField"/> + <parameter name="aValue"/> + <body> + <![CDATA[ + this._valueEntered = false; + + var prependZero = false; + if (aField == this.hourField) { + prependZero = this.hourLeadingZero; + if (!this.is24HourClock) { + if (aValue >= 12) { + if (aValue > 12) + aValue -= 12; + this._fieldAMPM.value = this.pmIndicator; + } + else { + if (aValue == 0) + aValue = 12; + this._fieldAMPM.value = this.amIndicator; + } + } + } + else if (aField == this.minuteField) { + prependZero = this.minuteLeadingZero; + } + else if (aField == this.secondField) { + prependZero = this.secondLeadingZero; + } + + if (prependZero && aValue < 10) + aField.value = "0" + aValue; + else + aField.value = aValue; + ]]> + </body> + </method> + <method name="_constrainValue"> + <parameter name="aField"/> + <parameter name="aValue"/> + <parameter name="aNoWrap"/> + <body> + <![CDATA[ + // aNoWrap is true when the user entered a value, so just + // constrain within limits. If false, the value is being + // incremented or decremented, so wrap around values + var max = (aField == this.hourField) ? 24 : 60; + if (aValue < 0) + return aNoWrap ? 0 : max + aValue; + if (aValue >= max) + return aNoWrap ? max - 1 : aValue - max; + return aValue; + ]]> + </body> + </method> + <method name="_init"> + <body> + <![CDATA[ + this.hourField = this._fieldOne; + this.minuteField = this._fieldTwo; + this.secondField = this._fieldThree; + + var numberOrder = /^(\D*)\s*(\d+)(\D*)(\d+)(\D*)(\d+)\s*(\D*)$/; + + var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory-nu-latn"; + + var pmTime = new Date(2000, 0, 1, 16, 7, 9).toLocaleTimeString(locale); + var numberFields = pmTime.match(numberOrder); + if (numberFields) { + this._separatorFirst.value = numberFields[3]; + this._separatorSecond.value = numberFields[5]; + if (Number(numberFields[2]) > 12) + this.is24HourClock = true; + else + this.pmIndicator = numberFields[1] || numberFields[7]; + } + + var amTime = new Date(2000, 0, 1, 1, 7, 9).toLocaleTimeString(locale); + numberFields = amTime.match(numberOrder); + if (numberFields) { + this.hourLeadingZero = (numberFields[2].length > 1); + this.minuteLeadingZero = (numberFields[4].length > 1); + this.secondLeadingZero = (numberFields[6].length > 1); + + if (!this.is24HourClock) { + this.amIndicator = numberFields[1] || numberFields[7]; + if (numberFields[1]) { + var mfield = this._fieldAMPM.parentNode; + var mcontainer = mfield.parentNode; + mcontainer.insertBefore(mfield, mcontainer.firstChild); + } + var size = (numberFields[1] || numberFields[7]).length; + if (this.pmIndicator.length > size) + size = this.pmIndicator.length; + this._fieldAMPM.size = size; + this._fieldAMPM.maxLength = size; + } + else { + this._fieldAMPM.parentNode.collapsed = true; + } + } + + this.hideSeconds = this.hideSeconds; + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="keypress"> + <![CDATA[ + // just allow any printable character to switch the AM/PM state + if (event.charCode && !this.disabled && !this.readOnly && + this._currentField == this._fieldAMPM) { + this.isPM = !this.isPM; + this._fieldAMPM.select(); + this._fireEvent("change", this); + event.preventDefault(); + } + ]]> + </handler> + </handlers> + + </binding> + + <binding id="datepicker" + extends="chrome://global/content/bindings/datetimepicker.xml#datetimepicker-base"> + + <implementation> + <field name="yearLeadingZero">false</field> + <field name="monthLeadingZero">true</field> + <field name="dateLeadingZero">true</field> + + <field name="yearField"/> + <field name="monthField"/> + <field name="dateField"/> + + <property name="value"> + <getter> + <![CDATA[ + var month = this._dateValue.getMonth(); + month = (month < 9) ? month = "0" + ++month : month + 1; + + var date = this._dateValue.getDate(); + if (date < 10) + date = "0" + date; + return this._dateValue.getFullYear() + "-" + month + "-" + date; + ]]> + + </getter> + <setter> + <![CDATA[ + var results = val.match(/^([0-9]{1,4})\-([0-9]{1,2})\-([0-9]{1,2})$/); + if (!results) + throw "Invalid Date"; + + this.dateValue = new Date(results[1] + "/" + results[2] + "/" + results[3]); + this.setAttribute("value", this.value); + return val; + ]]> + </setter> + </property> + <property name="year" onget="return this._dateValue.getFullYear();"> + <setter> + <![CDATA[ + var valnum = Number(val); + if (isNaN(valnum) || valnum < 1 || valnum > 9999) + throw "Invalid Year"; + this._setFieldValue(this.yearField, valnum); + return val; + ]]> + </setter> + </property> + <property name="month" onget="return this._dateValue.getMonth();"> + <setter> + <![CDATA[ + var valnum = Number(val); + if (isNaN(valnum) || valnum < 0 || valnum > 11) + throw "Invalid Month"; + this._setFieldValue(this.monthField, valnum); + return val; + ]]> + </setter> + </property> + <property name="date" onget="return this._dateValue.getDate();"> + <setter> + <![CDATA[ + var valnum = Number(val); + if (isNaN(valnum) || valnum < 1 || valnum > 31) + throw "Invalid Date"; + this._setFieldValue(this.dateField, valnum); + return val; + ]]> + </setter> + </property> + <property name="open" onget="return false;" onset="return val;"/> + + <property name="displayedMonth" onget="return this.month;" + onset="this.month = val; return val;"/> + <property name="displayedYear" onget="return this.year;" + onset="this.year = val; return val;"/> + + <method name="_setValueNoSync"> + <parameter name="aValue"/> + <body> + <![CDATA[ + var dt = new Date(aValue); + if (!isNaN(dt)) { + this._dateValue = dt; + this.setAttribute("value", this.value); + this._updateUI(this.yearField, this.year); + this._updateUI(this.monthField, this.month); + this._updateUI(this.dateField, this.date); + } + ]]> + </body> + </method> + <method name="_increaseOrDecrease"> + <parameter name="aDir"/> + <body> + <![CDATA[ + if (this.disabled || this.readOnly) + return; + + var field = this._currentField; + if (this._valueEntered) + this._setValueOnChange(field); + + var oldval; + if (field == this.yearField) + oldval = this.year; + else if (field == this.monthField) + oldval = this.month; + else if (field == this.dateField) + oldval = this.date; + + var newval = this._constrainValue(field, oldval + aDir, false); + + if (field == this.yearField) + this.year = newval; + else if (field == this.monthField) + this.month = newval; + else if (field == this.dateField) + this.date = newval; + + if (oldval != newval) + this._fireEvent("change", this); + field.select(); + ]]> + </body> + </method> + <method name="_setFieldValue"> + <parameter name="aField"/> + <parameter name="aValue"/> + <body> + <![CDATA[ + if (aField == this.yearField) { + let oldDate = this.date; + this._dateValue.setFullYear(aValue); + if (oldDate != this.date) { + this._dateValue.setDate(0); + this._updateUI(this.dateField, this.date); + } + } + else if (aField == this.monthField) { + let oldDate = this.date; + this._dateValue.setMonth(aValue); + if (oldDate != this.date) { + this._dateValue.setDate(0); + this._updateUI(this.dateField, this.date); + } + } + else if (aField == this.dateField) { + this._dateValue.setDate(aValue); + } + + this.setAttribute("value", this.value); + this._updateUI(aField, aValue); + + if (this.attachedControl) + this.attachedControl._setValueNoSync(this._dateValue); + ]]> + </body> + </method> + <method name="_updateUI"> + <parameter name="aField"/> + <parameter name="aValue"/> + <body> + <![CDATA[ + this._valueEntered = false; + + var prependZero = false; + if (aField == this.yearField) { + if (this.yearLeadingZero) { + aField.value = ("000" + aValue).slice(-4); + return; + } + } + else if (aField == this.monthField) { + aValue++; + prependZero = this.monthLeadingZero; + } + else if (aField == this.dateField) { + prependZero = this.dateLeadingZero; + } + if (prependZero && aValue < 10) + aField.value = "0" + aValue; + else + aField.value = aValue; + ]]> + </body> + </method> + <method name="_constrainValue"> + <parameter name="aField"/> + <parameter name="aValue"/> + <parameter name="aNoWrap"/> + <body> + <![CDATA[ + // the month will be 1 to 12 if entered by the user, so subtract 1 + if (aNoWrap && aField == this.monthField) + aValue--; + + if (aField == this.dateField) { + if (aValue < 1) + return new Date(this.year, this.month + 1, 0).getDate(); + + var currentMonth = this.month; + var dt = new Date(this.year, currentMonth, aValue); + return (dt.getMonth() != currentMonth ? 1 : aValue); + } + var min = (aField == this.monthField) ? 0 : 1; + var max = (aField == this.monthField) ? 11 : 9999; + if (aValue < min) + return aNoWrap ? min : max; + if (aValue > max) + return aNoWrap ? max : min; + return aValue; + ]]> + </body> + </method> + <method name="_init"> + <body> + <![CDATA[ + // We'll default to YYYY/MM/DD to start. + var yfield = "input-one"; + var mfield = "input-two"; + var dfield = "input-three"; + var twoDigitYear = false; + this.yearLeadingZero = true; + this.monthLeadingZero = true; + this.dateLeadingZero = true; + + var numberOrder = /^(\D*)\s*(\d+)(\D*)(\d+)(\D*)(\d+)\s*(\D*)$/; + + var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory-nu-latn"; + + var dt = new Date(2002, 9, 4).toLocaleDateString(locale); + var numberFields = dt.match(numberOrder); + if (numberFields) { + this._separatorFirst.value = numberFields[3]; + this._separatorSecond.value = numberFields[5]; + + var yi = 2, mi = 4, di = 6; + + function fieldForNumber(i) { + if (i == 2) + return "input-one"; + if (i == 4) + return "input-two"; + return "input-three"; + } + + for (var i = 1; i < numberFields.length; i++) { + switch (Number(numberFields[i])) { + case 2: + twoDigitYear = true; // fall through + case 2002: + yi = i; + yfield = fieldForNumber(i); + break; + case 9, 10: + mi = i; + mfield = fieldForNumber(i); + break; + case 4: + di = i; + dfield = fieldForNumber(i); + break; + } + } + + this.yearLeadingZero = (numberFields[yi].length > 1); + this.monthLeadingZero = (numberFields[mi].length > 1); + this.dateLeadingZero = (numberFields[di].length > 1); + } + + this.yearField = document.getAnonymousElementByAttribute(this, "anonid", yfield); + if (!twoDigitYear) + this.yearField.parentNode.classList.add("datetimepicker-input-subbox", "datetimepicker-year"); + this.monthField = document.getAnonymousElementByAttribute(this, "anonid", mfield); + this.dateField = document.getAnonymousElementByAttribute(this, "anonid", dfield); + + this._fieldAMPM.parentNode.collapsed = true; + this.yearField.size = twoDigitYear ? 2 : 4; + this.yearField.maxLength = twoDigitYear ? 2 : 4; + ]]> + </body> + </method> + </implementation> + + </binding> + + <binding id="datepicker-grid" + extends="chrome://global/content/bindings/datetimepicker.xml#datepicker"> + + <content> + <vbox class="datepicker-mainbox" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <hbox class="datepicker-monthbox" align="center"> + <button class="datepicker-previous datepicker-button" type="repeat" + xbl:inherits="disabled" + oncommand="document.getBindingParent(this)._increaseOrDecreaseMonth(-1);"/> + <spacer flex="1"/> + <deck anonid="monthlabeldeck"> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + <label class="datepicker-gridlabel" value=""/> + </deck> + <label anonid="yearlabel" class="datepicker-gridlabel"/> + <spacer flex="1"/> + <button class="datepicker-next datepicker-button" type="repeat" + xbl:inherits="disabled" + oncommand="document.getBindingParent(this)._increaseOrDecreaseMonth(1);"/> + </hbox> + <grid class="datepicker-grid" role="grid"> + <columns> + <column class="datepicker-gridrow" flex="1"/> + <column class="datepicker-gridrow" flex="1"/> + <column class="datepicker-gridrow" flex="1"/> + <column class="datepicker-gridrow" flex="1"/> + <column class="datepicker-gridrow" flex="1"/> + <column class="datepicker-gridrow" flex="1"/> + <column class="datepicker-gridrow" flex="1"/> + </columns> + <rows anonid="datebox"> + <row anonid="dayofweekbox"> + <label class="datepicker-weeklabel" role="columnheader"/> + <label class="datepicker-weeklabel" role="columnheader"/> + <label class="datepicker-weeklabel" role="columnheader"/> + <label class="datepicker-weeklabel" role="columnheader"/> + <label class="datepicker-weeklabel" role="columnheader"/> + <label class="datepicker-weeklabel" role="columnheader"/> + <label class="datepicker-weeklabel" role="columnheader"/> + </row> + <row> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + </row> + <row> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + </row> + <row> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + </row> + <row> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + </row> + <row> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + </row> + <row> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + <label class="datepicker-gridlabel" role="gridcell"/> + </row> + </rows> + </grid> + </vbox> + </content> + + <implementation> + <field name="_hasEntry">false</field> + <field name="_weekStart">&firstdayofweek.default;</field> + <field name="_displayedDate">null</field> + <field name="_todayItem">null</field> + + <field name="yearField"> + document.getAnonymousElementByAttribute(this, "anonid", "yearlabel"); + </field> + <field name="monthField"> + document.getAnonymousElementByAttribute(this, "anonid", "monthlabeldeck"); + </field> + <field name="dateField"> + document.getAnonymousElementByAttribute(this, "anonid", "datebox"); + </field> + + <field name="_selectedItem">null</field> + + <property name="selectedItem" onget="return this._selectedItem"> + <setter> + <![CDATA[ + if (!val.value) + return val; + if (val.parentNode.parentNode != this.dateField) + return val; + + if (this._selectedItem) + this._selectedItem.removeAttribute("selected"); + this._selectedItem = val; + val.setAttribute("selected", "true"); + this._displayedDate.setDate(val.value); + return val; + ]]> + </setter> + </property> + + <property name="displayedMonth"> + <getter> + return this._displayedDate.getMonth(); + </getter> + <setter> + this._updateUI(this.monthField, val, true); + return val; + </setter> + </property> + <property name="displayedYear"> + <getter> + return this._displayedDate.getFullYear(); + </getter> + <setter> + this._updateUI(this.yearField, val, true); + return val; + </setter> + </property> + + <method name="_init"> + <body> + <![CDATA[ + var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory"; + var dtfMonth = Intl.DateTimeFormat(locale, {month: "long"}); + var dtfWeekday = Intl.DateTimeFormat(locale, {weekday: "narrow"}); + + var monthLabel = this.monthField.firstChild; + var tempDate = new Date(2005, 0, 1); + for (var month = 0; month < 12; month++) { + tempDate.setMonth(month); + monthLabel.setAttribute("value", dtfMonth.format(tempDate)); + monthLabel = monthLabel.nextSibling; + } + + var fdow = Number(this.getAttribute("firstdayofweek")); + if (!isNaN(fdow) && fdow >= 0 && fdow <= 6) + this._weekStart = fdow; + + var weekbox = document.getAnonymousElementByAttribute(this, "anonid", "dayofweekbox").childNodes; + var date = new Date(); + date.setDate(date.getDate() - (date.getDay() - this._weekStart)); + for (var i = 0; i < weekbox.length; i++) { + weekbox[i].value = dtfWeekday.format(date); + date.setDate(date.getDate() + 1); + } + ]]> + </body> + </method> + <method name="_setValueNoSync"> + <parameter name="aValue"/> + <body> + <![CDATA[ + var dt = new Date(aValue); + if (!isNaN(dt)) { + this._dateValue = dt; + this.setAttribute("value", this.value); + this._updateUI(); + } + ]]> + </body> + </method> + <method name="_updateUI"> + <parameter name="aField"/> + <parameter name="aValue"/> + <parameter name="aCheckMonth"/> + <body> + <![CDATA[ + var date; + var currentMonth; + if (aCheckMonth) { + if (!this._displayedDate) + this._displayedDate = this.dateValue; + + var expectedMonth = aValue; + if (aField == this.monthField) { + this._displayedDate.setMonth(aValue); + } + else { + expectedMonth = this._displayedDate.getMonth(); + this._displayedDate.setFullYear(aValue); + } + + if (expectedMonth != -1 && expectedMonth != 12 && + expectedMonth != this._displayedDate.getMonth()) { + // If the month isn't what was expected, then the month overflowed. + // Setting the date to 0 will go back to the last day of the right month. + this._displayedDate.setDate(0); + } + + date = new Date(this._displayedDate); + currentMonth = this._displayedDate.getMonth(); + } + else { + var samemonth = (this._displayedDate && + this._displayedDate.getMonth() == this.month && + this._displayedDate.getFullYear() == this.year); + if (samemonth) { + var items = this.dateField.getElementsByAttribute("value", this.date); + if (items.length) + this.selectedItem = items[0]; + return; + } + + date = this.dateValue; + this._displayedDate = new Date(date); + currentMonth = this.month; + } + + if (this._todayItem) { + this._todayItem.removeAttribute("today"); + this._todayItem = null; + } + + if (this._selectedItem) { + this._selectedItem.removeAttribute("selected"); + this._selectedItem = null; + } + + // Update the month and year title + this.monthField.selectedIndex = currentMonth; + this.yearField.setAttribute("value", date.getFullYear()); + + date.setDate(1); + var firstWeekday = (7 + date.getDay() - this._weekStart) % 7; + date.setDate(date.getDate() - firstWeekday); + + var today = new Date(); + var datebox = this.dateField; + for (var k = 1; k < datebox.childNodes.length; k++) { + var row = datebox.childNodes[k]; + for (var i = 0; i < 7; i++) { + var item = row.childNodes[i]; + + if (currentMonth == date.getMonth()) { + item.value = date.getDate(); + + // highlight today + if (this._isSameDay(today, date)) { + this._todayItem = item; + item.setAttribute("today", "true"); + } + + // highlight the selected date + if (this._isSameDay(this._dateValue, date)) { + this._selectedItem = item; + item.setAttribute("selected", "true"); + } + } + else { + item.value = ""; + } + + date.setDate(date.getDate() + 1); + } + } + + this._fireEvent("monthchange", this); + if (this.hasAttribute("monthchange")) { + var fn = new Function("event", aTarget.getAttribute("onmonthchange")); + fn.call(aTarget, event); + } + ]]> + </body> + </method> + <method name="_increaseOrDecreaseDateFromEvent"> + <parameter name="aEvent"/> + <parameter name="aDiff"/> + <body> + <![CDATA[ + if (aEvent.originalTarget == this && !this.disabled && !this.readOnly) { + var newdate = this.dateValue; + newdate.setDate(newdate.getDate() + aDiff); + this.dateValue = newdate; + this._fireEvent("change", this); + } + aEvent.stopPropagation(); + aEvent.preventDefault(); + ]]> + </body> + </method> + <method name="_increaseOrDecreaseMonth"> + <parameter name="aDir"/> + <body> + <![CDATA[ + if (!this.disabled) { + var month = this._displayedDate ? this._displayedDate.getMonth() : + this.month; + this._updateUI(this.monthField, month + aDir, true); + } + ]]> + </body> + </method> + <method name="_isSameDay"> + <parameter name="aDate1"/> + <parameter name="aDate2"/> + <body> + <![CDATA[ + return (aDate1 && aDate2 && + aDate1.getDate() == aDate2.getDate() && + aDate1.getMonth() == aDate2.getMonth() && + aDate1.getFullYear() == aDate2.getFullYear()); + ]]> + </body> + </method> + + </implementation> + + <handlers> + <handler event="click"> + <![CDATA[ + if (event.button != 0 || this.disabled || this.readOnly) + return; + + var target = event.originalTarget; + if (target.classList.contains("datepicker-gridlabel") && + target != this.selectedItem) { + this.selectedItem = target; + this._dateValue = new Date(this._displayedDate); + if (this.attachedControl) + this.attachedControl._setValueNoSync(this._dateValue); + this._fireEvent("change", this); + + if (this.attachedControl && "open" in this.attachedControl) + this.attachedControl.open = false; // close the popup + } + ]]> + </handler> + <handler event="MozMousePixelScroll" preventdefault="true"/> + <handler event="DOMMouseScroll" preventdefault="true"> + <![CDATA[ + this._increaseOrDecreaseMonth(event.detail < 0 ? -1 : 1); + ]]> + </handler> + <handler event="keypress" keycode="VK_LEFT" + action="this._increaseOrDecreaseDateFromEvent(event, -1);"/> + <handler event="keypress" keycode="VK_RIGHT" + action="this._increaseOrDecreaseDateFromEvent(event, 1);"/> + <handler event="keypress" keycode="VK_UP" + action="this._increaseOrDecreaseDateFromEvent(event, -7);"/> + <handler event="keypress" keycode="VK_DOWN" + action="this._increaseOrDecreaseDateFromEvent(event, 7);"/> + <handler event="keypress" keycode="VK_PAGE_UP" preventdefault="true" + action="this._increaseOrDecreaseMonth(-1);"/> + <handler event="keypress" keycode="VK_PAGE_DOWN" preventdefault="true" + action="this._increaseOrDecreaseMonth(1);"/> + </handlers> + </binding> + + <binding id="datepicker-popup" display="xul:menu" + extends="chrome://global/content/bindings/datetimepicker.xml#datepicker"> + <content align="center"> + <xul:hbox class="textbox-input-box datetimepicker-input-box" align="center" + allowevents="true" xbl:inherits="context,disabled,readonly"> + <xul:hbox class="datetimepicker-input-subbox" align="baseline"> + <html:input class="datetimepicker-input textbox-input" anonid="input-one" + size="2" maxlength="2" + xbl:inherits="disabled,readonly"/> + </xul:hbox> + <xul:label anonid="sep-first" class="datetimepicker-separator" value=":"/> + <xul:hbox class="datetimepicker-input-subbox" align="baseline"> + <html:input class="datetimepicker-input textbox-input" anonid="input-two" + size="2" maxlength="2" + xbl:inherits="disabled,readonly"/> + </xul:hbox> + <xul:label anonid="sep-second" class="datetimepicker-separator" value=":"/> + <xul:hbox class="datetimepicker-input-subbox" align="center"> + <html:input class="datetimepicker-input textbox-input" anonid="input-three" + size="2" maxlength="2" + xbl:inherits="disabled,readonly"/> + </xul:hbox> + <xul:hbox class="datetimepicker-input-subbox" align="center"> + <html:input class="datetimepicker-input textbox-input" anonid="input-ampm" + size="2" maxlength="2" + xbl:inherits="disabled,readonly"/> + </xul:hbox> + </xul:hbox> + <xul:spinbuttons anonid="buttons" xbl:inherits="disabled" allowevents="true" + onup="this.parentNode._increaseOrDecrease(1);" + ondown="this.parentNode._increaseOrDecrease(-1);"/> + <xul:dropmarker class="datepicker-dropmarker" xbl:inherits="disabled"/> + <xul:panel onpopupshown="this.firstChild.focus();" level="top"> + <xul:datepicker anonid="grid" type="grid" class="datepicker-popupgrid" + xbl:inherits="disabled,readonly,firstdayofweek"/> + </xul:panel> + </content> + <implementation> + <constructor> + var grid = document.getAnonymousElementByAttribute(this, "anonid", "grid"); + this.attachedControl = grid; + grid.attachedControl = this; + grid._setValueNoSync(this._dateValue); + </constructor> + <property name="open" onget="return this.hasAttribute('open');"> + <setter> + <![CDATA[ + if (this.boxObject instanceof MenuBoxObject) + this.boxObject.openMenu(val); + return val; + ]]> + </setter> + </property> + <property name="displayedMonth"> + <getter> + return document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedMonth; + </getter> + <setter> + document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedMonth = val; + return val; + </setter> + </property> + <property name="displayedYear"> + <getter> + return document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedYear; + </getter> + <setter> + document.getAnonymousElementByAttribute(this, "anonid", "grid").displayedYear = val; + return val; + </setter> + </property> + </implementation> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml new file mode 100644 index 0000000000..327f453684 --- /dev/null +++ b/toolkit/content/widgets/datetimepopup.xml @@ -0,0 +1,181 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<bindings id="dateTimePopupBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + <binding id="datetime-popup" + extends="chrome://global/content/bindings/popup.xml#arrowpanel"> + <implementation> + <field name="dateTimePopupFrame"> + this.querySelector("#dateTimePopupFrame"); + </field> + <field name="TIME_PICKER_WIDTH" readonly="true">"12em"</field> + <field name="TIME_PICKER_HEIGHT" readonly="true">"21em"</field> + <method name="loadPicker"> + <parameter name="type"/> + <parameter name="detail"/> + <body><![CDATA[ + this.hidden = false; + this.type = type; + this.pickerState = {}; + // TODO: Resize picker according to content zoom level + this.style.fontSize = "10px"; + switch (type) { + case "time": { + this.detail = detail; + this.dateTimePopupFrame.addEventListener("load", this, true); + this.dateTimePopupFrame.setAttribute("src", "chrome://global/content/timepicker.xhtml"); + this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT; + break; + } + } + ]]></body> + </method> + <method name="closePicker"> + <body><![CDATA[ + this.hidden = true; + this.setInputBoxValue(true); + this.pickerState = {}; + this.type = undefined; + this.dateTimePopupFrame.removeEventListener("load", this, true); + this.dateTimePopupFrame.contentDocument.removeEventListener("TimePickerPopupChanged", this, false); + this.dateTimePopupFrame.setAttribute("src", ""); + ]]></body> + </method> + <method name="setPopupValue"> + <parameter name="data"/> + <body><![CDATA[ + switch (this.type) { + case "time": { + this.postMessageToPicker({ + name: "TimePickerSetValue", + detail: data.value + }); + break; + } + } + ]]></body> + </method> + <method name="initPicker"> + <parameter name="detail"/> + <body><![CDATA[ + switch (this.type) { + case "time": { + const { hour, minute } = detail.value; + const format = detail.format || "12"; + const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global"); + + this.postMessageToPicker({ + name: "TimePickerInit", + detail: { + hour, + minute, + format, + locale, + min: detail.min, + max: detail.max, + step: detail.step, + } + }); + break; + } + } + ]]></body> + </method> + <method name="setInputBoxValue"> + <parameter name="passAllValues"/> + <body><![CDATA[ + /** + * @param {Boolean} passAllValues: Pass spinner values regardless if they've been set/changed or not + */ + switch (this.type) { + case "time": { + const { hour, minute, isHourSet, isMinuteSet, isDayPeriodSet } = this.pickerState; + const isAnyValueSet = isHourSet || isMinuteSet || isDayPeriodSet; + if (passAllValues && isAnyValueSet) { + this.sendPickerValueChanged({ hour, minute }); + } else { + this.sendPickerValueChanged({ + hour: isHourSet || isDayPeriodSet ? hour : undefined, + minute: isMinuteSet ? minute : undefined + }); + } + break; + } + } + ]]></body> + </method> + <method name="sendPickerValueChanged"> + <parameter name="value"/> + <body><![CDATA[ + switch (this.type) { + case "time": { + this.dispatchEvent(new CustomEvent("DateTimePickerValueChanged", { + detail: { + hour: value.hour, + minute: value.minute + } + })); + break; + } + } + ]]></body> + </method> + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "load": { + this.initPicker(this.detail); + this.dateTimePopupFrame.contentWindow.addEventListener("message", this, false); + break; + } + case "message": { + this.handleMessage(aEvent); + break; + } + } + ]]></body> + </method> + <method name="handleMessage"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal) { + return; + } + + switch (aEvent.data.name) { + case "TimePickerPopupChanged": { + this.pickerState = aEvent.data.detail; + this.setInputBoxValue(); + break; + } + } + ]]></body> + </method> + <method name="postMessageToPicker"> + <parameter name="data"/> + <body><![CDATA[ + if (this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal) { + this.dateTimePopupFrame.contentWindow.postMessage(data, "*"); + } + ]]></body> + </method> + + </implementation> + <handlers> + <handler event="popuphiding"> + <![CDATA[ + this.closePicker(); + ]]> + </handler> + </handlers> + </binding> +</bindings> diff --git a/toolkit/content/widgets/dialog.xml b/toolkit/content/widgets/dialog.xml new file mode 100644 index 0000000000..d83570ac0f --- /dev/null +++ b/toolkit/content/widgets/dialog.xml @@ -0,0 +1,448 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="dialogBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="dialog" extends="chrome://global/content/bindings/general.xml#root-element"> + <resources> + <stylesheet src="chrome://global/skin/dialog.css"/> + </resources> + <content> + <xul:vbox class="box-inherit dialog-content-box" flex="1"> + <children/> + </xul:vbox> + + <xul:hbox class="dialog-button-box" anonid="buttons" + xbl:inherits="pack=buttonpack,align=buttonalign,dir=buttondir,orient=buttonorient" +#ifdef XP_UNIX + > + <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/> + <xul:button dlgtype="help" class="dialog-button" hidden="true"/> + <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/> + <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/> + <xul:spacer anonid="spacer" flex="1"/> + <xul:button dlgtype="cancel" class="dialog-button"/> + <xul:button dlgtype="accept" class="dialog-button" xbl:inherits="disabled=buttondisabledaccept"/> +#else + pack="end"> + <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/> + <xul:spacer anonid="spacer" flex="1" hidden="true"/> + <xul:button dlgtype="accept" class="dialog-button" xbl:inherits="disabled=buttondisabledaccept"/> + <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/> + <xul:button dlgtype="cancel" class="dialog-button"/> + <xul:button dlgtype="help" class="dialog-button" hidden="true"/> + <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/> +#endif + </xul:hbox> + </content> + + <implementation> + <field name="_mStrBundle">null</field> + <field name="_closeHandler">(function(event) { + if (!document.documentElement.cancelDialog()) + event.preventDefault(); + })</field> + + <property name="buttons" + onget="return this.getAttribute('buttons');" + onset="this._configureButtons(val); return val;"/> + + <property name="defaultButton"> + <getter> + <![CDATA[ + if (this.hasAttribute("defaultButton")) + return this.getAttribute("defaultButton"); + return "accept"; // default to the accept button + ]]> + </getter> + <setter> + <![CDATA[ + this._setDefaultButton(val); + return val; + ]]> + </setter> + </property> + + <method name="acceptDialog"> + <body> + <![CDATA[ + return this._doButtonCommand("accept"); + ]]> + </body> + </method> + + <method name="cancelDialog"> + <body> + <![CDATA[ + return this._doButtonCommand("cancel"); + ]]> + </body> + </method> + + <method name="getButton"> + <parameter name="aDlgType"/> + <body> + <![CDATA[ + return this._buttons[aDlgType]; + ]]> + </body> + </method> + + <method name="moveToAlertPosition"> + <body> + <![CDATA[ + // hack. we need this so the window has something like its final size + if (window.outerWidth == 1) { + dump("Trying to position a sizeless window; caller should have called sizeToContent() or sizeTo(). See bug 75649.\n"); + sizeToContent(); + } + + if (opener) { + var xOffset = (opener.outerWidth - window.outerWidth) / 2; + var yOffset = opener.outerHeight / 5; + + var newX = opener.screenX + xOffset; + var newY = opener.screenY + yOffset; + } else { + newX = (screen.availWidth - window.outerWidth) / 2; + newY = (screen.availHeight - window.outerHeight) / 2; + } + + // ensure the window is fully onscreen (if smaller than the screen) + if (newX < screen.availLeft) + newX = screen.availLeft + 20; + if ((newX + window.outerWidth) > (screen.availLeft + screen.availWidth)) + newX = (screen.availLeft + screen.availWidth) - window.outerWidth - 20; + + if (newY < screen.availTop) + newY = screen.availTop + 20; + if ((newY + window.outerHeight) > (screen.availTop + screen.availHeight)) + newY = (screen.availTop + screen.availHeight) - window.outerHeight - 60; + + window.moveTo( newX, newY ); + ]]> + </body> + </method> + + <method name="centerWindowOnScreen"> + <body> + <![CDATA[ + var xOffset = screen.availWidth/2 - window.outerWidth/2; + var yOffset = screen.availHeight/2 - window.outerHeight/2; + + xOffset = xOffset > 0 ? xOffset : 0; + yOffset = yOffset > 0 ? yOffset : 0; + window.moveTo(xOffset, yOffset); + ]]> + </body> + </method> + + <constructor> + <![CDATA[ + this._configureButtons(this.buttons); + + // listen for when window is closed via native close buttons + window.addEventListener("close", this._closeHandler, false); + + // for things that we need to initialize after onload fires + window.addEventListener("load", this.postLoadInit, false); + + window.moveToAlertPosition = this.moveToAlertPosition; + window.centerWindowOnScreen = this.centerWindowOnScreen; + ]]> + </constructor> + + <method name="postLoadInit"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + function focusInit() { + const dialog = document.documentElement; + const defaultButton = dialog.getButton(dialog.defaultButton); + // give focus to the first focusable element in the dialog + if (!document.commandDispatcher.focusedElement) { + document.commandDispatcher.advanceFocusIntoSubtree(dialog); + + var focusedElt = document.commandDispatcher.focusedElement; + if (focusedElt) { + var initialFocusedElt = focusedElt; + while (focusedElt.localName == "tab" || + focusedElt.getAttribute("noinitialfocus") == "true") { + document.commandDispatcher.advanceFocusIntoSubtree(focusedElt); + focusedElt = document.commandDispatcher.focusedElement; + if (focusedElt == initialFocusedElt) + break; + } + + if (initialFocusedElt.localName == "tab") { + if (focusedElt.hasAttribute("dlgtype")) { + // We don't want to focus on anonymous OK, Cancel, etc. buttons, + // so return focus to the tab itself + initialFocusedElt.focus(); + } + } + else if (!/Mac/.test(navigator.platform) && + focusedElt.hasAttribute("dlgtype") && focusedElt != defaultButton) { + defaultButton.focus(); + } + } + } + + try { + if (defaultButton) + window.notifyDefaultButtonLoaded(defaultButton); + } catch (e) { } + } + + // Give focus after onload completes, see bug 103197. + setTimeout(focusInit, 0); + ]]> + </body> + </method> + + <property name="mStrBundle"> + <getter> + <![CDATA[ + if (!this._mStrBundle) { + // need to create string bundle manually instead of using <xul:stringbundle/> + // see bug 63370 for details + this._mStrBundle = Components.classes["@mozilla.org/intl/stringbundle;1"] + .getService(Components.interfaces.nsIStringBundleService) + .createBundle("chrome://global/locale/dialog.properties"); + } + return this._mStrBundle; + ]]></getter> + </property> + + <method name="_configureButtons"> + <parameter name="aButtons"/> + <body> + <![CDATA[ + // by default, get all the anonymous button elements + var buttons = {}; + this._buttons = buttons; + buttons.accept = document.getAnonymousElementByAttribute(this, "dlgtype", "accept"); + buttons.cancel = document.getAnonymousElementByAttribute(this, "dlgtype", "cancel"); + buttons.extra1 = document.getAnonymousElementByAttribute(this, "dlgtype", "extra1"); + buttons.extra2 = document.getAnonymousElementByAttribute(this, "dlgtype", "extra2"); + buttons.help = document.getAnonymousElementByAttribute(this, "dlgtype", "help"); + buttons.disclosure = document.getAnonymousElementByAttribute(this, "dlgtype", "disclosure"); + + // look for any overriding explicit button elements + var exBtns = this.getElementsByAttribute("dlgtype", "*"); + var dlgtype; + var i; + for (i = 0; i < exBtns.length; ++i) { + dlgtype = exBtns[i].getAttribute("dlgtype"); + buttons[dlgtype].hidden = true; // hide the anonymous button + buttons[dlgtype] = exBtns[i]; + } + + // add the label and oncommand handler to each button + for (dlgtype in buttons) { + var button = buttons[dlgtype]; + button.addEventListener("command", this._handleButtonCommand, true); + + // don't override custom labels with pre-defined labels on explicit buttons + if (!button.hasAttribute("label")) { + // dialog attributes override the default labels in dialog.properties + if (this.hasAttribute("buttonlabel"+dlgtype)) { + button.setAttribute("label", this.getAttribute("buttonlabel"+dlgtype)); + if (this.hasAttribute("buttonaccesskey"+dlgtype)) + button.setAttribute("accesskey", this.getAttribute("buttonaccesskey"+dlgtype)); + } else if (dlgtype != "extra1" && dlgtype != "extra2") { + button.setAttribute("label", this.mStrBundle.GetStringFromName("button-"+dlgtype)); + var accessKey = this.mStrBundle.GetStringFromName("accesskey-"+dlgtype); + if (accessKey) + button.setAttribute("accesskey", accessKey); + } + } + // allow specifying alternate icons in the dialog header + if (!button.hasAttribute("icon")) { + // if there's an icon specified, use that + if (this.hasAttribute("buttonicon"+dlgtype)) + button.setAttribute("icon", this.getAttribute("buttonicon"+dlgtype)); + // otherwise set defaults + else + switch (dlgtype) { + case "accept": + button.setAttribute("icon", "accept"); + break; + case "cancel": + button.setAttribute("icon", "cancel"); + break; + case "disclosure": + button.setAttribute("icon", "properties"); + break; + case "help": + button.setAttribute("icon", "help"); + break; + default: + break; + } + } + } + + // ensure that hitting enter triggers the default button command + this.defaultButton = this.defaultButton; + + // if there is a special button configuration, use it + if (aButtons) { + // expect a comma delimited list of dlgtype values + var list = aButtons.split(","); + + // mark shown dlgtypes as true + var shown = { accept: false, cancel: false, help: false, + disclosure: false, extra1: false, extra2: false }; + for (i = 0; i < list.length; ++i) + shown[list[i].replace(/ /g, "")] = true; + + // hide/show the buttons we want + for (dlgtype in buttons) + buttons[dlgtype].hidden = !shown[dlgtype]; + + // show the spacer on Windows only when the extra2 button is present + if (/Win/.test(navigator.platform)) { + var spacer = document.getAnonymousElementByAttribute(this, "anonid", "spacer"); + spacer.removeAttribute("hidden"); + spacer.setAttribute("flex", shown["extra2"]?"1":"0"); + } + } + ]]> + </body> + </method> + + <method name="_setDefaultButton"> + <parameter name="aNewDefault"/> + <body> + <![CDATA[ + // remove the default attribute from the previous default button, if any + var oldDefaultButton = this.getButton(this.defaultButton); + if (oldDefaultButton) + oldDefaultButton.removeAttribute("default"); + + var newDefaultButton = this.getButton(aNewDefault); + if (newDefaultButton) { + this.setAttribute("defaultButton", aNewDefault); + newDefaultButton.setAttribute("default", "true"); + } + else { + this.setAttribute("defaultButton", "none"); + if (aNewDefault != "none") + dump("invalid new default button: " + aNewDefault + ", assuming: none\n"); + } + ]]> + </body> + </method> + + <method name="_handleButtonCommand"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + return document.documentElement._doButtonCommand( + aEvent.target.getAttribute("dlgtype")); + ]]> + </body> + </method> + + <method name="_doButtonCommand"> + <parameter name="aDlgType"/> + <body> + <![CDATA[ + var button = this.getButton(aDlgType); + if (!button.disabled) { + var noCancel = this._fireButtonEvent(aDlgType); + if (noCancel) { + if (aDlgType == "accept" || aDlgType == "cancel") { + var closingEvent = new CustomEvent("dialogclosing", { + bubbles: true, + detail: { button: aDlgType }, + }); + this.dispatchEvent(closingEvent); + window.close(); + } + } + return noCancel; + } + return true; + ]]> + </body> + </method> + + <method name="_fireButtonEvent"> + <parameter name="aDlgType"/> + <body> + <![CDATA[ + var event = document.createEvent("Events"); + event.initEvent("dialog"+aDlgType, true, true); + + // handle dom event handlers + var noCancel = this.dispatchEvent(event); + + // handle any xml attribute event handlers + var handler = this.getAttribute("ondialog"+aDlgType); + if (handler != "") { + var fn = new Function("event", handler); + var returned = fn(event); + if (returned == false) + noCancel = false; + } + + return noCancel; + ]]> + </body> + </method> + + <method name="_hitEnter"> + <parameter name="evt"/> + <body> + <![CDATA[ + if (evt.defaultPrevented) + return; + + var btn = this.getButton(this.defaultButton); + if (btn) + this._doButtonCommand(this.defaultButton); + ]]> + </body> + </method> + + </implementation> + + <handlers> + <handler event="keypress" keycode="VK_RETURN" + group="system" action="this._hitEnter(event);"/> + <handler event="keypress" keycode="VK_ESCAPE" group="system"> + if (!event.defaultPrevented) + this.cancelDialog(); + </handler> +#ifdef XP_MACOSX + <handler event="keypress" key="." modifiers="meta" phase="capturing" action="this.cancelDialog();"/> +#else + <handler event="focus" phase="capturing"> + var btn = this.getButton(this.defaultButton); + if (btn) + btn.setAttribute("default", event.originalTarget == btn || !(event.originalTarget instanceof Components.interfaces.nsIDOMXULButtonElement)); + </handler> +#endif + </handlers> + + </binding> + + <binding id="dialogheader"> + <resources> + <stylesheet src="chrome://global/skin/dialog.css"/> + </resources> + <content> + <xul:label class="dialogheader-title" xbl:inherits="value=title,crop" crop="right" flex="1"/> + <xul:label class="dialogheader-description" xbl:inherits="value=description"/> + </content> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/editor.xml b/toolkit/content/widgets/editor.xml new file mode 100644 index 0000000000..637586dc2c --- /dev/null +++ b/toolkit/content/widgets/editor.xml @@ -0,0 +1,195 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="editorBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="editor" role="outerdoc"> + <implementation type="application/javascript"> + <constructor> + <![CDATA[ + // Make window editable immediately only + // if the "editortype" attribute is supplied + // This allows using same contentWindow for different editortypes, + // where the type is determined during the apps's window.onload handler. + if (this.editortype) + this.makeEditable(this.editortype, true); + ]]> + </constructor> + <destructor/> + + <field name="_editorContentListener"> + <![CDATA[ + ({ + QueryInterface: function(iid) + { + if (iid.equals(Components.interfaces.nsIURIContentListener) || + iid.equals(Components.interfaces.nsISupportsWeakReference) || + iid.equals(Components.interfaces.nsISupports)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onStartURIOpen: function(uri) + { + return false; + }, + doContent: function(contentType, isContentPreferred, request, contentHandler) + { + return false; + }, + isPreferred: function(contentType, desiredContentType) + { + return false; + }, + canHandleContent: function(contentType, isContentPreferred, desiredContentType) + { + return false; + }, + loadCookie: null, + parentContentListener: null + }) + ]]> + </field> + <method name="makeEditable"> + <parameter name="editortype"/> + <parameter name="waitForUrlLoad"/> + <body> + <![CDATA[ + this.editingSession.makeWindowEditable(this.contentWindow, editortype, waitForUrlLoad, true, false); + this.setAttribute("editortype", editortype); + + this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIURIContentListener) + .parentContentListener = this._editorContentListener; + ]]> + </body> + </method> + <method name="getEditor"> + <parameter name="containingWindow"/> + <body> + <![CDATA[ + return this.editingSession.getEditorForWindow(containingWindow); + ]]> + </body> + </method> + <method name="getHTMLEditor"> + <parameter name="containingWindow"/> + <body> + <![CDATA[ + var editor = this.editingSession.getEditorForWindow(containingWindow); + return editor.QueryInterface(Components.interfaces.nsIHTMLEditor); + ]]> + </body> + </method> + + <field name="_finder">null</field> + <property name="finder" readonly="true"> + <getter><![CDATA[ + if (!this._finder) { + if (!this.docShell) + return null; + + let Finder = Components.utils.import("resource://gre/modules/Finder.jsm", {}).Finder; + this._finder = new Finder(this.docShell); + } + return this._finder; + ]]></getter> + </property> + + <field name="_fastFind">null</field> + <property name="fastFind" + readonly="true"> + <getter> + <![CDATA[ + if (!this._fastFind) { + if (!("@mozilla.org/typeaheadfind;1" in Components.classes)) + return null; + + if (!this.docShell) + return null; + + this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"] + .createInstance(Components.interfaces.nsITypeAheadFind); + this._fastFind.init(this.docShell); + } + return this._fastFind; + ]]> + </getter> + </property> + + <field name="_lastSearchString">null</field> + + <property name="editortype" + onget="return this.getAttribute('editortype');" + onset="this.setAttribute('editortype', val); return val;"/> + <property name="webNavigation" + onget="return this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation);" + readonly="true"/> + <property name="contentDocument" readonly="true" + onget="return this.webNavigation.document;"/> + <property name="docShell" readonly="true"> + <getter><![CDATA[ + let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader; + return frameLoader ? frameLoader.docShell : null; + ]]></getter> + </property> + <property name="currentURI" + readonly="true" + onget="return this.webNavigation.currentURI;"/> + <property name="contentWindow" + readonly="true" + onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);"/> + <property name="contentWindowAsCPOW" + readonly="true" + onget="return this.contentWindow;"/> + <property name="webBrowserFind" + readonly="true" + onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebBrowserFind);"/> + <property name="markupDocumentViewer" + readonly="true" + onget="return this.docShell.contentViewer;"/> + <property name="editingSession" + readonly="true" + onget="return this.webNavigation.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIEditingSession);"/> + <property name="commandManager" + readonly="true" + onget="return this.webNavigation.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsICommandManager);"/> + <property name="fullZoom" + onget="return this.markupDocumentViewer.fullZoom;" + onset="this.markupDocumentViewer.fullZoom = val;"/> + <property name="textZoom" + onget="return this.markupDocumentViewer.textZoom;" + onset="this.markupDocumentViewer.textZoom = val;"/> + <property name="isSyntheticDocument" + onget="return this.contentDocument.isSyntheticDocument;" + readonly="true"/> + <property name="messageManager" + readonly="true"> + <getter> + <![CDATA[ + var owner = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner); + if (!owner.frameLoader) { + return null; + } + return owner.frameLoader.messageManager; + ]]> + </getter> + </property> + <property name="outerWindowID" readonly="true"> + <getter><![CDATA[ + return this.contentWindow + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils) + .outerWindowID; + ]]></getter> + </property> + </implementation> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/expander.xml b/toolkit/content/widgets/expander.xml new file mode 100644 index 0000000000..a4ffea313c --- /dev/null +++ b/toolkit/content/widgets/expander.xml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<bindings id="expanderBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="expander" display="xul:vbox"> + <resources> + <stylesheet src="chrome://global/skin/expander.css"/> + </resources> + <content> + <xul:hbox align="center"> + <xul:button type="disclosure" class="expanderButton" anonid="disclosure" xbl:inherits="disabled" mousethrough="always"/> + <xul:label class="header expanderButton" anonid="label" xbl:inherits="value=label,disabled" mousethrough="always" flex="1"/> + <xul:button anonid="clear-button" xbl:inherits="label=clearlabel,disabled=cleardisabled,hidden=clearhidden" mousethrough="always" icon="clear"/> + </xul:hbox> + <xul:vbox flex="1" anonid="settings" class="settingsContainer" collapsed="true" xbl:inherits="align"> + <children/> + </xul:vbox> + </content> + <implementation> + <constructor><![CDATA[ + var settings = document.getAnonymousElementByAttribute(this, "anonid", "settings"); + var expander = document.getAnonymousElementByAttribute(this, "anonid", "disclosure"); + var open = this.getAttribute("open") == "true"; + settings.collapsed = !open; + expander.open = open; + ]]></constructor> + <property name="open"> + <setter> + <![CDATA[ + var settings = document.getAnonymousElementByAttribute(this, "anonid", "settings"); + var expander = document.getAnonymousElementByAttribute(this, "anonid", "disclosure"); + settings.collapsed = !val; + expander.open = val; + if (val) + this.setAttribute("open", "true"); + else + this.setAttribute("open", "false"); + return val; + ]]> + </setter> + <getter> + return this.getAttribute("open"); + </getter> + </property> + <method name="onCommand"> + <parameter name="aEvent"/> + <body><![CDATA[ + var element = aEvent.originalTarget; + var button = element.getAttribute("anonid"); + switch (button) { + case "disclosure": + case "label": + if (this.open == "true") + this.open = false; + else + this.open = true; + break; + case "clear-button": + var event = document.createEvent("Events"); + event.initEvent("clear", true, true); + this.dispatchEvent(event); + break; + } + ]]></body> + </method> + </implementation> + <handlers> + <handler event="command"><![CDATA[ + this.onCommand(event); + ]]></handler> + <handler event="click"><![CDATA[ + if (event.originalTarget.localName == "label") + this.onCommand(event); + ]]></handler> + </handlers> + </binding> + +</bindings> + + diff --git a/toolkit/content/widgets/filefield.xml b/toolkit/content/widgets/filefield.xml new file mode 100644 index 0000000000..f81761eb58 --- /dev/null +++ b/toolkit/content/widgets/filefield.xml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="filefieldBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="filefield" extends="chrome://global/content/bindings/general.xml#basetext"> + <resources> + <stylesheet src="chrome://global/skin/filefield.css"/> + </resources> + <content> + <xul:stringbundle anonid="bundle" src="chrome://global/locale/filefield.properties"/> + <xul:hbox class="fileFieldContentBox" align="center" flex="1" xbl:inherits="disabled"> + <xul:image class="fileFieldIcon" xbl:inherits="src=image,disabled"/> + <xul:textbox class="fileFieldLabel" xbl:inherits="value=label,disabled,accesskey,tabindex,aria-labelledby" flex="1" readonly="true"/> + </xul:hbox> + </content> + <implementation implements="nsIDOMXULLabeledControlElement"> + <property name="label" onget="return this.getAttribute('label');"> + <setter> + this.setAttribute('label', val); + var elt = document.getAnonymousElementByAttribute(this, "class", "fileFieldLabel"); + return (elt.value = val); + </setter> + </property> + + <field name="_file">null</field> + <property name="file" onget="return this._file"> + <setter> + <![CDATA[ + this._file = val; + if (val) { + this.image = this._getIconURLForFile(val); + this.label = this._getDisplayNameForFile(val); + } + else { + this.removeAttribute("image"); + var bundle = document.getAnonymousElementByAttribute(this, "anonid", "bundle"); + this.label = bundle.getString("downloadHelperNoneSelected"); + } + return val; + ]]> + </setter> + </property> + <method name="_getDisplayNameForFile"> + <parameter name="aFile"/> + <body> + <![CDATA[ + if (/Win/.test(navigator.platform)) { + var lfw = aFile.QueryInterface(Components.interfaces.nsILocalFileWin); + try { + return lfw.getVersionInfoField("FileDescription"); + } + catch (e) { + // fall through to the filename + } + } else if (/Mac/.test(navigator.platform)) { + var lfm = aFile.QueryInterface(Components.interfaces.nsILocalFileMac); + try { + return lfm.bundleDisplayName; + } + catch (e) { + // fall through to the file name + } + } + var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var url = ios.newFileURI(aFile).QueryInterface(Components.interfaces.nsIURL); + return url.fileName; + ]]> + </body> + </method> + + <method name="_getIconURLForFile"> + <parameter name="aFile"/> + <body> + <![CDATA[ + if (!aFile) + return ""; + var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var fph = ios.getProtocolHandler("file") + .QueryInterface(Components.interfaces.nsIFileProtocolHandler); + var urlspec = fph.getURLSpecFromFile(aFile); + return "moz-icon://" + urlspec + "?size=16"; + ]]> + </body> + </method> + </implementation> + </binding> +</bindings> diff --git a/toolkit/content/widgets/findbar.xml b/toolkit/content/widgets/findbar.xml new file mode 100644 index 0000000000..f90d412275 --- /dev/null +++ b/toolkit/content/widgets/findbar.xml @@ -0,0 +1,1397 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE bindings [ +<!ENTITY % findBarDTD SYSTEM "chrome://global/locale/findbar.dtd" > +%findBarDTD; +]> + +<bindings id="findbarBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <!-- Private binding --> + <binding id="findbar-textbox" + extends="chrome://global/content/bindings/textbox.xml#textbox"> + <implementation> + + <field name="_findbar">null</field> + <property name="findbar" readonly="true"> + <getter> + return this._findbar ? + this._findbar : this._findbar = document.getBindingParent(this); + </getter> + </property> + + <method name="_handleEnter"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (this.findbar._findMode == this.findbar.FIND_NORMAL) { + let findString = this.findbar._findField; + if (!findString.value) + return; + if (aEvent.getModifierState("Accel")) { + this.findbar.getElement("highlight").click(); + return; + } + + this.findbar.onFindAgainCommand(aEvent.shiftKey); + } else { + this.findbar._finishFAYT(aEvent); + } + ]]></body> + </method> + + <method name="_handleTab"> + <parameter name="aEvent"/> + <body><![CDATA[ + let shouldHandle = !aEvent.altKey && !aEvent.ctrlKey && + !aEvent.metaKey; + if (shouldHandle && + this.findbar._findMode != this.findbar.FIND_NORMAL) { + + this.findbar._finishFAYT(aEvent); + } + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="input"><![CDATA[ + // We should do nothing during composition. E.g., composing string + // before converting may matches a forward word of expected word. + // After that, even if user converts the composition string to the + // expected word, it may find second or later searching word in the + // document. + if (this.findbar._isIMEComposing) { + return; + } + + if (this._hadValue && !this.value) { + this._willfullyDeleted = true; + this._hadValue = false; + } else if (this.value.trim()) { + this._hadValue = true; + this._willfullyDeleted = false; + } + this.findbar._find(this.value); + ]]></handler> + + <handler event="keypress"><![CDATA[ + let shouldHandle = !event.altKey && !event.ctrlKey && + !event.metaKey && !event.shiftKey; + + switch (event.keyCode) { + case KeyEvent.DOM_VK_RETURN: + this._handleEnter(event); + break; + case KeyEvent.DOM_VK_TAB: + this._handleTab(event); + break; + case KeyEvent.DOM_VK_PAGE_UP: + case KeyEvent.DOM_VK_PAGE_DOWN: + if (shouldHandle) { + this.findbar.browser.finder.keyPress(event); + event.preventDefault(); + } + break; + case KeyEvent.DOM_VK_UP: + case KeyEvent.DOM_VK_DOWN: + this.findbar.browser.finder.keyPress(event); + event.preventDefault(); + break; + } + ]]></handler> + + <handler event="blur"><![CDATA[ + let findbar = this.findbar; + // Note: This code used to remove the selection + // if it matched an editable. + findbar.browser.finder.enableSelection(); + ]]></handler> + + <handler event="focus"><![CDATA[ + if (/Mac/.test(navigator.platform)) { + let findbar = this.findbar; + findbar._onFindFieldFocus(); + } + ]]></handler> + + <handler event="compositionstart"><![CDATA[ + // Don't close the find toolbar while IME is composing. + let findbar = this.findbar; + findbar._isIMEComposing = true; + if (findbar._quickFindTimeout) { + clearTimeout(findbar._quickFindTimeout); + findbar._quickFindTimeout = null; + } + ]]></handler> + + <handler event="compositionend"><![CDATA[ + let findbar = this.findbar; + findbar._isIMEComposing = false; + if (findbar._findMode != findbar.FIND_NORMAL) + findbar._setFindCloseTimeout(); + ]]></handler> + + <handler event="dragover"><![CDATA[ + if (event.dataTransfer.types.includes("text/plain")) + event.preventDefault(); + ]]></handler> + + <handler event="drop"><![CDATA[ + let value = event.dataTransfer.getData("text/plain"); + this.value = value; + this.findbar._find(value); + event.stopPropagation(); + event.preventDefault(); + ]]></handler> + </handlers> + </binding> + + <binding id="findbar" + extends="chrome://global/content/bindings/toolbar.xml#toolbar"> + <resources> + <stylesheet src="chrome://global/skin/findBar.css"/> + </resources> + + <content hidden="true"> + <xul:hbox anonid="findbar-container" class="findbar-container" flex="1" align="center"> + <xul:hbox anonid="findbar-textbox-wrapper" align="stretch"> + <xul:textbox anonid="findbar-textbox" + class="findbar-textbox findbar-find-fast" + xbl:inherits="flash"/> + <xul:toolbarbutton anonid="find-previous" + class="findbar-find-previous tabbable" + tooltiptext="&previous.tooltip;" + oncommand="onFindAgainCommand(true);" + disabled="true" + xbl:inherits="accesskey=findpreviousaccesskey"/> + <xul:toolbarbutton anonid="find-next" + class="findbar-find-next tabbable" + tooltiptext="&next.tooltip;" + oncommand="onFindAgainCommand(false);" + disabled="true" + xbl:inherits="accesskey=findnextaccesskey"/> + </xul:hbox> + <xul:toolbarbutton anonid="highlight" + class="findbar-highlight findbar-button tabbable" + label="&highlightAll.label;" + accesskey="&highlightAll.accesskey;" + tooltiptext="&highlightAll.tooltiptext;" + oncommand="toggleHighlight(this.checked);" + type="checkbox" + xbl:inherits="accesskey=highlightaccesskey"/> + <xul:toolbarbutton anonid="find-case-sensitive" + class="findbar-case-sensitive findbar-button tabbable" + label="&caseSensitive.label;" + accesskey="&caseSensitive.accesskey;" + tooltiptext="&caseSensitive.tooltiptext;" + oncommand="_setCaseSensitivity(this.checked ? 1 : 0);" + type="checkbox" + xbl:inherits="accesskey=matchcaseaccesskey"/> + <xul:toolbarbutton anonid="find-entire-word" + class="findbar-entire-word findbar-button tabbable" + label="&entireWord.label;" + accesskey="&entireWord.accesskey;" + tooltiptext="&entireWord.tooltiptext;" + oncommand="toggleEntireWord(this.checked);" + type="checkbox" + xbl:inherits="accesskey=entirewordaccesskey"/> + <xul:label anonid="match-case-status" class="findbar-find-fast"/> + <xul:label anonid="entire-word-status" class="findbar-find-fast"/> + <xul:label anonid="found-matches" class="findbar-find-fast found-matches" hidden="true"/> + <xul:image anonid="find-status-icon" class="findbar-find-fast find-status-icon"/> + <xul:description anonid="find-status" + control="findbar-textbox" + class="findbar-find-fast findbar-find-status"> + <!-- Do not use value, first child is used because it provides a11y with text change events --> + </xul:description> + </xul:hbox> + <xul:toolbarbutton anonid="find-closebutton" + class="findbar-closebutton close-icon" + tooltiptext="&findCloseButton.tooltip;" + oncommand="close();"/> + </content> + + <implementation implements="nsIMessageListener, nsIEditActionListener"> + <!-- Please keep in sync with toolkit/content/browser-content.js --> + <field name="FIND_NORMAL">0</field> + <field name="FIND_TYPEAHEAD">1</field> + <field name="FIND_LINKS">2</field> + + <field name="__findMode">0</field> + <property name="_findMode" onget="return this.__findMode;" + onset="this.__findMode = val; this._updateBrowserWithState(); return val;"/> + + <field name="_flashFindBar">0</field> + <field name="_initialFlashFindBarCount">6</field> + + <!-- + - For tests that need to know when the find bar is finished + - initializing, we store a promise to notify on. + --> + <field name="_startFindDeferred">null</field> + + <property name="prefillWithSelection" + onget="return this.getAttribute('prefillwithselection') != 'false'" + onset="this.setAttribute('prefillwithselection', val); return val;"/> + + <method name="getElement"> + <parameter name="aAnonymousID"/> + <body><![CDATA[ + return document.getAnonymousElementByAttribute(this, + "anonid", + aAnonymousID) + ]]></body> + </method> + + <property name="findMode" + readonly="true" + onget="return this._findMode;"/> + + <property name="hasTransactions" readonly="true"> + <getter><![CDATA[ + if (this._findField.value) + return true; + + // Watch out for lazy editor init + if (this._findField.editor) { + let tm = this._findField.editor.transactionManager; + return !!(tm.numberOfUndoItems || tm.numberOfRedoItems); + } + return false; + ]]></getter> + </property> + + <field name="_browser">null</field> + <property name="browser"> + <getter><![CDATA[ + if (!this._browser) { + this._browser = + document.getElementById(this.getAttribute("browserid")); + } + return this._browser; + ]]></getter> + <setter><![CDATA[ + if (this._browser) { + if (this._browser.messageManager) { + this._browser.messageManager.removeMessageListener("Findbar:Keypress", this); + this._browser.messageManager.removeMessageListener("Findbar:Mouseup", this); + } + let finder = this._browser.finder; + if (finder) + finder.removeResultListener(this); + } + + this._browser = val; + if (this._browser) { + // Need to do this to ensure the correct initial state. + this._updateBrowserWithState(); + this._browser.messageManager.addMessageListener("Findbar:Keypress", this); + this._browser.messageManager.addMessageListener("Findbar:Mouseup", this); + this._browser.finder.addResultListener(this); + + this._findField.value = this._browser._lastSearchString; + } + return val; + ]]></setter> + </property> + + <field name="__prefsvc">null</field> + <property name="_prefsvc"> + <getter><![CDATA[ + if (!this.__prefsvc) { + this.__prefsvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + } + return this.__prefsvc; + ]]></getter> + </property> + + <field name="_observer"><![CDATA[({ + _self: this, + + QueryInterface: function(aIID) { + if (aIID.equals(Components.interfaces.nsIObserver) || + aIID.equals(Components.interfaces.nsISupportsWeakReference) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + observe: function(aSubject, aTopic, aPrefName) { + if (aTopic != "nsPref:changed") + return; + + let prefsvc = this._self._prefsvc; + + switch (aPrefName) { + case "accessibility.typeaheadfind": + this._self._findAsYouType = prefsvc.getBoolPref(aPrefName); + break; + case "accessibility.typeaheadfind.linksonly": + this._self._typeAheadLinksOnly = prefsvc.getBoolPref(aPrefName); + break; + case "accessibility.typeaheadfind.casesensitive": + this._self._setCaseSensitivity(prefsvc.getIntPref(aPrefName)); + break; + case "findbar.entireword": + this._self._entireWord = prefsvc.getBoolPref(aPrefName); + this._self.toggleEntireWord(this._self._entireWord, true); + break; + case "findbar.highlightAll": + this._self.toggleHighlight(prefsvc.getBoolPref(aPrefName), true); + break; + case "findbar.modalHighlight": + this._self._useModalHighlight = prefsvc.getBoolPref(aPrefName); + if (this._self.browser.finder) + this._self.browser.finder.onModalHighlightChange(this._self._useModalHighlight); + break; + } + } + })]]></field> + + <field name="_destroyed">false</field> + + <constructor><![CDATA[ + // These elements are accessed frequently and are therefore cached + this._findField = this.getElement("findbar-textbox"); + this._foundMatches = this.getElement("found-matches"); + this._findStatusIcon = this.getElement("find-status-icon"); + this._findStatusDesc = this.getElement("find-status"); + + this._foundURL = null; + + let prefsvc = this._prefsvc; + + this._quickFindTimeoutLength = + prefsvc.getIntPref("accessibility.typeaheadfind.timeout"); + this._flashFindBar = + prefsvc.getIntPref("accessibility.typeaheadfind.flashBar"); + this._useModalHighlight = prefsvc.getBoolPref("findbar.modalHighlight"); + + prefsvc.addObserver("accessibility.typeaheadfind", + this._observer, false); + prefsvc.addObserver("accessibility.typeaheadfind.linksonly", + this._observer, false); + prefsvc.addObserver("accessibility.typeaheadfind.casesensitive", + this._observer, false); + prefsvc.addObserver("findbar.entireword", this._observer, false); + prefsvc.addObserver("findbar.highlightAll", this._observer, false); + prefsvc.addObserver("findbar.modalHighlight", this._observer, false); + + this._findAsYouType = + prefsvc.getBoolPref("accessibility.typeaheadfind"); + this._typeAheadLinksOnly = + prefsvc.getBoolPref("accessibility.typeaheadfind.linksonly"); + this._typeAheadCaseSensitive = + prefsvc.getIntPref("accessibility.typeaheadfind.casesensitive"); + this._entireWord = prefsvc.getBoolPref("findbar.entireword"); + this._highlightAll = prefsvc.getBoolPref("findbar.highlightAll"); + + // Convenience + this.nsITypeAheadFind = Components.interfaces.nsITypeAheadFind; + this.nsISelectionController = Components.interfaces.nsISelectionController; + this._findSelection = this.nsISelectionController.SELECTION_FIND; + + this._findResetTimeout = -1; + + // Make sure the FAYT keypress listener is attached by initializing the + // browser property + if (this.getAttribute("browserid")) + setTimeout(function(aSelf) { aSelf.browser = aSelf.browser; }, 0, this); + ]]></constructor> + + <destructor><![CDATA[ + this.destroy(); + ]]></destructor> + + <!-- This is necessary because the destructor isn't called when + we are removed from a document that is not destroyed. This + needs to be explicitly called in this case --> + <method name="destroy"> + <body><![CDATA[ + if (this._destroyed) + return; + this._destroyed = true; + + if (this.browser.finder) + this.browser.finder.destroy(); + + this.browser = null; + + let prefsvc = this._prefsvc; + prefsvc.removeObserver("accessibility.typeaheadfind", + this._observer); + prefsvc.removeObserver("accessibility.typeaheadfind.linksonly", + this._observer); + prefsvc.removeObserver("accessibility.typeaheadfind.casesensitive", + this._observer); + prefsvc.removeObserver("findbar.entireword", this._observer); + prefsvc.removeObserver("findbar.highlightAll", this._observer); + prefsvc.removeObserver("findbar.modalHighlight", this._observer); + + // Clear all timers that might still be running. + this._cancelTimers(); + ]]></body> + </method> + + <method name="_cancelTimers"> + <body><![CDATA[ + if (this._flashFindBarTimeout) { + clearInterval(this._flashFindBarTimeout); + this._flashFindBarTimeout = null; + } + if (this._quickFindTimeout) { + clearTimeout(this._quickFindTimeout); + this._quickFindTimeout = null; + } + if (this._findResetTimeout) { + clearTimeout(this._findResetTimeout); + this._findResetTimeout = null; + } + ]]></body> + </method> + + <method name="_setFindCloseTimeout"> + <body><![CDATA[ + if (this._quickFindTimeout) + clearTimeout(this._quickFindTimeout); + + // Don't close the find toolbar while IME is composing OR when the + // findbar is already hidden. + if (this._isIMEComposing || this.hidden) { + this._quickFindTimeout = null; + return; + } + + this._quickFindTimeout = setTimeout(() => { + if (this._findMode != this.FIND_NORMAL) + this.close(); + this._quickFindTimeout = null; + }, this._quickFindTimeoutLength); + ]]></body> + </method> + + <field name="_pluralForm">null</field> + <property name="pluralForm"> + <getter><![CDATA[ + if (!this._pluralForm) { + this._pluralForm = Components.utils.import( + "resource://gre/modules/PluralForm.jsm", {}).PluralForm; + } + return this._pluralForm; + ]]></getter> + </property> + + <!-- + - Updates the search match count after each find operation on a new string. + - @param aRes + - the result of the find operation + --> + <method name="_updateMatchesCount"> + <body><![CDATA[ + if (!this._dispatchFindEvent("matchescount")) + return; + + this.browser.finder.requestMatchesCount(this._findField.value, + this._findMode == this.FIND_LINKS); + ]]></body> + </method> + + <!-- + - Turns highlight on or off. + - @param aHighlight (boolean) + - Whether to turn the highlight on or off + - @param aFromPrefObserver (boolean) + - Whether the callee is the pref observer, which means we should + - not set the same pref again. + --> + <method name="toggleHighlight"> + <parameter name="aHighlight"/> + <parameter name="aFromPrefObserver"/> + <body><![CDATA[ + if (aHighlight === this._highlightAll) { + return; + } + + this.browser.finder.onHighlightAllChange(aHighlight); + + this._setHighlightAll(aHighlight, aFromPrefObserver); + + if (!this._dispatchFindEvent("highlightallchange")) { + return; + } + + let word = this._findField.value; + // Bug 429723. Don't attempt to highlight "" + if (aHighlight && !word) + return; + + this.browser.finder.highlight(aHighlight, word, + this._findMode == this.FIND_LINKS); + + // Update the matches count + this._updateMatchesCount(this.nsITypeAheadFind.FIND_FOUND); + ]]></body> + </method> + + <!-- + - Updates the highlight-all mode of the findbar and its UI. + - @param aHighlight (boolean) + - Whether to turn the highlight on or off. + - @param aFromPrefObserver (boolean) + - Whether the callee is the pref observer, which means we should + - not set the same pref again. + --> + <method name="_setHighlightAll"> + <parameter name="aHighlight"/> + <parameter name="aFromPrefObserver"/> + <body><![CDATA[ + if (typeof aHighlight != "boolean") { + aHighlight = this._highlightAll; + } + if (aHighlight !== this._highlightAll && !aFromPrefObserver) { + this._prefsvc.setBoolPref("findbar.highlightAll", aHighlight); + } + this._highlightAll = aHighlight; + let checkbox = this.getElement("highlight"); + checkbox.checked = this._highlightAll; + ]]></body> + </method> + + <method name="_maybeHighlightAll"> + <body><![CDATA[ + let word = this._findField.value; + // Bug 429723. Don't attempt to highlight "" + if (!this._highlightAll || !word) + return; + + this.browser.finder.highlight(true, word, + this._findMode == this.FIND_LINKS); + ]]></body> + </method> + + <!-- + - Updates the case-sensitivity mode of the findbar and its UI. + - @param [optional] aString + - The string for which case sensitivity might be turned on. + - This only used when case-sensitivity is in auto mode, + - @see _shouldBeCaseSensitive. The default value for this + - parameter is the find-field value. + --> + <method name="_updateCaseSensitivity"> + <parameter name="aString"/> + <body><![CDATA[ + let val = aString || this._findField.value; + + let caseSensitive = this._shouldBeCaseSensitive(val); + let checkbox = this.getElement("find-case-sensitive"); + let statusLabel = this.getElement("match-case-status"); + checkbox.checked = caseSensitive; + + statusLabel.value = caseSensitive ? this._caseSensitiveStr : ""; + + // Show the checkbox on the full Find bar in non-auto mode. + // Show the label in all other cases. + let hideCheckbox = this._findMode != this.FIND_NORMAL || + (this._typeAheadCaseSensitive != 0 && + this._typeAheadCaseSensitive != 1); + checkbox.hidden = hideCheckbox; + statusLabel.hidden = !hideCheckbox; + + this.browser.finder.caseSensitive = caseSensitive; + ]]></body> + </method> + + <!-- + - Sets the findbar case-sensitivity mode + - @param aCaseSensitivity (int) + - 0 - case insensitive + - 1 - case sensitive + - 2 - auto = case sensitive iff match string contains upper case letters + - @see _shouldBeCaseSensitive + --> + <method name="_setCaseSensitivity"> + <parameter name="aCaseSensitivity"/> + <body><![CDATA[ + this._typeAheadCaseSensitive = aCaseSensitivity; + this._updateCaseSensitivity(); + this._findFailedString = null; + this._find(); + + this._dispatchFindEvent("casesensitivitychange"); + ]]></body> + </method> + + <!-- + - Updates the entire-word mode of the findbar and its UI. + --> + <method name="_setEntireWord"> + <body><![CDATA[ + let entireWord = this._entireWord; + let checkbox = this.getElement("find-entire-word"); + let statusLabel = this.getElement("entire-word-status"); + checkbox.checked = entireWord; + + statusLabel.value = entireWord ? this._entireWordStr : ""; + + // Show the checkbox on the full Find bar in non-auto mode. + // Show the label in all other cases. + let hideCheckbox = this._findMode != this.FIND_NORMAL; + checkbox.hidden = hideCheckbox; + statusLabel.hidden = !hideCheckbox; + + this.browser.finder.entireWord = entireWord; + ]]></body> + </method> + + <!-- + - Sets the findbar entire-word mode + - @param aEntireWord (boolean) + - Whether or not entire-word mode should be turned on. + --> + <method name="toggleEntireWord"> + <parameter name="aEntireWord"/> + <parameter name="aFromPrefObserver"/> + <body><![CDATA[ + if (!aFromPrefObserver) { + // Just set the pref; our observer will change the find bar behavior. + this._prefsvc.setBoolPref("findbar.entireword", aEntireWord); + return; + } + + this._findFailedString = null; + this._find(); + ]]></body> + </method> + + <field name="_strBundle">null</field> + <property name="strBundle"> + <getter><![CDATA[ + if (!this._strBundle) { + this._strBundle = + Components.classes["@mozilla.org/intl/stringbundle;1"] + .getService(Components.interfaces.nsIStringBundleService) + .createBundle("chrome://global/locale/findbar.properties"); + } + return this._strBundle; + ]]></getter> + </property> + + <!-- + - Opens and displays the find bar. + - + - @param aMode + - the find mode to be used, which is either FIND_NORMAL, + - FIND_TYPEAHEAD or FIND_LINKS. If not passed, the last + - find mode if any or FIND_NORMAL. + - @returns true if the find bar wasn't previously open, false otherwise. + --> + <method name="open"> + <parameter name="aMode"/> + <body><![CDATA[ + if (aMode != undefined) + this._findMode = aMode; + + if (!this._notFoundStr) { + var stringsBundle = this.strBundle; + this._notFoundStr = stringsBundle.GetStringFromName("NotFound"); + this._wrappedToTopStr = + stringsBundle.GetStringFromName("WrappedToTop"); + this._wrappedToBottomStr = + stringsBundle.GetStringFromName("WrappedToBottom"); + this._normalFindStr = + stringsBundle.GetStringFromName("NormalFind"); + this._fastFindStr = + stringsBundle.GetStringFromName("FastFind"); + this._fastFindLinksStr = + stringsBundle.GetStringFromName("FastFindLinks"); + this._caseSensitiveStr = + stringsBundle.GetStringFromName("CaseSensitive"); + this._entireWordStr = + stringsBundle.GetStringFromName("EntireWord"); + } + + this._findFailedString = null; + + this._updateFindUI(); + if (this.hidden) { + this.removeAttribute("noanim"); + this.hidden = false; + + this._updateStatusUI(this.nsITypeAheadFind.FIND_FOUND); + + let event = document.createEvent("Events"); + event.initEvent("findbaropen", true, false); + this.dispatchEvent(event); + + this.browser.finder.onFindbarOpen(); + + return true; + } + return false; + ]]></body> + </method> + + <!-- + - Closes the findbar. + --> + <method name="close"> + <parameter name="aNoAnim"/> + <body><![CDATA[ + if (this.hidden) + return; + + if (aNoAnim) + this.setAttribute("noanim", true); + this.hidden = true; + + // 'focusContent()' iterates over all listeners in the chrome + // process, so we need to call it from here. + this.browser.finder.focusContent(); + this.browser.finder.onFindbarClose(); + + this._cancelTimers(); + + this._findFailedString = null; + ]]></body> + </method> + + <method name="clear"> + <body><![CDATA[ + this.browser.finder.removeSelection(); + this._findField.reset(); + this.toggleHighlight(false); + this._updateStatusUI(); + this._enableFindButtons(false); + ]]></body> + </method> + + <method name="_dispatchKeypressEvent"> + <parameter name="aTarget"/> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!aTarget) + return; + + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent(aEvent.type, aEvent.bubbles, aEvent.cancelable, + aEvent.view, aEvent.ctrlKey, aEvent.altKey, + aEvent.shiftKey, aEvent.metaKey, aEvent.keyCode, + aEvent.charCode); + aTarget.dispatchEvent(event); + ]]></body> + </method> + + <field name="_xulBrowserWindow">null</field> + <method name="_updateStatusUIBar"> + <parameter name="aFoundURL"/> + <body><![CDATA[ + if (!this._xulBrowserWindow) { + try { + this._xulBrowserWindow = + window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsIDocShellTreeItem) + .treeOwner + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIXULWindow) + .XULBrowserWindow; + } + catch (ex) { } + if (!this._xulBrowserWindow) + return false; + } + + // Call this has the same effect like hovering over link, + // the browser shows the URL as a tooltip. + this._xulBrowserWindow.setOverLink(aFoundURL || "", null); + return true; + ]]></body> + </method> + + <method name="_finishFAYT"> + <parameter name="aKeypressEvent"/> + <body><![CDATA[ + this.browser.finder.focusContent(); + + if (aKeypressEvent) + aKeypressEvent.preventDefault(); + + this.browser.finder.keyPress(aKeypressEvent); + + this.close(); + return true; + ]]></body> + </method> + + <method name="_shouldBeCaseSensitive"> + <parameter name="aString"/> + <body><![CDATA[ + if (this._typeAheadCaseSensitive == 0) + return false; + if (this._typeAheadCaseSensitive == 1) + return true; + + return aString != aString.toLowerCase(); + ]]></body> + </method> + + <!-- We get a fake event object through an IPC message which contains the + data we need to make a decision. We then return |true| if and only if + the page gets to deal with the event itself. Everywhere we return + false, the message sender will take care of calling event.preventDefault + on the real event. --> + <method name="_onBrowserKeypress"> + <parameter name="aFakeEvent"/> + <parameter name="aShouldFastFind"/> + <body><![CDATA[ + const FAYT_LINKS_KEY = "'"; + const FAYT_TEXT_KEY = "/"; + + // Fast keypresses can stack up when the content process is slow or + // hangs when in e10s mode. We make sure the findbar isn't 'opened' + // several times in a row, because then the find query is selected + // each time, losing characters typed initially. + let inputField = this._findField.inputField; + if (!this.hidden && document.activeElement == inputField) { + this._dispatchKeypressEvent(inputField, aFakeEvent); + return false; + } + + if (this._findMode != this.FIND_NORMAL && this._quickFindTimeout) { + if (!aFakeEvent.charCode) + return true; + + this._findField.select(); + this._findField.focus(); + this._dispatchKeypressEvent(this._findField.inputField, aFakeEvent); + return false; + } + + if (!aShouldFastFind) + return true; + + let key = aFakeEvent.charCode ? String.fromCharCode(aFakeEvent.charCode) : null; + let manualstartFAYT = (key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY); + let autostartFAYT = !manualstartFAYT && this._findAsYouType && + key && key != " "; + if (manualstartFAYT || autostartFAYT) { + let mode = (key == FAYT_LINKS_KEY || + (autostartFAYT && this._typeAheadLinksOnly)) ? + this.FIND_LINKS : this.FIND_TYPEAHEAD; + + // Clear bar first, so that when openFindBar() calls setCaseSensitivity() + // it doesn't get confused by a lingering value + this._findField.value = ""; + + this.open(mode); + this._setFindCloseTimeout(); + this._findField.select(); + this._findField.focus(); + + if (autostartFAYT) + this._dispatchKeypressEvent(this._findField.inputField, aFakeEvent); + else + this._updateStatusUI(this.nsITypeAheadFind.FIND_FOUND); + + return false; + } + return undefined; + ]]></body> + </method> + + <!-- See nsIMessageListener --> + <method name="receiveMessage"> + <parameter name="aMessage"/> + <body><![CDATA[ + if (aMessage.target != this._browser) { + return undefined; + } + switch (aMessage.name) { + case "Findbar:Mouseup": + if (!this.hidden && this._findMode != this.FIND_NORMAL) + this.close(); + break; + + case "Findbar:Keypress": + return this._onBrowserKeypress(aMessage.data.fakeEvent, + aMessage.data.shouldFastFind); + } + return undefined; + ]]></body> + </method> + + <method name="_updateBrowserWithState"> + <body><![CDATA[ + if (this._browser && this._browser.messageManager) { + this._browser.messageManager.sendAsyncMessage("Findbar:UpdateState", { + findMode: this._findMode + }); + } + ]]></body> + </method> + + <method name="_enableFindButtons"> + <parameter name="aEnable"/> + <body><![CDATA[ + this.getElement("find-next").disabled = + this.getElement("find-previous").disabled = !aEnable; + ]]></body> + </method> + + <!-- + - Determines whether minimalist or general-purpose search UI is to be + - displayed when the find bar is activated. + --> + <method name="_updateFindUI"> + <body><![CDATA[ + let showMinimalUI = this._findMode != this.FIND_NORMAL; + + let nodes = this.getElement("findbar-container").childNodes; + let wrapper = this.getElement("findbar-textbox-wrapper"); + let foundMatches = this._foundMatches; + for (let node of nodes) { + if (node == wrapper || node == foundMatches) + continue; + node.hidden = showMinimalUI; + } + this.getElement("find-next").hidden = + this.getElement("find-previous").hidden = showMinimalUI; + foundMatches.hidden = showMinimalUI || !foundMatches.value; + this._updateCaseSensitivity(); + this._setEntireWord(); + this._setHighlightAll(); + + if (showMinimalUI) + this._findField.classList.add("minimal"); + else + this._findField.classList.remove("minimal"); + + if (this._findMode == this.FIND_TYPEAHEAD) + this._findField.placeholder = this._fastFindStr; + else if (this._findMode == this.FIND_LINKS) + this._findField.placeholder = this._fastFindLinksStr; + else + this._findField.placeholder = this._normalFindStr; + ]]></body> + </method> + + <method name="_find"> + <parameter name="aValue"/> + <body><![CDATA[ + if (!this._dispatchFindEvent("")) + return; + + let val = aValue || this._findField.value; + + // We have to carry around an explicit version of this, + // because finder.searchString doesn't update on failed + // searches. + this.browser._lastSearchString = val; + + // Only search on input if we don't have a last-failed string, + // or if the current search string doesn't start with it. + // In entire-word mode we always attemp a find; since sequential matching + // is not guaranteed, the first character typed may not be a word (no + // match), but the with the second character it may well be a word, + // thus a match. + if (!this._findFailedString || + !val.startsWith(this._findFailedString) || + this._entireWord) { + // Getting here means the user commanded a find op. Make sure any + // initial prefilling is ignored if it hasn't happened yet. + if (this._startFindDeferred) { + this._startFindDeferred.resolve(); + this._startFindDeferred = null; + } + + this._enableFindButtons(val); + this._updateCaseSensitivity(val); + this._setEntireWord(); + + this.browser.finder.fastFind(val, this._findMode == this.FIND_LINKS, + this._findMode != this.FIND_NORMAL); + } + + if (this._findMode != this.FIND_NORMAL) + this._setFindCloseTimeout(); + + if (this._findResetTimeout != -1) + clearTimeout(this._findResetTimeout); + + // allow a search to happen on input again after a second has + // expired since the previous input, to allow for dynamic + // content and/or page loading + this._findResetTimeout = setTimeout(() => { + this._findFailedString = null; + this._findResetTimeout = -1; + }, 1000); + ]]></body> + </method> + + <method name="_flash"> + <body><![CDATA[ + if (this._flashFindBarCount === undefined) + this._flashFindBarCount = this._initialFlashFindBarCount; + + if (this._flashFindBarCount-- == 0) { + clearInterval(this._flashFindBarTimeout); + this.removeAttribute("flash"); + this._flashFindBarCount = 6; + return; + } + + this.setAttribute("flash", + (this._flashFindBarCount % 2 == 0) ? + "false" : "true"); + ]]></body> + </method> + + <method name="_findAgain"> + <parameter name="aFindPrevious"/> + <body><![CDATA[ + this.browser.finder.findAgain(aFindPrevious, + this._findMode == this.FIND_LINKS, + this._findMode != this.FIND_NORMAL); + ]]></body> + </method> + + <method name="_updateStatusUI"> + <parameter name="res"/> + <parameter name="aFindPrevious"/> + <body><![CDATA[ + switch (res) { + case this.nsITypeAheadFind.FIND_WRAPPED: + this._findStatusIcon.setAttribute("status", "wrapped"); + this._findStatusDesc.textContent = + aFindPrevious ? this._wrappedToBottomStr : this._wrappedToTopStr; + this._findField.removeAttribute("status"); + break; + case this.nsITypeAheadFind.FIND_NOTFOUND: + this._findStatusIcon.setAttribute("status", "notfound"); + this._findStatusDesc.textContent = this._notFoundStr; + this._findField.setAttribute("status", "notfound"); + break; + case this.nsITypeAheadFind.FIND_PENDING: + this._findStatusIcon.setAttribute("status", "pending"); + this._findStatusDesc.textContent = ""; + this._findField.removeAttribute("status"); + break; + case this.nsITypeAheadFind.FIND_FOUND: + default: + this._findStatusIcon.removeAttribute("status"); + this._findStatusDesc.textContent = ""; + this._findField.removeAttribute("status"); + break; + } + ]]></body> + </method> + + <method name="updateControlState"> + <parameter name="aResult"/> + <parameter name="aFindPrevious"/> + <body><![CDATA[ + this._updateStatusUI(aResult, aFindPrevious); + this._enableFindButtons(aResult !== this.nsITypeAheadFind.FIND_NOTFOUND); + ]]></body> + </method> + + <method name="_dispatchFindEvent"> + <parameter name="aType"/> + <parameter name="aFindPrevious"/> + <body><![CDATA[ + let event = document.createEvent("CustomEvent"); + event.initCustomEvent("find" + aType, true, true, { + query: this._findField.value, + caseSensitive: !!this._typeAheadCaseSensitive, + entireWord: this._entireWord, + highlightAll: this._highlightAll, + findPrevious: aFindPrevious + }); + return this.dispatchEvent(event); + ]]></body> + </method> + + + <!-- + - Opens the findbar, focuses the findfield and selects its contents. + - Also flashes the findbar the first time it's used. + - @param aMode + - the find mode to be used, which is either FIND_NORMAL, + - FIND_TYPEAHEAD or FIND_LINKS. If not passed, the last + - find mode if any or FIND_NORMAL. + --> + <method name="startFind"> + <parameter name="aMode"/> + <body><![CDATA[ + let prefsvc = this._prefsvc; + let userWantsPrefill = true; + this.open(aMode); + + if (this._flashFindBar) { + this._flashFindBarTimeout = setInterval(() => this._flash(), 500); + prefsvc.setIntPref("accessibility.typeaheadfind.flashBar", + --this._flashFindBar); + } + + let {PromiseUtils} = + Components.utils.import("resource://gre/modules/PromiseUtils.jsm", {}); + this._startFindDeferred = PromiseUtils.defer(); + let startFindPromise = this._startFindDeferred.promise; + + if (this.prefillWithSelection) + userWantsPrefill = + prefsvc.getBoolPref("accessibility.typeaheadfind.prefillwithselection"); + + if (this.prefillWithSelection && userWantsPrefill) { + // NB: We have to focus this._findField here so tests that send + // key events can open and close the find bar synchronously. + this._findField.focus(); + + // (e10s) since we focus lets also select it, otherwise that would + // only happen in this.onCurrentSelection and, because it is async, + // there's a chance keypresses could come inbetween, leading to + // jumbled up queries. + this._findField.select(); + + this.browser.finder.getInitialSelection(); + return startFindPromise; + } + + // If userWantsPrefill is false but prefillWithSelection is true, + // then we might need to check the selection clipboard. Call + // onCurrentSelection to do so. + // Note: this.onCurrentSelection clears this._startFindDeferred. + this.onCurrentSelection("", true); + return startFindPromise; + ]]></body> + </method> + + <!-- + - Convenient alias to startFind(gFindBar.FIND_NORMAL); + - + - You should generally map the window's find command to this method. + - e.g. <command name="cmd_find" oncommand="gFindBar.onFindCommand();"/> + --> + <method name="onFindCommand"> + <body><![CDATA[ + return this.startFind(this.FIND_NORMAL); + ]]></body> + </method> + + <!-- + - Stub for find-next and find-previous commands + - @param aFindPrevious + - true for find-previous, false otherwise. + --> + <method name="onFindAgainCommand"> + <parameter name="aFindPrevious"/> + <body><![CDATA[ + let findString = this._browser.finder.searchString || this._findField.value; + if (!findString) + return this.startFind(); + + // We dispatch the findAgain event here instead of in _findAgain since + // if there is a find event handler that prevents the default then + // finder.searchString will never get updated which in turn means + // there would never be findAgain events because of the logic below. + if (!this._dispatchFindEvent("again", aFindPrevious)) + return undefined; + + // user explicitly requested another search, so do it even if we think it'll fail + this._findFailedString = null; + + // Ensure the stored SearchString is in sync with what we want to find + if (this._findField.value != this._browser.finder.searchString) { + this._find(this._findField.value); + } else { + this._findAgain(aFindPrevious); + if (this._useModalHighlight) { + this.open(); + this._findField.select(); + this._findField.focus(); + } + } + + return undefined; + ]]></body> + </method> + +#ifdef XP_MACOSX + <!-- + - Fetches the currently selected text and sets that as the text to search + - next. This is a MacOS specific feature. + --> + <method name="onFindSelectionCommand"> + <body><![CDATA[ + let searchString = this.browser.finder.setSearchStringToSelection(); + if (searchString) + this._findField.value = searchString; + ]]></body> + </method> + + <method name="_onFindFieldFocus"> + <body><![CDATA[ + let prefsvc = this._prefsvc; + const kPref = "accessibility.typeaheadfind.prefillwithselection"; + if (this.prefillWithSelection && prefsvc.getBoolPref(kPref)) + return; + + let clipboardSearchString = this._browser.finder.clipboardSearchString; + if (clipboardSearchString && this._findField.value != clipboardSearchString && + !this._findField._willfullyDeleted) { + this._findField.value = clipboardSearchString; + this._findField._hadValue = true; + // Changing the search string makes the previous status invalid, so + // we better clear it here. + this._updateStatusUI(); + } + ]]></body> + </method> +#endif + + <!-- + - This handles all the result changes for both + - type-ahead-find and highlighting. + - @param aResult + - One of the nsITypeAheadFind.FIND_* constants + - indicating the result of a search operation. + - @param aFindBackwards + - If the search was done from the bottom to + - the top. This is used for right error messages + - when reaching "the end of the page". + - @param aLinkURL + - When a link matched then its URK. Always null + - when not in FIND_LINKS mode. + --> + <method name="onFindResult"> + <parameter name="aData"/> + <body><![CDATA[ + if (aData.result == this.nsITypeAheadFind.FIND_NOTFOUND) { + // If an explicit Find Again command fails, re-open the toolbar. + if (aData.storeResult && this.open()) { + this._findField.select(); + this._findField.focus(); + } + this._findFailedString = aData.searchString; + } else { + this._findFailedString = null; + } + + this._updateStatusUI(aData.result, aData.findBackwards); + this._updateStatusUIBar(aData.linkURL); + + if (this._findMode != this.FIND_NORMAL) + this._setFindCloseTimeout(); + ]]></body> + </method> + + <!-- + - This handles all the result changes for matches counts. + - @param aResult + - Result Object, containing the total amount of matches and a vector + - of the current result. + --> + <method name="onMatchesCountResult"> + <parameter name="aResult"/> + <body><![CDATA[ + if (aResult.total !== 0) { + if (aResult.total == -1) { + this._foundMatches.value = this.pluralForm.get( + aResult.limit, + this.strBundle.GetStringFromName("FoundMatchesCountLimit") + ).replace("#1", aResult.limit); + } else { + this._foundMatches.value = this.pluralForm.get( + aResult.total, + this.strBundle.GetStringFromName("FoundMatches") + ).replace("#1", aResult.current) + .replace("#2", aResult.total); + } + this._foundMatches.hidden = false; + } else { + this._foundMatches.hidden = true; + this._foundMatches.value = ""; + } + ]]></body> + </method> + + <method name="onHighlightFinished"> + <parameter name="result"/> + <body><![CDATA[ + // Noop. + ]]></body> + </method> + + <method name="onCurrentSelection"> + <parameter name="aSelectionString" /> + <parameter name="aIsInitialSelection" /> + <body><![CDATA[ + // Ignore the prefill if the user has already typed in the findbar, + // it would have been overwritten anyway. See bug 1198465. + if (aIsInitialSelection && !this._startFindDeferred) + return; + + if (/Mac/.test(navigator.platform) && aIsInitialSelection && !aSelectionString) { + let clipboardSearchString = this.browser.finder.clipboardSearchString; + if (clipboardSearchString) + aSelectionString = clipboardSearchString; + } + + if (aSelectionString) + this._findField.value = aSelectionString; + + if (aIsInitialSelection) { + this._enableFindButtons(!!this._findField.value); + this._findField.select(); + this._findField.focus(); + + this._startFindDeferred.resolve(); + this._startFindDeferred = null; + } + ]]></body> + </method> + + <!-- + - This handler may cancel a request to focus content by returning |false| + - explicitly. + --> + <method name="shouldFocusContent"> + <body><![CDATA[ + const fm = Components.classes["@mozilla.org/focus-manager;1"] + .getService(Components.interfaces.nsIFocusManager); + if (fm.focusedWindow != window) + return false; + + let focusedElement = fm.focusedElement; + if (!focusedElement) + return false; + + let bindingParent = document.getBindingParent(focusedElement); + if (bindingParent != this && bindingParent != this._findField) + return false; + + return true; + ]]></body> + </method> + + </implementation> + + <handlers> + <!-- + - We have to guard against `this.close` being |null| due to an unknown + - issue, which is tracked in bug 957999. + --> + <handler event="keypress" keycode="VK_ESCAPE" phase="capturing" + action="if (this.close) this.close();" preventdefault="true"/> + </handlers> + </binding> +</bindings> diff --git a/toolkit/content/widgets/general.xml b/toolkit/content/widgets/general.xml new file mode 100644 index 0000000000..b4538e41d1 --- /dev/null +++ b/toolkit/content/widgets/general.xml @@ -0,0 +1,231 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="generalBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="basecontrol"> + <implementation implements="nsIDOMXULControlElement"> + <!-- public implementation --> + <property name="disabled" onset="if (val) this.setAttribute('disabled', 'true'); + else this.removeAttribute('disabled'); + return val;" + onget="return this.getAttribute('disabled') == 'true';"/> + <property name="tabIndex" onget="return parseInt(this.getAttribute('tabindex')) || 0" + onset="if (val) this.setAttribute('tabindex', val); + else this.removeAttribute('tabindex'); return val;"/> + </implementation> + </binding> + + <binding id="basetext" extends="chrome://global/content/bindings/general.xml#basecontrol"> + <implementation implements="nsIDOMXULLabeledControlElement"> + <!-- public implementation --> + <property name="label" onset="this.setAttribute('label',val); return val;" + onget="return this.getAttribute('label');"/> + <property name="crop" onset="this.setAttribute('crop',val); return val;" + onget="return this.getAttribute('crop');"/> + <property name="image" onset="this.setAttribute('image',val); return val;" + onget="return this.getAttribute('image');"/> + <property name="command" onset="this.setAttribute('command',val); return val;" + onget="return this.getAttribute('command');"/> + <property name="accessKey"> + <getter> + <![CDATA[ + return this.labelElement ? this.labelElement.accessKey : this.getAttribute('accesskey'); + ]]> + </getter> + <setter> + <![CDATA[ + // Always store on the control + this.setAttribute('accesskey', val); + // If there is a label, change the accesskey on the labelElement + // if it's also set there + if (this.labelElement) { + this.labelElement.accessKey = val; + } + return val; + ]]> + </setter> + </property> + + <field name="labelElement"/> + </implementation> + </binding> + + <binding id="control-item" extends="chrome://global/content/bindings/general.xml#basetext"> + <implementation> + <property name="value" onset="this.setAttribute('value', val); return val;" + onget="return this.getAttribute('value');"/> + </implementation> + </binding> + + <binding id="root-element"> + <implementation> + <field name="_lightweightTheme">null</field> + <constructor><![CDATA[ + if (this.hasAttribute("lightweightthemes")) { + let temp = {}; + Components.utils.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp); + this._lightweightTheme = new temp.LightweightThemeConsumer(this.ownerDocument); + } + ]]></constructor> + <destructor><![CDATA[ + if (this._lightweightTheme) { + this._lightweightTheme.destroy(); + this._lightweightTheme = null; + } + ]]></destructor> + </implementation> + </binding> + + <binding id="iframe" role="outerdoc"> + <implementation> + <property name="docShell" readonly="true"> + <getter><![CDATA[ + let frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader; + return frameLoader ? frameLoader.docShell : null; + ]]></getter> + </property> + <property name="contentWindow" + readonly="true" + onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);"/> + <property name="webNavigation" + onget="return this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation);" + readonly="true"/> + <property name="contentDocument" readonly="true" + onget="return this.webNavigation.document;"/> + </implementation> + </binding> + + <binding id="statusbarpanel" display="xul:button"> + <content> + <children> + <xul:label class="statusbarpanel-text" xbl:inherits="value=label,crop" crop="right" flex="1"/> + </children> + </content> + + <implementation> + <property name="label" + onget="return this.getAttribute('label');" + onset="this.setAttribute('label',val); return val;"/> + <property name="image" + onget="return this.getAttribute('image');" + onset="this.setAttribute('image',val); return val;"/> + <property name="src" + onget="return this.getAttribute('src');" + onset="this.setAttribute('src',val); return val;"/> + </implementation> + </binding> + + <binding id="statusbarpanel-menu-iconic" display="xul:menu" + extends="chrome://global/content/bindings/general.xml#statusbarpanel"> + <content> + <xul:image class="statusbarpanel-icon" xbl:inherits="src,src=image"/> + <children/> + </content> + </binding> + + <binding id="statusbar" role="xul:statusbar"> + <content> + <children/> + <xul:statusbarpanel class="statusbar-resizerpanel"> + <xul:resizer dir="bottomend"/> + </xul:statusbarpanel> + </content> + </binding> + + <binding id="statusbarpanel-iconic" display="xul:button" role="xul:button" + extends="chrome://global/content/bindings/general.xml#statusbarpanel"> + <content> + <xul:image class="statusbarpanel-icon" xbl:inherits="src,src=image"/> + </content> + </binding> + + <binding id="statusbarpanel-iconic-text" display="xul:button" role="xul:button" + extends="chrome://global/content/bindings/general.xml#statusbarpanel"> + <content> + <xul:image class="statusbarpanel-icon" xbl:inherits="src,src=image"/> + <xul:label class="statusbarpanel-text" xbl:inherits="value=label,crop"/> + </content> + </binding> + + <binding id="image" role="xul:image"> + <implementation implements="nsIDOMXULImageElement"> + <property name="src" + onget="return this.getAttribute('src');" + onset="this.setAttribute('src',val); return val;"/> + </implementation> + </binding> + + <binding id="deck"> + <implementation> + <property name="selectedIndex" + onget="return this.getAttribute('selectedIndex') || '0'"> + <setter> + <![CDATA[ + if (this.selectedIndex == val) + return val; + this.setAttribute("selectedIndex", val); + var event = document.createEvent('Events'); + event.initEvent('select', true, true); + this.dispatchEvent(event); + return val; + ]]> + </setter> + </property> + + <property name="selectedPanel"> + <getter> + <![CDATA[ + return this.childNodes[this.selectedIndex]; + ]]> + </getter> + + <setter> + <![CDATA[ + var selectedIndex = -1; + for (var panel = val; panel != null; panel = panel.previousSibling) + ++selectedIndex; + this.selectedIndex = selectedIndex; + return val; + ]]> + </setter> + </property> + </implementation> + </binding> + + <binding id="dropmarker" extends="xul:button" role="xul:dropmarker"> + <resources> + <stylesheet src="chrome://global/skin/dropmarker.css"/> + </resources> + + <content> + <xul:image class="dropmarker-icon"/> + </content> + </binding> + + <binding id="windowdragbox"> + <implementation> + <field name="_dragBindingAlive">true</field> + <constructor> + if (!this._draggableStarted) { + this._draggableStarted = true; + try { + let tmp = {}; + Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp); + let draghandle = new tmp.WindowDraggingElement(this); + draghandle.mouseDownCheck = function () { + return this._dragBindingAlive; + }; + } catch (e) {} + } + </constructor> + </implementation> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/groupbox.xml b/toolkit/content/widgets/groupbox.xml new file mode 100644 index 0000000000..7cd3276b47 --- /dev/null +++ b/toolkit/content/widgets/groupbox.xml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="groupboxBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="groupbox-base"> + <resources> + <stylesheet src="chrome://global/skin/groupbox.css"/> + </resources> + </binding> + + <binding id="groupbox" role="xul:groupbox" + extends="chrome://global/content/bindings/groupbox.xml#groupbox-base"> + <content> + <xul:hbox class="groupbox-title" align="center" pack="start"> + <children includes="caption"/> + </xul:hbox> + <xul:box flex="1" class="groupbox-body" xbl:inherits="orient,align,pack"> + <children/> + </xul:box> + </content> + </binding> + + <binding id="caption" extends="chrome://global/content/bindings/general.xml#basetext"> + <resources> + <stylesheet src="chrome://global/skin/groupbox.css"/> + </resources> + + <content> + <children> + <xul:image class="caption-icon" xbl:inherits="src=image"/> + <xul:label class="caption-text" flex="1" + xbl:inherits="default,value=label,crop,accesskey"/> + </children> + </content> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/listbox.xml b/toolkit/content/widgets/listbox.xml new file mode 100644 index 0000000000..9fae616690 --- /dev/null +++ b/toolkit/content/widgets/listbox.xml @@ -0,0 +1,1144 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<bindings id="listboxBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <!-- + Interface binding that is base for bindings of xul:listbox and + xul:richlistbox elements. This binding assumes that successors bindings + will implement the following properties and methods: + + /** Return the number of items */ + readonly itemCount + + /** Return index of given item + * @param aItem - given item element */ + getIndexOfItem(aItem) + + /** Return item at given index + * @param aIndex - index of item element */ + getItemAtIndex(aIndex) + + /** Return count of item elements */ + getRowCount() + + /** Return count of visible item elements */ + getNumberOfVisibleRows() + + /** Return index of first visible item element */ + getIndexOfFirstVisibleRow() + + /** Return true if item of given index is visible + * @param aIndex - index of item element + * + * @note XXX: this method should be removed after bug 364612 is fixed + */ + ensureIndexIsVisible(aIndex) + + /** Return true if item element is visible + * @param aElement - given item element */ + ensureElementIsVisible(aElement) + + /** Scroll list control to make visible item of given index + * @param aIndex - index of item element + * + * @note XXX: this method should be removed after bug 364612 is fixed + */ + scrollToIndex(aIndex) + + /** Create item element and append it to the end of listbox + * @param aLabel - label of new item element + * @param aValue - value of new item element */ + appendItem(aLabel, aValue) + + /** Create item element and insert it to given position + * @param aIndex - insertion position + * @param aLabel - label of new item element + * @param aValue - value of new item element */ + insertItemAt(aIndex, aLabel, aValue) + + /** Scroll up/down one page + * @param aDirection - specifies scrolling direction, should be either -1 or 1 + * @return the number of elements the selection scrolled + */ + scrollOnePage(aDirection) + + /** Fire "select" event */ + _fireOnSelect() + --> + <binding id="listbox-base" role="xul:listbox" + extends="chrome://global/content/bindings/general.xml#basecontrol"> + + <implementation implements="nsIDOMXULMultiSelectControlElement"> + <field name="_lastKeyTime">0</field> + <field name="_incrementalString">""</field> + + <!-- nsIDOMXULSelectControlElement --> + <property name="selectedItem" + onset="this.selectItem(val);"> + <getter> + <![CDATA[ + return this.selectedItems.length > 0 ? this.selectedItems[0] : null; + ]]> + </getter> + </property> + + <property name="selectedIndex"> + <getter> + <![CDATA[ + if (this.selectedItems.length > 0) + return this.getIndexOfItem(this.selectedItems[0]); + return -1; + ]]> + </getter> + <setter> + <![CDATA[ + if (val >= 0) { + this.selectItem(this.getItemAtIndex(val)); + } else { + this.clearSelection(); + this.currentItem = null; + } + ]]> + </setter> + </property> + + <property name="value"> + <getter> + <![CDATA[ + if (this.selectedItems.length > 0) + return this.selectedItem.value; + return null; + ]]> + </getter> + <setter> + <![CDATA[ + var kids = this.getElementsByAttribute("value", val); + if (kids && kids.item(0)) + this.selectItem(kids[0]); + return val; + ]]> + </setter> + </property> + + <method name="removeItemAt"> + <parameter name="index"/> + <body> + <![CDATA[ + var remove = this.getItemAtIndex(index); + if (remove) + this.removeChild(remove); + return remove; + ]]> + </body> + </method> + + <!-- nsIDOMXULMultiSelectControlElement --> + <property name="selType" + onget="return this.getAttribute('seltype');" + onset="this.setAttribute('seltype', val); return val;"/> + + <property name="currentItem" onget="return this._currentItem;"> + <setter> + if (this._currentItem == val) + return val; + + if (this._currentItem) + this._currentItem.current = false; + this._currentItem = val; + + if (val) + val.current = true; + + return val; + </setter> + </property> + + <property name="currentIndex"> + <getter> + return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1; + </getter> + <setter> + <![CDATA[ + if (val >= 0) + this.currentItem = this.getItemAtIndex(val); + else + this.currentItem = null; + ]]> + </setter> + </property> + + <field name="selectedItems">new ChromeNodeList()</field> + + <method name="addItemToSelection"> + <parameter name="aItem"/> + <body> + <![CDATA[ + if (this.selType != "multiple" && this.selectedCount) + return; + + if (aItem.selected) + return; + + this.selectedItems.append(aItem); + aItem.selected = true; + + this._fireOnSelect(); + ]]> + </body> + </method> + + <method name="removeItemFromSelection"> + <parameter name="aItem"/> + <body> + <![CDATA[ + if (!aItem.selected) + return; + + this.selectedItems.remove(aItem); + aItem.selected = false; + this._fireOnSelect(); + ]]> + </body> + </method> + + <method name="toggleItemSelection"> + <parameter name="aItem"/> + <body> + <![CDATA[ + if (aItem.selected) + this.removeItemFromSelection(aItem); + else + this.addItemToSelection(aItem); + ]]> + </body> + </method> + + <method name="selectItem"> + <parameter name="aItem"/> + <body> + <![CDATA[ + if (!aItem) + return; + + if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem) + return; + + this._selectionStart = null; + + var suppress = this._suppressOnSelect; + this._suppressOnSelect = true; + + this.clearSelection(); + this.addItemToSelection(aItem); + this.currentItem = aItem; + + this._suppressOnSelect = suppress; + this._fireOnSelect(); + ]]> + </body> + </method> + + <method name="selectItemRange"> + <parameter name="aStartItem"/> + <parameter name="aEndItem"/> + <body> + <![CDATA[ + if (this.selType != "multiple") + return; + + if (!aStartItem) + aStartItem = this._selectionStart ? + this._selectionStart : this.currentItem; + + if (!aStartItem) + aStartItem = aEndItem; + + var suppressSelect = this._suppressOnSelect; + this._suppressOnSelect = true; + + this._selectionStart = aStartItem; + + var currentItem; + var startIndex = this.getIndexOfItem(aStartItem); + var endIndex = this.getIndexOfItem(aEndItem); + if (endIndex < startIndex) { + currentItem = aEndItem; + aEndItem = aStartItem; + aStartItem = currentItem; + } else { + currentItem = aStartItem; + } + + while (currentItem) { + this.addItemToSelection(currentItem); + if (currentItem == aEndItem) { + currentItem = this.getNextItem(currentItem, 1); + break; + } + currentItem = this.getNextItem(currentItem, 1); + } + + // Clear around new selection + // Don't use clearSelection() because it causes a lot of noise + // with respect to selection removed notifications used by the + // accessibility API support. + var userSelecting = this._userSelecting; + this._userSelecting = false; // that's US automatically unselecting + for (; currentItem; currentItem = this.getNextItem(currentItem, 1)) + this.removeItemFromSelection(currentItem); + + for (currentItem = this.getItemAtIndex(0); currentItem != aStartItem; + currentItem = this.getNextItem(currentItem, 1)) + this.removeItemFromSelection(currentItem); + this._userSelecting = userSelecting; + + this._suppressOnSelect = suppressSelect; + + this._fireOnSelect(); + ]]> + </body> + </method> + + <method name="selectAll"> + <body> + this._selectionStart = null; + + var suppress = this._suppressOnSelect; + this._suppressOnSelect = true; + + var item = this.getItemAtIndex(0); + while (item) { + this.addItemToSelection(item); + item = this.getNextItem(item, 1); + } + + this._suppressOnSelect = suppress; + this._fireOnSelect(); + </body> + </method> + + <method name="invertSelection"> + <body> + this._selectionStart = null; + + var suppress = this._suppressOnSelect; + this._suppressOnSelect = true; + + var item = this.getItemAtIndex(0); + while (item) { + if (item.selected) + this.removeItemFromSelection(item); + else + this.addItemToSelection(item); + item = this.getNextItem(item, 1); + } + + this._suppressOnSelect = suppress; + this._fireOnSelect(); + </body> + </method> + + <method name="clearSelection"> + <body> + <![CDATA[ + if (this.selectedItems) { + while (this.selectedItems.length > 0) { + let item = this.selectedItems[0]; + item.selected = false; + this.selectedItems.remove(item); + } + } + + this._selectionStart = null; + this._fireOnSelect(); + ]]> + </body> + </method> + + <property name="selectedCount" readonly="true" + onget="return this.selectedItems.length;"/> + + <method name="getSelectedItem"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + return aIndex < this.selectedItems.length ? + this.selectedItems[aIndex] : null; + ]]> + </body> + </method> + + <!-- Other public members --> + <property name="disableKeyNavigation" + onget="return this.hasAttribute('disableKeyNavigation');"> + <setter> + if (val) + this.setAttribute("disableKeyNavigation", "true"); + else + this.removeAttribute("disableKeyNavigation"); + return val; + </setter> + </property> + + <property name="suppressOnSelect" + onget="return this.getAttribute('suppressonselect') == 'true';" + onset="this.setAttribute('suppressonselect', val);"/> + + <property name="_selectDelay" + onset="this.setAttribute('_selectDelay', val);" + onget="return this.getAttribute('_selectDelay') || 50;"/> + + <method name="timedSelect"> + <parameter name="aItem"/> + <parameter name="aTimeout"/> + <body> + <![CDATA[ + var suppress = this._suppressOnSelect; + if (aTimeout != -1) + this._suppressOnSelect = true; + + this.selectItem(aItem); + + this._suppressOnSelect = suppress; + + if (aTimeout != -1) { + if (this._selectTimeout) + window.clearTimeout(this._selectTimeout); + this._selectTimeout = + window.setTimeout(this._selectTimeoutHandler, aTimeout, this); + } + ]]> + </body> + </method> + + <method name="moveByOffset"> + <parameter name="aOffset"/> + <parameter name="aIsSelecting"/> + <parameter name="aIsSelectingRange"/> + <body> + <![CDATA[ + if ((aIsSelectingRange || !aIsSelecting) && + this.selType != "multiple") + return; + + var newIndex = this.currentIndex + aOffset; + if (newIndex < 0) + newIndex = 0; + + var numItems = this.getRowCount(); + if (newIndex > numItems - 1) + newIndex = numItems - 1; + + var newItem = this.getItemAtIndex(newIndex); + // make sure that the item is actually visible/selectable + if (this._userSelecting && newItem && !this._canUserSelect(newItem)) + newItem = + aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) : + this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1); + if (newItem) { + this.ensureIndexIsVisible(this.getIndexOfItem(newItem)); + if (aIsSelectingRange) + this.selectItemRange(null, newItem); + else if (aIsSelecting) + this.selectItem(newItem); + + this.currentItem = newItem; + } + ]]> + </body> + </method> + + <!-- Private --> + <method name="getNextItem"> + <parameter name="aStartItem"/> + <parameter name="aDelta"/> + <body> + <![CDATA[ + while (aStartItem) { + aStartItem = aStartItem.nextSibling; + if (aStartItem && aStartItem instanceof + Components.interfaces.nsIDOMXULSelectControlItemElement && + (!this._userSelecting || this._canUserSelect(aStartItem))) { + --aDelta; + if (aDelta == 0) + return aStartItem; + } + } + return null; + ]]></body> + </method> + + <method name="getPreviousItem"> + <parameter name="aStartItem"/> + <parameter name="aDelta"/> + <body> + <![CDATA[ + while (aStartItem) { + aStartItem = aStartItem.previousSibling; + if (aStartItem && aStartItem instanceof + Components.interfaces.nsIDOMXULSelectControlItemElement && + (!this._userSelecting || this._canUserSelect(aStartItem))) { + --aDelta; + if (aDelta == 0) + return aStartItem; + } + } + return null; + ]]> + </body> + </method> + + <method name="_moveByOffsetFromUserEvent"> + <parameter name="aOffset"/> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (!aEvent.defaultPrevented) { + this._userSelecting = true; + this._mayReverse = true; + this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey); + this._userSelecting = false; + this._mayReverse = false; + aEvent.preventDefault(); + } + ]]> + </body> + </method> + + <method name="_canUserSelect"> + <parameter name="aItem"/> + <body> + <![CDATA[ + var style = document.defaultView.getComputedStyle(aItem, ""); + return style.display != "none" && style.visibility == "visible"; + ]]> + </body> + </method> + + <method name="_selectTimeoutHandler"> + <parameter name="aMe"/> + <body> + aMe._fireOnSelect(); + aMe._selectTimeout = null; + </body> + </method> + + <field name="_suppressOnSelect">false</field> + <field name="_userSelecting">false</field> + <field name="_mayReverse">false</field> + <field name="_selectTimeout">null</field> + <field name="_currentItem">null</field> + <field name="_selectionStart">null</field> + </implementation> + + <handlers> + <handler event="keypress" keycode="VK_UP" modifiers="control shift any" + action="this._moveByOffsetFromUserEvent(-1, event);" + group="system"/> + <handler event="keypress" keycode="VK_DOWN" modifiers="control shift any" + action="this._moveByOffsetFromUserEvent(1, event);" + group="system"/> + <handler event="keypress" keycode="VK_HOME" modifiers="control shift any" + group="system"> + <![CDATA[ + this._mayReverse = true; + this._moveByOffsetFromUserEvent(-this.currentIndex, event); + this._mayReverse = false; + ]]> + </handler> + <handler event="keypress" keycode="VK_END" modifiers="control shift any" + group="system"> + <![CDATA[ + this._mayReverse = true; + this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event); + this._mayReverse = false; + ]]> + </handler> + <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any" + group="system"> + <![CDATA[ + this._mayReverse = true; + this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event); + this._mayReverse = false; + ]]> + </handler> + <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any" + group="system"> + <![CDATA[ + this._mayReverse = true; + this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event); + this._mayReverse = false; + ]]> + </handler> + <handler event="keypress" key=" " modifiers="control" phase="target"> + <![CDATA[ + if (this.currentItem && this.selType == "multiple") + this.toggleItemSelection(this.currentItem); + ]]> + </handler> + <handler event="focus"> + <![CDATA[ + if (this.getRowCount() > 0) { + if (this.currentIndex == -1) { + this.currentIndex = this.getIndexOfFirstVisibleRow(); + } + else { + this.currentItem._fireEvent("DOMMenuItemActive"); + } + } + this._lastKeyTime = 0; + ]]> + </handler> + <handler event="keypress" phase="target"> + <![CDATA[ + if (this.disableKeyNavigation || !event.charCode || + event.altKey || event.ctrlKey || event.metaKey) + return; + + if (event.timeStamp - this._lastKeyTime > 1000) + this._incrementalString = ""; + + var key = String.fromCharCode(event.charCode).toLowerCase(); + this._incrementalString += key; + this._lastKeyTime = event.timeStamp; + + // If all letters in the incremental string are the same, just + // try to match the first one + var incrementalString = /^(.)\1+$/.test(this._incrementalString) ? + RegExp.$1 : this._incrementalString; + var length = incrementalString.length; + + var rowCount = this.getRowCount(); + var l = this.selectedItems.length; + var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1; + // start from the first element if none was selected or from the one + // following the selected one if it's a new or a repeated-letter search + if (start == -1 || length == 1) + start++; + + for (var i = 0; i < rowCount; i++) { + var k = (start + i) % rowCount; + var listitem = this.getItemAtIndex(k); + if (!this._canUserSelect(listitem)) + continue; + // allow richlistitems to specify the string being searched for + var searchText = "searchLabel" in listitem ? listitem.searchLabel : + listitem.getAttribute("label"); // (see also bug 250123) + searchText = searchText.substring(0, length).toLowerCase(); + if (searchText == incrementalString) { + this.ensureIndexIsVisible(k); + this.timedSelect(listitem, this._selectDelay); + break; + } + } + ]]> + </handler> + </handlers> + </binding> + + + <!-- Binding for xul:listbox element. + --> + <binding id="listbox" + extends="#listbox-base"> + + <resources> + <stylesheet src="chrome://global/skin/listbox.css"/> + </resources> + + <content> + <children includes="listcols"> + <xul:listcols> + <xul:listcol flex="1"/> + </xul:listcols> + </children> + <xul:listrows> + <children includes="listhead"/> + <xul:listboxbody xbl:inherits="rows,size,minheight"> + <children includes="listitem"/> + </xul:listboxbody> + </xul:listrows> + </content> + + <implementation> + + <!-- ///////////////// public listbox members ///////////////// --> + + <property name="listBoxObject" + onget="return this.boxObject;" + readonly="true"/> + + <!-- ///////////////// private listbox members ///////////////// --> + + <method name="_fireOnSelect"> + <body> + <![CDATA[ + if (!this._suppressOnSelect && !this.suppressOnSelect) { + var event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + } + ]]> + </body> + </method> + + <constructor> + <![CDATA[ + var count = this.itemCount; + for (var index = 0; index < count; index++) { + var item = this.getItemAtIndex(index); + if (item.getAttribute("selected") == "true") + this.selectedItems.append(item); + } + ]]> + </constructor> + + <!-- ///////////////// nsIDOMXULSelectControlElement ///////////////// --> + + <method name="appendItem"> + <parameter name="aLabel"/> + <parameter name="aValue"/> + <body> + const XULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + var item = this.ownerDocument.createElementNS(XULNS, "listitem"); + item.setAttribute("label", aLabel); + item.setAttribute("value", aValue); + this.appendChild(item); + return item; + </body> + </method> + + <method name="insertItemAt"> + <parameter name="aIndex"/> + <parameter name="aLabel"/> + <parameter name="aValue"/> + <body> + const XULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + var item = this.ownerDocument.createElementNS(XULNS, "listitem"); + item.setAttribute("label", aLabel); + item.setAttribute("value", aValue); + var before = this.getItemAtIndex(aIndex); + if (before) + this.insertBefore(item, before); + else + this.appendChild(item); + return item; + </body> + </method> + + <property name="itemCount" readonly="true" + onget="return this.listBoxObject.getRowCount()"/> + + <!-- ///////////////// nsIListBoxObject ///////////////// --> + <method name="getIndexOfItem"> + <parameter name="item"/> + <body> + return this.listBoxObject.getIndexOfItem(item); + </body> + </method> + <method name="getItemAtIndex"> + <parameter name="index"/> + <body> + return this.listBoxObject.getItemAtIndex(index); + </body> + </method> + <method name="ensureIndexIsVisible"> + <parameter name="index"/> + <body> + return this.listBoxObject.ensureIndexIsVisible(index); + </body> + </method> + <method name="ensureElementIsVisible"> + <parameter name="element"/> + <body> + return this.ensureIndexIsVisible(this.listBoxObject.getIndexOfItem(element)); + </body> + </method> + <method name="scrollToIndex"> + <parameter name="index"/> + <body> + return this.listBoxObject.scrollToIndex(index); + </body> + </method> + <method name="getNumberOfVisibleRows"> + <body> + return this.listBoxObject.getNumberOfVisibleRows(); + </body> + </method> + <method name="getIndexOfFirstVisibleRow"> + <body> + return this.listBoxObject.getIndexOfFirstVisibleRow(); + </body> + </method> + <method name="getRowCount"> + <body> + return this.listBoxObject.getRowCount(); + </body> + </method> + + <method name="scrollOnePage"> + <parameter name="direction"/> <!-- Must be -1 or 1 --> + <body> + <![CDATA[ + var pageOffset = this.getNumberOfVisibleRows() * direction; + // skip over invisible elements - the user won't care about them + for (var i = 0; i != pageOffset; i += direction) { + var item = this.getItemAtIndex(this.currentIndex + i); + if (item && !this._canUserSelect(item)) + pageOffset += direction; + } + var newTop = this.getIndexOfFirstVisibleRow() + pageOffset; + if (direction == 1) { + var maxTop = this.getRowCount() - this.getNumberOfVisibleRows(); + for (i = this.getRowCount(); i >= 0 && i > maxTop; i--) { + item = this.getItemAtIndex(i); + if (item && !this._canUserSelect(item)) + maxTop--; + } + if (newTop >= maxTop) + newTop = maxTop; + } + if (newTop < 0) + newTop = 0; + this.scrollToIndex(newTop); + return pageOffset; + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="keypress" key=" " phase="target"> + <![CDATA[ + if (this.currentItem) { + if (this.currentItem.getAttribute("type") != "checkbox") { + this.addItemToSelection(this.currentItem); + // Prevent page from scrolling on the space key. + event.preventDefault(); + } + else if (!this.currentItem.disabled) { + this.currentItem.checked = !this.currentItem.checked; + this.currentItem.doCommand(); + // Prevent page from scrolling on the space key. + event.preventDefault(); + } + } + ]]> + </handler> + + <handler event="MozSwipeGesture"> + <![CDATA[ + // Figure out which index to show + let targetIndex = 0; + + // Only handle swipe gestures up and down + switch (event.direction) { + case event.DIRECTION_DOWN: + targetIndex = this.itemCount - 1; + // Fall through for actual action + case event.DIRECTION_UP: + this.ensureIndexIsVisible(targetIndex); + break; + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="listrows"> + + <resources> + <stylesheet src="chrome://global/skin/listbox.css"/> + </resources> + + <handlers> + <handler event="DOMMouseScroll" phase="capturing"> + <![CDATA[ + if (event.axis == event.HORIZONTAL_AXIS) + return; + + var listBox = this.parentNode.listBoxObject; + var rows = event.detail; + if (rows == UIEvent.SCROLL_PAGE_UP) + rows = -listBox.getNumberOfVisibleRows(); + else if (rows == UIEvent.SCROLL_PAGE_DOWN) + rows = listBox.getNumberOfVisibleRows(); + + listBox.scrollByLines(rows); + event.preventDefault(); + ]]> + </handler> + + <handler event="MozMousePixelScroll" phase="capturing"> + <![CDATA[ + if (event.axis == event.HORIZONTAL_AXIS) + return; + + // shouldn't be scrolled by pixel scrolling events before a line/page + // scrolling event. + event.preventDefault(); + ]]> + </handler> + </handlers> + </binding> + + <binding id="listitem" role="xul:listitem" + extends="chrome://global/content/bindings/general.xml#basetext"> + <resources> + <stylesheet src="chrome://global/skin/listbox.css"/> + </resources> + + <content> + <children> + <xul:listcell xbl:inherits="label,crop,disabled,flexlabel"/> + </children> + </content> + + <implementation implements="nsIDOMXULSelectControlItemElement"> + <property name="current" onget="return this.getAttribute('current') == 'true';"> + <setter><![CDATA[ + if (val) + this.setAttribute("current", "true"); + else + this.removeAttribute("current"); + + let control = this.control; + if (!control || !control.suppressMenuItemEvent) { + this._fireEvent(val ? "DOMMenuItemActive" : "DOMMenuItemInactive"); + } + + return val; + ]]></setter> + </property> + + <!-- ///////////////// nsIDOMXULSelectControlItemElement ///////////////// --> + + <property name="value" onget="return this.getAttribute('value');" + onset="this.setAttribute('value', val); return val;"/> + <property name="label" onget="return this.getAttribute('label');" + onset="this.setAttribute('label', val); return val;"/> + + <property name="selected" onget="return this.getAttribute('selected') == 'true';"> + <setter><![CDATA[ + if (val) + this.setAttribute("selected", "true"); + else + this.removeAttribute("selected"); + + return val; + ]]></setter> + </property> + + <property name="control"> + <getter><![CDATA[ + var parent = this.parentNode; + while (parent) { + if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement) + return parent; + parent = parent.parentNode; + } + return null; + ]]></getter> + </property> + + <method name="_fireEvent"> + <parameter name="name"/> + <body> + <![CDATA[ + var event = document.createEvent("Events"); + event.initEvent(name, true, true); + this.dispatchEvent(event); + ]]> + </body> + </method> + </implementation> + <handlers> + <!-- If there is no modifier key, we select on mousedown, not + click, so that drags work correctly. --> + <handler event="mousedown"> + <![CDATA[ + var control = this.control; + if (!control || control.disabled) + return; + if ((!event.ctrlKey || (/Mac/.test(navigator.platform) && event.button == 2)) && + !event.shiftKey && !event.metaKey) { + if (!this.selected) { + control.selectItem(this); + } + control.currentItem = this; + } + ]]> + </handler> + + <!-- On a click (up+down on the same item), deselect everything + except this item. --> + <handler event="click" button="0"> + <![CDATA[ + var control = this.control; + if (!control || control.disabled) + return; + control._userSelecting = true; + if (control.selType != "multiple") { + control.selectItem(this); + } + else if (event.ctrlKey || event.metaKey) { + control.toggleItemSelection(this); + control.currentItem = this; + } + else if (event.shiftKey) { + control.selectItemRange(null, this); + control.currentItem = this; + } + else { + /* We want to deselect all the selected items except what was + clicked, UNLESS it was a right-click. We have to do this + in click rather than mousedown so that you can drag a + selected group of items */ + + // use selectItemRange instead of selectItem, because this + // doesn't de- and reselect this item if it is selected + control.selectItemRange(this, this); + } + control._userSelecting = false; + ]]> + </handler> + </handlers> + </binding> + + <binding id="listitem-iconic" + extends="chrome://global/content/bindings/listbox.xml#listitem"> + <content> + <children> + <xul:listcell class="listcell-iconic" xbl:inherits="label,image,crop,disabled,flexlabel"/> + </children> + </content> + </binding> + + <binding id="listitem-checkbox" + extends="chrome://global/content/bindings/listbox.xml#listitem"> + <content> + <children> + <xul:listcell type="checkbox" xbl:inherits="label,crop,checked,disabled,flexlabel"/> + </children> + </content> + + <implementation> + <property name="checked" + onget="return this.getAttribute('checked') == 'true';"> + <setter><![CDATA[ + if (val) + this.setAttribute('checked', 'true'); + else + this.removeAttribute('checked'); + var event = document.createEvent('Events'); + event.initEvent('CheckboxStateChange', true, true); + this.dispatchEvent(event); + return val; + ]]></setter> + </property> + </implementation> + + <handlers> + <handler event="mousedown" button="0"> + <![CDATA[ + if (!this.disabled && !this.control.disabled) { + this.checked = !this.checked; + this.doCommand(); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="listitem-checkbox-iconic" + extends="chrome://global/content/bindings/listbox.xml#listitem-checkbox"> + <content> + <children> + <xul:listcell type="checkbox" class="listcell-iconic" xbl:inherits="label,image,crop,checked,disabled,flexlabel"/> + </children> + </content> + </binding> + + <binding id="listcell" role="xul:listcell" + extends="chrome://global/content/bindings/general.xml#basecontrol"> + + <resources> + <stylesheet src="chrome://global/skin/listbox.css"/> + </resources> + + <content> + <children> + <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/> + </children> + </content> + </binding> + + <binding id="listcell-iconic" + extends="chrome://global/content/bindings/listbox.xml#listcell"> + <content> + <children> + <xul:image class="listcell-icon" xbl:inherits="src=image"/> + <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/> + </children> + </content> + </binding> + + <binding id="listcell-checkbox" + extends="chrome://global/content/bindings/listbox.xml#listcell"> + <content> + <children> + <xul:image class="listcell-check" xbl:inherits="checked,disabled"/> + <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/> + </children> + </content> + </binding> + + <binding id="listcell-checkbox-iconic" + extends="chrome://global/content/bindings/listbox.xml#listcell-checkbox"> + <content> + <children> + <xul:image class="listcell-check" xbl:inherits="checked,disabled"/> + <xul:image class="listcell-icon" xbl:inherits="src=image"/> + <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/> + </children> + </content> + </binding> + + <binding id="listhead" role="xul:listhead"> + + <resources> + <stylesheet src="chrome://global/skin/listbox.css"/> + </resources> + + <content> + <xul:listheaditem> + <children includes="listheader"/> + </xul:listheaditem> + </content> + </binding> + + <binding id="listheader" display="xul:button" role="xul:listheader"> + + <resources> + <stylesheet src="chrome://global/skin/listbox.css"/> + </resources> + + <content> + <xul:image class="listheader-icon"/> + <xul:label class="listheader-label" xbl:inherits="value=label,crop" flex="1" crop="right"/> + <xul:image class="listheader-sortdirection" xbl:inherits="sortDirection"/> + </content> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/menu.xml b/toolkit/content/widgets/menu.xml new file mode 100644 index 0000000000..26dcad454b --- /dev/null +++ b/toolkit/content/widgets/menu.xml @@ -0,0 +1,286 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="menuitemBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="menuitem-base" role="xul:menuitem" + extends="chrome://global/content/bindings/general.xml#control-item"> + <resources> + <stylesheet src="chrome://global/skin/menu.css"/> + </resources> + <implementation implements="nsIDOMXULSelectControlItemElement, nsIDOMXULContainerItemElement"> + <!-- nsIDOMXULSelectControlItemElement --> + <property name="selected" readonly="true" + onget="return this.getAttribute('selected') == 'true';"/> + <property name="control" readonly="true"> + <getter> + <![CDATA[ + var parent = this.parentNode; + if (parent && + parent.parentNode instanceof Components.interfaces.nsIDOMXULSelectControlElement) + return parent.parentNode; + return null; + ]]> + </getter> + </property> + + <!-- nsIDOMXULContainerItemElement --> + <property name="parentContainer" readonly="true"> + <getter> + for (var parent = this.parentNode; parent; parent = parent.parentNode) { + if (parent instanceof Components.interfaces.nsIDOMXULContainerElement) + return parent; + } + return null; + </getter> + </property> + </implementation> + </binding> + + <binding id="menu-base" + extends="chrome://global/content/bindings/menu.xml#menuitem-base"> + + <implementation implements="nsIDOMXULContainerElement"> + <property name="open" onget="return this.hasAttribute('open');"> + <setter><![CDATA[ + this.boxObject.openMenu(val); + return val; + ]]></setter> + </property> + + <property name="openedWithKey" readonly="true"> + <getter><![CDATA[ + return this.boxObject.openedWithKey; + ]]></getter> + </property> + + <!-- nsIDOMXULContainerElement interface --> + <method name="appendItem"> + <parameter name="aLabel"/> + <parameter name="aValue"/> + <body> + return this.insertItemAt(-1, aLabel, aValue); + </body> + </method> + + <method name="insertItemAt"> + <parameter name="aIndex"/> + <parameter name="aLabel"/> + <parameter name="aValue"/> + <body> + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + var menupopup = this.menupopup; + if (!menupopup) { + menupopup = this.ownerDocument.createElementNS(XUL_NS, "menupopup"); + this.appendChild(menupopup); + } + + var menuitem = this.ownerDocument.createElementNS(XUL_NS, "menuitem"); + menuitem.setAttribute("label", aLabel); + menuitem.setAttribute("value", aValue); + + var before = this.getItemAtIndex(aIndex); + if (before) + return menupopup.insertBefore(menuitem, before); + return menupopup.appendChild(menuitem); + </body> + </method> + + <method name="removeItemAt"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + var menupopup = this.menupopup; + if (menupopup) { + var item = this.getItemAtIndex(aIndex); + if (item) + return menupopup.removeChild(item); + } + return null; + ]]> + </body> + </method> + + <property name="itemCount" readonly="true"> + <getter> + var menupopup = this.menupopup; + return menupopup ? menupopup.childNodes.length : 0; + </getter> + </property> + + <method name="getIndexOfItem"> + <parameter name="aItem"/> + <body> + <![CDATA[ + var menupopup = this.menupopup; + if (menupopup) { + var items = menupopup.childNodes; + var length = items.length; + for (var index = 0; index < length; ++index) { + if (items[index] == aItem) + return index; + } + } + return -1; + ]]> + </body> + </method> + + <method name="getItemAtIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + var menupopup = this.menupopup; + if (!menupopup || aIndex < 0 || aIndex >= menupopup.childNodes.length) + return null; + + return menupopup.childNodes[aIndex]; + ]]> + </body> + </method> + + <property name="menupopup" readonly="true"> + <getter> + <![CDATA[ + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + for (var child = this.firstChild; child; child = child.nextSibling) { + if (child.namespaceURI == XUL_NS && child.localName == "menupopup") + return child; + } + return null; + ]]> + </getter> + </property> + </implementation> + </binding> + + <binding id="menu" + extends="chrome://global/content/bindings/menu.xml#menu-base"> + <content> + <xul:label class="menu-text" xbl:inherits="value=label,accesskey,crop" crop="right"/> + <xul:hbox class="menu-accel-container" anonid="accel"> + <xul:label class="menu-accel" xbl:inherits="value=acceltext"/> + </xul:hbox> + <xul:hbox align="center" class="menu-right" xbl:inherits="_moz-menuactive,disabled"> + <xul:image/> + </xul:hbox> + <children includes="menupopup"/> + </content> + </binding> + + <binding id="menuitem" extends="chrome://global/content/bindings/menu.xml#menuitem-base"> + <content> + <xul:label class="menu-text" xbl:inherits="value=label,accesskey,crop" crop="right"/> + <xul:hbox class="menu-accel-container" anonid="accel"> + <xul:label class="menu-accel" xbl:inherits="value=acceltext"/> + </xul:hbox> + </content> + </binding> + + <binding id="menucaption" extends="chrome://global/content/bindings/menu.xml#menu-base"> + <content> + <xul:label class="menu-text" xbl:inherits="value=label,crop" crop="right"/> + </content> + </binding> + + <binding id="menu-menubar" + extends="chrome://global/content/bindings/menu.xml#menu-base"> + <content> + <xul:label class="menubar-text" xbl:inherits="value=label,accesskey,crop" crop="right"/> + <children includes="menupopup"/> + </content> + </binding> + + <binding id="menu-menubar-iconic" + extends="chrome://global/content/bindings/menu.xml#menu-base"> + <content> + <xul:image class="menubar-left" xbl:inherits="src=image"/> + <xul:label class="menubar-text" xbl:inherits="value=label,accesskey,crop" crop="right"/> + <children includes="menupopup"/> + </content> + </binding> + + <binding id="menuitem-iconic" extends="chrome://global/content/bindings/menu.xml#menuitem"> + <content> + <xul:hbox class="menu-iconic-left" align="center" pack="center" + xbl:inherits="selected,_moz-menuactive,disabled,checked"> + <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/> + </xul:hbox> + <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/> + <children/> + <xul:hbox class="menu-accel-container" anonid="accel"> + <xul:label class="menu-iconic-accel" xbl:inherits="value=acceltext"/> + </xul:hbox> + </content> + </binding> + + <binding id="menuitem-iconic-noaccel" extends="chrome://global/content/bindings/menu.xml#menuitem"> + <content> + <xul:hbox class="menu-iconic-left" align="center" pack="center" + xbl:inherits="selected,disabled,checked"> + <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/> + </xul:hbox> + <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/> + </content> + </binding> + + <binding id="menucaption-inmenulist" extends="chrome://global/content/bindings/menu.xml#menucaption"> + <content> + <xul:hbox class="menu-iconic-left" align="center" pack="center" + xbl:inherits="selected,disabled,checked"> + <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/> + </xul:hbox> + <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,crop" crop="right"/> + </content> + </binding> + + <binding id="menuitem-iconic-desc-noaccel" extends="chrome://global/content/bindings/menu.xml#menuitem"> + <content> + <xul:hbox class="menu-iconic-left" align="center" pack="center" + xbl:inherits="selected,disabled,checked"> + <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/> + </xul:hbox> + <xul:label class="menu-iconic-text" xbl:inherits="value=label,accesskey,crop" crop="right" flex="1"/> + <xul:label class="menu-iconic-text menu-description" xbl:inherits="value=description" crop="right" flex="10000"/> + </content> + </binding> + + <binding id="menu-iconic" + extends="chrome://global/content/bindings/menu.xml#menu-base"> + <content> + <xul:hbox class="menu-iconic-left" align="center" pack="center"> + <xul:image class="menu-iconic-icon" xbl:inherits="src=image"/> + </xul:hbox> + <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/> + <xul:hbox class="menu-accel-container" anonid="accel"> + <xul:label class="menu-iconic-accel" xbl:inherits="value=acceltext"/> + </xul:hbox> + <xul:hbox align="center" class="menu-right" xbl:inherits="_moz-menuactive,disabled"> + <xul:image/> + </xul:hbox> + <children includes="menupopup|template"/> + </content> + </binding> + + <binding id="menubutton-item" extends="chrome://global/content/bindings/menu.xml#menuitem-base"> + <content> + <xul:label class="menubutton-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/> + <children includes="menupopup"/> + </content> + </binding> + + <binding id="menuseparator" role="xul:menuseparator" + extends="chrome://global/content/bindings/menu.xml#menuitem-base"> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/menulist.xml b/toolkit/content/widgets/menulist.xml new file mode 100644 index 0000000000..ccdf3bd26d --- /dev/null +++ b/toolkit/content/widgets/menulist.xml @@ -0,0 +1,606 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="menulistBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="menulist-base" extends="chrome://global/content/bindings/general.xml#basecontrol"> + <resources> + <stylesheet src="chrome://global/content/menulist.css"/> + <stylesheet src="chrome://global/skin/menulist.css"/> + </resources> + </binding> + + <binding id="menulist" display="xul:menu" role="xul:menulist" + extends="chrome://global/content/bindings/menulist.xml#menulist-base"> + <content sizetopopup="pref"> + <xul:hbox class="menulist-label-box" flex="1"> + <xul:image class="menulist-icon" xbl:inherits="src=image,src"/> + <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey" crop="right" flex="1"/> + </xul:hbox> + <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/> + <children includes="menupopup"/> + </content> + + <handlers> + <handler event="command" phase="capturing" + action="if (event.target.parentNode.parentNode == this) this.selectedItem = event.target;"/> + + <handler event="popupshowing"> + <![CDATA[ + if (event.target.parentNode == this) { + this.menuBoxObject.activeChild = null; + if (this.selectedItem) + // Not ready for auto-setting the active child in hierarchies yet. + // For now, only do this when the outermost menupopup opens. + this.menuBoxObject.activeChild = this.mSelectedInternal; + } + ]]> + </handler> + + <handler event="keypress" modifiers="shift any" group="system"> + <![CDATA[ + if (!event.defaultPrevented && + (event.keyCode == KeyEvent.DOM_VK_UP || + event.keyCode == KeyEvent.DOM_VK_DOWN || + event.keyCode == KeyEvent.DOM_VK_PAGE_UP || + event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN || + event.keyCode == KeyEvent.DOM_VK_HOME || + event.keyCode == KeyEvent.DOM_VK_END || + event.keyCode == KeyEvent.DOM_VK_BACK_SPACE || + event.charCode > 0)) { + // Moving relative to an item: start from the currently selected item + this.menuBoxObject.activeChild = this.mSelectedInternal; + if (this.menuBoxObject.handleKeyPress(event)) { + this.menuBoxObject.activeChild.doCommand(); + event.preventDefault(); + } + } + ]]> + </handler> + </handlers> + + <implementation implements="nsIDOMXULMenuListElement"> + <constructor> + this.mInputField = null; + this.mSelectedInternal = null; + this.mAttributeObserver = null; + this.menuBoxObject = this.boxObject; + this.setInitialSelection(); + </constructor> + + <method name="setInitialSelection"> + <body> + <![CDATA[ + var popup = this.menupopup; + if (popup) { + var arr = popup.getElementsByAttribute('selected', 'true'); + + var editable = this.editable; + var value = this.value; + if (!arr.item(0) && value) + arr = popup.getElementsByAttribute(editable ? 'label' : 'value', value); + + if (arr.item(0)) + this.selectedItem = arr[0]; + else if (!editable) + this.selectedIndex = 0; + } + ]]> + </body> + </method> + + <property name="value" onget="return this.getAttribute('value');"> + <setter> + <![CDATA[ + // if the new value is null, we still need to remove the old value + if (val == null) + return this.selectedItem = val; + + var arr = null; + var popup = this.menupopup; + if (popup) + arr = popup.getElementsByAttribute('value', val); + + if (arr && arr.item(0)) + this.selectedItem = arr[0]; + else { + this.selectedItem = null; + this.setAttribute('value', val); + } + + return val; + ]]> + </setter> + </property> + + <property name="inputField" readonly="true" onget="return null;"/> + + <property name="crop" onset="this.setAttribute('crop',val); return val;" + onget="return this.getAttribute('crop');"/> + <property name="image" onset="this.setAttribute('image',val); return val;" + onget="return this.getAttribute('image');"/> + <property name="label" readonly="true" onget="return this.getAttribute('label');"/> + <property name="description" onset="this.setAttribute('description',val); return val;" + onget="return this.getAttribute('description');"/> + <property name="editable" onset="this.setAttribute('editable',val); return val;" + onget="return this.getAttribute('editable') == 'true';"/> + + <property name="open" onset="this.menuBoxObject.openMenu(val); + return val;" + onget="return this.hasAttribute('open');"/> + + <property name="itemCount" readonly="true" + onget="return this.menupopup ? this.menupopup.childNodes.length : 0"/> + + <property name="menupopup" readonly="true"> + <getter> + <![CDATA[ + var popup = this.firstChild; + while (popup && popup.localName != "menupopup") + popup = popup.nextSibling; + return popup; + ]]> + </getter> + </property> + + <method name="contains"> + <parameter name="item"/> + <body> + <![CDATA[ + if (!item) + return false; + + var parent = item.parentNode; + return (parent && parent.parentNode == this); + ]]> + </body> + </method> + + <property name="selectedIndex"> + <getter> + <![CDATA[ + // Quick and dirty. We won't deal with hierarchical menulists yet. + if (!this.selectedItem || + !this.mSelectedInternal.parentNode || + this.mSelectedInternal.parentNode.parentNode != this) + return -1; + + var children = this.mSelectedInternal.parentNode.childNodes; + var i = children.length; + while (i--) + if (children[i] == this.mSelectedInternal) + break; + + return i; + ]]> + </getter> + <setter> + <![CDATA[ + var popup = this.menupopup; + if (popup && 0 <= val) { + if (val < popup.childNodes.length) + this.selectedItem = popup.childNodes[val]; + } + else + this.selectedItem = null; + return val; + ]]> + </setter> + </property> + + <property name="selectedItem"> + <getter> + <![CDATA[ + return this.mSelectedInternal; + ]]> + </getter> + <setter> + <![CDATA[ + var oldval = this.mSelectedInternal; + if (oldval == val) + return val; + + if (val && !this.contains(val)) + return val; + + if (oldval) { + oldval.removeAttribute('selected'); + this.mAttributeObserver.disconnect(); + } + + this.mSelectedInternal = val; + let attributeFilter = ["value", "label", "image", "description"]; + if (val) { + val.setAttribute('selected', 'true'); + for (let attr of attributeFilter) { + if (val.hasAttribute(attr)) { + this.setAttribute(attr, val.getAttribute(attr)); + } + else { + this.removeAttribute(attr); + } + } + + this.mAttributeObserver = new MutationObserver(this.handleMutation.bind(this)); + this.mAttributeObserver.observe(val, { attributeFilter }); + } + else { + for (let attr of attributeFilter) { + this.removeAttribute(attr); + } + } + + var event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + + event = document.createEvent("Events"); + event.initEvent("ValueChange", true, true); + this.dispatchEvent(event); + + return val; + ]]> + </setter> + </property> + + <method name="handleMutation"> + <parameter name="aRecords"/> + <body> + <![CDATA[ + for (let record of aRecords) { + let t = record.target; + if (t == this.mSelectedInternal) { + let attrName = record.attributeName; + switch (attrName) { + case "value": + case "label": + case "image": + case "description": + if (t.hasAttribute(attrName)) { + this.setAttribute(attrName, t.getAttribute(attrName)); + } + else { + this.removeAttribute(attrName); + } + } + } + } + ]]> + </body> + </method> + + <method name="getIndexOfItem"> + <parameter name="item"/> + <body> + <![CDATA[ + var popup = this.menupopup; + if (popup) { + var children = popup.childNodes; + var i = children.length; + while (i--) + if (children[i] == item) + return i; + } + return -1; + ]]> + </body> + </method> + + <method name="getItemAtIndex"> + <parameter name="index"/> + <body> + <![CDATA[ + var popup = this.menupopup; + if (popup) { + var children = popup.childNodes; + if (index >= 0 && index < children.length) + return children[index]; + } + return null; + ]]> + </body> + </method> + + <method name="appendItem"> + <parameter name="label"/> + <parameter name="value"/> + <parameter name="description"/> + <body> + <![CDATA[ + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var popup = this.menupopup || + this.appendChild(document.createElementNS(XULNS, "menupopup")); + var item = document.createElementNS(XULNS, "menuitem"); + item.setAttribute("label", label); + item.setAttribute("value", value); + if (description) + item.setAttribute("description", description); + + popup.appendChild(item); + return item; + ]]> + </body> + </method> + + <method name="insertItemAt"> + <parameter name="index"/> + <parameter name="label"/> + <parameter name="value"/> + <parameter name="description"/> + <body> + <![CDATA[ + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var popup = this.menupopup || + this.appendChild(document.createElementNS(XULNS, "menupopup")); + var item = document.createElementNS(XULNS, "menuitem"); + item.setAttribute("label", label); + item.setAttribute("value", value); + if (description) + item.setAttribute("description", description); + + if (index >= 0 && index < popup.childNodes.length) + popup.insertBefore(item, popup.childNodes[index]); + else + popup.appendChild(item); + return item; + ]]> + </body> + </method> + + <method name="removeItemAt"> + <parameter name="index"/> + <body> + <![CDATA[ + var popup = this.menupopup; + if (popup && 0 <= index && index < popup.childNodes.length) { + var remove = popup.childNodes[index]; + popup.removeChild(remove); + return remove; + } + return null; + ]]> + </body> + </method> + + <method name="removeAllItems"> + <body> + <![CDATA[ + this.selectedItem = null; + var popup = this.menupopup; + if (popup) + this.removeChild(popup); + ]]> + </body> + </method> + + <destructor> + <![CDATA[ + if (this.mAttributeObserver) { + this.mAttributeObserver.disconnect(); + } + ]]> + </destructor> + </implementation> + </binding> + + <binding id="menulist-editable" extends="chrome://global/content/bindings/menulist.xml#menulist"> + <content sizetopopup="pref"> + <xul:hbox class="menulist-editable-box textbox-input-box" xbl:inherits="context,disabled,readonly,focused" flex="1"> + <html:input class="menulist-editable-input" anonid="input" allowevents="true" + xbl:inherits="value=label,value,disabled,tabindex,readonly,placeholder"/> + </xul:hbox> + <xul:dropmarker class="menulist-dropmarker" type="menu" + xbl:inherits="open,disabled,parentfocused=focused"/> + <children includes="menupopup"/> + </content> + + <implementation> + <method name="_selectInputFieldValueInList"> + <body> + <![CDATA[ + if (this.hasAttribute("disableautoselect")) + return; + + // Find and select the menuitem that matches inputField's "value" + var arr = null; + var popup = this.menupopup; + + if (popup) + arr = popup.getElementsByAttribute('label', this.inputField.value); + + this.setSelectionInternal(arr ? arr.item(0) : null); + ]]> + </body> + </method> + + <method name="setSelectionInternal"> + <parameter name="val"/> + <body> + <![CDATA[ + // This is called internally to set selected item + // without triggering infinite loop + // when using selectedItem's setter + if (this.mSelectedInternal == val) + return val; + + if (this.mSelectedInternal) + this.mSelectedInternal.removeAttribute('selected'); + + this.mSelectedInternal = val; + + if (val) + val.setAttribute('selected', 'true'); + + // Do NOT change the "value", which is owned by inputField + return val; + ]]> + </body> + </method> + + <property name="inputField" readonly="true"> + <getter><![CDATA[ + if (!this.mInputField) + this.mInputField = document.getAnonymousElementByAttribute(this, "anonid", "input"); + return this.mInputField; + ]]></getter> + </property> + + <property name="label" onset="this.inputField.value = val; return val;" + onget="return this.inputField.value;"/> + + <property name="value" onget="return this.inputField.value;"> + <setter> + <![CDATA[ + // Override menulist's value setter to refer to the inputField's value + // (Allows using "menulist.value" instead of "menulist.inputField.value") + this.inputField.value = val; + this.setAttribute('value', val); + this.setAttribute('label', val); + this._selectInputFieldValueInList(); + return val; + ]]> + </setter> + </property> + + <property name="selectedItem"> + <getter> + <![CDATA[ + // Make sure internally-selected item + // is in sync with inputField.value + this._selectInputFieldValueInList(); + return this.mSelectedInternal; + ]]> + </getter> + <setter> + <![CDATA[ + var oldval = this.mSelectedInternal; + if (oldval == val) + return val; + + if (val && !this.contains(val)) + return val; + + // This doesn't touch inputField.value or "value" and "label" attributes + this.setSelectionInternal(val); + if (val) { + // Editable menulist uses "label" as its "value" + var label = val.getAttribute('label'); + this.inputField.value = label; + this.setAttribute('value', label); + this.setAttribute('label', label); + } + else { + this.inputField.value = ""; + this.removeAttribute('value'); + this.removeAttribute('label'); + } + + var event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + + event = document.createEvent("Events"); + event.initEvent("ValueChange", true, true); + this.dispatchEvent(event); + + return val; + ]]> + </setter> + </property> + <property name="disableautoselect" + onset="if (val) this.setAttribute('disableautoselect','true'); + else this.removeAttribute('disableautoselect'); return val;" + onget="return this.hasAttribute('disableautoselect');"/> + + <property name="editor" readonly="true"> + <getter><![CDATA[ + const nsIDOMNSEditableElement = Components.interfaces.nsIDOMNSEditableElement; + return this.inputField.QueryInterface(nsIDOMNSEditableElement).editor; + ]]></getter> + </property> + + <property name="readOnly" onset="this.inputField.readOnly = val; + if (val) this.setAttribute('readonly', 'true'); + else this.removeAttribute('readonly'); return val;" + onget="return this.inputField.readOnly;"/> + + <method name="select"> + <body> + this.inputField.select(); + </body> + </method> + </implementation> + + <handlers> + <handler event="focus" phase="capturing"> + <![CDATA[ + this.setAttribute('focused', 'true'); + ]]> + </handler> + + <handler event="blur" phase="capturing"> + <![CDATA[ + this.removeAttribute('focused'); + ]]> + </handler> + + <handler event="popupshowing"> + <![CDATA[ + // editable menulists elements aren't in the focus order, + // so when the popup opens we need to force the focus to the inputField + if (event.target.parentNode == this) { + if (document.commandDispatcher.focusedElement != this.inputField) + this.inputField.focus(); + + this.menuBoxObject.activeChild = null; + if (this.selectedItem) + // Not ready for auto-setting the active child in hierarchies yet. + // For now, only do this when the outermost menupopup opens. + this.menuBoxObject.activeChild = this.mSelectedInternal; + } + ]]> + </handler> + + <handler event="keypress"> + <![CDATA[ + // open popup if key is up arrow, down arrow, or F4 + if (!event.ctrlKey && !event.shiftKey) { + if (event.keyCode == KeyEvent.DOM_VK_UP || + event.keyCode == KeyEvent.DOM_VK_DOWN || + (event.keyCode == KeyEvent.DOM_VK_F4 && !event.altKey)) { + event.preventDefault(); + this.open = true; + } + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="menulist-description" display="xul:menu" + extends="chrome://global/content/bindings/menulist.xml#menulist"> + <content sizetopopup="pref"> + <xul:hbox class="menulist-label-box" flex="1"> + <xul:image class="menulist-icon" xbl:inherits="src=image,src"/> + <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey" crop="right" flex="1"/> + <xul:label class="menulist-label menulist-description" xbl:inherits="value=description" crop="right" flex="10000"/> + </xul:hbox> + <xul:dropmarker class="menulist-dropmarker" type="menu" xbl:inherits="disabled,open"/> + <children includes="menupopup"/> + </content> + </binding> + + <binding id="menulist-popuponly" display="xul:menu" + extends="chrome://global/content/bindings/menulist.xml#menulist"> + <content sizetopopup="pref"> + <children includes="menupopup"/> + </content> + </binding> +</bindings> diff --git a/toolkit/content/widgets/notification.xml b/toolkit/content/widgets/notification.xml new file mode 100644 index 0000000000..2cc2f4b2c5 --- /dev/null +++ b/toolkit/content/widgets/notification.xml @@ -0,0 +1,551 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<!DOCTYPE bindings [ +<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd"> +%notificationDTD; +]> + +<bindings id="notificationBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="notificationbox"> + <content> + <xul:stack xbl:inherits="hidden=notificationshidden" + class="notificationbox-stack"> + <xul:spacer/> + <children includes="notification"/> + </xul:stack> + <children/> + </content> + + <implementation> + <field name="PRIORITY_INFO_LOW" readonly="true">1</field> + <field name="PRIORITY_INFO_MEDIUM" readonly="true">2</field> + <field name="PRIORITY_INFO_HIGH" readonly="true">3</field> + <field name="PRIORITY_WARNING_LOW" readonly="true">4</field> + <field name="PRIORITY_WARNING_MEDIUM" readonly="true">5</field> + <field name="PRIORITY_WARNING_HIGH" readonly="true">6</field> + <field name="PRIORITY_CRITICAL_LOW" readonly="true">7</field> + <field name="PRIORITY_CRITICAL_MEDIUM" readonly="true">8</field> + <field name="PRIORITY_CRITICAL_HIGH" readonly="true">9</field> + <field name="PRIORITY_CRITICAL_BLOCK" readonly="true">10</field> + + <field name="currentNotification">null</field> + + <field name="_closedNotification">null</field> + <field name="_blockingCanvas">null</field> + <field name="_animating">false</field> + + <property name="notificationsHidden" + onget="return this.getAttribute('notificationshidden') == 'true';"> + <setter> + if (val) + this.setAttribute('notificationshidden', true); + else this.removeAttribute('notificationshidden'); + return val; + </setter> + </property> + + <property name="allNotifications" readonly="true"> + <getter> + <![CDATA[ + var closedNotification = this._closedNotification; + var notifications = this.getElementsByTagName('notification'); + return Array.filter(notifications, n => n != closedNotification); + ]]> + </getter> + </property> + + <method name="getNotificationWithValue"> + <parameter name="aValue"/> + <body> + <![CDATA[ + var notifications = this.allNotifications; + for (var n = notifications.length - 1; n >= 0; n--) { + if (aValue == notifications[n].getAttribute("value")) + return notifications[n]; + } + return null; + ]]> + </body> + </method> + + <method name="appendNotification"> + <parameter name="aLabel"/> + <parameter name="aValue"/> + <parameter name="aImage"/> + <parameter name="aPriority"/> + <parameter name="aButtons"/> + <parameter name="aEventCallback"/> + <body> + <![CDATA[ + if (aPriority < this.PRIORITY_INFO_LOW || + aPriority > this.PRIORITY_CRITICAL_BLOCK) + throw "Invalid notification priority " + aPriority; + + // check for where the notification should be inserted according to + // priority. If two are equal, the existing one appears on top. + var notifications = this.allNotifications; + var insertPos = null; + for (var n = notifications.length - 1; n >= 0; n--) { + if (notifications[n].priority < aPriority) + break; + insertPos = notifications[n]; + } + + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var newitem = document.createElementNS(XULNS, "notification"); + // Can't use instanceof in case this was created from a different document: + let labelIsDocFragment = aLabel && typeof aLabel == "object" && aLabel.nodeType && + aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE; + if (!labelIsDocFragment) + newitem.setAttribute("label", aLabel); + newitem.setAttribute("value", aValue); + if (aImage) + newitem.setAttribute("image", aImage); + newitem.eventCallback = aEventCallback; + + if (aButtons) { + // The notification-button-default class is added to the button + // with isDefault set to true. If there is no such button, it is + // added to the first button (unless that button has isDefault + // set to false). There cannot be multiple default buttons. + var defaultElem; + + for (var b = 0; b < aButtons.length; b++) { + var button = aButtons[b]; + var buttonElem = document.createElementNS(XULNS, "button"); + buttonElem.setAttribute("label", button.label); + if (typeof button.accessKey == "string") + buttonElem.setAttribute("accesskey", button.accessKey); + if (typeof button.type == "string") { + buttonElem.setAttribute("type", button.type); + if ((button.type == "menu-button" || button.type == "menu") && + "popup" in button) { + buttonElem.appendChild(button.popup); + delete button.popup; + } + if (typeof button.anchor == "string") + buttonElem.setAttribute("anchor", button.anchor); + } + buttonElem.classList.add("notification-button"); + + if (button.isDefault || + b == 0 && !("isDefault" in button)) + defaultElem = buttonElem; + + newitem.appendChild(buttonElem); + buttonElem.buttonInfo = button; + } + + if (defaultElem) + defaultElem.classList.add("notification-button-default"); + } + + newitem.setAttribute("priority", aPriority); + if (aPriority >= this.PRIORITY_CRITICAL_LOW) + newitem.setAttribute("type", "critical"); + else if (aPriority <= this.PRIORITY_INFO_HIGH) + newitem.setAttribute("type", "info"); + else + newitem.setAttribute("type", "warning"); + + if (!insertPos) { + newitem.style.position = "fixed"; + newitem.style.top = "100%"; + newitem.style.marginTop = "-15px"; + newitem.style.opacity = "0"; + } + this.insertBefore(newitem, insertPos); + // Can only insert the document fragment after the item has been created because + // otherwise the XBL structure isn't there yet: + if (labelIsDocFragment) { + document.getAnonymousElementByAttribute(newitem, "anonid", "messageText") + .appendChild(aLabel); + } + + if (!insertPos) + this._showNotification(newitem, true); + + // Fire event for accessibility APIs + var event = document.createEvent("Events"); + event.initEvent("AlertActive", true, true); + newitem.dispatchEvent(event); + + return newitem; + ]]> + </body> + </method> + + <method name="removeNotification"> + <parameter name="aItem"/> + <parameter name="aSkipAnimation"/> + <body> + <![CDATA[ + if (aItem == this.currentNotification) + this.removeCurrentNotification(aSkipAnimation); + else if (aItem != this._closedNotification) + this._removeNotificationElement(aItem); + return aItem; + ]]> + </body> + </method> + + <method name="_removeNotificationElement"> + <parameter name="aChild"/> + <body> + <![CDATA[ + if (aChild.eventCallback) + aChild.eventCallback("removed"); + this.removeChild(aChild); + + // make sure focus doesn't get lost (workaround for bug 570835) + let fm = Components.classes["@mozilla.org/focus-manager;1"] + .getService(Components.interfaces.nsIFocusManager); + if (!fm.getFocusedElementForWindow(window, false, {})) + fm.moveFocus(window, this, fm.MOVEFOCUS_FORWARD, 0); + ]]> + </body> + </method> + + <method name="removeCurrentNotification"> + <parameter name="aSkipAnimation"/> + <body> + <![CDATA[ + this._showNotification(this.currentNotification, false, aSkipAnimation); + ]]> + </body> + </method> + + <method name="removeAllNotifications"> + <parameter name="aImmediate"/> + <body> + <![CDATA[ + var notifications = this.allNotifications; + for (var n = notifications.length - 1; n >= 0; n--) { + if (aImmediate) + this._removeNotificationElement(notifications[n]); + else + this.removeNotification(notifications[n]); + } + this.currentNotification = null; + + // Must clear up any currently animating notification + if (aImmediate) + this._finishAnimation(); + ]]> + </body> + </method> + + <method name="removeTransientNotifications"> + <body> + <![CDATA[ + var notifications = this.allNotifications; + for (var n = notifications.length - 1; n >= 0; n--) { + var notification = notifications[n]; + if (notification.persistence) + notification.persistence--; + else if (Date.now() > notification.timeout) + this.removeNotification(notification); + } + ]]> + </body> + </method> + + <method name="_showNotification"> + <parameter name="aNotification"/> + <parameter name="aSlideIn"/> + <parameter name="aSkipAnimation"/> + <body> + <![CDATA[ + this._finishAnimation(); + + var height = aNotification.boxObject.height; + var skipAnimation = aSkipAnimation || (height == 0); + + if (aSlideIn) { + this.currentNotification = aNotification; + aNotification.style.removeProperty("position"); + aNotification.style.removeProperty("top"); + aNotification.style.removeProperty("margin-top"); + aNotification.style.removeProperty("opacity"); + + if (skipAnimation) { + this._setBlockingState(this.currentNotification); + return; + } + } + else { + this._closedNotification = aNotification; + var notifications = this.allNotifications; + var idx = notifications.length - 1; + this.currentNotification = (idx >= 0) ? notifications[idx] : null; + + if (skipAnimation) { + this._removeNotificationElement(this._closedNotification); + this._closedNotification = null; + this._setBlockingState(this.currentNotification); + return; + } + + aNotification.style.marginTop = -height + "px"; + aNotification.style.opacity = 0; + } + + this._animating = true; + ]]> + </body> + </method> + + <method name="_finishAnimation"> + <body><![CDATA[ + if (this._animating) { + this._animating = false; + if (this._closedNotification) { + this._removeNotificationElement(this._closedNotification); + this._closedNotification = null; + } + this._setBlockingState(this.currentNotification); + } + ]]></body> + </method> + + <method name="_setBlockingState"> + <parameter name="aNotification"/> + <body> + <![CDATA[ + var isblock = aNotification && + aNotification.priority == this.PRIORITY_CRITICAL_BLOCK; + var canvas = this._blockingCanvas; + if (isblock) { + if (!canvas) + canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let content = this.firstChild; + if (!content || + content.namespaceURI != XULNS || + content.localName != "browser") + return; + + var width = content.boxObject.width; + var height = content.boxObject.height; + content.collapsed = true; + + canvas.setAttribute("width", width); + canvas.setAttribute("height", height); + canvas.setAttribute("flex", "1"); + + this.appendChild(canvas); + this._blockingCanvas = canvas; + + var bgcolor = "white"; + try { + var prefService = Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch); + bgcolor = prefService.getCharPref("browser.display.background_color"); + + var win = content.contentWindow; + var context = canvas.getContext("2d"); + context.globalAlpha = 0.5; + context.drawWindow(win, win.scrollX, win.scrollY, + width, height, bgcolor); + } + catch (ex) { } + } + else if (canvas) { + canvas.parentNode.removeChild(canvas); + this._blockingCanvas = null; + let content = this.firstChild; + if (content) + content.collapsed = false; + } + ]]> + </body> + </method> + + </implementation> + + <handlers> + <handler event="transitionend"><![CDATA[ + if (event.target.localName == "notification" && + event.propertyName == "margin-top") + this._finishAnimation(); + ]]></handler> + </handlers> + + </binding> + + <binding id="notification" role="xul:alert"> + <content> + <xul:hbox class="notification-inner" flex="1" xbl:inherits="type"> + <xul:hbox anonid="details" align="center" flex="1" + oncommand="this.parentNode.parentNode._doButtonCommand(event);"> + <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/> + <xul:description anonid="messageText" class="messageText" flex="1" xbl:inherits="xbl:text=label"/> + <xul:spacer flex="1"/> + <children/> + </xul:hbox> + <xul:toolbarbutton ondblclick="event.stopPropagation();" + class="messageCloseButton close-icon tabbable" + xbl:inherits="hidden=hideclose" + tooltiptext="&closeNotification.tooltip;" + oncommand="document.getBindingParent(this).dismiss();"/> + </xul:hbox> + </content> + <resources> + <stylesheet src="chrome://global/skin/notification.css"/> + </resources> + <implementation> + <property name="label" onset="this.setAttribute('label', val); return val;" + onget="return this.getAttribute('label');"/> + <property name="value" onset="this.setAttribute('value', val); return val;" + onget="return this.getAttribute('value');"/> + <property name="image" onset="this.setAttribute('image', val); return val;" + onget="return this.getAttribute('image');"/> + <property name="type" onset="this.setAttribute('type', val); return val;" + onget="return this.getAttribute('type');"/> + <property name="priority" onget="return parseInt(this.getAttribute('priority')) || 0;" + onset="this.setAttribute('priority', val); return val;"/> + <property name="persistence" onget="return parseInt(this.getAttribute('persistence')) || 0;" + onset="this.setAttribute('persistence', val); return val;"/> + <field name="timeout">0</field> + + <property name="control" readonly="true"> + <getter> + <![CDATA[ + var parent = this.parentNode; + while (parent) { + if (parent.localName == "notificationbox") + return parent; + parent = parent.parentNode; + } + return null; + ]]> + </getter> + </property> + + <!-- This method should only be called when the user has + manually closed the notification. If you want to + programmatically close the notification, you should + call close() instead. --> + <method name="dismiss"> + <body> + <![CDATA[ + if (this.eventCallback) { + this.eventCallback("dismissed"); + } + this.close(); + ]]> + </body> + </method> + + <method name="close"> + <body> + <![CDATA[ + var control = this.control; + if (control) + control.removeNotification(this); + else + this.hidden = true; + ]]> + </body> + </method> + + <method name="_doButtonCommand"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (!("buttonInfo" in aEvent.target)) + return; + + var button = aEvent.target.buttonInfo; + if (button.popup) { + document.getElementById(button.popup). + openPopup(aEvent.originalTarget, "after_start", 0, 0, false, false, aEvent); + aEvent.stopPropagation(); + } + else { + var callback = button.callback; + if (callback) { + var result = callback(this, button, aEvent.target); + if (!result) + this.close(); + aEvent.stopPropagation(); + } + } + ]]> + </body> + </method> + </implementation> + </binding> + + <binding id="popup-notification"> + <content> + <xul:vbox> + <xul:image class="popup-notification-icon" + xbl:inherits="popupid,src=icon,class=iconclass"/> + </xul:vbox> + <xul:vbox class="popup-notification-body" xbl:inherits="popupid"> + <xul:hbox align="start"> + <xul:vbox flex="1"> + <xul:label class="popup-notification-origin header" + xbl:inherits="value=origin,tooltiptext=origin" + crop="center"/> + <xul:description class="popup-notification-description" + xbl:inherits="xbl:text=label,popupid"/> + </xul:vbox> + <xul:toolbarbutton anonid="closebutton" + class="messageCloseButton close-icon popup-notification-closebutton tabbable" + xbl:inherits="oncommand=closebuttoncommand" + tooltiptext="&closeNotification.tooltip;"/> + </xul:hbox> + <children includes="popupnotificationcontent"/> + <xul:label class="text-link popup-notification-learnmore-link" + xbl:inherits="onclick=learnmoreclick,href=learnmoreurl">&learnMore;</xul:label> + <xul:checkbox anonid="checkbox" + xbl:inherits="hidden=checkboxhidden,checked=checkboxchecked,label=checkboxlabel,oncommand=checkboxcommand" /> + <xul:description class="popup-notification-warning" xbl:inherits="hidden=warninghidden,xbl:text=warninglabel"/> + <xul:spacer flex="1"/> + <xul:hbox class="popup-notification-button-container" + pack="end" align="center"> + <children includes="button"/> + <xul:button anonid="button" + class="popup-notification-menubutton" + type="menu-button" + xbl:inherits="oncommand=buttoncommand,onpopupshown=buttonpopupshown,label=buttonlabel,accesskey=buttonaccesskey,disabled=mainactiondisabled"> + <xul:menupopup anonid="menupopup" + xbl:inherits="oncommand=menucommand"> + <children/> + <xul:menuitem class="menuitem-iconic popup-notification-closeitem" + label="&closeNotificationItem.label;" + xbl:inherits="oncommand=closeitemcommand,hidden=hidenotnow"/> + </xul:menupopup> + </xul:button> + </xul:hbox> + </xul:vbox> + </content> + <resources> + <stylesheet src="chrome://global/skin/notification.css"/> + </resources> + <implementation> + <field name="checkbox" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "checkbox"); + </field> + <field name="closebutton" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "closebutton"); + </field> + <field name="button" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "button"); + </field> + <field name="menupopup" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "menupopup"); + </field> + </implementation> + </binding> +</bindings> diff --git a/toolkit/content/widgets/numberbox.xml b/toolkit/content/widgets/numberbox.xml new file mode 100644 index 0000000000..0e1225f09b --- /dev/null +++ b/toolkit/content/widgets/numberbox.xml @@ -0,0 +1,304 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="numberboxBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="numberbox" + extends="chrome://global/content/bindings/textbox.xml#textbox"> + + <resources> + <stylesheet src="chrome://global/skin/numberbox.css"/> + </resources> + + <content> + <xul:hbox class="textbox-input-box numberbox-input-box" flex="1" xbl:inherits="context,disabled,focused"> + <html:input class="numberbox-input textbox-input" anonid="input" + xbl:inherits="value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/> + </xul:hbox> + <xul:spinbuttons anonid="buttons" xbl:inherits="disabled,hidden=hidespinbuttons"/> + </content> + + <implementation> + <field name="_valueEntered">false</field> + <field name="_spinButtons">null</field> + <field name="_value">0</field> + <field name="decimalSymbol">"."</field> + + <property name="spinButtons" readonly="true"> + <getter> + <![CDATA[ + if (!this._spinButtons) + this._spinButtons = document.getAnonymousElementByAttribute(this, "anonid", "buttons"); + return this._spinButtons; + ]]> + </getter> + </property> + + <property name="value" onget="return '' + this.valueNumber" + onset="return this.valueNumber = val;"/> + + <property name="valueNumber"> + <getter> + if (this._valueEntered) { + var newval = this.inputField.value; + newval = newval.replace(this.decimalSymbol, "."); + this._validateValue(newval, false); + } + return this._value; + </getter> + <setter> + this._validateValue(val, false); + return val; + </setter> + </property> + + <property name="wrapAround"> + <getter> + <![CDATA[ + return (this.getAttribute('wraparound') == 'true') + ]]> + </getter> + <setter> + <![CDATA[ + if (val) + this.setAttribute('wraparound', 'true'); + else + this.removeAttribute('wraparound'); + this._enableDisableButtons(); + return val; + ]]> + </setter> + </property> + + <property name="min"> + <getter> + var min = this.getAttribute("min"); + return min ? Number(min) : 0; + </getter> + <setter> + <![CDATA[ + if (typeof val == "number") { + this.setAttribute("min", val); + if (this.valueNumber < val) + this._validateValue(val, false); + } + return val; + ]]> + </setter> + </property> + + <property name="max"> + <getter> + var max = this.getAttribute("max"); + return max ? Number(max) : Infinity; + </getter> + <setter> + <![CDATA[ + if (typeof val != "number") + return val; + var min = this.min; + if (val < min) + val = min; + this.setAttribute("max", val); + if (this.valueNumber > val) + this._validateValue(val, false); + return val; + ]]> + </setter> + </property> + + <property name="decimalPlaces"> + <getter> + var places = this.getAttribute("decimalplaces"); + return places ? Number(places) : 0; + </getter> + <setter> + if (typeof val == "number") { + this.setAttribute("decimalplaces", val); + this._validateValue(this.valueNumber, false); + } + return val; + </setter> + </property> + + <property name="increment"> + <getter> + var increment = this.getAttribute("increment"); + return increment ? Number(increment) : 1; + </getter> + <setter> + <![CDATA[ + if (typeof val == "number") + this.setAttribute("increment", val); + return val; + ]]> + </setter> + </property> + + <method name="decrease"> + <body> + return this._validateValue(this.valueNumber - this.increment, true); + </body> + </method> + + <method name="increase"> + <body> + return this._validateValue(this.valueNumber + this.increment, true); + </body> + </method> + + <method name="_modifyUp"> + <body> + <![CDATA[ + if (this.disabled || this.readOnly) + return; + var oldval = this.valueNumber; + var newval = this.increase(); + this.inputField.select(); + if (oldval != newval) + this._fireChange(); + ]]> + </body> + </method> + <method name="_modifyDown"> + <body> + <![CDATA[ + if (this.disabled || this.readOnly) + return; + var oldval = this.valueNumber; + var newval = this.decrease(); + this.inputField.select(); + if (oldval != newval) + this._fireChange(); + ]]> + </body> + </method> + + <method name="_enableDisableButtons"> + <body> + <![CDATA[ + var buttons = this.spinButtons; + if (this.wrapAround) { + buttons.decreaseDisabled = buttons.increaseDisabled = false; + } + else if (this.disabled || this.readOnly) { + buttons.decreaseDisabled = buttons.increaseDisabled = true; + } + else { + buttons.decreaseDisabled = (this.valueNumber <= this.min); + buttons.increaseDisabled = (this.valueNumber >= this.max); + } + ]]> + </body> + </method> + + <method name="_validateValue"> + <parameter name="aValue"/> + <parameter name="aIsIncDec"/> + <body> + <![CDATA[ + aValue = Number(aValue) || 0; + + var min = this.min; + var max = this.max; + var wrapAround = this.wrapAround && + min != -Infinity && max != Infinity; + if (aValue < min) + aValue = (aIsIncDec && wrapAround ? max : min); + else if (aValue > max) + aValue = (aIsIncDec && wrapAround ? min : max); + + var places = this.decimalPlaces; + aValue = (places == Infinity) ? "" + aValue : aValue.toFixed(places); + + this._valueEntered = false; + this._value = Number(aValue); + this.inputField.value = aValue.replace(/\./, this.decimalSymbol); + + if (!wrapAround) + this._enableDisableButtons(); + + return aValue; + ]]> + </body> + </method> + + <method name="_fireChange"> + <body> + var evt = document.createEvent("Events"); + evt.initEvent("change", true, true); + this.dispatchEvent(evt); + </body> + </method> + + <constructor><![CDATA[ + if (this.max < this.min) + this.max = this.min; + + var dsymbol = (Number(5.4)).toLocaleString().match(/\D/); + if (dsymbol != null) + this.decimalSymbol = dsymbol[0]; + + var value = this.inputField.value || 0; + this._validateValue(value, false); + ]]></constructor> + + </implementation> + + <handlers> + <handler event="input" phase="capturing"> + this._valueEntered = true; + </handler> + + <handler event="keypress"> + <![CDATA[ + if (!event.ctrlKey && !event.metaKey && !event.altKey && event.charCode) { + if (event.charCode == this.decimalSymbol.charCodeAt(0) && + this.decimalPlaces && + String(this.inputField.value).indexOf(this.decimalSymbol) == -1) + return; + + if (event.charCode == 45 && this.min < 0) + return; + + if (event.charCode < 48 || event.charCode > 57) + event.preventDefault(); + } + ]]> + </handler> + + <handler event="keypress" keycode="VK_UP"> + this._modifyUp(); + </handler> + + <handler event="keypress" keycode="VK_DOWN"> + this._modifyDown(); + </handler> + + <handler event="up" preventdefault="true"> + this._modifyUp(); + </handler> + + <handler event="down" preventdefault="true"> + this._modifyDown(); + </handler> + + <handler event="change"> + if (event.originalTarget == this.inputField) { + var newval = this.inputField.value; + newval = newval.replace(this.decimalSymbol, "."); + this._validateValue(newval, false); + } + </handler> + </handlers> + + </binding> + +</bindings> diff --git a/toolkit/content/widgets/optionsDialog.xml b/toolkit/content/widgets/optionsDialog.xml new file mode 100644 index 0000000000..f0cdba62f2 --- /dev/null +++ b/toolkit/content/widgets/optionsDialog.xml @@ -0,0 +1,43 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="optionsDialogBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="optionsDialog" + extends="chrome://global/content/bindings/dialog.xml#dialog"> + <content> +#ifndef XP_MACOSX + <xul:hbox flex="1"> + <xul:categoryBox anonid="prefsCategories"> + <children/> + </xul:categoryBox> + <xul:vbox flex="1"> + <xul:dialogheader id="panelHeader"/> + <xul:iframe anonid="panelFrame" name="panelFrame" style="width: 0px;" flex="1"/> + </xul:vbox> + </xul:hbox> +#else + <xul:vbox flex="1"> + <xul:categoryBox anonid="prefsCategories"> + <children/> + </xul:categoryBox> + <xul:vbox flex="1"> + <xul:iframe anonid="panelFrame" name="panelFrame" style="width: 0px;" flex="1"/> + </xul:vbox> + </xul:vbox> +#endif + </content> + + <implementation> + + + </implementation> + + </binding> + +</bindings>
\ No newline at end of file diff --git a/toolkit/content/widgets/popup.xml b/toolkit/content/widgets/popup.xml new file mode 100644 index 0000000000..bb1a5eeee8 --- /dev/null +++ b/toolkit/content/widgets/popup.xml @@ -0,0 +1,650 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="popupBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="popup-base"> + <resources> + <stylesheet src="chrome://global/skin/popup.css"/> + </resources> + + <implementation implements="nsIDOMXULPopupElement"> + <property name="label" onget="return this.getAttribute('label');" + onset="this.setAttribute('label', val); return val;"/> + <property name="position" onget="return this.getAttribute('position');" + onset="this.setAttribute('position', val); return val;"/> + <property name="popupBoxObject"> + <getter> + return this.boxObject; + </getter> + </property> + + <property name="state" readonly="true" + onget="return this.popupBoxObject.popupState"/> + + <property name="triggerNode" readonly="true" + onget="return this.popupBoxObject.triggerNode"/> + + <property name="anchorNode" readonly="true" + onget="return this.popupBoxObject.anchorNode"/> + + <method name="openPopup"> + <parameter name="aAnchorElement"/> + <parameter name="aPosition"/> + <parameter name="aX"/> + <parameter name="aY"/> + <parameter name="aIsContextMenu"/> + <parameter name="aAttributesOverride"/> + <parameter name="aTriggerEvent"/> + <body> + <![CDATA[ + try { + var popupBox = this.popupBoxObject; + if (popupBox) + popupBox.openPopup(aAnchorElement, aPosition, aX, aY, + aIsContextMenu, aAttributesOverride, aTriggerEvent); + } catch (e) {} + ]]> + </body> + </method> + + <method name="openPopupAtScreen"> + <parameter name="aX"/> + <parameter name="aY"/> + <parameter name="aIsContextMenu"/> + <parameter name="aTriggerEvent"/> + <body> + <![CDATA[ + try { + var popupBox = this.popupBoxObject; + if (popupBox) + popupBox.openPopupAtScreen(aX, aY, aIsContextMenu, aTriggerEvent); + } catch (e) {} + ]]> + </body> + </method> + + <method name="openPopupAtScreenRect"> + <parameter name="aPosition"/> + <parameter name="aX"/> + <parameter name="aY"/> + <parameter name="aWidth"/> + <parameter name="aHeight"/> + <parameter name="aIsContextMenu"/> + <parameter name="aAttributesOverride"/> + <parameter name="aTriggerEvent"/> + <body> + <![CDATA[ + try { + var popupBox = this.popupBoxObject; + if (popupBox) + popupBox.openPopupAtScreenRect(aPosition, aX, aY, aWidth, aHeight, + aIsContextMenu, aAttributesOverride, aTriggerEvent); + } catch (e) {} + ]]> + </body> + </method> + + <method name="showPopup"> + <parameter name="element"/> + <parameter name="xpos"/> + <parameter name="ypos"/> + <parameter name="popuptype"/> + <parameter name="anchoralignment"/> + <parameter name="popupalignment"/> + <body> + <![CDATA[ + var popupBox = null; + var menuBox = null; + try { + popupBox = this.popupBoxObject; + } catch (e) {} + try { + menuBox = this.parentNode.boxObject; + } catch (e) {} + if (menuBox instanceof MenuBoxObject) + menuBox.openMenu(true); + else if (popupBox) + popupBox.showPopup(element, this, xpos, ypos, popuptype, anchoralignment, popupalignment); + ]]> + </body> + </method> + + <method name="hidePopup"> + <parameter name="cancel"/> + <body> + <![CDATA[ + var popupBox = null; + var menuBox = null; + try { + popupBox = this.popupBoxObject; + } catch (e) {} + try { + menuBox = this.parentNode.boxObject; + } catch (e) {} + if (menuBox instanceof MenuBoxObject) + menuBox.openMenu(false); + else if (popupBox instanceof PopupBoxObject) + popupBox.hidePopup(cancel); + ]]> + </body> + </method> + + <property name="autoPosition"> + <getter> + <![CDATA[ + return this.popupBoxObject.autoPosition; + ]]> + </getter> + <setter> + <![CDATA[ + return this.popupBoxObject.autoPosition = val; + ]]> + </setter> + </property> + + <property name="alignmentPosition" readonly="true"> + <getter> + <![CDATA[ + return this.popupBoxObject.alignmentPosition; + ]]> + </getter> + </property> + + <property name="alignmentOffset" readonly="true"> + <getter> + <![CDATA[ + return this.popupBoxObject.alignmentOffset; + ]]> + </getter> + </property> + + <method name="enableKeyboardNavigator"> + <parameter name="aEnableKeyboardNavigator"/> + <body> + <![CDATA[ + this.popupBoxObject.enableKeyboardNavigator(aEnableKeyboardNavigator); + ]]> + </body> + </method> + + <method name="enableRollup"> + <parameter name="aEnableRollup"/> + <body> + <![CDATA[ + this.popupBoxObject.enableRollup(aEnableRollup); + ]]> + </body> + </method> + + <method name="sizeTo"> + <parameter name="aWidth"/> + <parameter name="aHeight"/> + <body> + <![CDATA[ + this.popupBoxObject.sizeTo(aWidth, aHeight); + ]]> + </body> + </method> + + <method name="moveTo"> + <parameter name="aLeft"/> + <parameter name="aTop"/> + <body> + <![CDATA[ + this.popupBoxObject.moveTo(aLeft, aTop); + ]]> + </body> + </method> + + <method name="moveToAnchor"> + <parameter name="aAnchorElement"/> + <parameter name="aPosition"/> + <parameter name="aX"/> + <parameter name="aY"/> + <parameter name="aAttributesOverride"/> + <body> + <![CDATA[ + this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride); + ]]> + </body> + </method> + + <method name="getOuterScreenRect"> + <body> + <![CDATA[ + return this.popupBoxObject.getOuterScreenRect(); + ]]> + </body> + </method> + + <method name="setConstraintRect"> + <parameter name="aRect"/> + <body> + <![CDATA[ + this.popupBoxObject.setConstraintRect(aRect); + ]]> + </body> + </method> + </implementation> + + </binding> + + <binding id="popup" role="xul:menupopup" + extends="chrome://global/content/bindings/popup.xml#popup-base"> + + <content> + <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical" + smoothscroll="false"> + <children/> + </xul:arrowscrollbox> + </content> + + <handlers> + <handler event="popupshowing" phase="target"> + <![CDATA[ + var array = []; + var width = 0; + for (var menuitem = this.firstChild; menuitem; menuitem = menuitem.nextSibling) { + if (menuitem.localName == "menuitem" && menuitem.hasAttribute("acceltext")) { + var accel = document.getAnonymousElementByAttribute(menuitem, "anonid", "accel"); + if (accel && accel.boxObject) { + array.push(accel); + if (accel.boxObject.width > width) + width = accel.boxObject.width; + } + } + } + for (var i = 0; i < array.length; i++) + array[i].width = width; + ]]> + </handler> + </handlers> + </binding> + + <binding id="panel" role="xul:panel" + extends="chrome://global/content/bindings/popup.xml#popup-base"> + <implementation implements="nsIDOMXULPopupElement"> + <field name="_prevFocus">0</field> + <field name="_dragBindingAlive">true</field> + <constructor> + <![CDATA[ + if (this.getAttribute("backdrag") == "true" && !this._draggableStarted) { + this._draggableStarted = true; + try { + let tmp = {}; + Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp); + let draghandle = new tmp.WindowDraggingElement(this); + draghandle.mouseDownCheck = function () { + return this._dragBindingAlive; + } + } catch (e) {} + } + ]]> + </constructor> + </implementation> + + <handlers> + <handler event="popupshowing"><![CDATA[ + // Capture the previous focus before has a chance to get set inside the panel + try { + this._prevFocus = Components.utils + .getWeakReference(document.commandDispatcher.focusedElement); + if (this._prevFocus.get()) + return; + } catch (ex) { } + + this._prevFocus = Components.utils.getWeakReference(document.activeElement); + ]]></handler> + <handler event="popupshown"><![CDATA[ + // Fire event for accessibility APIs + var alertEvent = document.createEvent("Events"); + alertEvent.initEvent("AlertActive", true, true); + this.dispatchEvent(alertEvent); + ]]></handler> + <handler event="popuphiding"><![CDATA[ + try { + this._currentFocus = document.commandDispatcher.focusedElement; + } catch (e) { + this._currentFocus = document.activeElement; + } + ]]></handler> + <handler event="popuphidden"><![CDATA[ + function doFocus() { + // Focus was set on an element inside this panel, + // so we need to move it back to where it was previously + try { + let fm = Components.classes["@mozilla.org/focus-manager;1"] + .getService(Components.interfaces.nsIFocusManager); + fm.setFocus(prevFocus, fm.FLAG_NOSCROLL); + } catch (e) { + prevFocus.focus(); + } + } + var currentFocus = this._currentFocus; + var prevFocus = this._prevFocus ? this._prevFocus.get() : null; + this._currentFocus = null; + this._prevFocus = null; + + // Avoid changing focus if focus changed while we hide the popup + // (This can happen e.g. if the popup is hiding as a result of a + // click/keypress that focused something) + let nowFocus; + try { + nowFocus = document.commandDispatcher.focusedElement; + } catch (e) { + nowFocus = document.activeElement; + } + if (nowFocus && nowFocus != currentFocus) + return; + + if (prevFocus && this.getAttribute("norestorefocus") != "true") { + // Try to restore focus + try { + if (document.commandDispatcher.focusedWindow != window) + return; // Focus has already been set to a window outside of this panel + } catch (ex) {} + + if (!currentFocus) { + doFocus(); + return; + } + while (currentFocus) { + if (currentFocus == this) { + doFocus(); + return; + } + currentFocus = currentFocus.parentNode; + } + } + ]]></handler> + </handlers> + </binding> + + <binding id="arrowpanel" extends="chrome://global/content/bindings/popup.xml#panel"> + <content flip="both" side="top" position="bottomcenter topleft" consumeoutsideclicks="false"> + <xul:vbox anonid="container" class="panel-arrowcontainer" flex="1" + xbl:inherits="side,panelopen"> + <xul:box anonid="arrowbox" class="panel-arrowbox"> + <xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/> + </xul:box> + <xul:box class="panel-arrowcontent" xbl:inherits="side,align,dir,orient,pack" flex="1"> + <children/> + </xul:box> + </xul:vbox> + </content> + <implementation> + <field name="_fadeTimer">null</field> + <method name="sizeTo"> + <parameter name="aWidth"/> + <parameter name="aHeight"/> + <body> + <![CDATA[ + this.popupBoxObject.sizeTo(aWidth, aHeight); + if (this.state == "open") { + this.adjustArrowPosition(); + } + ]]> + </body> + </method> + <method name="moveToAnchor"> + <parameter name="aAnchorElement"/> + <parameter name="aPosition"/> + <parameter name="aX"/> + <parameter name="aY"/> + <parameter name="aAttributesOverride"/> + <body> + <![CDATA[ + this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride); + ]]> + </body> + </method> + <method name="adjustArrowPosition"> + <body> + <![CDATA[ + var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow"); + + var anchor = this.anchorNode; + if (!anchor) { + return; + } + + var container = document.getAnonymousElementByAttribute(this, "anonid", "container"); + var arrowbox = document.getAnonymousElementByAttribute(this, "anonid", "arrowbox"); + + var position = this.alignmentPosition; + var offset = this.alignmentOffset; + + this.setAttribute("arrowposition", position); + + if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) { + container.orient = "horizontal"; + arrowbox.orient = "vertical"; + if (position.indexOf("_after") > 0) { + arrowbox.pack = "end"; + } else { + arrowbox.pack = "start"; + } + arrowbox.style.transform = "translate(0, " + -offset + "px)"; + + // The assigned side stays the same regardless of direction. + var isRTL = (window.getComputedStyle(this).direction == "rtl"); + + if (position.indexOf("start_") == 0) { + container.dir = "reverse"; + this.setAttribute("side", isRTL ? "left" : "right"); + } + else { + container.dir = ""; + this.setAttribute("side", isRTL ? "right" : "left"); + } + } + else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) { + container.orient = ""; + arrowbox.orient = ""; + if (position.indexOf("_end") > 0) { + arrowbox.pack = "end"; + } else { + arrowbox.pack = "start"; + } + arrowbox.style.transform = "translate(" + -offset + "px, 0)"; + + if (position.indexOf("before_") == 0) { + container.dir = "reverse"; + this.setAttribute("side", "bottom"); + } + else { + container.dir = ""; + this.setAttribute("side", "top"); + } + } + ]]> + </body> + </method> + </implementation> + <handlers> + <handler event="popupshowing" phase="target"> + <![CDATA[ + var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow"); + arrow.hidden = this.anchorNode == null; + document.getAnonymousElementByAttribute(this, "anonid", "arrowbox") + .style.removeProperty("transform"); + + this.adjustArrowPosition(); + + if (this.getAttribute("animate") != "false") { + this.setAttribute("animate", "open"); + } + + // set fading + var fade = this.getAttribute("fade"); + var fadeDelay = 0; + if (fade == "fast") { + fadeDelay = 1; + } + else if (fade == "slow") { + fadeDelay = 4000; + } + else { + return; + } + + this._fadeTimer = setTimeout(() => this.hidePopup(true), fadeDelay, this); + ]]> + </handler> + <handler event="popuphiding" phase="target"> + let animate = (this.getAttribute("animate") != "false"); + + if (this._fadeTimer) { + clearTimeout(this._fadeTimer); + if (animate) { + this.setAttribute("animate", "fade"); + } + } + else if (animate) { + this.setAttribute("animate", "cancel"); + } + </handler> + <handler event="popupshown" phase="target"> + this.setAttribute("panelopen", "true"); + </handler> + <handler event="popuphidden" phase="target"> + this.removeAttribute("panelopen"); + if (this.getAttribute("animate") != "false") { + this.removeAttribute("animate"); + } + </handler> + <handler event="popuppositioned" phase="target"> + this.adjustArrowPosition(); + </handler> + </handlers> + </binding> + + <binding id="tooltip" role="xul:tooltip" + extends="chrome://global/content/bindings/popup.xml#popup-base"> + <content> + <children> + <xul:label class="tooltip-label" xbl:inherits="xbl:text=label" flex="1"/> + </children> + </content> + + <implementation> + <field name="_mouseOutCount">0</field> + <field name="_isMouseOver">false</field> + + <property name="label" + onget="return this.getAttribute('label');" + onset="this.setAttribute('label', val); return val;"/> + + <property name="page" onset="if (val) this.setAttribute('page', 'true'); + else this.removeAttribute('page'); + return val;" + onget="return this.getAttribute('page') == 'true';"/> + <property name="textProvider" + readonly="true"> + <getter> + <![CDATA[ + if (!this._textProvider) { + this._textProvider = Components.classes["@mozilla.org/embedcomp/default-tooltiptextprovider;1"] + .getService(Components.interfaces.nsITooltipTextProvider); + } + return this._textProvider; + ]]> + </getter> + </property> + + <!-- Given the supplied element within a page, set the tooltip's text to the text + for that element. Returns true if text was assigned, and false if the no text + is set, which normally would be used to cancel tooltip display. + --> + <method name="fillInPageTooltip"> + <parameter name="tipElement"/> + <body> + <![CDATA[ + let tttp = this.textProvider; + let textObj = {}, dirObj = {}; + let shouldChangeText = tttp.getNodeText(tipElement, textObj, dirObj); + if (shouldChangeText) { + this.style.direction = dirObj.value; + this.label = textObj.value; + } + return shouldChangeText; + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="mouseover"><![CDATA[ + var rel = event.relatedTarget; + if (!rel) + return; + + // find out if the node we entered from is one of our anonymous children + while (rel) { + if (rel == this) + break; + rel = rel.parentNode; + } + + // if the exited node is not a descendant of ours, we are entering for the first time + if (rel != this) + this._isMouseOver = true; + ]]></handler> + + <handler event="mouseout"><![CDATA[ + var rel = event.relatedTarget; + + // relatedTarget is null when the titletip is first shown: a mouseout event fires + // because the mouse is exiting the main window and entering the titletip "window". + // relatedTarget is also null when the mouse exits the main window completely, + // so count how many times relatedTarget was null after titletip is first shown + // and hide popup the 2nd time + if (!rel) { + ++this._mouseOutCount; + if (this._mouseOutCount > 1) + this.hidePopup(); + return; + } + + // find out if the node we are entering is one of our anonymous children + while (rel) { + if (rel == this) + break; + rel = rel.parentNode; + } + + // if the entered node is not a descendant of ours, hide the tooltip + if (rel != this && this._isMouseOver) { + this.hidePopup(); + } + ]]></handler> + + <handler event="popupshowing"><![CDATA[ + if (this.page && !this.fillInPageTooltip(this.triggerNode)) { + event.preventDefault(); + } + ]]></handler> + + <handler event="popuphiding"><![CDATA[ + this._isMouseOver = false; + this._mouseOutCount = 0; + ]]></handler> + </handlers> + </binding> + + <binding id="popup-scrollbars" extends="chrome://global/content/bindings/popup.xml#popup"> + <content> + <xul:hbox class="popup-internal-box" flex="1" orient="vertical" style="overflow: auto;"> + <children/> + </xul:hbox> + </content> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/preferences.xml b/toolkit/content/widgets/preferences.xml new file mode 100644 index 0000000000..9fb2ac7ea1 --- /dev/null +++ b/toolkit/content/widgets/preferences.xml @@ -0,0 +1,1411 @@ +<?xml version="1.0"?> + +<!DOCTYPE bindings [ + <!ENTITY % preferencesDTD SYSTEM "chrome://global/locale/preferences.dtd"> + %preferencesDTD; + <!ENTITY % globalKeysDTD SYSTEM "chrome://global/locale/globalKeys.dtd"> + %globalKeysDTD; +]> + +<bindings id="preferencesBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +# +# = Preferences Window Framework +# +# The syntax for use looks something like: +# +# <prefwindow> +# <prefpane id="prefPaneA"> +# <preferences> +# <preference id="preference1" name="app.preference1" type="bool" onchange="foo();"/> +# <preference id="preference2" name="app.preference2" type="bool" useDefault="true"/> +# </preferences> +# <checkbox label="Preference" preference="preference1"/> +# </prefpane> +# </prefwindow> +# + + <binding id="preferences"> + <implementation implements="nsIObserver"> + <method name="_constructAfterChildren"> + <body> + <![CDATA[ + // This method will be called after each one of the child + // <preference> elements is constructed. Its purpose is to propagate + // the values to the associated form elements + + var elements = this.getElementsByTagName("preference"); + for (let element of elements) { + if (!element._constructed) { + return; + } + } + for (let element of elements) { + element.updateElements(); + } + ]]> + </body> + </method> + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body> + <![CDATA[ + for (var i = 0; i < this.childNodes.length; ++i) { + var preference = this.childNodes[i]; + if (preference.name == aData) { + preference.value = preference.valueFromPreferences; + } + } + ]]> + </body> + </method> + + <method name="fireChangedEvent"> + <parameter name="aPreference"/> + <body> + <![CDATA[ + // Value changed, synthesize an event + try { + var event = document.createEvent("Events"); + event.initEvent("change", true, true); + aPreference.dispatchEvent(event); + } + catch (e) { + Components.utils.reportError(e); + } + ]]> + </body> + </method> + + <field name="service"> + Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + </field> + <field name="rootBranch"> + Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + </field> + <field name="defaultBranch"> + this.service.getDefaultBranch(""); + </field> + <field name="rootBranchInternal"> + Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranchInternal); + </field> + <property name="type" readonly="true"> + <getter> + <![CDATA[ + return document.documentElement.type || ""; + ]]> + </getter> + </property> + <property name="instantApply" readonly="true"> + <getter> + <![CDATA[ + var doc = document.documentElement; + return this.type == "child" ? doc.instantApply + : doc.instantApply || this.rootBranch.getBoolPref("browser.preferences.instantApply"); + ]]> + </getter> + </property> + </implementation> + </binding> + + <binding id="preference"> + <implementation> + <constructor> + <![CDATA[ + this._constructed = true; + + // if the element has been inserted without the name attribute set, + // we have nothing to do here + if (!this.name) + return; + + this.preferences.rootBranchInternal + .addObserver(this.name, this.preferences, false); + // In non-instant apply mode, we must try and use the last saved state + // from any previous opens of a child dialog instead of the value from + // preferences, to pick up any edits a user may have made. + + var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Components.interfaces.nsIScriptSecurityManager); + if (this.preferences.type == "child" && + !this.instantApply && window.opener && + secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) { + var pdoc = window.opener.document; + + // Try to find a preference element for the same preference. + var preference = null; + var parentPreferences = pdoc.getElementsByTagName("preferences"); + for (var k = 0; (k < parentPreferences.length && !preference); ++k) { + var parentPrefs = parentPreferences[k] + .getElementsByAttribute("name", this.name); + for (var l = 0; (l < parentPrefs.length && !preference); ++l) { + if (parentPrefs[l].localName == "preference") + preference = parentPrefs[l]; + } + } + + // Don't use the value setter here, we don't want updateElements to be prematurely fired. + this._value = preference ? preference.value : this.valueFromPreferences; + } + else + this._value = this.valueFromPreferences; + this.preferences._constructAfterChildren(); + ]]> + </constructor> + <destructor> + this.preferences.rootBranchInternal + .removeObserver(this.name, this.preferences); + </destructor> + <field name="_constructed">false</field> + <property name="instantApply"> + <getter> + if (this.getAttribute("instantApply") == "false") + return false; + return this.getAttribute("instantApply") == "true" || this.preferences.instantApply; + </getter> + </property> + + <property name="preferences" onget="return this.parentNode"/> + <property name="name" onget="return this.getAttribute('name');"> + <setter> + if (val == this.name) + return val; + + this.preferences.rootBranchInternal + .removeObserver(this.name, this.preferences); + this.setAttribute('name', val); + this.preferences.rootBranchInternal + .addObserver(val, this.preferences, false); + + return val; + </setter> + </property> + <property name="type" onget="return this.getAttribute('type');" + onset="this.setAttribute('type', val); return val;"/> + <property name="inverted" onget="return this.getAttribute('inverted') == 'true';" + onset="this.setAttribute('inverted', val); return val;"/> + <property name="readonly" onget="return this.getAttribute('readonly') == 'true';" + onset="this.setAttribute('readonly', val); return val;"/> + + <field name="_value">null</field> + <method name="_setValue"> + <parameter name="aValue"/> + <body> + <![CDATA[ + if (this.value !== aValue) { + this._value = aValue; + if (this.instantApply) + this.valueFromPreferences = aValue; + this.preferences.fireChangedEvent(this); + } + return aValue; + ]]> + </body> + </method> + <property name="value" onget="return this._value" onset="return this._setValue(val);"/> + + <property name="locked"> + <getter> + return this.preferences.rootBranch.prefIsLocked(this.name); + </getter> + </property> + + <property name="disabled"> + <getter> + return this.getAttribute("disabled") == "true"; + </getter> + <setter> + <![CDATA[ + if (val) + this.setAttribute("disabled", "true"); + else + this.removeAttribute("disabled"); + + if (!this.id) + return val; + + var elements = document.getElementsByAttribute("preference", this.id); + for (var i = 0; i < elements.length; ++i) { + elements[i].disabled = val; + + var labels = document.getElementsByAttribute("control", elements[i].id); + for (var j = 0; j < labels.length; ++j) + labels[j].disabled = val; + } + + return val; + ]]> + </setter> + </property> + + <property name="tabIndex"> + <getter> + return parseInt(this.getAttribute("tabindex")); + </getter> + <setter> + <![CDATA[ + if (val) + this.setAttribute("tabindex", val); + else + this.removeAttribute("tabindex"); + + if (!this.id) + return val; + + var elements = document.getElementsByAttribute("preference", this.id); + for (var i = 0; i < elements.length; ++i) { + elements[i].tabIndex = val; + + var labels = document.getElementsByAttribute("control", elements[i].id); + for (var j = 0; j < labels.length; ++j) + labels[j].tabIndex = val; + } + + return val; + ]]> + </setter> + </property> + + <property name="hasUserValue"> + <getter> + <![CDATA[ + return this.preferences.rootBranch.prefHasUserValue(this.name) && + this.value !== undefined; + ]]> + </getter> + </property> + + <method name="reset"> + <body> + // defer reset until preference update + this.value = undefined; + </body> + </method> + + <field name="_useDefault">false</field> + <property name="defaultValue"> + <getter> + <![CDATA[ + this._useDefault = true; + var val = this.valueFromPreferences; + this._useDefault = false; + return val; + ]]> + </getter> + </property> + + <property name="_branch"> + <getter> + return this._useDefault ? this.preferences.defaultBranch : this.preferences.rootBranch; + </getter> + </property> + + <field name="batching">false</field> + + <method name="_reportUnknownType"> + <body> + <![CDATA[ + var consoleService = Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService); + var msg = "<preference> with id='" + this.id + "' and name='" + + this.name + "' has unknown type '" + this.type + "'."; + consoleService.logStringMessage(msg); + ]]> + </body> + </method> + + <property name="valueFromPreferences"> + <getter> + <![CDATA[ + try { + // Force a resync of value with preferences. + switch (this.type) { + case "int": + return this._branch.getIntPref(this.name); + case "bool": + var val = this._branch.getBoolPref(this.name); + return this.inverted ? !val : val; + case "wstring": + return this._branch + .getComplexValue(this.name, Components.interfaces.nsIPrefLocalizedString) + .data; + case "string": + case "unichar": + return this._branch + .getComplexValue(this.name, Components.interfaces.nsISupportsString) + .data; + case "fontname": + var family = this._branch + .getComplexValue(this.name, Components.interfaces.nsISupportsString) + .data; + var fontEnumerator = Components.classes["@mozilla.org/gfx/fontenumerator;1"] + .createInstance(Components.interfaces.nsIFontEnumerator); + return fontEnumerator.getStandardFamilyName(family); + case "file": + var f = this._branch + .getComplexValue(this.name, Components.interfaces.nsILocalFile); + return f; + default: + this._reportUnknownType(); + } + } + catch (e) { } + return null; + ]]> + </getter> + <setter> + <![CDATA[ + // Exit early if nothing to do. + if (this.readonly || this.valueFromPreferences == val) + return val; + + // The special value undefined means 'reset preference to default'. + if (val === undefined) { + this.preferences.rootBranch.clearUserPref(this.name); + return val; + } + + // Force a resync of preferences with value. + switch (this.type) { + case "int": + this.preferences.rootBranch.setIntPref(this.name, val); + break; + case "bool": + this.preferences.rootBranch.setBoolPref(this.name, this.inverted ? !val : val); + break; + case "wstring": + var pls = Components.classes["@mozilla.org/pref-localizedstring;1"] + .createInstance(Components.interfaces.nsIPrefLocalizedString); + pls.data = val; + this.preferences.rootBranch + .setComplexValue(this.name, Components.interfaces.nsIPrefLocalizedString, pls); + break; + case "string": + case "unichar": + case "fontname": + var iss = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); + iss.data = val; + this.preferences.rootBranch + .setComplexValue(this.name, Components.interfaces.nsISupportsString, iss); + break; + case "file": + var lf; + if (typeof(val) == "string") { + lf = Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsILocalFile); + lf.persistentDescriptor = val; + if (!lf.exists()) + lf.initWithPath(val); + } + else + lf = val.QueryInterface(Components.interfaces.nsILocalFile); + this.preferences.rootBranch + .setComplexValue(this.name, Components.interfaces.nsILocalFile, lf); + break; + default: + this._reportUnknownType(); + } + if (!this.batching) + this.preferences.service.savePrefFile(null); + return val; + ]]> + </setter> + </property> + + <method name="setElementValue"> + <parameter name="aElement"/> + <body> + <![CDATA[ + if (this.locked) + aElement.disabled = true; + + if (!this.isElementEditable(aElement)) + return; + + var rv = undefined; + if (aElement.hasAttribute("onsyncfrompreference")) { + // Value changed, synthesize an event + try { + var event = document.createEvent("Events"); + event.initEvent("syncfrompreference", true, true); + var f = new Function ("event", + aElement.getAttribute("onsyncfrompreference")); + rv = f.call(aElement, event); + } + catch (e) { + Components.utils.reportError(e); + } + } + var val = rv; + if (val === undefined) + val = this.instantApply ? this.valueFromPreferences : this.value; + // if the preference is marked for reset, show default value in UI + if (val === undefined) + val = this.defaultValue; + + /** + * Initialize a UI element property with a value. Handles the case + * where an element has not yet had a XBL binding attached for it and + * the property setter does not yet exist by setting the same attribute + * on the XUL element using DOM apis and assuming the element's + * constructor or property getters appropriately handle this state. + */ + function setValue(element, attribute, value) { + if (attribute in element) + element[attribute] = value; + else + element.setAttribute(attribute, value); + } + if (aElement.localName == "checkbox" || + aElement.localName == "listitem") + setValue(aElement, "checked", val); + else if (aElement.localName == "colorpicker") + setValue(aElement, "color", val); + else if (aElement.localName == "textbox") { + // XXXmano Bug 303998: Avoid a caret placement issue if either the + // preference observer or its setter calls updateElements as a result + // of the input event handler. + if (aElement.value !== val) + setValue(aElement, "value", val); + } + else + setValue(aElement, "value", val); + ]]> + </body> + </method> + + <method name="getElementValue"> + <parameter name="aElement"/> + <body> + <![CDATA[ + if (aElement.hasAttribute("onsynctopreference")) { + // Value changed, synthesize an event + try { + var event = document.createEvent("Events"); + event.initEvent("synctopreference", true, true); + var f = new Function ("event", + aElement.getAttribute("onsynctopreference")); + var rv = f.call(aElement, event); + if (rv !== undefined) + return rv; + } + catch (e) { + Components.utils.reportError(e); + } + } + + /** + * Read the value of an attribute from an element, assuming the + * attribute is a property on the element's node API. If the property + * is not present in the API, then assume its value is contained in + * an attribute, as is the case before a binding has been attached. + */ + function getValue(element, attribute) { + if (attribute in element) + return element[attribute]; + return element.getAttribute(attribute); + } + if (aElement.localName == "checkbox" || + aElement.localName == "listitem") + var value = getValue(aElement, "checked"); + else if (aElement.localName == "colorpicker") + value = getValue(aElement, "color"); + else + value = getValue(aElement, "value"); + + switch (this.type) { + case "int": + return parseInt(value, 10) || 0; + case "bool": + return typeof(value) == "boolean" ? value : value == "true"; + } + return value; + ]]> + </body> + </method> + + <method name="isElementEditable"> + <parameter name="aElement"/> + <body> + <![CDATA[ + switch (aElement.localName) { + case "checkbox": + case "colorpicker": + case "radiogroup": + case "textbox": + case "listitem": + case "listbox": + case "menulist": + return true; + } + return aElement.getAttribute("preference-editable") == "true"; + ]]> + </body> + </method> + + <method name="updateElements"> + <body> + <![CDATA[ + if (!this.id) + return; + + // This "change" event handler tracks changes made to preferences by + // sources other than the user in this window. + var elements = document.getElementsByAttribute("preference", this.id); + for (var i = 0; i < elements.length; ++i) + this.setElementValue(elements[i]); + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="change"> + this.updateElements(); + </handler> + </handlers> + </binding> + + <binding id="prefwindow" + extends="chrome://global/content/bindings/dialog.xml#dialog"> + <resources> + <stylesheet src="chrome://global/skin/preferences.css"/> + </resources> + <content dlgbuttons="accept,cancel" persist="lastSelected screenX screenY" + closebuttonlabel="&preferencesCloseButton.label;" + closebuttonaccesskey="&preferencesCloseButton.accesskey;" + role="dialog" +#ifdef XP_WIN + title="&preferencesDefaultTitleWin.title;"> +#else + title="&preferencesDefaultTitleMac.title;"> +#endif + <xul:windowdragbox orient="vertical"> + <xul:radiogroup anonid="selector" orient="horizontal" class="paneSelector chromeclass-toolbar" + role="listbox"/> <!-- Expose to accessibility APIs as a listbox --> + </xul:windowdragbox> + <xul:hbox flex="1" class="paneDeckContainer"> + <xul:deck anonid="paneDeck" flex="1"> + <children includes="prefpane"/> + </xul:deck> + </xul:hbox> + <xul:hbox anonid="dlg-buttons" class="prefWindow-dlgbuttons" pack="end"> +#ifdef XP_UNIX + <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/> + <xul:button dlgtype="help" class="dialog-button" hidden="true" icon="help"/> + <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/> + <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/> + <xul:spacer anonid="spacer" flex="1"/> + <xul:button dlgtype="cancel" class="dialog-button" icon="cancel"/> + <xul:button dlgtype="accept" class="dialog-button" icon="accept"/> +#else + <xul:button dlgtype="extra2" class="dialog-button" hidden="true"/> + <xul:spacer anonid="spacer" flex="1"/> + <xul:button dlgtype="accept" class="dialog-button" icon="accept"/> + <xul:button dlgtype="extra1" class="dialog-button" hidden="true"/> + <xul:button dlgtype="cancel" class="dialog-button" icon="cancel"/> + <xul:button dlgtype="help" class="dialog-button" hidden="true" icon="help"/> + <xul:button dlgtype="disclosure" class="dialog-button" hidden="true"/> +#endif + </xul:hbox> + <xul:hbox> + <children/> + </xul:hbox> + </content> + <implementation implements="nsITimerCallback"> + <constructor> + <![CDATA[ + if (this.type != "child") { + if (!this._instantApplyInitialized) { + let psvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + this.instantApply = psvc.getBoolPref("browser.preferences.instantApply"); + } + if (this.instantApply) { + var docElt = document.documentElement; + var acceptButton = docElt.getButton("accept"); + acceptButton.hidden = true; + var cancelButton = docElt.getButton("cancel"); + if (/Mac/.test(navigator.platform)) { + // no buttons on Mac except Help + cancelButton.hidden = true; + // Move Help button to the end + document.getAnonymousElementByAttribute(this, "anonid", "spacer").hidden = true; + // Also, don't fire onDialogAccept on enter + acceptButton.disabled = true; + } else { + // morph the Cancel button into the Close button + cancelButton.setAttribute ("icon", "close"); + cancelButton.label = docElt.getAttribute("closebuttonlabel"); + cancelButton.accesskey = docElt.getAttribute("closebuttonaccesskey"); + } + } + } + this.setAttribute("animated", this._shouldAnimate ? "true" : "false"); + var panes = this.preferencePanes; + + var lastPane = null; + if (this.lastSelected) { + lastPane = document.getElementById(this.lastSelected); + if (!lastPane) { + this.lastSelected = ""; + } + } + + var paneToLoad; + if ("arguments" in window && window.arguments[0] && document.getElementById(window.arguments[0]) && document.getElementById(window.arguments[0]).nodeName == "prefpane") { + paneToLoad = document.getElementById(window.arguments[0]); + this.lastSelected = paneToLoad.id; + } + else if (lastPane) + paneToLoad = lastPane; + else + paneToLoad = panes[0]; + + for (var i = 0; i < panes.length; ++i) { + this._makePaneButton(panes[i]); + if (panes[i].loaded) { + // Inline pane content, fire load event to force initialization. + this._fireEvent("paneload", panes[i]); + } + } + this.showPane(paneToLoad); + + if (panes.length == 1) + this._selector.setAttribute("collapsed", "true"); + ]]> + </constructor> + + <destructor> + <![CDATA[ + // Release timers to avoid reference cycles. + if (this._animateTimer) { + this._animateTimer.cancel(); + this._animateTimer = null; + } + if (this._fadeTimer) { + this._fadeTimer.cancel(); + this._fadeTimer = null; + } + ]]> + </destructor> + + <!-- Derived bindings can set this to true to cause us to skip + reading the browser.preferences.instantApply pref in the constructor. + Then they can set instantApply to their wished value. --> + <field name="_instantApplyInitialized">false</field> + <!-- Controls whether changed pref values take effect immediately. --> + <field name="instantApply">false</field> + + <property name="preferencePanes" + onget="return this.getElementsByTagName('prefpane');"/> + + <property name="type" onget="return this.getAttribute('type');"/> + <property name="_paneDeck" + onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'paneDeck');"/> + <property name="_paneDeckContainer" + onget="return document.getAnonymousElementByAttribute(this, 'class', 'paneDeckContainer');"/> + <property name="_selector" + onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'selector');"/> + <property name="lastSelected" + onget="return this.getAttribute('lastSelected');"> + <setter> + this.setAttribute("lastSelected", val); + document.persist(this.id, "lastSelected"); + return val; + </setter> + </property> + <property name="currentPane" + onset="return this._currentPane = val;"> + <getter> + if (!this._currentPane) + this._currentPane = this.preferencePanes[0]; + + return this._currentPane; + </getter> + </property> + <field name="_currentPane">null</field> + + + <method name="_makePaneButton"> + <parameter name="aPaneElement"/> + <body> + <![CDATA[ + var radio = document.createElement("radio"); + radio.setAttribute("pane", aPaneElement.id); + radio.setAttribute("label", aPaneElement.label); + // Expose preference group choice to accessibility APIs as an unchecked list item + // The parent group is exposed to accessibility APIs as a list + if (aPaneElement.image) + radio.setAttribute("src", aPaneElement.image); + radio.style.listStyleImage = aPaneElement.style.listStyleImage; + this._selector.appendChild(radio); + return radio; + ]]> + </body> + </method> + + <method name="showPane"> + <parameter name="aPaneElement"/> + <body> + <![CDATA[ + if (!aPaneElement) + return; + + this._selector.selectedItem = document.getAnonymousElementByAttribute(this, "pane", aPaneElement.id); + if (!aPaneElement.loaded) { + let OverlayLoadObserver = function(aPane) + { + this._pane = aPane; + } + OverlayLoadObserver.prototype = { + _outer: this, + observe: function (aSubject, aTopic, aData) + { + this._pane.loaded = true; + this._outer._fireEvent("paneload", this._pane); + this._outer._selectPane(this._pane); + } + }; + + var obs = new OverlayLoadObserver(aPaneElement); + document.loadOverlay(aPaneElement.src, obs); + } + else + this._selectPane(aPaneElement); + ]]> + </body> + </method> + + <method name="_fireEvent"> + <parameter name="aEventName"/> + <parameter name="aTarget"/> + <body> + <![CDATA[ + // Panel loaded, synthesize a load event. + try { + var event = document.createEvent("Events"); + event.initEvent(aEventName, true, true); + var cancel = !aTarget.dispatchEvent(event); + if (aTarget.hasAttribute("on" + aEventName)) { + var fn = new Function ("event", aTarget.getAttribute("on" + aEventName)); + var rv = fn.call(aTarget, event); + if (rv == false) + cancel = true; + } + return !cancel; + } + catch (e) { + Components.utils.reportError(e); + } + return false; + ]]> + </body> + </method> + + <field name="_initialized">false</field> + <method name="_selectPane"> + <parameter name="aPaneElement"/> + <body> + <![CDATA[ + if (/Mac/.test(navigator.platform)) { + var paneTitle = aPaneElement.label; + if (paneTitle != "") + document.title = paneTitle; + } + var helpButton = document.documentElement.getButton("help"); + if (aPaneElement.helpTopic) + helpButton.hidden = false; + else + helpButton.hidden = true; + + // Find this pane's index in the deck and set the deck's + // selectedIndex to that value to switch to it. + var prefpanes = this.preferencePanes; + for (var i = 0; i < prefpanes.length; ++i) { + if (prefpanes[i] == aPaneElement) { + this._paneDeck.selectedIndex = i; + + if (this.type != "child") { + if (aPaneElement.hasAttribute("flex") && this._shouldAnimate && + prefpanes.length > 1) + aPaneElement.removeAttribute("flex"); + // Calling sizeToContent after the first prefpane is loaded + // will size the windows contents so style information is + // available to calculate correct sizing. + if (!this._initialized && prefpanes.length > 1) { + if (this._shouldAnimate) + this.style.minHeight = 0; + window.sizeToContent(); + } + + var oldPane = this.lastSelected ? document.getElementById(this.lastSelected) : this.preferencePanes[0]; + oldPane.selected = !(aPaneElement.selected = true); + this.lastSelected = aPaneElement.id; + this.currentPane = aPaneElement; + this._initialized = true; + + // Only animate if we've switched between prefpanes + if (this._shouldAnimate && oldPane.id != aPaneElement.id) { + aPaneElement.style.opacity = 0.0; + this.animate(oldPane, aPaneElement); + } + else if (!this._shouldAnimate && prefpanes.length > 1) { + var targetHeight = parseInt(window.getComputedStyle(this._paneDeckContainer, "").height); + var verticalPadding = parseInt(window.getComputedStyle(aPaneElement, "").paddingTop); + verticalPadding += parseInt(window.getComputedStyle(aPaneElement, "").paddingBottom); + if (aPaneElement.contentHeight > targetHeight - verticalPadding) { + // To workaround the bottom border of a groupbox from being + // cutoff an hbox with a class of bottomBox may enclose it. + // This needs to include its padding to resize properly. + // See bug 394433 + var bottomPadding = 0; + var bottomBox = aPaneElement.getElementsByAttribute("class", "bottomBox")[0]; + if (bottomBox) + bottomPadding = parseInt(window.getComputedStyle(bottomBox, "").paddingBottom); + window.innerHeight += bottomPadding + verticalPadding + aPaneElement.contentHeight - targetHeight; + } + + // XXX rstrong - extend the contents of the prefpane to + // prevent elements from being cutoff (see bug 349098). + if (aPaneElement.contentHeight + verticalPadding < targetHeight) + aPaneElement._content.style.height = targetHeight - verticalPadding + "px"; + } + } + break; + } + } + ]]> + </body> + </method> + + <property name="_shouldAnimate"> + <getter> + <![CDATA[ + var psvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + var animate = /Mac/.test(navigator.platform); + try { + animate = psvc.getBoolPref("browser.preferences.animateFadeIn"); + } + catch (e) { } + return animate; + ]]> + </getter> + </property> + + <method name="animate"> + <parameter name="aOldPane"/> + <parameter name="aNewPane"/> + <body> + <![CDATA[ + // if we are already resizing, use currentHeight + var oldHeight = this._currentHeight ? this._currentHeight : aOldPane.contentHeight; + + this._multiplier = aNewPane.contentHeight > oldHeight ? 1 : -1; + var sizeDelta = Math.abs(oldHeight - aNewPane.contentHeight); + this._animateRemainder = sizeDelta % this._animateIncrement; + + this._setUpAnimationTimer(oldHeight); + ]]> + </body> + </method> + + <property name="_sizeIncrement"> + <getter> + <![CDATA[ + var lastSelectedPane = document.getElementById(this.lastSelected); + var increment = this._animateIncrement * this._multiplier; + var newHeight = this._currentHeight + increment; + if ((this._multiplier > 0 && this._currentHeight >= lastSelectedPane.contentHeight) || + (this._multiplier < 0 && this._currentHeight <= lastSelectedPane.contentHeight)) + return 0; + + if ((this._multiplier > 0 && newHeight > lastSelectedPane.contentHeight) || + (this._multiplier < 0 && newHeight < lastSelectedPane.contentHeight)) + increment = this._animateRemainder * this._multiplier; + return increment; + ]]> + </getter> + </property> + + <method name="notify"> + <parameter name="aTimer"/> + <body> + <![CDATA[ + if (!document) + aTimer.cancel(); + + if (aTimer == this._animateTimer) { + var increment = this._sizeIncrement; + if (increment != 0) { + window.innerHeight += increment; + this._currentHeight += increment; + } + else { + aTimer.cancel(); + this._setUpFadeTimer(); + } + } else if (aTimer == this._fadeTimer) { + var elt = document.getElementById(this.lastSelected); + var newOpacity = parseFloat(window.getComputedStyle(elt, "").opacity) + this._fadeIncrement; + if (newOpacity < 1.0) + elt.style.opacity = newOpacity; + else { + aTimer.cancel(); + elt.style.opacity = 1.0; + } + } + ]]> + </body> + </method> + + <method name="_setUpAnimationTimer"> + <parameter name="aStartHeight"/> + <body> + <![CDATA[ + if (!this._animateTimer) + this._animateTimer = Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + else + this._animateTimer.cancel(); + this._currentHeight = aStartHeight; + + this._animateTimer.initWithCallback(this, this._animateDelay, + Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); + ]]> + </body> + </method> + + <method name="_setUpFadeTimer"> + <body> + <![CDATA[ + if (!this._fadeTimer) + this._fadeTimer = Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + else + this._fadeTimer.cancel(); + + this._fadeTimer.initWithCallback(this, this._fadeDelay, + Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); + ]]> + </body> + </method> + + <field name="_animateTimer">null</field> + <field name="_fadeTimer">null</field> + <field name="_animateDelay">15</field> + <field name="_animateIncrement">40</field> + <field name="_fadeDelay">5</field> + <field name="_fadeIncrement">0.40</field> + <field name="_animateRemainder">0</field> + <field name="_currentHeight">0</field> + <field name="_multiplier">0</field> + + <method name="addPane"> + <parameter name="aPaneElement"/> + <body> + <![CDATA[ + this.appendChild(aPaneElement); + + // Set up pane button + this._makePaneButton(aPaneElement); + ]]> + </body> + </method> + + <method name="openSubDialog"> + <parameter name="aURL"/> + <parameter name="aFeatures"/> + <parameter name="aParams"/> + <body> + return openDialog(aURL, "", "modal,centerscreen,resizable=no" + (aFeatures != "" ? ("," + aFeatures) : ""), aParams); + </body> + </method> + + <method name="openWindow"> + <parameter name="aWindowType"/> + <parameter name="aURL"/> + <parameter name="aFeatures"/> + <parameter name="aParams"/> + <body> + <![CDATA[ + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var win = aWindowType ? wm.getMostRecentWindow(aWindowType) : null; + if (win) { + if ("initWithParams" in win) + win.initWithParams(aParams); + win.focus(); + } + else { + var features = "resizable,dialog=no,centerscreen" + (aFeatures != "" ? ("," + aFeatures) : ""); + var parentWindow = (this.instantApply || !window.opener || window.opener.closed) ? window : window.opener; + win = parentWindow.openDialog(aURL, "_blank", features, aParams); + } + return win; + ]]> + </body> + </method> + </implementation> + <handlers> + <handler event="dialogaccept"> + <![CDATA[ + if (!this._fireEvent("beforeaccept", this)) { + return false; + } + + var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Components.interfaces.nsIScriptSecurityManager); + if (this.type == "child" && window.opener && + secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) { + let psvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + var pdocEl = window.opener.document.documentElement; + if (pdocEl.instantApply) { + let panes = this.preferencePanes; + for (let i = 0; i < panes.length; ++i) + panes[i].writePreferences(true); + } + else { + // Clone all the preferences elements from the child document and + // insert them into the pane collection of the parent. + var pdoc = window.opener.document; + if (pdoc.documentElement.localName == "prefwindow") { + var currentPane = pdoc.documentElement.currentPane; + var id = window.location.href + "#childprefs"; + var childPrefs = pdoc.getElementById(id); + if (!childPrefs) { + childPrefs = pdoc.createElement("preferences"); + currentPane.appendChild(childPrefs); + childPrefs.id = id; + } + let panes = this.preferencePanes; + for (let i = 0; i < panes.length; ++i) { + var preferences = panes[i].preferences; + for (var j = 0; j < preferences.length; ++j) { + // Try to find a preference element for the same preference. + var preference = null; + var parentPreferences = pdoc.getElementsByTagName("preferences"); + for (var k = 0; (k < parentPreferences.length && !preference); ++k) { + var parentPrefs = parentPreferences[k] + .getElementsByAttribute("name", preferences[j].name); + for (var l = 0; (l < parentPrefs.length && !preference); ++l) { + if (parentPrefs[l].localName == "preference") + preference = parentPrefs[l]; + } + } + if (!preference) { + // No matching preference in the parent window. + preference = pdoc.createElement("preference"); + childPrefs.appendChild(preference); + preference.name = preferences[j].name; + preference.type = preferences[j].type; + preference.inverted = preferences[j].inverted; + preference.readonly = preferences[j].readonly; + preference.disabled = preferences[j].disabled; + } + preference.value = preferences[j].value; + } + } + } + } + } + else { + let panes = this.preferencePanes; + for (var i = 0; i < panes.length; ++i) + panes[i].writePreferences(false); + + let psvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + psvc.savePrefFile(null); + } + + return true; + ]]> + </handler> + <handler event="command"> + if (event.originalTarget.hasAttribute("pane")) { + var pane = document.getElementById(event.originalTarget.getAttribute("pane")); + this.showPane(pane); + } + </handler> + + <handler event="keypress" key="&windowClose.key;" modifiers="accel" phase="capturing"> + <![CDATA[ + if (this.instantApply) + window.close(); + event.stopPropagation(); + event.preventDefault(); + ]]> + </handler> + + <handler event="keypress" +#ifdef XP_MACOSX + key="&openHelpMac.commandkey;" modifiers="accel" +#else + keycode="&openHelp.commandkey;" +#endif + phase="capturing"> + <![CDATA[ + var helpButton = this.getButton("help"); + if (helpButton.disabled || helpButton.hidden) + return; + this._fireEvent("dialoghelp", this); + event.stopPropagation(); + event.preventDefault(); + ]]> + </handler> + </handlers> + </binding> + + <binding id="prefpane"> + <resources> + <stylesheet src="chrome://global/skin/preferences.css"/> + </resources> + <content> + <xul:vbox class="content-box" xbl:inherits="flex"> + <children/> + </xul:vbox> + </content> + <implementation> + <method name="writePreferences"> + <parameter name="aFlushToDisk"/> + <body> + <![CDATA[ + // Write all values to preferences. + if (this._deferredValueUpdateElements.size) { + this._finalizeDeferredElements(); + } + + var preferences = this.preferences; + for (var i = 0; i < preferences.length; ++i) { + var preference = preferences[i]; + preference.batching = true; + preference.valueFromPreferences = preference.value; + preference.batching = false; + } + if (aFlushToDisk) { + var psvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + psvc.savePrefFile(null); + } + ]]> + </body> + </method> + + <property name="src" + onget="return this.getAttribute('src');" + onset="this.setAttribute('src', val); return val;"/> + <property name="selected" + onget="return this.getAttribute('selected') == 'true';" + onset="this.setAttribute('selected', val); return val;"/> + <property name="image" + onget="return this.getAttribute('image');" + onset="this.setAttribute('image', val); return val;"/> + <property name="label" + onget="return this.getAttribute('label');" + onset="this.setAttribute('label', val); return val;"/> + + <property name="preferenceElements" + onget="return this.getElementsByAttribute('preference', '*');"/> + <property name="preferences" + onget="return this.getElementsByTagName('preference');"/> + + <property name="helpTopic"> + <getter> + <![CDATA[ + // if there are tabs, and the selected tab provides a helpTopic, return that + var box = this.getElementsByTagName("tabbox"); + if (box[0]) { + var tab = box[0].selectedTab; + if (tab && tab.hasAttribute("helpTopic")) + return tab.getAttribute("helpTopic"); + } + + // otherwise, return the helpTopic of the current panel + return this.getAttribute("helpTopic"); + ]]> + </getter> + </property> + + <field name="_loaded">false</field> + <property name="loaded" + onget="return !this.src ? true : this._loaded;" + onset="this._loaded = val; return val;"/> + + <method name="preferenceForElement"> + <parameter name="aElement"/> + <body> + return document.getElementById(aElement.getAttribute("preference")); + </body> + </method> + + <method name="getPreferenceElement"> + <parameter name="aStartElement"/> + <body> + <![CDATA[ + var temp = aStartElement; + while (temp && temp.nodeType == Node.ELEMENT_NODE && + !temp.hasAttribute("preference")) + temp = temp.parentNode; + return temp.nodeType == Node.ELEMENT_NODE ? temp : aStartElement; + ]]> + </body> + </method> + + <property name="DeferredTask" readonly="true"> + <getter><![CDATA[ + let module = {}; + Components.utils.import("resource://gre/modules/DeferredTask.jsm", module); + Object.defineProperty(this, "DeferredTask", { + configurable: true, + enumerable: true, + writable: true, + value: module.DeferredTask + }); + return module.DeferredTask; + ]]></getter> + </property> + <method name="_deferredValueUpdate"> + <parameter name="aElement"/> + <body> + <![CDATA[ + delete aElement._deferredValueUpdateTask; + let preference = document.getElementById(aElement.getAttribute("preference")); + let prefVal = preference.getElementValue(aElement); + preference.value = prefVal; + this._deferredValueUpdateElements.delete(aElement); + ]]> + </body> + </method> + <field name="_deferredValueUpdateElements"> + new Set(); + </field> + <method name="_finalizeDeferredElements"> + <body> + <![CDATA[ + for (let el of this._deferredValueUpdateElements) { + if (el._deferredValueUpdateTask) { + el._deferredValueUpdateTask.finalize(); + } + } + ]]> + </body> + </method> + <method name="userChangedValue"> + <parameter name="aElement"/> + <body> + <![CDATA[ + let element = this.getPreferenceElement(aElement); + if (element.hasAttribute("preference")) { + if (element.getAttribute("delayprefsave") != "true") { + var preference = document.getElementById(element.getAttribute("preference")); + var prefVal = preference.getElementValue(element); + preference.value = prefVal; + } else { + if (!element._deferredValueUpdateTask) { + element._deferredValueUpdateTask = new this.DeferredTask(this._deferredValueUpdate.bind(this, element), 1000); + this._deferredValueUpdateElements.add(element); + } else { + // Each time the preference is changed, restart the delay. + element._deferredValueUpdateTask.disarm(); + } + element._deferredValueUpdateTask.arm(); + } + } + ]]> + </body> + </method> + + <property name="contentHeight"> + <getter> + var targetHeight = parseInt(window.getComputedStyle(this._content, "").height); + targetHeight += parseInt(window.getComputedStyle(this._content, "").marginTop); + targetHeight += parseInt(window.getComputedStyle(this._content, "").marginBottom); + return targetHeight; + </getter> + </property> + <field name="_content"> + document.getAnonymousElementByAttribute(this, "class", "content-box"); + </field> + </implementation> + <handlers> + <handler event="command"> + // This "command" event handler tracks changes made to preferences by + // the user in this window. + if (event.sourceEvent) + event = event.sourceEvent; + this.userChangedValue(event.target); + </handler> + <handler event="select"> + // This "select" event handler tracks changes made to colorpicker + // preferences by the user in this window. + if (event.target.localName == "colorpicker") + this.userChangedValue(event.target); + </handler> + <handler event="change"> + // This "change" event handler tracks changes made to preferences by + // the user in this window. + this.userChangedValue(event.target); + </handler> + <handler event="input"> + // This "input" event handler tracks changes made to preferences by + // the user in this window. + this.userChangedValue(event.target); + </handler> + <handler event="paneload"> + <![CDATA[ + // Initialize all values from preferences. + var elements = this.preferenceElements; + for (var i = 0; i < elements.length; ++i) { + try { + var preference = this.preferenceForElement(elements[i]); + preference.setElementValue(elements[i]); + } + catch (e) { + dump("*** No preference found for " + elements[i].getAttribute("preference") + "\n"); + } + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="panebutton" role="xul:listitem" + extends="chrome://global/content/bindings/radio.xml#radio"> + <resources> + <stylesheet src="chrome://global/skin/preferences.css"/> + </resources> + <content> + <xul:image class="paneButtonIcon" xbl:inherits="src"/> + <xul:label class="paneButtonLabel" xbl:inherits="value=label"/> + </content> + </binding> + +</bindings> + +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# 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 is PrefWindow 6. The Code Could Well Be Ready, Are You? +# +# Historical References: +# PrefWindow V (February 1, 2003) +# PrefWindow IV (April 24, 2000) +# PrefWindow III (January 6, 2000) +# PrefWindow II (???) +# PrefWindow I (June 4, 1999) +# diff --git a/toolkit/content/widgets/progressmeter.xml b/toolkit/content/widgets/progressmeter.xml new file mode 100644 index 0000000000..82f28ffbad --- /dev/null +++ b/toolkit/content/widgets/progressmeter.xml @@ -0,0 +1,116 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="progressmeterBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="progressmeter" role="xul:progressmeter"> + <resources> + <stylesheet src="chrome://global/skin/progressmeter.css"/> + </resources> + + <content> + <xul:spacer class="progress-bar" xbl:inherits="mode"/> + <xul:spacer class="progress-remainder" xbl:inherits="mode"/> + </content> + + <implementation> + <property name="mode" onset="if (this.mode != val) this.setAttribute('mode', val); return val;" + onget="return this.getAttribute('mode');"/> + + <property name="value" onget="return this.getAttribute('value') || '0';"> + <setter><![CDATA[ + var p = Math.round(val); + var max = Math.round(this.max); + if (p < 0) + p = 0; + else if (p > max) + p = max; + var c = this.value; + if (p != c) { + var delta = p - c; + if (delta < 0) + delta = -delta; + if (delta > 3 || p == 0 || p == max) { + this.setAttribute("value", p); + // Fire DOM event so that accessible value change events occur + var event = document.createEvent('Events'); + event.initEvent('ValueChange', true, true); + this.dispatchEvent(event); + } + } + + return val; + ]]></setter> + </property> + <property name="max" + onget="return this.getAttribute('max') || '100';" + onset="this.setAttribute('max', isNaN(val) ? 100 : Math.max(val, 1)); + this.value = this.value; + return val;" /> + </implementation> + </binding> + + <binding id="progressmeter-undetermined" + extends="chrome://global/content/bindings/progressmeter.xml#progressmeter"> + <content> + <xul:stack class="progress-remainder" flex="1" anonid="stack" style="overflow: -moz-hidden-unscrollable;"> + <xul:spacer class="progress-bar" anonid="spacer" top="0" style="margin-right: -1000px;"/> + </xul:stack> + </content> + + <implementation> + <field name="_alive">true</field> + <method name="_init"> + <body><![CDATA[ + var stack = + document.getAnonymousElementByAttribute(this, "anonid", "stack"); + var spacer = + document.getAnonymousElementByAttribute(this, "anonid", "spacer"); + var isLTR = + document.defaultView.getComputedStyle(this, null).direction == "ltr"; + var startTime = performance.now(); + var self = this; + + function nextStep(t) { + try { + var width = stack.boxObject.width; + if (!width) { + // Maybe we've been removed from the document. + if (self._alive) + requestAnimationFrame(nextStep); + return; + } + + var elapsedTime = t - startTime; + + // Width of chunk is 1/5 (determined by the ratio 2000:400) of the + // total width of the progress bar. The left edge of the chunk + // starts at -1 and moves all the way to 4. It covers the distance + // in 2 seconds. + var position = isLTR ? ((elapsedTime % 2000) / 400) - 1 : + ((elapsedTime % 2000) / -400) + 4; + + width = width >> 2; + spacer.height = stack.boxObject.height; + spacer.width = width; + spacer.left = width * position; + + requestAnimationFrame(nextStep); + } catch (e) { + } + } + requestAnimationFrame(nextStep); + ]]></body> + </method> + + <constructor>this._init();</constructor> + </implementation> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/radio.xml b/toolkit/content/widgets/radio.xml new file mode 100644 index 0000000000..de3acfbf64 --- /dev/null +++ b/toolkit/content/widgets/radio.xml @@ -0,0 +1,526 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="radioBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="radiogroup" role="xul:radiogroup" + extends="chrome://global/content/bindings/general.xml#basecontrol"> + <resources> + <stylesheet src="chrome://global/skin/radio.css"/> + </resources> + + <implementation implements="nsIDOMXULSelectControlElement"> + <constructor> + <![CDATA[ + if (this.getAttribute("disabled") == "true") + this.disabled = true; + + var children = this._getRadioChildren(); + var length = children.length; + for (var i = 0; i < length; i++) { + if (children[i].getAttribute("selected") == "true") { + this.selectedIndex = i; + return; + } + } + + var value = this.value; + if (value) + this.value = value; + else + this.selectedIndex = 0; + ]]> + </constructor> + + <property name="value" onget="return this.getAttribute('value');"> + <setter> + <![CDATA[ + this.setAttribute("value", val); + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; i++) { + if (String(children[i].value) == String(val)) { + this.selectedItem = children[i]; + break; + } + } + return val; + ]]> + </setter> + </property> + <property name="disabled"> + <getter> + <![CDATA[ + if (this.getAttribute('disabled') == 'true') + return true; + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (!children[i].hidden && !children[i].collapsed && !children[i].disabled) + return false; + } + return true; + ]]> + </getter> + <setter> + <![CDATA[ + if (val) + this.setAttribute('disabled', 'true'); + else + this.removeAttribute('disabled'); + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + children[i].disabled = val; + } + return val; + ]]> + </setter> + </property> + + <property name="itemCount" readonly="true" + onget="return this._getRadioChildren().length"/> + + <property name="selectedIndex"> + <getter> + <![CDATA[ + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (children[i].selected) + return i; + } + return -1; + ]]> + </getter> + <setter> + <![CDATA[ + this.selectedItem = this._getRadioChildren()[val]; + return val; + ]]> + </setter> + </property> + + <property name="selectedItem"> + <getter> + <![CDATA[ + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (children[i].selected) + return children[i]; + } + return null; + ]]> + </getter> + <setter> + <![CDATA[ + var focused = this.getAttribute("focused") == "true"; + var alreadySelected = false; + + if (val) { + alreadySelected = val.getAttribute("selected") == "true"; + val.setAttribute("focused", focused); + val.setAttribute("selected", "true"); + this.setAttribute("value", val.value); + } + else { + this.removeAttribute("value"); + } + + // uncheck all other group nodes + var children = this._getRadioChildren(); + var previousItem = null; + for (var i = 0; i < children.length; ++i) { + if (children[i] != val) { + if (children[i].getAttribute("selected") == "true") + previousItem = children[i]; + + children[i].removeAttribute("selected"); + children[i].removeAttribute("focused"); + } + } + + var event = document.createEvent("Events"); + event.initEvent("select", false, true); + this.dispatchEvent(event); + + if (!alreadySelected && focused) { + // Only report if actual change + var myEvent; + if (val) { + myEvent = document.createEvent("Events"); + myEvent.initEvent("RadioStateChange", true, true); + val.dispatchEvent(myEvent); + } + + if (previousItem) { + myEvent = document.createEvent("Events"); + myEvent.initEvent("RadioStateChange", true, true); + previousItem.dispatchEvent(myEvent); + } + } + + return val; + ]]> + </setter> + </property> + + <property name="focusedItem"> + <getter> + <![CDATA[ + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (children[i].getAttribute("focused") == "true") + return children[i]; + } + return null; + ]]> + </getter> + <setter> + <![CDATA[ + if (val) val.setAttribute("focused", "true"); + + // unfocus all other group nodes + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (children[i] != val) + children[i].removeAttribute("focused"); + } + return val; + ]]> + </setter> + </property> + + <method name="checkAdjacentElement"> + <parameter name="aNextFlag"/> + <body> + <![CDATA[ + var currentElement = this.focusedItem || this.selectedItem; + var i; + var children = this._getRadioChildren(); + for (i = 0; i < children.length; ++i ) { + if (children[i] == currentElement) + break; + } + var index = i; + + if (aNextFlag) { + do { + if (++i == children.length) + i = 0; + if (i == index) + break; + } + while (children[i].hidden || children[i].collapsed || children[i].disabled); + // XXX check for display/visibility props too + + this.selectedItem = children[i]; + children[i].doCommand(); + } + else { + do { + if (i == 0) + i = children.length; + if (--i == index) + break; + } + while (children[i].hidden || children[i].collapsed || children[i].disabled); + // XXX check for display/visibility props too + + this.selectedItem = children[i]; + children[i].doCommand(); + } + ]]> + </body> + </method> + <field name="_radioChildren">null</field> + <method name="_getRadioChildren"> + <body> + <![CDATA[ + if (this._radioChildren) + return this._radioChildren; + + var radioChildren = []; + var doc = this.ownerDocument; + + if (this.hasChildNodes()) { + // Don't store the collected child nodes immediately, + // collecting the child nodes could trigger constructors + // which would blow away our list. + + const nsIDOMNodeFilter = Components.interfaces.nsIDOMNodeFilter; + var iterator = doc.createTreeWalker(this, + nsIDOMNodeFilter.SHOW_ELEMENT, + this._filterRadioGroup); + while (iterator.nextNode()) + radioChildren.push(iterator.currentNode); + return this._radioChildren = radioChildren; + } + + // We don't have child nodes. + const XUL_NS = "http://www.mozilla.org/keymaster/" + + "gatekeeper/there.is.only.xul"; + var elems = doc.getElementsByAttribute("group", this.id); + for (var i = 0; i < elems.length; i++) { + if ((elems[i].namespaceURI == XUL_NS) && + (elems[i].localName == "radio")) { + radioChildren.push(elems[i]); + } + } + return this._radioChildren = radioChildren; + ]]> + </body> + </method> + <method name="_filterRadioGroup"> + <parameter name="node"/> + <body> + <![CDATA[ + switch (node.localName) { + case "radio": return NodeFilter.FILTER_ACCEPT; + case "template": + case "radiogroup": return NodeFilter.FILTER_REJECT; + default: return NodeFilter.FILTER_SKIP; + } + ]]> + </body> + </method> + + <method name="getIndexOfItem"> + <parameter name="item"/> + <body> + return this._getRadioChildren().indexOf(item); + </body> + </method> + + <method name="getItemAtIndex"> + <parameter name="index"/> + <body> + <![CDATA[ + var children = this._getRadioChildren(); + return (index >= 0 && index < children.length) ? children[index] : null; + ]]> + </body> + </method> + + <method name="appendItem"> + <parameter name="label"/> + <parameter name="value"/> + <body> + <![CDATA[ + var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var radio = document.createElementNS(XULNS, "radio"); + radio.setAttribute("label", label); + radio.setAttribute("value", value); + this.appendChild(radio); + this._radioChildren = null; + return radio; + ]]> + </body> + </method> + + <method name="insertItemAt"> + <parameter name="index"/> + <parameter name="label"/> + <parameter name="value"/> + <body> + <![CDATA[ + var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var radio = document.createElementNS(XULNS, "radio"); + radio.setAttribute("label", label); + radio.setAttribute("value", value); + var before = this.getItemAtIndex(index); + if (before) + before.parentNode.insertBefore(radio, before); + else + this.appendChild(radio); + this._radioChildren = null; + return radio; + ]]> + </body> + </method> + + <method name="removeItemAt"> + <parameter name="index"/> + <body> + <![CDATA[ + var remove = this.getItemAtIndex(index); + if (remove) { + remove.parentNode.removeChild(remove); + this._radioChildren = null; + } + return remove; + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="mousedown"> + if (this.disabled) + event.preventDefault(); + </handler> + + <!-- keyboard navigation --> + <!-- Here's how keyboard navigation works in radio groups on Windows: + The group takes 'focus' + The user is then free to navigate around inside the group + using the arrow keys. Accessing previous or following radio buttons + is done solely through the arrow keys and not the tab button. Tab + takes you to the next widget in the tab order --> + <handler event="keypress" key=" " phase="target"> + this.selectedItem = this.focusedItem; + this.selectedItem.doCommand(); + // Prevent page from scrolling on the space key. + event.preventDefault(); + </handler> + <handler event="keypress" keycode="VK_UP" phase="target"> + this.checkAdjacentElement(false); + event.stopPropagation(); + event.preventDefault(); + </handler> + <handler event="keypress" keycode="VK_LEFT" phase="target"> + // left arrow goes back when we are ltr, forward when we are rtl + this.checkAdjacentElement(document.defaultView.getComputedStyle( + this, "").direction == "rtl"); + event.stopPropagation(); + event.preventDefault(); + </handler> + <handler event="keypress" keycode="VK_DOWN" phase="target"> + this.checkAdjacentElement(true); + event.stopPropagation(); + event.preventDefault(); + </handler> + <handler event="keypress" keycode="VK_RIGHT" phase="target"> + // right arrow goes forward when we are ltr, back when we are rtl + this.checkAdjacentElement(document.defaultView.getComputedStyle( + this, "").direction == "ltr"); + event.stopPropagation(); + event.preventDefault(); + </handler> + + <!-- set a focused attribute on the selected item when the group + receives focus so that we can style it as if it were focused even though + it is not (Windows platform behaviour is for the group to receive focus, + not the item --> + <handler event="focus" phase="target"> + <![CDATA[ + this.setAttribute("focused", "true"); + if (this.focusedItem) + return; + + var val = this.selectedItem; + if (!val || val.disabled || val.hidden || val.collapsed) { + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (!children[i].hidden && !children[i].collapsed && !children[i].disabled) { + val = children[i]; + break; + } + } + } + this.focusedItem = val; + ]]> + </handler> + <handler event="blur" phase="target"> + this.removeAttribute("focused"); + this.focusedItem = null; + </handler> + </handlers> + </binding> + + <binding id="radio" role="xul:radiobutton" + extends="chrome://global/content/bindings/general.xml#control-item"> + <resources> + <stylesheet src="chrome://global/skin/radio.css"/> + </resources> + + <content> + <xul:image class="radio-check" xbl:inherits="disabled,selected"/> + <xul:hbox class="radio-label-box" align="center" flex="1"> + <xul:image class="radio-icon" xbl:inherits="src"/> + <xul:label class="radio-label" xbl:inherits="xbl:text=label,accesskey,crop" flex="1"/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMXULSelectControlItemElement"> + <constructor> + <![CDATA[ + // Just clear out the parent's cached list of radio children + var control = this.control; + if (control) + control._radioChildren = null; + ]]> + </constructor> + <destructor> + <![CDATA[ + if (!this.control) + return; + + var radioList = this.control._radioChildren; + if (!radioList) + return; + for (var i = 0; i < radioList.length; ++i) { + if (radioList[i] == this) { + radioList.splice(i, 1); + return; + } + } + ]]> + </destructor> + <property name="selected" readonly="true"> + <getter> + <![CDATA[ + return this.hasAttribute('selected'); + ]]> + </getter> + </property> + <property name="radioGroup" readonly="true" onget="return this.control"/> + <property name="control" readonly="true"> + <getter> + <![CDATA[ + const XUL_NS = "http://www.mozilla.org/keymaster/" + + "gatekeeper/there.is.only.xul"; + var parent = this.parentNode; + while (parent) { + if ((parent.namespaceURI == XUL_NS) && + (parent.localName == "radiogroup")) { + return parent; + } + parent = parent.parentNode; + } + + var group = this.getAttribute("group"); + if (!group) { + return null; + } + + parent = this.ownerDocument.getElementById(group); + if (!parent || + (parent.namespaceURI != XUL_NS) || + (parent.localName != "radiogroup")) { + parent = null; + } + return parent; + ]]> + </getter> + </property> + </implementation> + <handlers> + <handler event="click" button="0"> + <![CDATA[ + if (!this.disabled) + this.control.selectedItem = this; + ]]> + </handler> + + <handler event="mousedown" button="0"> + <![CDATA[ + if (!this.disabled) + this.control.focusedItem = this; + ]]> + </handler> + </handlers> + </binding> +</bindings> diff --git a/toolkit/content/widgets/remote-browser.xml b/toolkit/content/widgets/remote-browser.xml new file mode 100644 index 0000000000..b78179944d --- /dev/null +++ b/toolkit/content/widgets/remote-browser.xml @@ -0,0 +1,591 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<bindings id="firefoxBrowserBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="remote-browser" extends="chrome://global/content/bindings/browser.xml#browser"> + + <implementation type="application/javascript" + implements="nsIObserver, nsIDOMEventListener, nsIMessageListener, nsIRemoteBrowser"> + + <field name="_securityUI">null</field> + + <property name="securityUI" + readonly="true"> + <getter><![CDATA[ + if (!this._securityUI) { + // Don't attempt to create the remote web progress if the + // messageManager has already gone away + if (!this.messageManager) + return null; + + let jsm = "resource://gre/modules/RemoteSecurityUI.jsm"; + let RemoteSecurityUI = Components.utils.import(jsm, {}).RemoteSecurityUI; + this._securityUI = new RemoteSecurityUI(); + } + + // We want to double-wrap the JS implemented interface, so that QI and instanceof works. + var ptr = Components.classes["@mozilla.org/supports-interface-pointer;1"] + .createInstance(Components.interfaces.nsISupportsInterfacePointer); + ptr.data = this._securityUI; + return ptr.data.QueryInterface(Components.interfaces.nsISecureBrowserUI); + ]]></getter> + </property> + + <!-- increases or decreases the browser's network priority --> + <method name="adjustPriority"> + <parameter name="adjustment"/> + <body><![CDATA[ + this.messageManager.sendAsyncMessage("NetworkPrioritizer:AdjustPriority", + {adjustment}); + ]]></body> + </method> + + <!-- sets the browser's network priority to a discrete value --> + <method name="setPriority"> + <parameter name="priority"/> + <body><![CDATA[ + this.messageManager.sendAsyncMessage("NetworkPrioritizer:SetPriority", + {priority}); + ]]></body> + </method> + + <field name="_controller">null</field> + + <field name="_selectParentHelper">null</field> + + <field name="_remoteWebNavigation">null</field> + + <property name="webNavigation" + onget="return this._remoteWebNavigation;" + readonly="true"/> + + <field name="_remoteWebProgress">null</field> + + <property name="webProgress" readonly="true"> + <getter> + <![CDATA[ + if (!this._remoteWebProgress) { + // Don't attempt to create the remote web progress if the + // messageManager has already gone away + if (!this.messageManager) + return null; + + let jsm = "resource://gre/modules/RemoteWebProgress.jsm"; + let { RemoteWebProgressManager } = Components.utils.import(jsm, {}); + this._remoteWebProgressManager = new RemoteWebProgressManager(this); + this._remoteWebProgress = this._remoteWebProgressManager.topLevelWebProgress; + } + return this._remoteWebProgress; + ]]> + </getter> + </property> + + <field name="_remoteFinder">null</field> + + <property name="finder" readonly="true"> + <getter><![CDATA[ + if (!this._remoteFinder) { + // Don't attempt to create the remote finder if the + // messageManager has already gone away + if (!this.messageManager) + return null; + + let jsm = "resource://gre/modules/RemoteFinder.jsm"; + let { RemoteFinder } = Components.utils.import(jsm, {}); + this._remoteFinder = new RemoteFinder(this); + } + return this._remoteFinder; + ]]></getter> + </property> + + <field name="_documentURI">null</field> + + <field name="_documentContentType">null</field> + + <!-- + Used by session restore to ensure that currentURI is set so + that switch-to-tab works before the tab is fully + restored. This function also invokes onLocationChanged + listeners in tabbrowser.xml. + --> + <method name="_setCurrentURI"> + <parameter name="aURI"/> + <body><![CDATA[ + this._remoteWebProgressManager.setCurrentURI(aURI); + ]]></body> + </method> + + <property name="documentURI" + onget="return this._documentURI;" + readonly="true"/> + + <property name="documentContentType" + onget="return this._documentContentType;" + readonly="true"/> + + <field name="_contentTitle">""</field> + + <property name="contentTitle" + onget="return this._contentTitle" + readonly="true"/> + + <field name="_characterSet">""</field> + + <property name="characterSet" + onget="return this._characterSet"> + <setter><![CDATA[ + this.messageManager.sendAsyncMessage("UpdateCharacterSet", {value: val}); + this._characterSet = val; + ]]></setter> + </property> + + <field name="_mayEnableCharacterEncodingMenu">null</field> + + <property name="mayEnableCharacterEncodingMenu" + onget="return this._mayEnableCharacterEncodingMenu;" + readonly="true"/> + + <field name="_contentWindow">null</field> + + <property name="contentWindow" + onget="return null" + readonly="true"/> + + <property name="contentWindowAsCPOW" + onget="return this._contentWindow" + readonly="true"/> + + <property name="contentDocument" + onget="return null" + readonly="true"/> + + <field name="_contentPrincipal">null</field> + + <property name="contentPrincipal" + onget="return this._contentPrincipal" + readonly="true"/> + + <property name="contentDocumentAsCPOW" + onget="return this.contentWindowAsCPOW ? this.contentWindowAsCPOW.document : null" + readonly="true"/> + + <field name="_imageDocument">null</field> + + <property name="imageDocument" + onget="return this._imageDocument" + readonly="true"/> + + <field name="_fullZoom">1</field> + <property name="fullZoom"> + <getter><![CDATA[ + return this._fullZoom; + ]]></getter> + <setter><![CDATA[ + let changed = val.toFixed(2) != this._fullZoom.toFixed(2); + + this._fullZoom = val; + this.messageManager.sendAsyncMessage("FullZoom", {value: val}); + + if (changed) { + let event = new Event("FullZoomChange", {bubbles: true}); + this.dispatchEvent(event); + } + ]]></setter> + </property> + + <field name="_textZoom">1</field> + <property name="textZoom"> + <getter><![CDATA[ + return this._textZoom; + ]]></getter> + <setter><![CDATA[ + let changed = val.toFixed(2) != this._textZoom.toFixed(2); + + this._textZoom = val; + this.messageManager.sendAsyncMessage("TextZoom", {value: val}); + + if (changed) { + let event = new Event("TextZoomChange", {bubbles: true}); + this.dispatchEvent(event); + } + ]]></setter> + </property> + + <field name="_isSyntheticDocument">false</field> + <property name="isSyntheticDocument"> + <getter><![CDATA[ + return this._isSyntheticDocument; + ]]></getter> + </property> + + <property name="hasContentOpener"> + <getter><![CDATA[ + let {frameLoader} = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner); + return frameLoader.tabParent.hasContentOpener; + ]]></getter> + </property> + + <field name="_outerWindowID">null</field> + <property name="outerWindowID" + onget="return this._outerWindowID" + readonly="true"/> + + <field name="_innerWindowID">null</field> + <property name="innerWindowID"> + <getter><![CDATA[ + return this._innerWindowID; + ]]></getter> + </property> + + <property name="docShellIsActive"> + <getter> + <![CDATA[ + let {frameLoader} = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner); + return frameLoader.tabParent.docShellIsActive; + ]]> + </getter> + <setter> + <![CDATA[ + let {frameLoader} = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner); + frameLoader.tabParent.docShellIsActive = val; + return val; + ]]> + </setter> + </property> + + <method name="preserveLayers"> + <parameter name="preserve"/> + <body><![CDATA[ + let {frameLoader} = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner); + if (frameLoader.tabParent) { + frameLoader.tabParent.preserveLayers(preserve); + } + ]]></body> + </method> + + <field name="_manifestURI"/> + <property name="manifestURI" + onget="return this._manifestURI" + readonly="true"/> + + <field name="mDestroyed">false</field> + + <field name="_permitUnloadId">0</field> + + <method name="getInPermitUnload"> + <parameter name="aCallback"/> + <body> + <![CDATA[ + let id = this._permitUnloadId++; + let mm = this.messageManager; + mm.sendAsyncMessage("InPermitUnload", {id}); + mm.addMessageListener("InPermitUnload", function listener(msg) { + if (msg.data.id != id) { + return; + } + aCallback(msg.data.inPermitUnload); + }); + ]]> + </body> + </method> + + <method name="permitUnload"> + <body> + <![CDATA[ + const kTimeout = 5000; + + let finished = false; + let responded = false; + let permitUnload; + let id = this._permitUnloadId++; + let mm = this.messageManager; + let Services = Components.utils.import("resource://gre/modules/Services.jsm", {}).Services; + + let msgListener = msg => { + if (msg.data.id != id) { + return; + } + if (msg.data.kind == "start") { + responded = true; + return; + } + done(msg.data.permitUnload); + }; + + let observer = subject => { + if (subject == mm) { + done(true); + } + }; + + function done(result) { + finished = true; + permitUnload = result; + mm.removeMessageListener("PermitUnload", msgListener); + Services.obs.removeObserver(observer, "message-manager-close"); + } + + mm.sendAsyncMessage("PermitUnload", {id}); + mm.addMessageListener("PermitUnload", msgListener); + Services.obs.addObserver(observer, "message-manager-close", false); + + let timedOut = false; + function timeout() { + if (!responded) { + timedOut = true; + } + + // Dispatch something to ensure that the main thread wakes up. + Services.tm.mainThread.dispatch(function() {}, Components.interfaces.nsIThread.DISPATCH_NORMAL); + } + + let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); + timer.initWithCallback(timeout, kTimeout, timer.TYPE_ONE_SHOT); + + while (!finished && !timedOut) { + Services.tm.currentThread.processNextEvent(true); + } + + return {permitUnload, timedOut}; + ]]> + </body> + </method> + + <constructor> + <![CDATA[ + /* + * Don't try to send messages from this function. The message manager for + * the <browser> element may not be initialized yet. + */ + + this._remoteWebNavigation = Components.classes["@mozilla.org/remote-web-navigation;1"] + .createInstance(Components.interfaces.nsIWebNavigation); + this._remoteWebNavigationImpl = this._remoteWebNavigation.wrappedJSObject; + this._remoteWebNavigationImpl.swapBrowser(this); + + // Initialize contentPrincipal to the about:blank principal for this loadcontext + let {Services} = Components.utils.import("resource://gre/modules/Services.jsm", {}); + let aboutBlank = Services.io.newURI("about:blank", null, null); + let ssm = Services.scriptSecurityManager; + this._contentPrincipal = ssm.getLoadContextCodebasePrincipal(aboutBlank, this.loadContext); + + this.messageManager.addMessageListener("Browser:Init", this); + this.messageManager.addMessageListener("DOMTitleChanged", this); + this.messageManager.addMessageListener("ImageDocumentLoaded", this); + this.messageManager.addMessageListener("FullZoomChange", this); + this.messageManager.addMessageListener("TextZoomChange", this); + this.messageManager.addMessageListener("ZoomChangeUsingMouseWheel", this); + this.messageManager.addMessageListener("DOMFullscreen:RequestExit", this); + this.messageManager.addMessageListener("DOMFullscreen:RequestRollback", this); + this.messageManager.addMessageListener("MozApplicationManifest", this); + this.messageManager.loadFrameScript("chrome://global/content/browser-child.js", true); + + if (this.hasAttribute("selectmenulist")) { + this.messageManager.addMessageListener("Forms:ShowDropDown", this); + this.messageManager.addMessageListener("Forms:HideDropDown", this); + this.messageManager.loadFrameScript("chrome://global/content/select-child.js", true); + } + + if (!this.hasAttribute("disablehistory")) { + Services.obs.addObserver(this, "browser:purge-session-history", true); + } + + let jsm = "resource://gre/modules/RemoteController.jsm"; + let RemoteController = Components.utils.import(jsm, {}).RemoteController; + this._controller = new RemoteController(this); + this.controllers.appendController(this._controller); + ]]> + </constructor> + + <destructor> + <![CDATA[ + this.destroy(); + ]]> + </destructor> + + <!-- This is necessary because the destructor doesn't always get called when + we are removed from a tabbrowser. This will be explicitly called by tabbrowser. + + Note: This overrides the destroy() method from browser.xml. --> + <method name="destroy"> + <body><![CDATA[ + // Make sure that any open select is closed. + if (this._selectParentHelper) { + let menulist = document.getElementById(this.getAttribute("selectmenulist")); + this._selectParentHelper.hide(menulist, this); + } + + if (this.mDestroyed) + return; + this.mDestroyed = true; + + try { + this.controllers.removeController(this._controller); + } catch (ex) { + // This can fail when this browser element is not attached to a + // BrowserDOMWindow. + } + + if (!this.hasAttribute("disablehistory")) { + let Services = Components.utils.import("resource://gre/modules/Services.jsm", {}).Services; + try { + Services.obs.removeObserver(this, "browser:purge-session-history"); + } catch (ex) { + // It's not clear why this sometimes throws an exception. + } + } + ]]></body> + </method> + + <method name="receiveMessage"> + <parameter name="aMessage"/> + <body><![CDATA[ + let data = aMessage.data; + switch (aMessage.name) { + case "Browser:Init": + this._outerWindowID = data.outerWindowID; + break; + case "DOMTitleChanged": + this._contentTitle = data.title; + break; + case "ImageDocumentLoaded": + this._imageDocument = { + width: data.width, + height: data.height + }; + break; + + case "Forms:ShowDropDown": { + if (!this._selectParentHelper) { + this._selectParentHelper = + Cu.import("resource://gre/modules/SelectParentHelper.jsm", {}).SelectParentHelper; + } + + let menulist = document.getElementById(this.getAttribute("selectmenulist")); + menulist.menupopup.style.direction = data.direction; + + let zoom = Services.prefs.getBoolPref("browser.zoom.full") || + this.isSyntheticDocument ? this._fullZoom : this._textZoom; + this._selectParentHelper.populate(menulist, data.options, data.selectedIndex, zoom); + this._selectParentHelper.open(this, menulist, data.rect, data.isOpenedViaTouch); + break; + } + + case "FullZoomChange": { + this._fullZoom = data.value; + let event = document.createEvent("Events"); + event.initEvent("FullZoomChange", true, false); + this.dispatchEvent(event); + break; + } + + case "TextZoomChange": { + this._textZoom = data.value; + let event = document.createEvent("Events"); + event.initEvent("TextZoomChange", true, false); + this.dispatchEvent(event); + break; + } + + case "ZoomChangeUsingMouseWheel": { + let event = document.createEvent("Events"); + event.initEvent("ZoomChangeUsingMouseWheel", true, false); + this.dispatchEvent(event); + break; + } + + case "Forms:HideDropDown": { + if (this._selectParentHelper) { + let menulist = document.getElementById(this.getAttribute("selectmenulist")); + this._selectParentHelper.hide(menulist, this); + } + break; + } + + case "DOMFullscreen:RequestExit": { + let windowUtils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils); + windowUtils.exitFullscreen(); + break; + } + + case "DOMFullscreen:RequestRollback": { + let windowUtils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils); + windowUtils.remoteFrameFullscreenReverted(); + break; + } + + case "MozApplicationManifest": + this._manifestURI = aMessage.data.manifest; + break; + + default: + // Delegate to browser.xml. + return this._receiveMessage(aMessage); + } + return undefined; + ]]></body> + </method> + + <method name="enableDisableCommands"> + <parameter name="aAction"/> + <parameter name="aEnabledLength"/> + <parameter name="aEnabledCommands"/> + <parameter name="aDisabledLength"/> + <parameter name="aDisabledCommands"/> + <body> + if (this._controller) { + this._controller.enableDisableCommands(aAction, + aEnabledLength, aEnabledCommands, + aDisabledLength, aDisabledCommands); + } + </body> + </method> + + <method name="purgeSessionHistory"> + <body> + <![CDATA[ + try { + this.messageManager.sendAsyncMessage("Browser:PurgeSessionHistory"); + } catch (ex) { + // This can throw if the browser has started to go away. + if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED) { + throw ex; + } + } + this._remoteWebNavigationImpl.canGoBack = false; + this._remoteWebNavigationImpl.canGoForward = false; + ]]> + </body> + </method> + + <method name="createAboutBlankContentViewer"> + <parameter name="aPrincipal"/> + <body> + <![CDATA[ + this.messageManager.sendAsyncMessage("Browser:CreateAboutBlank", aPrincipal); + ]]> + </body> + </method> + </implementation> + <handlers> + <handler event="dragstart"> + <![CDATA[ + // If we're a remote browser dealing with a dragstart, stop it + // from propagating up, since our content process should be dealing + // with the mouse movement. + event.stopPropagation(); + ]]> + </handler> + </handlers> + + </binding> + +</bindings> diff --git a/toolkit/content/widgets/resizer.xml b/toolkit/content/widgets/resizer.xml new file mode 100644 index 0000000000..006877a4f6 --- /dev/null +++ b/toolkit/content/widgets/resizer.xml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="resizerBindings" + xmlns="http://www.mozilla.org/xbl"> + + <binding id="resizer"> + <resources> + <stylesheet src="chrome://global/skin/resizer.css"/> + </resources> + <implementation> + <constructor> + <![CDATA[ + // don't do this for viewport resizers; causes a crash related to + // bugs 563665 and 581536 otherwise + if (this.parentNode == this.ownerDocument.documentElement) + return; + + // if the direction is rtl, set the rtl attribute so that the + // stylesheet can use this to make the cursor appear properly + var cs = window.getComputedStyle(this, ""); + if (cs.writingMode === undefined || cs.writingMode == "horizontal-tb") { + if (cs.direction == "rtl") { + this.setAttribute("rtl", "true"); + } + } else if (cs.writingMode.endsWith("-rl")) { + // writing-modes 'vertical-rl' and 'sideways-rl' want rtl resizers, + // as they will appear at the bottom left of the element + this.setAttribute("rtl", "true"); + } + ]]> + </constructor> + </implementation> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/richlistbox.xml b/toolkit/content/widgets/richlistbox.xml new file mode 100644 index 0000000000..dd04a0cfff --- /dev/null +++ b/toolkit/content/widgets/richlistbox.xml @@ -0,0 +1,589 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<bindings id="richlistboxBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="richlistbox" + extends="chrome://global/content/bindings/listbox.xml#listbox-base"> + <resources> + <stylesheet src="chrome://global/skin/richlistbox.css"/> + </resources> + + <content> + <children includes="listheader"/> + <xul:scrollbox allowevents="true" orient="vertical" anonid="main-box" + flex="1" style="overflow: auto;" xbl:inherits="dir,pack"> + <children/> + </xul:scrollbox> + </content> + + <implementation> + <field name="_scrollbox"> + document.getAnonymousElementByAttribute(this, "anonid", "main-box"); + </field> + <field name="scrollBoxObject"> + this._scrollbox.boxObject; + </field> + <constructor> + <![CDATA[ + // add a template build listener + if (this.builder) + this.builder.addListener(this._builderListener); + else + this._refreshSelection(); + ]]> + </constructor> + + <destructor> + <![CDATA[ + // remove the template build listener + if (this.builder) + this.builder.removeListener(this._builderListener); + ]]> + </destructor> + + <!-- Overriding baselistbox --> + <method name="_fireOnSelect"> + <body> + <![CDATA[ + // make sure not to modify last-selected when suppressing select events + // (otherwise we'll lose the selection when a template gets rebuilt) + if (this._suppressOnSelect || this.suppressOnSelect) + return; + + // remember the current item and all selected items with IDs + var state = this.currentItem ? this.currentItem.id : ""; + if (this.selType == "multiple" && this.selectedCount) { + let getId = function getId(aItem) { return aItem.id; } + state += " " + [... this.selectedItems].filter(getId).map(getId).join(" "); + } + if (state) + this.setAttribute("last-selected", state); + else + this.removeAttribute("last-selected"); + + // preserve the index just in case no IDs are available + if (this.currentIndex > -1) + this._currentIndex = this.currentIndex + 1; + + var event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + + // always call this (allows a commandupdater without controller) + document.commandDispatcher.updateCommands("richlistbox-select"); + ]]> + </body> + </method> + + <!-- We override base-listbox here because those methods don't take dir + into account on listbox (which doesn't support dir yet) --> + <method name="getNextItem"> + <parameter name="aStartItem"/> + <parameter name="aDelta"/> + <body> + <![CDATA[ + var prop = this.dir == "reverse" && this._mayReverse ? + "previousSibling" : + "nextSibling"; + while (aStartItem) { + aStartItem = aStartItem[prop]; + if (aStartItem && aStartItem instanceof + Components.interfaces.nsIDOMXULSelectControlItemElement && + (!this._userSelecting || this._canUserSelect(aStartItem))) { + --aDelta; + if (aDelta == 0) + return aStartItem; + } + } + return null; + ]]></body> + </method> + + <method name="getPreviousItem"> + <parameter name="aStartItem"/> + <parameter name="aDelta"/> + <body> + <![CDATA[ + var prop = this.dir == "reverse" && this._mayReverse ? + "nextSibling" : + "previousSibling"; + while (aStartItem) { + aStartItem = aStartItem[prop]; + if (aStartItem && aStartItem instanceof + Components.interfaces.nsIDOMXULSelectControlItemElement && + (!this._userSelecting || this._canUserSelect(aStartItem))) { + --aDelta; + if (aDelta == 0) + return aStartItem; + } + } + return null; + ]]> + </body> + </method> + + <method name="appendItem"> + <parameter name="aLabel"/> + <parameter name="aValue"/> + <body> + return this.insertItemAt(-1, aLabel, aValue); + </body> + </method> + + <method name="insertItemAt"> + <parameter name="aIndex"/> + <parameter name="aLabel"/> + <parameter name="aValue"/> + <body> + const XULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + var item = + this.ownerDocument.createElementNS(XULNS, "richlistitem"); + item.setAttribute("value", aValue); + + var label = this.ownerDocument.createElementNS(XULNS, "label"); + label.setAttribute("value", aLabel); + label.setAttribute("flex", "1"); + label.setAttribute("crop", "end"); + item.appendChild(label); + + var before = this.getItemAtIndex(aIndex); + if (!before) + this.appendChild(item); + else + this.insertBefore(item, before); + + return item; + </body> + </method> + + <property name="itemCount" readonly="true" + onget="return this.children.length"/> + + <method name="getIndexOfItem"> + <parameter name="aItem"/> + <body> + <![CDATA[ + // don't search the children, if we're looking for none of them + if (aItem == null) + return -1; + + return this.children.indexOf(aItem); + ]]> + </body> + </method> + + <method name="getItemAtIndex"> + <parameter name="aIndex"/> + <body> + return this.children[aIndex] || null; + </body> + </method> + + <method name="ensureIndexIsVisible"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + // work around missing implementation in scrollBoxObject + return this.ensureElementIsVisible(this.getItemAtIndex(aIndex)); + ]]> + </body> + </method> + + <method name="ensureElementIsVisible"> + <parameter name="aElement"/> + <body> + <![CDATA[ + if (!aElement) + return; + var targetRect = aElement.getBoundingClientRect(); + var scrollRect = this._scrollbox.getBoundingClientRect(); + var offset = targetRect.top - scrollRect.top; + if (offset >= 0) { + // scrollRect.bottom wouldn't take a horizontal scroll bar into account + let scrollRectBottom = scrollRect.top + this._scrollbox.clientHeight; + offset = targetRect.bottom - scrollRectBottom; + if (offset <= 0) + return; + } + this._scrollbox.scrollTop += offset; + ]]> + </body> + </method> + + <method name="scrollToIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + var item = this.getItemAtIndex(aIndex); + if (item) + this.scrollBoxObject.scrollToElement(item); + ]]> + </body> + </method> + + <method name="getNumberOfVisibleRows"> + <!-- returns the number of currently visible rows --> + <!-- don't rely on this function, if the items' height can vary! --> + <body> + <![CDATA[ + var children = this.children; + + for (var top = 0; top < children.length && !this._isItemVisible(children[top]); top++); + for (var ix = top; ix < children.length && this._isItemVisible(children[ix]); ix++); + + return ix - top; + ]]> + </body> + </method> + + <method name="getIndexOfFirstVisibleRow"> + <body> + <![CDATA[ + var children = this.children; + + for (var ix = 0; ix < children.length; ix++) + if (this._isItemVisible(children[ix])) + return ix; + + return -1; + ]]> + </body> + </method> + + <method name="getRowCount"> + <body> + <![CDATA[ + return this.children.length; + ]]> + </body> + </method> + + <method name="scrollOnePage"> + <parameter name="aDirection"/> <!-- Must be -1 or 1 --> + <body> + <![CDATA[ + var children = this.children; + + if (children.length == 0) + return 0; + + // If nothing is selected, we just select the first element + // at the extreme we're moving away from + if (!this.currentItem) + return aDirection == -1 ? children.length : 0; + + // If the current item is visible, scroll by one page so that + // the new current item is at approximately the same position as + // the existing current item. + if (this._isItemVisible(this.currentItem)) + this.scrollBoxObject.scrollBy(0, this.scrollBoxObject.height * aDirection); + + // Figure out, how many items fully fit into the view port + // (including the currently selected one), and determine + // the index of the first one lying (partially) outside + var height = this.scrollBoxObject.height; + var startBorder = this.currentItem.boxObject.y; + if (aDirection == -1) + startBorder += this.currentItem.boxObject.height; + + var index = this.currentIndex; + for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) { + var boxObject = children[ix].boxObject; + if (boxObject.height == 0) + continue; // hidden children have a y of 0 + var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0); + if ((endBorder - startBorder) * aDirection > height) + break; // we've reached the desired distance + index = ix; + } + + return index != this.currentIndex ? index - this.currentIndex : aDirection; + ]]> + </body> + </method> + + <!-- richlistbox specific --> + <property name="children" readonly="true"> + <getter> + <![CDATA[ + var childNodes = []; + var isReverse = this.dir == "reverse" && this._mayReverse; + var child = isReverse ? this.lastChild : this.firstChild; + var prop = isReverse ? "previousSibling" : "nextSibling"; + while (child) { + if (child instanceof Components.interfaces.nsIDOMXULSelectControlItemElement) + childNodes.push(child); + child = child[prop]; + } + return childNodes; + ]]> + </getter> + </property> + + <field name="_builderListener" readonly="true"> + <![CDATA[ + ({ + mOuter: this, + item: null, + willRebuild: function(builder) { }, + didRebuild: function(builder) { + this.mOuter._refreshSelection(); + } + }); + ]]> + </field> + + <method name="_refreshSelection"> + <body> + <![CDATA[ + // when this method is called, we know that either the currentItem + // and selectedItems we have are null (ctor) or a reference to an + // element no longer in the DOM (template). + + // first look for the last-selected attribute + var state = this.getAttribute("last-selected"); + if (state) { + var ids = state.split(" "); + + var suppressSelect = this._suppressOnSelect; + this._suppressOnSelect = true; + this.clearSelection(); + for (let i = 1; i < ids.length; i++) { + var selectedItem = document.getElementById(ids[i]); + if (selectedItem) + this.addItemToSelection(selectedItem); + } + + var currentItem = document.getElementById(ids[0]); + if (!currentItem && this._currentIndex) + currentItem = this.getItemAtIndex(Math.min( + this._currentIndex - 1, this.getRowCount())); + if (currentItem) { + this.currentItem = currentItem; + if (this.selType != "multiple" && this.selectedCount == 0) + this.selectedItem = currentItem; + + if (this.scrollBoxObject.height) { + this.ensureElementIsVisible(currentItem); + } + else { + // XXX hack around a bug in ensureElementIsVisible as it will + // scroll beyond the last element, bug 493645. + var previousElement = this.dir == "reverse" ? currentItem.nextSibling : + currentItem.previousSibling; + this.ensureElementIsVisible(previousElement); + } + } + this._suppressOnSelect = suppressSelect; + // XXX actually it's just a refresh, but at least + // the Extensions manager expects this: + this._fireOnSelect(); + return; + } + + // try to restore the selected items according to their IDs + // (applies after a template rebuild, if last-selected was not set) + if (this.selectedItems) { + let itemIds = []; + for (let i = this.selectedCount - 1; i >= 0; i--) { + let selectedItem = this.selectedItems[i]; + itemIds.push(selectedItem.id); + this.selectedItems.remove(selectedItem); + } + for (let i = 0; i < itemIds.length; i++) { + let selectedItem = document.getElementById(itemIds[i]); + if (selectedItem) { + this.selectedItems.append(selectedItem); + } + } + } + if (this.currentItem && this.currentItem.id) + this.currentItem = document.getElementById(this.currentItem.id); + else + this.currentItem = null; + + // if we have no previously current item or if the above check fails to + // find the previous nodes (which causes it to clear selection) + if (!this.currentItem && this.selectedCount == 0) { + this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0; + + // cf. listbox constructor: + // select items according to their attributes + var children = this.children; + for (let i = 0; i < children.length; ++i) { + if (children[i].getAttribute("selected") == "true") + this.selectedItems.append(children[i]); + } + } + + if (this.selType != "multiple" && this.selectedCount == 0) + this.selectedItem = this.currentItem; + ]]> + </body> + </method> + + <method name="_isItemVisible"> + <parameter name="aItem"/> + <body> + <![CDATA[ + if (!aItem) + return false; + + var y = this.scrollBoxObject.positionY + this.scrollBoxObject.y; + + // Partially visible items are also considered visible + return (aItem.boxObject.y + aItem.boxObject.height > y) && + (aItem.boxObject.y < y + this.scrollBoxObject.height); + ]]> + </body> + </method> + + <field name="_currentIndex">null</field> + + <!-- For backwards-compatibility and for convenience. + Use getIndexOfItem instead. --> + <method name="getIndexOf"> + <parameter name="aElement"/> + <body> + <![CDATA[ + return this.getIndexOfItem(aElement); + ]]> + </body> + </method> + + <!-- For backwards-compatibility and for convenience. + Use ensureElementIsVisible instead --> + <method name="ensureSelectedElementIsVisible"> + <body> + <![CDATA[ + return this.ensureElementIsVisible(this.selectedItem); + ]]> + </body> + </method> + + <!-- For backwards-compatibility and for convenience. + Use moveByOffset instead. --> + <method name="goUp"> + <body> + <![CDATA[ + var index = this.currentIndex; + this.moveByOffset(-1, true, false); + return index != this.currentIndex; + ]]> + </body> + </method> + <method name="goDown"> + <body> + <![CDATA[ + var index = this.currentIndex; + this.moveByOffset(1, true, false); + return index != this.currentIndex; + ]]> + </body> + </method> + + <!-- deprecated (is implied by currentItem and selectItem) --> + <method name="fireActiveItemEvent"><body/></method> + </implementation> + + <handlers> + <handler event="click"> + <![CDATA[ + // clicking into nothing should unselect + if (event.originalTarget == this._scrollbox) { + this.clearSelection(); + this.currentItem = null; + } + ]]> + </handler> + + <handler event="MozSwipeGesture"> + <![CDATA[ + // Only handle swipe gestures up and down + switch (event.direction) { + case event.DIRECTION_DOWN: + this._scrollbox.scrollTop = this._scrollbox.scrollHeight; + break; + case event.DIRECTION_UP: + this._scrollbox.scrollTop = 0; + break; + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="richlistitem" + extends="chrome://global/content/bindings/listbox.xml#listitem"> + <content> + <children/> + </content> + + <resources> + <stylesheet src="chrome://global/skin/richlistbox.css"/> + </resources> + + <implementation> + <destructor> + <![CDATA[ + var control = this.control; + if (!control) + return; + // When we are destructed and we are current or selected, unselect ourselves + // so that richlistbox's selection doesn't point to something not in the DOM. + // We don't want to reset last-selected, so we set _suppressOnSelect. + if (this.selected) { + var suppressSelect = control._suppressOnSelect; + control._suppressOnSelect = true; + control.removeItemFromSelection(this); + control._suppressOnSelect = suppressSelect; + } + if (this.current) + control.currentItem = null; + ]]> + </destructor> + + <property name="label" readonly="true"> + <!-- Setter purposely not implemented; the getter returns a + concatentation of label text to expose via accessibility APIs --> + <getter> + <![CDATA[ + const XULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + return Array.map(this.getElementsByTagNameNS(XULNS, "label"), + label => label.value) + .join(" "); + ]]> + </getter> + </property> + + <property name="searchLabel"> + <getter> + <![CDATA[ + return this.hasAttribute("searchlabel") ? + this.getAttribute("searchlabel") : this.label; + ]]> + </getter> + <setter> + <![CDATA[ + if (val !== null) + this.setAttribute("searchlabel", val); + else + // fall back to the label property (default value) + this.removeAttribute("searchlabel"); + return val; + ]]> + </setter> + </property> + </implementation> + </binding> +</bindings> diff --git a/toolkit/content/widgets/scale.xml b/toolkit/content/widgets/scale.xml new file mode 100644 index 0000000000..3e5f5aeb2b --- /dev/null +++ b/toolkit/content/widgets/scale.xml @@ -0,0 +1,232 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="scaleBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="scalethumb" extends="xul:button" role="xul:thumb"> + <resources> + <stylesheet src="chrome://global/skin/scale.css"/> + </resources> + </binding> + + <binding id="scaleslider" display="xul:slider" + extends="chrome://global/content/bindings/general.xml#basecontrol"> + <resources> + <stylesheet src="chrome://global/skin/scale.css"/> + </resources> + </binding> + + <binding id="scale" role="xul:scale" + extends="chrome://global/content/bindings/general.xml#basecontrol"> + <resources> + <stylesheet src="chrome://global/skin/scale.css"/> + </resources> + + <content align="center" pack="center"> + <xul:slider anonid="slider" class="scale-slider" snap="true" flex="1" + xbl:inherits="disabled,orient,dir,curpos=value,minpos=min,maxpos=max,increment,pageincrement,movetoclick"> + <xul:thumb class="scale-thumb" xbl:inherits="disabled,orient"/> + </xul:slider> + </content> + + <implementation implements="nsISliderListener"> + <property name="value" onget="return this._getIntegerAttribute('curpos', 0);" + onset="return this._setIntegerAttribute('curpos', val);"/> + <property name="min" onget="return this._getIntegerAttribute('minpos', 0);" + onset="return this._setIntegerAttribute('minpos', val);"/> + <property name="max" onget="return this._getIntegerAttribute('maxpos', 100);" + onset="return this._setIntegerAttribute('maxpos', val);"/> + <property name="increment" onget="return this._getIntegerAttribute('increment', 1);" + onset="return this._setIntegerAttribute('increment', val);"/> + <property name="pageIncrement" onget="return this._getIntegerAttribute('pageincrement', 10);" + onset="return this._setIntegerAttribute('pageincrement', val);"/> + + <property name="_slider" readonly="true"> + <getter> + if (!this._sliderElement) + this._sliderElement = document.getAnonymousElementByAttribute(this, "anonid", "slider"); + return this._sliderElement; + </getter> + </property> + + <constructor> + <![CDATA[ + this._userChanged = false; + var value = parseInt(this.getAttribute("value"), 10); + if (!isNaN(value)) + this.value = value; + else if (this.min > 0) + this.value = this.min; + else if (this.max < 0) + this.value = this.max; + ]]> + </constructor> + + <method name="_getIntegerAttribute"> + <parameter name="aAttr"/> + <parameter name="aDefaultValue"/> + <body> + var value = this._slider.getAttribute(aAttr); + var intvalue = parseInt(value, 10); + if (!isNaN(intvalue)) + return intvalue; + return aDefaultValue; + </body> + </method> + + <method name="_setIntegerAttribute"> + <parameter name="aAttr"/> + <parameter name="aValue"/> + <body> + <![CDATA[ + var intvalue = parseInt(aValue, 10); + if (!isNaN(intvalue)) { + if (aAttr == "curpos") { + if (intvalue < this.min) + intvalue = this.min; + else if (intvalue > this.max) + intvalue = this.max; + } + this._slider.setAttribute(aAttr, intvalue); + } + return aValue; + ]]> + </body> + </method> + + <method name="decrease"> + <body> + <![CDATA[ + var newpos = this.value - this.increment; + var startpos = this.min; + this.value = (newpos > startpos) ? newpos : startpos; + ]]> + </body> + </method> + <method name="increase"> + <body> + <![CDATA[ + var newpos = this.value + this.increment; + var endpos = this.max; + this.value = (newpos < endpos) ? newpos : endpos; + ]]> + </body> + </method> + + <method name="decreasePage"> + <body> + <![CDATA[ + var newpos = this.value - this.pageIncrement; + var startpos = this.min; + this.value = (newpos > startpos) ? newpos : startpos; + ]]> + </body> + </method> + <method name="increasePage"> + <body> + <![CDATA[ + var newpos = this.value + this.pageIncrement; + var endpos = this.max; + this.value = (newpos < endpos) ? newpos : endpos; + ]]> + </body> + </method> + + <method name="valueChanged"> + <parameter name="which"/> + <parameter name="newValue"/> + <parameter name="userChanged"/> + <body> + <![CDATA[ + switch (which) { + case "curpos": + this.setAttribute("value", newValue); + + // in the future, only fire the change event when userChanged + // or _userChanged is true + var changeEvent = document.createEvent("Events"); + changeEvent.initEvent("change", true, true); + this.dispatchEvent(changeEvent); + break; + + case "minpos": + this.setAttribute("min", newValue); + break; + + case "maxpos": + this.setAttribute("max", newValue); + break; + } + ]]> + </body> + </method> + + <method name="dragStateChanged"> + <parameter name="isDragging"/> + <body/> + </method> + </implementation> + + <handlers> + <handler event="keypress" keycode="VK_LEFT" preventdefault="true"> + <![CDATA[ + this._userChanged = true; + (this.orient != "vertical" && this.dir == "reverse") ? this.increase() : this.decrease(); + this._userChanged = false; + ]]> + </handler> + <handler event="keypress" keycode="VK_RIGHT" preventdefault="true"> + <![CDATA[ + this._userChanged = true; + (this.orient != "vertical" && this.dir == "reverse") ? this.decrease() : this.increase(); + this._userChanged = false; + ]]> + </handler> + <handler event="keypress" keycode="VK_UP" preventdefault="true"> + <![CDATA[ + this._userChanged = true; + (this.orient == "vertical" && this.dir != "reverse") ? this.decrease() : this.increase(); + this._userChanged = false; + ]]> + </handler> + <handler event="keypress" keycode="VK_DOWN" preventdefault="true"> + <![CDATA[ + this._userChanged = true; + (this.orient == "vertical" && this.dir != "reverse") ? this.increase() : this.decrease(); + this._userChanged = false; + ]]> + </handler> + <handler event="keypress" keycode="VK_PAGE_UP" preventdefault="true"> + <![CDATA[ + this._userChanged = true; + (this.orient == "vertical" && this.dir != "reverse") ? this.decreasePage() : this.increasePage(); + this._userChanged = false; + ]]> + </handler> + <handler event="keypress" keycode="VK_PAGE_DOWN" preventdefault="true"> + <![CDATA[ + this._userChanged = true; + (this.orient == "vertical" && this.dir != "reverse") ? this.increasePage() : this.decreasePage(); + this._userChanged = false; + ]]> + </handler> + <handler event="keypress" keycode="VK_HOME" preventdefault="true"> + this._userChanged = true; + this.value = (this.dir == "reverse") ? this.max : this.min; + this._userChanged = false; + </handler> + <handler event="keypress" keycode="VK_END" preventdefault="true"> + this._userChanged = true; + this.value = (this.dir == "reverse") ? this.min : this.max; + this._userChanged = false; + </handler> + </handlers> + + </binding> +</bindings> diff --git a/toolkit/content/widgets/scrollbar.xml b/toolkit/content/widgets/scrollbar.xml new file mode 100644 index 0000000000..ce2eff11b0 --- /dev/null +++ b/toolkit/content/widgets/scrollbar.xml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="scrollbarBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="thumb" extends="xul:button" /> + + <binding id="scrollbar-base" bindToUntrustedContent="true"> + <handlers> + <handler event="contextmenu" preventdefault="true" action="event.stopPropagation();"/> + <handler event="click" preventdefault="true" action="event.stopPropagation();"/> + <handler event="dblclick" action="event.stopPropagation();"/> + <handler event="command" action="event.stopPropagation();"/> + </handlers> + </binding> + + <binding id="scrollbar" bindToUntrustedContent="true" extends="chrome://global/content/bindings/scrollbar.xml#scrollbar-base"> + <content clickthrough="always"> + <xul:scrollbarbutton sbattr="scrollbar-up-top" type="decrement" xbl:inherits="curpos,maxpos,disabled,sborient=orient"/> + <xul:scrollbarbutton sbattr="scrollbar-down-top" type="increment" xbl:inherits="curpos,maxpos,disabled,sborient=orient"/> + <xul:slider flex="1" xbl:inherits="disabled,curpos,maxpos,pageincrement,increment,orient,sborient=orient"> + <xul:thumb sbattr="scrollbar-thumb" xbl:inherits="orient,sborient=orient,collapsed=disabled" + align="center" pack="center"/> + </xul:slider> + <xul:scrollbarbutton sbattr="scrollbar-up-bottom" type="decrement" xbl:inherits="curpos,maxpos,disabled,sborient=orient"/> + <xul:scrollbarbutton sbattr="scrollbar-down-bottom" type="increment" xbl:inherits="curpos,maxpos,disabled,sborient=orient"/> + </content> + </binding> +</bindings> diff --git a/toolkit/content/widgets/scrollbox.xml b/toolkit/content/widgets/scrollbox.xml new file mode 100644 index 0000000000..ff57a59114 --- /dev/null +++ b/toolkit/content/widgets/scrollbox.xml @@ -0,0 +1,908 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="arrowscrollboxBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="scrollbox-base" extends="chrome://global/content/bindings/general.xml#basecontrol"> + <resources> + <stylesheet src="chrome://global/skin/scrollbox.css"/> + </resources> + </binding> + + <binding id="scrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base"> + <content> + <xul:box class="box-inherit scrollbox-innerbox" xbl:inherits="orient,align,pack,dir" flex="1"> + <children/> + </xul:box> + </content> + </binding> + + <binding id="arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base"> + <content> + <xul:autorepeatbutton class="autorepeatbutton-up" + anonid="scrollbutton-up" + xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart" + oncommand="_autorepeatbuttonScroll(event);"/> + <xul:spacer class="arrowscrollbox-overflow-start-indicator" + xbl:inherits="collapsed=scrolledtostart"/> + <xul:scrollbox class="arrowscrollbox-scrollbox" + anonid="scrollbox" + flex="1" + xbl:inherits="orient,align,pack,dir"> + <children/> + </xul:scrollbox> + <xul:spacer class="arrowscrollbox-overflow-end-indicator" + xbl:inherits="collapsed=scrolledtoend"/> + <xul:autorepeatbutton class="autorepeatbutton-down" + anonid="scrollbutton-down" + xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend" + oncommand="_autorepeatbuttonScroll(event);"/> + </content> + + <implementation> + <constructor><![CDATA[ + this.setAttribute("notoverflowing", "true"); + this._updateScrollButtonsDisabledState(); + ]]></constructor> + + <destructor><![CDATA[ + this._stopSmoothScroll(); + ]]></destructor> + + <field name="_scrollbox"> + document.getAnonymousElementByAttribute(this, "anonid", "scrollbox"); + </field> + <field name="_scrollButtonUp"> + document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-up"); + </field> + <field name="_scrollButtonDown"> + document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-down"); + </field> + + <field name="__prefBranch">null</field> + <property name="_prefBranch" readonly="true"> + <getter><![CDATA[ + if (this.__prefBranch === null) { + this.__prefBranch = Components.classes['@mozilla.org/preferences-service;1'] + .getService(Components.interfaces.nsIPrefBranch); + } + return this.__prefBranch; + ]]></getter> + </property> + + <field name="_scrollIncrement">null</field> + <property name="scrollIncrement" readonly="true"> + <getter><![CDATA[ + if (this._scrollIncrement === null) { + try { + this._scrollIncrement = this._prefBranch + .getIntPref("toolkit.scrollbox.scrollIncrement"); + } + catch (ex) { + this._scrollIncrement = 20; + } + } + return this._scrollIncrement; + ]]></getter> + </property> + + <field name="_smoothScroll">null</field> + <property name="smoothScroll"> + <getter><![CDATA[ + if (this._smoothScroll === null) { + if (this.hasAttribute("smoothscroll")) { + this._smoothScroll = (this.getAttribute("smoothscroll") == "true"); + } else { + try { + this._smoothScroll = this._prefBranch + .getBoolPref("toolkit.scrollbox.smoothScroll"); + } + catch (ex) { + this._smoothScroll = true; + } + } + } + return this._smoothScroll; + ]]></getter> + <setter><![CDATA[ + this._smoothScroll = val; + return val; + ]]></setter> + </property> + + <field name="_scrollBoxObject">null</field> + <property name="scrollBoxObject" readonly="true"> + <getter><![CDATA[ + if (!this._scrollBoxObject) { + this._scrollBoxObject = this._scrollbox.boxObject; + } + return this._scrollBoxObject; + ]]></getter> + </property> + + <property name="scrollClientRect" readonly="true"> + <getter><![CDATA[ + return this._scrollbox.getBoundingClientRect(); + ]]></getter> + </property> + + <property name="scrollClientSize" readonly="true"> + <getter><![CDATA[ + return this.orient == "vertical" ? + this._scrollbox.clientHeight : + this._scrollbox.clientWidth; + ]]></getter> + </property> + + <property name="scrollSize" readonly="true"> + <getter><![CDATA[ + return this.orient == "vertical" ? + this._scrollbox.scrollHeight : + this._scrollbox.scrollWidth; + ]]></getter> + </property> + <property name="scrollPaddingRect" readonly="true"> + <getter><![CDATA[ + // This assumes that this._scrollbox doesn't have any border. + var outerRect = this.scrollClientRect; + var innerRect = {}; + innerRect.left = outerRect.left - this._scrollbox.scrollLeft; + innerRect.top = outerRect.top - this._scrollbox.scrollTop; + innerRect.right = innerRect.left + this._scrollbox.scrollWidth; + innerRect.bottom = innerRect.top + this._scrollbox.scrollHeight; + return innerRect; + ]]></getter> + </property> + <property name="scrollboxPaddingStart" readonly="true"> + <getter><![CDATA[ + var ltr = (window.getComputedStyle(this, null).direction == "ltr"); + var paddingStartName = ltr ? "padding-left" : "padding-right"; + var scrollboxStyle = window.getComputedStyle(this._scrollbox, null); + return parseFloat(scrollboxStyle.getPropertyValue(paddingStartName)); + ]]></getter> + </property> + <property name="scrollPosition"> + <getter><![CDATA[ + return this.orient == "vertical" ? + this._scrollbox.scrollTop : + this._scrollbox.scrollLeft; + ]]></getter> + <setter><![CDATA[ + if (this.orient == "vertical") + this._scrollbox.scrollTop = val; + else + this._scrollbox.scrollLeft = val; + return val; + ]]></setter> + </property> + + <property name="_startEndProps" readonly="true"> + <getter><![CDATA[ + return this.orient == "vertical" ? + ["top", "bottom"] : ["left", "right"]; + ]]></getter> + </property> + + <field name="_isRTLScrollbox"><![CDATA[ + this.orient != "vertical" && + document.defaultView.getComputedStyle(this._scrollbox, "").direction == "rtl"; + ]]></field> + + <field name="_scrollTarget">null</field> + + <method name="_canScrollToElement"> + <parameter name="element"/> + <body><![CDATA[ + return window.getComputedStyle(element).display != "none"; + ]]></body> + </method> + + <method name="ensureElementIsVisible"> + <parameter name="element"/> + <parameter name="aSmoothScroll"/> + <body><![CDATA[ + if (!this._canScrollToElement(element)) + return; + + var vertical = this.orient == "vertical"; + var rect = this.scrollClientRect; + var containerStart = vertical ? rect.top : rect.left; + var containerEnd = vertical ? rect.bottom : rect.right; + rect = element.getBoundingClientRect(); + var elementStart = vertical ? rect.top : rect.left; + var elementEnd = vertical ? rect.bottom : rect.right; + + var scrollPaddingRect = this.scrollPaddingRect; + let style = window.getComputedStyle(this._scrollbox, null); + var scrollContentRect = { + left: scrollPaddingRect.left + parseFloat(style.paddingLeft), + top: scrollPaddingRect.top + parseFloat(style.paddingTop), + right: scrollPaddingRect.right - parseFloat(style.paddingRight), + bottom: scrollPaddingRect.bottom - parseFloat(style.paddingBottom) + }; + + // Provide an entry point for derived bindings to adjust these values. + if (this._adjustElementStartAndEnd) { + [elementStart, elementEnd] = + this._adjustElementStartAndEnd(element, elementStart, elementEnd); + } + + if (elementStart <= (vertical ? scrollContentRect.top : scrollContentRect.left)) { + elementStart = vertical ? scrollPaddingRect.top : scrollPaddingRect.left; + } + if (elementEnd >= (vertical ? scrollContentRect.bottom : scrollContentRect.right)) { + elementEnd = vertical ? scrollPaddingRect.bottom : scrollPaddingRect.right; + } + + var amountToScroll; + + if (elementStart < containerStart) { + amountToScroll = elementStart - containerStart; + } else if (containerEnd < elementEnd) { + amountToScroll = elementEnd - containerEnd; + } else if (this._isScrolling) { + // decelerate if a currently-visible element is selected during the scroll + const STOP_DISTANCE = 15; + if (this._isScrolling == -1 && elementStart - STOP_DISTANCE < containerStart) + amountToScroll = elementStart - containerStart; + else if (this._isScrolling == 1 && containerEnd - STOP_DISTANCE < elementEnd) + amountToScroll = elementEnd - containerEnd; + else + amountToScroll = this._isScrolling * STOP_DISTANCE; + } else { + return; + } + + this._stopSmoothScroll(); + + if (aSmoothScroll != false && this.smoothScroll) { + this._smoothScrollByPixels(amountToScroll, element); + } else { + this.scrollByPixels(amountToScroll); + } + ]]></body> + </method> + + <method name="_smoothScrollByPixels"> + <parameter name="amountToScroll"/> + <parameter name="element"/><!-- optional --> + <body><![CDATA[ + this._stopSmoothScroll(); + if (amountToScroll == 0) + return; + + this._scrollTarget = element; + // Positive amountToScroll makes us scroll right (elements fly left), negative scrolls left. + this._isScrolling = amountToScroll < 0 ? -1 : 1; + + this._scrollAnim.start(amountToScroll); + ]]></body> + </method> + + <field name="_scrollAnim"><![CDATA[({ + scrollbox: this, + requestHandle: 0, /* 0 indicates there is no pending request */ + start: function scrollAnim_start(distance) { + this.distance = distance; + this.startPos = this.scrollbox.scrollPosition; + this.duration = Math.min(1000, Math.round(50 * Math.sqrt(Math.abs(distance)))); + this.startTime = window.performance.now(); + + if (!this.requestHandle) + this.requestHandle = window.requestAnimationFrame(this.sample.bind(this)); + }, + stop: function scrollAnim_stop() { + window.cancelAnimationFrame(this.requestHandle); + this.requestHandle = 0; + }, + sample: function scrollAnim_handleEvent(timeStamp) { + const timePassed = timeStamp - this.startTime; + const pos = timePassed >= this.duration ? 1 : + 1 - Math.pow(1 - timePassed / this.duration, 4); + + this.scrollbox.scrollPosition = this.startPos + (this.distance * pos); + + if (pos == 1) + this.scrollbox._stopSmoothScroll(); + else + this.requestHandle = window.requestAnimationFrame(this.sample.bind(this)); + } + })]]></field> + + <method name="scrollByIndex"> + <parameter name="index"/> + <parameter name="aSmoothScroll"/> + <body><![CDATA[ + if (index == 0) + return; + + // Each scrollByIndex call is expected to scroll the given number of + // items. If a previous call is still in progress because of smooth + // scrolling, we need to complete it before starting a new one. + if (this._scrollTarget) { + let elements = this._getScrollableElements(); + if (this._scrollTarget != elements[0] && + this._scrollTarget != elements[elements.length - 1]) + this.ensureElementIsVisible(this._scrollTarget, false); + } + + var rect = this.scrollClientRect; + var [start, end] = this._startEndProps; + var x = index > 0 ? rect[end] + 1 : rect[start] - 1; + var nextElement = this._elementFromPoint(x, index); + if (!nextElement) + return; + + var targetElement; + if (this._isRTLScrollbox) + index *= -1; + while (index < 0 && nextElement) { + if (this._canScrollToElement(nextElement)) + targetElement = nextElement; + nextElement = nextElement.previousSibling; + index++; + } + while (index > 0 && nextElement) { + if (this._canScrollToElement(nextElement)) + targetElement = nextElement; + nextElement = nextElement.nextSibling; + index--; + } + if (!targetElement) + return; + + this.ensureElementIsVisible(targetElement, aSmoothScroll); + ]]></body> + </method> + + <method name="scrollByPage"> + <parameter name="pageDelta"/> + <parameter name="aSmoothScroll"/> + <body><![CDATA[ + if (pageDelta == 0) + return; + + // If a previous call is still in progress because of smooth + // scrolling, we need to complete it before starting a new one. + if (this._scrollTarget) { + let elements = this._getScrollableElements(); + if (this._scrollTarget != elements[0] && + this._scrollTarget != elements[elements.length - 1]) + this.ensureElementIsVisible(this._scrollTarget, false); + } + + var [start, end] = this._startEndProps; + var rect = this.scrollClientRect; + var containerEdge = pageDelta > 0 ? rect[end] + 1 : rect[start] - 1; + var pixelDelta = pageDelta * (rect[end] - rect[start]); + var destinationPosition = containerEdge + pixelDelta; + var nextElement = this._elementFromPoint(containerEdge, pageDelta); + if (!nextElement) + return; + + // We need to iterate over our elements in the direction of pageDelta. + // pageDelta is the physical direction, so in a horizontal scroll box, + // positive values scroll to the right no matter if the scrollbox is + // LTR or RTL. But RTL changes how we need to advance the iteration + // (whether to get the next or the previous sibling of the current + // element). + var logicalAdvanceDir = pageDelta * (this._isRTLScrollbox ? -1 : 1); + var advance = logicalAdvanceDir > 0 ? (e => e.nextSibling) : (e => e.previousSibling); + + var extendsPastTarget = (pageDelta > 0) + ? (e => e.getBoundingClientRect()[end] > destinationPosition) + : (e => e.getBoundingClientRect()[start] < destinationPosition); + + // We want to scroll to the last element we encounter before we find + // an element which extends past destinationPosition. + var targetElement; + do { + if (this._canScrollToElement(nextElement)) + targetElement = nextElement; + nextElement = advance(nextElement); + } while (nextElement && !extendsPastTarget(nextElement)); + + if (!targetElement) + return; + + this.ensureElementIsVisible(targetElement, aSmoothScroll); + ]]></body> + </method> + + <method name="_getScrollableElements"> + <body><![CDATA[ + var nodes = this.childNodes; + if (nodes.length == 1 && + nodes[0].localName == "children" && + nodes[0].namespaceURI == "http://www.mozilla.org/xbl") { + nodes = document.getBindingParent(this).childNodes; + } + + return Array.filter(nodes, this._canScrollToElement, this); + ]]></body> + </method> + + <method name="_elementFromPoint"> + <parameter name="aX"/> + <parameter name="aPhysicalScrollDir"/> + <body><![CDATA[ + var elements = this._getScrollableElements(); + if (!elements.length) + return null; + + if (this._isRTLScrollbox) + elements.reverse(); + + var [start, end] = this._startEndProps; + var low = 0; + var high = elements.length - 1; + + if (aX < elements[low].getBoundingClientRect()[start] || + aX > elements[high].getBoundingClientRect()[end]) + return null; + + var mid, rect; + while (low <= high) { + mid = Math.floor((low + high) / 2); + rect = elements[mid].getBoundingClientRect(); + if (rect[start] > aX) + high = mid - 1; + else if (rect[end] < aX) + low = mid + 1; + else + return elements[mid]; + } + + // There's no element at the requested coordinate, but the algorithm + // from above yields an element next to it, in a random direction. + // The desired scrolling direction leads to the correct element. + + if (!aPhysicalScrollDir) + return null; + + if (aPhysicalScrollDir < 0 && rect[start] > aX) + mid = Math.max(mid - 1, 0); + else if (aPhysicalScrollDir > 0 && rect[end] < aX) + mid = Math.min(mid + 1, elements.length - 1); + + return elements[mid]; + ]]></body> + </method> + + <method name="_autorepeatbuttonScroll"> + <parameter name="event"/> + <body><![CDATA[ + var dir = event.originalTarget == this._scrollButtonUp ? -1 : 1; + if (this._isRTLScrollbox) + dir *= -1; + + this.scrollByPixels(this.scrollIncrement * dir); + + event.stopPropagation(); + ]]></body> + </method> + + <method name="scrollByPixels"> + <parameter name="px"/> + <body><![CDATA[ + this.scrollPosition += px; + ]]></body> + </method> + + <!-- 0: idle + 1: scrolling right + -1: scrolling left --> + <field name="_isScrolling">0</field> + <field name="_prevMouseScrolls">[null, null]</field> + + <field name="_touchStart">-1</field> + + <method name="_stopSmoothScroll"> + <body><![CDATA[ + if (this._isScrolling) { + this._scrollAnim.stop(); + this._isScrolling = 0; + this._scrollTarget = null; + } + ]]></body> + </method> + + <method name="_updateScrollButtonsDisabledState"> + <body><![CDATA[ + var scrolledToStart = false; + var scrolledToEnd = false; + + if (this.hasAttribute("notoverflowing")) { + scrolledToStart = true; + scrolledToEnd = true; + } + else if (this.scrollPosition == 0) { + // In the RTL case, this means the _last_ element in the + // scrollbox is visible + if (this._isRTLScrollbox) + scrolledToEnd = true; + else + scrolledToStart = true; + } + else if (this.scrollClientSize + this.scrollPosition == this.scrollSize) { + // In the RTL case, this means the _first_ element in the + // scrollbox is visible + if (this._isRTLScrollbox) + scrolledToStart = true; + else + scrolledToEnd = true; + } + + if (scrolledToEnd) + this.setAttribute("scrolledtoend", "true"); + else + this.removeAttribute("scrolledtoend"); + + if (scrolledToStart) + this.setAttribute("scrolledtostart", "true"); + else + this.removeAttribute("scrolledtostart"); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="wheel"><![CDATA[ + if (this.orient == "vertical") { + if (event.deltaMode == event.DOM_DELTA_PIXEL) + this.scrollByPixels(event.deltaY); + else if (event.deltaMode == event.DOM_DELTA_PAGE) + this.scrollByPage(event.deltaY); + else + this.scrollByIndex(event.deltaY); + } + // We allow vertical scrolling to scroll a horizontal scrollbox + // because many users have a vertical scroll wheel but no + // horizontal support. + // Because of this, we need to avoid scrolling chaos on trackpads + // and mouse wheels that support simultaneous scrolling in both axes. + // We do this by scrolling only when the last two scroll events were + // on the same axis as the current scroll event. + // For diagonal scroll events we only respect the dominant axis. + else { + let isVertical = Math.abs(event.deltaY) > Math.abs(event.deltaX); + let delta = isVertical ? event.deltaY : event.deltaX; + let scrollByDelta = isVertical && this._isRTLScrollbox ? -delta : delta; + + if (this._prevMouseScrolls.every(prev => prev == isVertical)) { + if (event.deltaMode == event.DOM_DELTA_PIXEL) + this.scrollByPixels(scrollByDelta); + else if (event.deltaMode == event.DOM_DELTA_PAGE) + this.scrollByPage(scrollByDelta); + else + this.scrollByIndex(scrollByDelta); + } + + if (this._prevMouseScrolls.length > 1) + this._prevMouseScrolls.shift(); + this._prevMouseScrolls.push(isVertical); + } + + event.stopPropagation(); + event.preventDefault(); + ]]></handler> + + <handler event="touchstart"><![CDATA[ + if (event.touches.length > 1) { + // Multiple touch points detected, abort. In particular this aborts + // the panning gesture when the user puts a second finger down after + // already panning with one finger. Aborting at this point prevents + // the pan gesture from being resumed until all fingers are lifted + // (as opposed to when the user is back down to one finger). + this._touchStart = -1; + } else { + this._touchStart = (this.orient == "vertical" + ? event.touches[0].screenY + : event.touches[0].screenX); + } + ]]></handler> + + <handler event="touchmove"><![CDATA[ + if (event.touches.length == 1 && + this._touchStart >= 0) { + var touchPoint = (this.orient == "vertical" + ? event.touches[0].screenY + : event.touches[0].screenX); + var delta = this._touchStart - touchPoint; + if (Math.abs(delta) > 0) { + this.scrollByPixels(delta); + this._touchStart = touchPoint; + } + event.preventDefault(); + } + ]]></handler> + + <handler event="touchend"><![CDATA[ + this._touchStart = -1; + ]]></handler> + + <handler event="underflow" phase="capturing"><![CDATA[ + // filter underflow events which were dispatched on nested scrollboxes + if (event.target != this) + return; + + // Ignore events that doesn't match our orientation. + // Scrollport event orientation: + // 0: vertical + // 1: horizontal + // 2: both + if (this.orient == "vertical") { + if (event.detail == 1) + return; + } + else if (event.detail == 0) { + // horizontal scrollbox + return; + } + + this.setAttribute("notoverflowing", "true"); + + try { + // See bug 341047 and comments in overflow handler as to why + // try..catch is needed here + this._updateScrollButtonsDisabledState(); + + let childNodes = this._getScrollableElements(); + if (childNodes && childNodes.length) + this.ensureElementIsVisible(childNodes[0], false); + } + catch (e) { + this.removeAttribute("notoverflowing"); + } + ]]></handler> + + <handler event="overflow" phase="capturing"><![CDATA[ + // filter underflow events which were dispatched on nested scrollboxes + if (event.target != this) + return; + + // Ignore events that doesn't match our orientation. + // Scrollport event orientation: + // 0: vertical + // 1: horizontal + // 2: both + if (this.orient == "vertical") { + if (event.detail == 1) + return; + } + else if (event.detail == 0) { + // horizontal scrollbox + return; + } + + this.removeAttribute("notoverflowing"); + + try { + // See bug 341047, the overflow event is dispatched when the + // scrollbox already is mostly destroyed. This causes some code in + // _updateScrollButtonsDisabledState() to throw an error. It also + // means that the notoverflowing attribute was removed erroneously, + // as the whole overflow event should not be happening in that case. + this._updateScrollButtonsDisabledState(); + } + catch (e) { + this.setAttribute("notoverflowing", "true"); + } + ]]></handler> + + <handler event="scroll" action="this._updateScrollButtonsDisabledState()"/> + </handlers> + </binding> + + <binding id="autorepeatbutton" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base"> + <content repeat="hover"> + <xul:image class="autorepeatbutton-icon"/> + </content> + </binding> + + <binding id="arrowscrollbox-clicktoscroll" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox"> + <content> + <xul:toolbarbutton class="scrollbutton-up" + xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart" + anonid="scrollbutton-up" + onclick="_distanceScroll(event);" + onmousedown="if (event.button == 0) _startScroll(-1);" + onmouseup="if (event.button == 0) _stopScroll();" + onmouseover="_continueScroll(-1);" + onmouseout="_pauseScroll();"/> + <xul:spacer class="arrowscrollbox-overflow-start-indicator" + xbl:inherits="collapsed=scrolledtostart"/> + <xul:scrollbox class="arrowscrollbox-scrollbox" + anonid="scrollbox" + flex="1" + xbl:inherits="orient,align,pack,dir"> + <children/> + </xul:scrollbox> + <xul:spacer class="arrowscrollbox-overflow-end-indicator" + xbl:inherits="collapsed=scrolledtoend"/> + <xul:toolbarbutton class="scrollbutton-down" + xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend" + anonid="scrollbutton-down" + onclick="_distanceScroll(event);" + onmousedown="if (event.button == 0) _startScroll(1);" + onmouseup="if (event.button == 0) _stopScroll();" + onmouseover="_continueScroll(1);" + onmouseout="_pauseScroll();"/> + </content> + <implementation implements="nsITimerCallback, nsIDOMEventListener"> + <constructor><![CDATA[ + try { + this._scrollDelay = this._prefBranch + .getIntPref("toolkit.scrollbox.clickToScroll.scrollDelay"); + } + catch (ex) { + } + ]]></constructor> + + <destructor><![CDATA[ + // Release timer to avoid reference cycles. + if (this._scrollTimer) { + this._scrollTimer.cancel(); + this._scrollTimer = null; + } + ]]></destructor> + + <field name="_scrollIndex">0</field> + <field name="_scrollDelay">150</field> + + <method name="notify"> + <parameter name="aTimer"/> + <body> + <![CDATA[ + if (!document) + aTimer.cancel(); + + this.scrollByIndex(this._scrollIndex); + ]]> + </body> + </method> + + <field name="_arrowScrollAnim"><![CDATA[({ + scrollbox: this, + requestHandle: 0, /* 0 indicates there is no pending request */ + start: function arrowSmoothScroll_start() { + this.lastFrameTime = window.performance.now(); + if (!this.requestHandle) + this.requestHandle = window.requestAnimationFrame(this.sample.bind(this)); + }, + stop: function arrowSmoothScroll_stop() { + window.cancelAnimationFrame(this.requestHandle); + this.requestHandle = 0; + }, + sample: function arrowSmoothScroll_handleEvent(timeStamp) { + const scrollIndex = this.scrollbox._scrollIndex; + const timePassed = timeStamp - this.lastFrameTime; + this.lastFrameTime = timeStamp; + + const scrollDelta = 0.5 * timePassed * scrollIndex; + this.scrollbox.scrollPosition += scrollDelta; + + this.requestHandle = window.requestAnimationFrame(this.sample.bind(this)); + } + })]]></field> + + <method name="_startScroll"> + <parameter name="index"/> + <body><![CDATA[ + if (this._isRTLScrollbox) + index *= -1; + this._scrollIndex = index; + this._mousedown = true; + if (this.smoothScroll) { + this._arrowScrollAnim.start(); + return; + } + + if (!this._scrollTimer) + this._scrollTimer = + Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + else + this._scrollTimer.cancel(); + + this._scrollTimer.initWithCallback(this, this._scrollDelay, + this._scrollTimer.TYPE_REPEATING_SLACK); + this.notify(this._scrollTimer); + ]]> + </body> + </method> + + <method name="_stopScroll"> + <body><![CDATA[ + if (this._scrollTimer) + this._scrollTimer.cancel(); + this._mousedown = false; + if (!this._scrollIndex || !this.smoothScroll) + return; + + this.scrollByIndex(this._scrollIndex); + this._scrollIndex = 0; + this._arrowScrollAnim.stop(); + ]]></body> + </method> + + <method name="_pauseScroll"> + <body><![CDATA[ + if (this._mousedown) { + this._stopScroll(); + this._mousedown = true; + document.addEventListener("mouseup", this, false); + document.addEventListener("blur", this, true); + } + ]]></body> + </method> + + <method name="_continueScroll"> + <parameter name="index"/> + <body><![CDATA[ + if (this._mousedown) + this._startScroll(index); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.type == "mouseup" || + aEvent.type == "blur" && aEvent.target == document) { + this._mousedown = false; + document.removeEventListener("mouseup", this, false); + document.removeEventListener("blur", this, true); + } + ]]></body> + </method> + + <method name="_distanceScroll"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.detail < 2 || aEvent.detail > 3) + return; + + var scrollBack = (aEvent.originalTarget == this._scrollButtonUp); + var scrollLeftOrUp = this._isRTLScrollbox ? !scrollBack : scrollBack; + var targetElement; + + if (aEvent.detail == 2) { + // scroll by the size of the scrollbox + let [start, end] = this._startEndProps; + let x; + if (scrollLeftOrUp) + x = this.scrollClientRect[start] - this.scrollClientSize; + else + x = this.scrollClientRect[end] + this.scrollClientSize; + targetElement = this._elementFromPoint(x, scrollLeftOrUp ? -1 : 1); + + // the next partly-hidden element will become fully visible, + // so don't scroll too far + if (targetElement) + targetElement = scrollBack ? + targetElement.nextSibling : + targetElement.previousSibling; + } + + if (!targetElement) { + // scroll to the first resp. last element + let elements = this._getScrollableElements(); + targetElement = scrollBack ? + elements[0] : + elements[elements.length - 1]; + } + + this.ensureElementIsVisible(targetElement); + ]]></body> + </method> + + </implementation> + </binding> +</bindings> diff --git a/toolkit/content/widgets/spinbuttons.xml b/toolkit/content/widgets/spinbuttons.xml new file mode 100644 index 0000000000..3a695beacf --- /dev/null +++ b/toolkit/content/widgets/spinbuttons.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- 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/. --> + +<bindings id="spinbuttonsBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="spinbuttons" + extends="chrome://global/content/bindings/general.xml#basecontrol"> + + <resources> + <stylesheet src="chrome://global/skin/spinbuttons.css"/> + </resources> + + <content> + <xul:vbox class="spinbuttons-box" flex="1"> + <xul:button anonid="increaseButton" type="repeat" flex="1" + class="spinbuttons-button spinbuttons-up" + xbl:inherits="disabled,disabled=increasedisabled"/> + <xul:button anonid="decreaseButton" type="repeat" flex="1" + class="spinbuttons-button spinbuttons-down" + xbl:inherits="disabled,disabled=decreasedisabled"/> + </xul:vbox> + </content> + + <implementation> + <property name="_increaseButton" readonly="true"> + <getter> + return document.getAnonymousElementByAttribute(this, "anonid", "increaseButton"); + </getter> + </property> + <property name="_decreaseButton" readonly="true"> + <getter> + return document.getAnonymousElementByAttribute(this, "anonid", "decreaseButton"); + </getter> + </property> + + <property name="increaseDisabled" + onget="return this._increaseButton.getAttribute('disabled') == 'true';" + onset="if (val) this._increaseButton.setAttribute('disabled', 'true'); + else this._increaseButton.removeAttribute('disabled'); return val;"/> + <property name="decreaseDisabled" + onget="return this._decreaseButton.getAttribute('disabled') == 'true';" + onset="if (val) this._decreaseButton.setAttribute('disabled', 'true'); + else this._decreaseButton.removeAttribute('disabled'); return val;"/> + </implementation> + + <handlers> + <handler event="mousedown"> + <![CDATA[ + // on the Mac, the native theme draws the spinbutton as a single widget + // so a state attribute is set based on where the mouse button was pressed + if (event.originalTarget == this._increaseButton) + this.setAttribute("state", "up"); + else if (event.originalTarget == this._decreaseButton) + this.setAttribute("state", "down"); + ]]> + </handler> + + <handler event="mouseup"> + this.removeAttribute("state"); + </handler> + <handler event="mouseout"> + this.removeAttribute("state"); + </handler> + + <handler event="command"> + <![CDATA[ + var eventname; + if (event.originalTarget == this._increaseButton) + eventname = "up"; + else if (event.originalTarget == this._decreaseButton) + eventname = "down"; + + var evt = document.createEvent("Events"); + evt.initEvent(eventname, true, true); + var cancel = this.dispatchEvent(evt); + + if (this.hasAttribute("on" + eventname)) { + var fn = new Function("event", this.getAttribute("on" + eventname)); + if (fn.call(this, event) == false) + cancel = true; + } + + return !cancel; + ]]> + </handler> + + </handlers> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js new file mode 100644 index 0000000000..208ab1931b --- /dev/null +++ b/toolkit/content/widgets/spinner.js @@ -0,0 +1,514 @@ +/* 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/. */ + +"use strict"; + +/* + * The spinner is responsible for displaying the items, and does + * not care what the values represent. The setValue function is called + * when it detects a change in value triggered by scroll event. + * Supports scrolling, clicking on up or down, clicking on item, and + * dragging. + */ + +function Spinner(props, context) { + this.context = context; + this._init(props); +} + +{ + const debug = 0 ? console.log.bind(console, "[spinner]") : function() {}; + + const ITEM_HEIGHT = 2.5, + VIEWPORT_SIZE = 7, + VIEWPORT_COUNT = 5, + SCROLL_TIMEOUT = 100; + + Spinner.prototype = { + /** + * Initializes a spinner. Set the default states and properties, cache + * element references, create the HTML markup, and add event listeners. + * + * @param {Object} props [Properties passed in from parent] + * { + * {Function} setValue: Takes a value and set the state to + * the parent component. + * {Function} getDisplayString: Takes a value, and output it + * as localized strings. + * {Number} viewportSize [optional]: Number of items in a + * viewport. + * {Boolean} hideButtons [optional]: Hide up & down buttons + * {Number} rootFontSize [optional]: Used to support zoom in/out + * } + */ + _init(props) { + const { setValue, getDisplayString, hideButtons, rootFontSize = 10 } = props; + + const spinnerTemplate = document.getElementById("spinner-template"); + const spinnerElement = document.importNode(spinnerTemplate.content, true); + + // Make sure viewportSize is an odd number because we want to have the selected + // item in the center. If it's an even number, use the default size instead. + const viewportSize = props.viewportSize % 2 ? props.viewportSize : VIEWPORT_SIZE; + + this.state = { + items: [], + isScrolling: false + }; + this.props = { + setValue, getDisplayString, viewportSize, rootFontSize, + // We can assume that the viewportSize is an odd number. Calculate how many + // items we need to insert on top of the spinner so that the selected is at + // the center. Ex: if viewportSize is 5, we need 2 items on top. + viewportTopOffset: (viewportSize - 1) / 2 + }; + this.elements = { + container: spinnerElement.querySelector(".spinner-container"), + spinner: spinnerElement.querySelector(".spinner"), + up: spinnerElement.querySelector(".up"), + down: spinnerElement.querySelector(".down"), + itemsViewElements: [] + }; + + this.elements.spinner.style.height = (ITEM_HEIGHT * viewportSize) + "rem"; + + if (hideButtons) { + this.elements.container.classList.add("hide-buttons"); + } + + this.context.appendChild(spinnerElement); + this._attachEventListeners(); + }, + + /** + * Only the parent component calls setState on the spinner. + * It checks if the items have changed and updates the spinner. + * If only the value has changed, smooth scrolls to the new value. + * + * @param {Object} newState [The new spinner state] + * { + * {Number/String} value: The centered value + * {Array} items: The list of items for display + * {Boolean} isInfiniteScroll: Whether or not the spinner should + * have infinite scroll capability + * {Boolean} isValueSet: true if user has selected a value + * } + */ + setState(newState) { + const { spinner } = this.elements; + const { value, items } = this.state; + const { value: newValue, items: newItems, isValueSet, isInvalid } = newState; + + if (this._isArrayDiff(newItems, items)) { + this.state = Object.assign(this.state, newState); + this._updateItems(); + this._scrollTo(newValue, true); + } else if (newValue != value) { + this.state = Object.assign(this.state, newState); + this._smoothScrollTo(newValue); + } + + if (isValueSet) { + if (isInvalid) { + this._removeSelection(); + } else { + this._updateSelection(); + } + } + }, + + /** + * Whenever scroll event is detected: + * - Update the index state + * - If a smooth scroll has reached its destination, set [isScrolling] state + * to false + * - If the value has changed, update the [value] state and call [setValue] + * - If infinite scrolling is on, reset the scrolling position if necessary + */ + _onScroll() { + const { items, itemsView, isInfiniteScroll } = this.state; + const { viewportSize, viewportTopOffset } = this.props; + const { spinner, itemsViewElements } = this.elements; + + this.state.index = this._getIndexByOffset(spinner.scrollTop); + + const value = itemsView[this.state.index + viewportTopOffset].value; + + // Check if smooth scrolling has reached its destination. + // This prevents input box jump when input box changes values. + if (this.state.value == value && this.state.isScrolling) { + this.state.isScrolling = false; + } + + // Call setValue if value has changed, and is not smooth scrolling + if (this.state.value != value && !this.state.isScrolling) { + this.state.value = value; + this.props.setValue(value); + } + + // Do infinite scroll when items length is bigger or equal to viewport + // and isInfiniteScroll is not false. + if (items.length >= viewportSize && isInfiniteScroll) { + // If the scroll position is near the top or bottom, jump back to the middle + // so user can keep scrolling up or down. + if (this.state.index < viewportSize || + this.state.index > itemsView.length - viewportSize) { + this._scrollTo(this.state.value, true); + } + } + + // Use a timer to detect if a scroll event has not fired within some time + // (defined in SCROLL_TIMEOUT). This is required because we need to hide + // highlight and hover state when user is scrolling. + clearTimeout(this.state.scrollTimer); + this.elements.spinner.classList.add("scrolling"); + this.state.scrollTimer = setTimeout(() => { + this.elements.spinner.classList.remove("scrolling"); + this.elements.spinner.dispatchEvent(new CustomEvent("ScrollStop")); + }, SCROLL_TIMEOUT); + }, + + /** + * Updates the spinner items to the current states. + */ + _updateItems() { + const { viewportSize, viewportTopOffset } = this.props; + const { items, isInfiniteScroll } = this.state; + + // Prepends null elements so the selected value is centered in spinner + let itemsView = new Array(viewportTopOffset).fill({}).concat(items); + + if (items.length >= viewportSize && isInfiniteScroll) { + // To achieve infinite scroll, we move the scroll position back to the + // center when it is near the top or bottom. The scroll momentum could + // be lost in the process, so to minimize that, we need at least 2 sets + // of items to act as buffer: one for the top and one for the bottom. + // But if the number of items is small ( < viewportSize * viewport count) + // we should add more sets. + let count = Math.ceil(viewportSize * VIEWPORT_COUNT / items.length) * 2; + for (let i = 0; i < count; i += 1) { + itemsView.push(...items); + } + } + + // Reuse existing DOM nodes when possible. Create or remove + // nodes based on how big itemsView is. + this._prepareNodes(itemsView.length, this.elements.spinner); + // Once DOM nodes are ready, set display strings using textContent + this._setDisplayStringAndClass(itemsView, this.elements.itemsViewElements); + + this.state.itemsView = itemsView; + }, + + /** + * Make sure the number or child elements is the same as length + * and keep the elements' references for updating textContent + * + * @param {Number} length [The number of child elements] + * @param {DOMElement} parent [The parent element reference] + */ + _prepareNodes(length, parent) { + const diff = length - parent.childElementCount; + + if (!diff) { + return; + } + + if (diff > 0) { + // Add more elements if length is greater than current + let frag = document.createDocumentFragment(); + + // Remove margin bottom on the last element before appending + if (parent.lastChild) { + parent.lastChild.style.marginBottom = ""; + } + + for (let i = 0; i < diff; i++) { + let el = document.createElement("div"); + frag.appendChild(el); + this.elements.itemsViewElements.push(el); + } + parent.appendChild(frag); + } else if (diff < 0) { + // Remove elements if length is less than current + for (let i = 0; i < Math.abs(diff); i++) { + parent.removeChild(parent.lastChild); + } + this.elements.itemsViewElements.splice(diff); + } + + parent.lastChild.style.marginBottom = + (ITEM_HEIGHT * this.props.viewportTopOffset) + "rem"; + }, + + /** + * Set the display string and class name to the elements. + * + * @param {Array<Object>} items + * [{ + * {Number/String} value: The value in its original form + * {Boolean} enabled: Whether or not the item is enabled + * }] + * @param {Array<DOMElement>} elements + */ + _setDisplayStringAndClass(items, elements) { + const { getDisplayString } = this.props; + + items.forEach((item, index) => { + elements[index].textContent = + item.value != undefined ? getDisplayString(item.value) : ""; + elements[index].className = item.enabled ? "" : "disabled"; + }); + }, + + /** + * Attach event listeners to the spinner and buttons. + */ + _attachEventListeners() { + const { spinner } = this.elements; + + spinner.addEventListener("scroll", this, { passive: true }); + document.addEventListener("mouseup", this, { passive: true }); + document.addEventListener("mousedown", this); + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + const { mouseState = {}, index, itemsView } = this.state; + const { viewportTopOffset, setValue } = this.props; + const { spinner, up, down } = this.elements; + + switch (event.type) { + case "scroll": { + this._onScroll(); + break; + } + case "mousedown": { + // Use preventDefault to keep focus on input boxes + event.preventDefault(); + event.target.setCapture(); + this.state.mouseState = { + down: true, + layerX: event.layerX, + layerY: event.layerY + }; + if (event.target == up) { + // An "active" class is needed to simulate :active pseudo-class + // because element is not focused. + event.target.classList.add("active"); + this._smoothScrollToIndex(index + 1); + } + if (event.target == down) { + event.target.classList.add("active"); + this._smoothScrollToIndex(index - 1); + } + if (event.target.parentNode == spinner) { + // Listen to dragging events + spinner.addEventListener("mousemove", this, { passive: true }); + spinner.addEventListener("mouseleave", this, { passive: true }); + } + break; + } + case "mouseup": { + this.state.mouseState.down = false; + if (event.target == up || event.target == down) { + event.target.classList.remove("active"); + } + if (event.target.parentNode == spinner) { + // Check if user clicks or drags, scroll to the item if clicked, + // otherwise get the current index and smooth scroll there. + if (event.layerX == mouseState.layerX && event.layerY == mouseState.layerY) { + const newIndex = this._getIndexByOffset(event.target.offsetTop) - viewportTopOffset; + if (index == newIndex) { + // Set value manually if the clicked element is already centered. + // This happens when the picker first opens, and user pick the + // default value. + setValue(itemsView[index + viewportTopOffset].value); + } else { + this._smoothScrollToIndex(newIndex); + } + } else { + this._smoothScrollToIndex(this._getIndexByOffset(spinner.scrollTop)); + } + // Stop listening to dragging + spinner.removeEventListener("mousemove", this, { passive: true }); + spinner.removeEventListener("mouseleave", this, { passive: true }); + } + break; + } + case "mouseleave": { + if (event.target == spinner) { + // Stop listening to drag event if mouse is out of the spinner + this._smoothScrollToIndex(this._getIndexByOffset(spinner.scrollTop)); + spinner.removeEventListener("mousemove", this, { passive: true }); + spinner.removeEventListener("mouseleave", this, { passive: true }); + } + break; + } + case "mousemove": { + // Change spinner position on drag + spinner.scrollTop -= event.movementY; + break; + } + } + }, + + /** + * Find the index by offset + * @param {Number} offset: Offset value in pixel. + * @return {Number} Index number + */ + _getIndexByOffset(offset) { + return Math.round(offset / (ITEM_HEIGHT * this.props.rootFontSize)); + }, + + /** + * Find the index of a value that is the closest to the current position. + * If centering is true, find the index closest to the center. + * + * @param {Number/String} value: The value to find + * @param {Boolean} centering: Whether or not to find the value closest to center + * @return {Number} index of the value, returns -1 if value is not found + */ + _getScrollIndex(value, centering) { + const { itemsView } = this.state; + const { viewportTopOffset } = this.props; + + // If index doesn't exist, or centering is true, start from the middle point + let currentIndex = centering || (this.state.index == undefined) ? + Math.round((itemsView.length - viewportTopOffset) / 2) : + this.state.index; + let closestIndex = itemsView.length; + let indexes = []; + let diff = closestIndex; + let isValueFound = false; + + // Find indexes of items match the value + itemsView.forEach((item, index) => { + if (item.value == value) { + indexes.push(index); + } + }); + + // Find the index closest to currentIndex + indexes.forEach(index => { + let d = Math.abs(index - currentIndex); + if (d < diff) { + diff = d; + closestIndex = index; + isValueFound = true; + } + }); + + return isValueFound ? (closestIndex - viewportTopOffset) : -1; + }, + + /** + * Scroll to a value. + * + * @param {Number/String} value: Value to scroll to + * @param {Boolean} centering: Whether or not to scroll to center location + */ + _scrollTo(value, centering) { + const index = this._getScrollIndex(value, centering); + // Do nothing if the value is not found + if (index > -1) { + this.state.index = index; + this.elements.spinner.scrollTop = this.state.index * ITEM_HEIGHT * this.props.rootFontSize; + } + }, + + /** + * Smooth scroll to a value. + * + * @param {Number/String} value: Value to scroll to + */ + _smoothScrollTo(value) { + const index = this._getScrollIndex(value); + // Do nothing if the value is not found + if (index > -1) { + this.state.index = index; + this._smoothScrollToIndex(this.state.index); + } + }, + + /** + * Smooth scroll to a value based on the index + * + * @param {Number} index: Index number + */ + _smoothScrollToIndex(index) { + const element = this.elements.spinner.children[index]; + if (element) { + // Set the isScrolling flag before smooth scrolling begins + // and remove it when it has reached the destination. + // This prevents input box jump when input box changes values + this.state.isScrolling = true; + element.scrollIntoView({ + behavior: "smooth", block: "start" + }); + } + }, + + /** + * Update the selection state. + */ + _updateSelection() { + const { itemsViewElements, selected } = this.elements; + const { itemsView, index } = this.state; + const { viewportTopOffset } = this.props; + const currentItemIndex = index + viewportTopOffset; + + if (selected && selected != itemsViewElements[currentItemIndex]) { + this._removeSelection(); + } + + this.elements.selected = itemsViewElements[currentItemIndex]; + if (itemsView[currentItemIndex] && itemsView[currentItemIndex].enabled) { + this.elements.selected.classList.add("selection"); + } + }, + + /** + * Remove selection if selected exists and different from current + */ + _removeSelection() { + const { selected } = this.elements; + if (selected) { + selected.classList.remove("selection"); + } + }, + + /** + * Compares arrays of objects. It assumes the structure is an array of + * objects, and objects in a and b have the same number of properties. + * + * @param {Array<Object>} a + * @param {Array<Object>} b + * @return {Boolean} Returns true if a and b are different + */ + _isArrayDiff(a, b) { + // Check reference first, exit early if reference is the same. + if (a == b) { + return false; + } + + if (a.length != b.length) { + return true; + } + + for (let i = 0; i < a.length; i++) { + for (let prop in a[i]) { + if (a[i][prop] != b[i][prop]) { + return true; + } + } + } + return false; + } + }; +} diff --git a/toolkit/content/widgets/splitter.xml b/toolkit/content/widgets/splitter.xml new file mode 100644 index 0000000000..d23631fbe7 --- /dev/null +++ b/toolkit/content/widgets/splitter.xml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="splitterBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="splitter" extends="xul:splitter"> + <resources> + <stylesheet src="chrome://global/skin/splitter.css"/> + </resources> + </binding> + + <binding id="grippy" extends="xul:button"> + <resources> + <stylesheet src="chrome://global/skin/splitter.css"/> + </resources> + <handlers> + <handler event="command"> + <![CDATA[ + var splitter = this.parentNode; + if (splitter) { + var state = splitter.getAttribute("state"); + if (state == "collapsed") + splitter.setAttribute("state", "open"); + else + splitter.setAttribute("state", "collapsed"); + } + ]]> + </handler> + </handlers> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/stringbundle.xml b/toolkit/content/widgets/stringbundle.xml new file mode 100644 index 0000000000..3365bd61f6 --- /dev/null +++ b/toolkit/content/widgets/stringbundle.xml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="stringBundleBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="stringbundleset" extends="xul:box"/> + + <binding id="stringbundle" extends="xul:spacer"> + <implementation name="XStringBundle"> + + <method name="getString"> + <parameter name="aStringKey"/> + <body> + <![CDATA[ + try { + return this.stringBundle.GetStringFromName(aStringKey); + } + catch (e) { + dump("*** Failed to get string " + aStringKey + " in bundle: " + this.src + "\n"); + throw e; + } + ]]> + </body> + </method> + + <method name="getFormattedString"> + <parameter name="aStringKey"/> + <parameter name="aStringsArray"/> + <body> + <![CDATA[ + try { + return this.stringBundle.formatStringFromName(aStringKey, aStringsArray, aStringsArray.length); + } + catch (e) { + dump("*** Failed to format string " + aStringKey + " in bundle: " + this.src + "\n"); + throw e; + } + ]]> + </body> + </method> + + <property name="stringBundle" readonly="true"> + <getter> + <![CDATA[ + if (!this._bundle) { + try { + this._bundle = Components.classes["@mozilla.org/intl/stringbundle;1"] + .getService(Components.interfaces.nsIStringBundleService) + .createBundle(this.src); + } + catch (e) { + dump("Failed to get stringbundle:\n"); + dump(e + "\n"); + } + } + return this._bundle; + ]]> + </getter> + </property> + + <property name="src"> + <getter> + <![CDATA[ + return this.getAttribute("src"); + ]]> + </getter> + <setter> + <![CDATA[ + this._bundle = null; + this.setAttribute("src", val); + return val; + ]]> + </setter> + </property> + + <property name="strings"> + <getter> + <![CDATA[ + // Note: this is a sucky method name! Should be: + // readonly attribute nsISimpleEnumerator strings; + return this.stringBundle.getSimpleEnumeration(); + ]]> + </getter> + </property> + + <field name="_bundle">null</field> + + </implementation> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/tabbox.xml b/toolkit/content/widgets/tabbox.xml new file mode 100644 index 0000000000..02adb70b31 --- /dev/null +++ b/toolkit/content/widgets/tabbox.xml @@ -0,0 +1,892 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="tabBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="tab-base"> + <resources> + <stylesheet src="chrome://global/skin/tabbox.css"/> + </resources> + </binding> + + <binding id="tabbox" + extends="chrome://global/content/bindings/tabbox.xml#tab-base"> + <implementation implements="nsIDOMEventListener"> + <property name="handleCtrlTab"> + <setter> + <![CDATA[ + this.setAttribute("handleCtrlTab", val); + return val; + ]]> + </setter> + <getter> + <![CDATA[ + return (this.getAttribute("handleCtrlTab") != "false"); + ]]> + </getter> + </property> + + <property name="handleCtrlPageUpDown"> + <setter> + <![CDATA[ + this.setAttribute("handleCtrlPageUpDown", val); + return val; + ]]> + </setter> + <getter> + <![CDATA[ + return (this.getAttribute("handleCtrlPageUpDown") != "false"); + ]]> + </getter> + </property> + + <field name="_handleMetaAltArrows" readonly="true"> + /Mac/.test(navigator.platform) + </field> + + <!-- _tabs and _tabpanels are deprecated, they exist only for + backwards compatibility. --> + <property name="_tabs" readonly="true" onget="return this.tabs;"/> + <property name="_tabpanels" readonly="true" onget="return this.tabpanels;"/> + + <property name="tabs" readonly="true"> + <getter> + <![CDATA[ + return this.getElementsByTagNameNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "tabs").item(0); + ]]> + </getter> + </property> + + <property name="tabpanels" readonly="true"> + <getter> + <![CDATA[ + return this.getElementsByTagNameNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "tabpanels").item(0); + ]]> + </getter> + </property> + + <property name="selectedIndex"> + <getter> + <![CDATA[ + var tabs = this.tabs; + return tabs ? tabs.selectedIndex : -1; + ]]> + </getter> + + <setter> + <![CDATA[ + var tabs = this.tabs; + if (tabs) + tabs.selectedIndex = val; + this.setAttribute("selectedIndex", val); + return val; + ]]> + </setter> + </property> + + <property name="selectedTab"> + <getter> + <![CDATA[ + var tabs = this.tabs; + return tabs && tabs.selectedItem; + ]]> + </getter> + + <setter> + <![CDATA[ + if (val) { + var tabs = this.tabs; + if (tabs) + tabs.selectedItem = val; + } + return val; + ]]> + </setter> + </property> + + <property name="selectedPanel"> + <getter> + <![CDATA[ + var tabpanels = this.tabpanels; + return tabpanels && tabpanels.selectedPanel; + ]]> + </getter> + + <setter> + <![CDATA[ + if (val) { + var tabpanels = this.tabpanels; + if (tabpanels) + tabpanels.selectedPanel = val; + } + return val; + ]]> + </setter> + </property> + + <method name="handleEvent"> + <parameter name="event"/> + <body> + <![CDATA[ + if (!event.isTrusted) { + // Don't let untrusted events mess with tabs. + return; + } + + // Don't check if the event was already consumed because tab + // navigation should always work for better user experience. + + switch (event.keyCode) { + case event.DOM_VK_TAB: + if (event.ctrlKey && !event.altKey && !event.metaKey) + if (this.tabs && this.handleCtrlTab) { + this.tabs.advanceSelectedTab(event.shiftKey ? -1 : 1, true); + event.preventDefault(); + } + break; + case event.DOM_VK_PAGE_UP: + if (event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) + if (this.tabs && this.handleCtrlPageUpDown) { + this.tabs.advanceSelectedTab(-1, true); + event.preventDefault(); + } + break; + case event.DOM_VK_PAGE_DOWN: + if (event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) + if (this.tabs && this.handleCtrlPageUpDown) { + this.tabs.advanceSelectedTab(1, true); + event.preventDefault(); + } + break; + case event.DOM_VK_LEFT: + if (event.metaKey && event.altKey && !event.shiftKey && !event.ctrlKey) + if (this.tabs && this._handleMetaAltArrows) { + var offset = window.getComputedStyle(this, "") + .direction == "ltr" ? -1 : 1; + this.tabs.advanceSelectedTab(offset, true); + event.preventDefault(); + } + break; + case event.DOM_VK_RIGHT: + if (event.metaKey && event.altKey && !event.shiftKey && !event.ctrlKey) + if (this.tabs && this._handleMetaAltArrows) { + offset = window.getComputedStyle(this, "") + .direction == "ltr" ? 1 : -1; + this.tabs.advanceSelectedTab(offset, true); + event.preventDefault(); + } + break; + } + ]]> + </body> + </method> + + <field name="_eventNode">this</field> + + <property name="eventNode" onget="return this._eventNode;"> + <setter> + <![CDATA[ + if (val != this._eventNode) { + const nsIEventListenerService = + Components.interfaces.nsIEventListenerService; + let els = Components.classes["@mozilla.org/eventlistenerservice;1"] + .getService(nsIEventListenerService); + els.addSystemEventListener(val, "keydown", this, false); + els.removeSystemEventListener(this._eventNode, "keydown", this, false); + this._eventNode = val; + } + return val; + ]]> + </setter> + </property> + + <constructor> + switch (this.getAttribute("eventnode")) { + case "parent": this._eventNode = this.parentNode; break; + case "window": this._eventNode = window; break; + case "document": this._eventNode = document; break; + } + const nsIEventListenerService = + Components.interfaces.nsIEventListenerService; + let els = Components.classes["@mozilla.org/eventlistenerservice;1"] + .getService(nsIEventListenerService); + els.addSystemEventListener(this._eventNode, "keydown", this, false); + </constructor> + + <destructor> + const nsIEventListenerService = + Components.interfaces.nsIEventListenerService; + let els = Components.classes["@mozilla.org/eventlistenerservice;1"] + .getService(nsIEventListenerService); + els.removeSystemEventListener(this._eventNode, "keydown", this, false); + </destructor> + </implementation> + </binding> + + <binding id="tabs" role="xul:tabs" + extends="chrome://global/content/bindings/general.xml#basecontrol"> + <resources> + <stylesheet src="chrome://global/skin/tabbox.css"/> + </resources> + + <content> + <xul:spacer class="tabs-left"/> + <children/> + <xul:spacer class="tabs-right" flex="1"/> + </content> + + <implementation implements="nsIDOMXULSelectControlElement, nsIDOMXULRelatedElement"> + <constructor> + <![CDATA[ + // first and last tabs need to be able to have unique styles + // and also need to select first tab on startup. + if (this.firstChild) + this.firstChild.setAttribute("first-tab", "true"); + if (this.lastChild) + this.lastChild.setAttribute("last-tab", "true"); + + if (!this.hasAttribute("orient")) + this.setAttribute("orient", "horizontal"); + + if (this.tabbox && this.tabbox.hasAttribute("selectedIndex")) { + let selectedIndex = parseInt(this.tabbox.getAttribute("selectedIndex")); + this.selectedIndex = selectedIndex > 0 ? selectedIndex : 0; + return; + } + + var children = this.childNodes; + var length = children.length; + for (var i = 0; i < length; i++) { + if (children[i].getAttribute("selected") == "true") { + this.selectedIndex = i; + return; + } + } + + var value = this.value; + if (value) + this.value = value; + else + this.selectedIndex = 0; + ]]> + </constructor> + + <!-- nsIDOMXULRelatedElement --> + <method name="getRelatedElement"> + <parameter name="aTabElm"/> + <body> + <![CDATA[ + if (!aTabElm) + return null; + + let tabboxElm = this.tabbox; + if (!tabboxElm) + return null; + + let tabpanelsElm = tabboxElm.tabpanels; + if (!tabpanelsElm) + return null; + + // Get linked tab panel by 'linkedpanel' attribute on the given tab + // element. + let linkedPanelElm = null; + + let linkedPanelId = aTabElm.linkedPanel; + if (linkedPanelId) { + let ownerDoc = this.ownerDocument; + + // XXX bug 565858: if XUL tab element is anonymous element then + // suppose linked tab panel is hosted within the same XBL binding + // and search it by ID attribute inside an anonymous content of + // the binding. This is not robust assumption since tab elements may + // live outside a tabbox element so that for example tab elements + // can be explicit content but tab panels can be anonymous. + + let bindingParent = ownerDoc.getBindingParent(aTabElm); + if (bindingParent) + return ownerDoc.getAnonymousElementByAttribute(bindingParent, + "id", + linkedPanelId); + + return ownerDoc.getElementById(linkedPanelId); + } + + // otherwise linked tabpanel element has the same index as the given + // tab element. + let tabElmIdx = this.getIndexOfItem(aTabElm); + return tabpanelsElm.childNodes[tabElmIdx]; + ]]> + </body> + </method> + + <!-- nsIDOMXULSelectControlElement --> + <property name="itemCount" readonly="true" + onget="return this.childNodes.length"/> + + <property name="value" onget="return this.getAttribute('value');"> + <setter> + <![CDATA[ + this.setAttribute("value", val); + var children = this.childNodes; + for (var c = children.length - 1; c >= 0; c--) { + if (children[c].value == val) { + this.selectedIndex = c; + break; + } + } + return val; + ]]> + </setter> + </property> + + <field name="_tabbox">null</field> + <property name="tabbox" readonly="true"> + <getter><![CDATA[ + // Memoize the result in a field rather than replacing this property, + // so that it can be reset along with the binding. + if (this._tabbox) { + return this._tabbox; + } + + let parent = this.parentNode; + while (parent) { + if (parent.localName == "tabbox") { + break; + } + parent = parent.parentNode; + } + + return this._tabbox = parent; + ]]></getter> + </property> + + <!-- _tabbox is deprecated, it exists only for backwards compatibility. --> + <field name="_tabbox" readonly="true"><![CDATA[ + this.tabbox; + ]]></field> + + <property name="selectedIndex"> + <getter> + <![CDATA[ + const tabs = this.childNodes; + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].selected) + return i; + } + return -1; + ]]> + </getter> + + <setter> + <![CDATA[ + var tab = this.getItemAtIndex(val); + if (tab) { + var alreadySelected = tab.selected; + + Array.forEach(this.childNodes, function (aTab) { + if (aTab.selected && aTab != tab) + aTab._selected = false; + }); + tab._selected = true; + + this.setAttribute("value", tab.value); + + let linkedPanel = this.getRelatedElement(tab); + if (linkedPanel) { + this.tabbox.setAttribute("selectedIndex", val); + + // This will cause an onselect event to fire for the tabpanel + // element. + this.tabbox.tabpanels.selectedPanel = linkedPanel; + } + + if (!alreadySelected) { + // Fire an onselect event for the tabs element. + var event = document.createEvent('Events'); + event.initEvent('select', true, true); + this.dispatchEvent(event); + } + } + return val; + ]]> + </setter> + </property> + + <property name="selectedItem"> + <getter> + <![CDATA[ + const tabs = this.childNodes; + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].selected) + return tabs[i]; + } + return null; + ]]> + </getter> + + <setter> + <![CDATA[ + if (val && !val.selected) + // The selectedIndex setter ignores invalid values + // such as -1 if |val| isn't one of our child nodes. + this.selectedIndex = this.getIndexOfItem(val); + return val; + ]]> + </setter> + </property> + + <method name="getIndexOfItem"> + <parameter name="item"/> + <body> + <![CDATA[ + return Array.indexOf(this.childNodes, item); + ]]> + </body> + </method> + + <method name="getItemAtIndex"> + <parameter name="index"/> + <body> + <![CDATA[ + return this.childNodes.item(index); + ]]> + </body> + </method> + + <method name="_selectNewTab"> + <parameter name="aNewTab"/> + <parameter name="aFallbackDir"/> + <parameter name="aWrap"/> + <body> + <![CDATA[ + var requestedTab = aNewTab; + while (aNewTab.hidden || aNewTab.disabled || !this._canAdvanceToTab(aNewTab)) { + aNewTab = aFallbackDir == -1 ? aNewTab.previousSibling : aNewTab.nextSibling; + if (!aNewTab && aWrap) + aNewTab = aFallbackDir == -1 ? this.childNodes[this.childNodes.length - 1] : + this.childNodes[0]; + if (!aNewTab || aNewTab == requestedTab) + return; + } + + var isTabFocused = false; + try { + isTabFocused = + (document.commandDispatcher.focusedElement == this.selectedItem); + } catch (e) {} + this.selectedItem = aNewTab; + if (isTabFocused) { + aNewTab.focus(); + } + else if (this.getAttribute("setfocus") != "false") { + let selectedPanel = this.tabbox.selectedPanel; + document.commandDispatcher.advanceFocusIntoSubtree(selectedPanel); + + // Make sure that the focus doesn't move outside the tabbox + if (this.tabbox) { + try { + let el = document.commandDispatcher.focusedElement; + while (el && el != this.tabbox.tabpanels) { + if (el == this.tabbox || el == selectedPanel) + return; + el = el.parentNode; + } + aNewTab.focus(); + } catch (e) { + } + } + } + ]]> + </body> + </method> + + <method name="_canAdvanceToTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + return true; + ]]> + </body> + </method> + + <method name="advanceSelectedTab"> + <parameter name="aDir"/> + <parameter name="aWrap"/> + <body> + <![CDATA[ + var startTab = this.selectedItem; + var next = startTab[aDir == -1 ? "previousSibling" : "nextSibling"]; + if (!next && aWrap) { + next = aDir == -1 ? this.childNodes[this.childNodes.length - 1] : + this.childNodes[0]; + } + if (next && next != startTab) { + this._selectNewTab(next, aDir, aWrap); + } + ]]> + </body> + </method> + + <method name="appendItem"> + <parameter name="label"/> + <parameter name="value"/> + <body> + <![CDATA[ + var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var tab = document.createElementNS(XULNS, "tab"); + tab.setAttribute("label", label); + tab.setAttribute("value", value); + this.appendChild(tab); + return tab; + ]]> + </body> + </method> + + <method name="insertItemAt"> + <parameter name="index"/> + <parameter name="label"/> + <parameter name="value"/> + <body> + <![CDATA[ + var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var tab = document.createElementNS(XULNS, "tab"); + tab.setAttribute("label", label); + tab.setAttribute("value", value); + var before = this.getItemAtIndex(index); + if (before) + this.insertBefore(tab, before); + else + this.appendChild(tab); + return tab; + ]]> + </body> + </method> + + <method name="removeItemAt"> + <parameter name="index"/> + <body> + <![CDATA[ + var remove = this.getItemAtIndex(index); + if (remove) + this.removeChild(remove); + return remove; + ]]> + </body> + </method> + </implementation> + +#ifdef MOZ_WIDGET_GTK + <handlers> + <handler event="DOMMouseScroll"> + <![CDATA[ + if (event.detail > 0) + this.advanceSelectedTab(1, false); + else + this.advanceSelectedTab(-1, false); + + event.stopPropagation(); + ]]> + </handler> + </handlers> +#endif + </binding> + + <binding id="tabpanels" role="xul:tabpanels" + extends="chrome://global/content/bindings/tabbox.xml#tab-base"> + <implementation implements="nsIDOMXULRelatedElement"> + <!-- nsIDOMXULRelatedElement --> + <method name="getRelatedElement"> + <parameter name="aTabPanelElm"/> + <body> + <![CDATA[ + if (!aTabPanelElm) + return null; + + let tabboxElm = this.tabbox; + if (!tabboxElm) + return null; + + let tabsElm = tabboxElm.tabs; + if (!tabsElm) + return null; + + // Return tab element having 'linkedpanel' attribute equal to the id + // of the tab panel or the same index as the tab panel element. + let tabpanelIdx = Array.indexOf(this.childNodes, aTabPanelElm); + if (tabpanelIdx == -1) + return null; + + let tabElms = tabsElm.childNodes; + let tabElmFromIndex = tabElms[tabpanelIdx]; + + let tabpanelId = aTabPanelElm.id; + if (tabpanelId) { + for (let idx = 0; idx < tabElms.length; idx++) { + var tabElm = tabElms[idx]; + if (tabElm.linkedPanel == tabpanelId) + return tabElm; + } + } + + return tabElmFromIndex; + ]]> + </body> + </method> + + <!-- public --> + <field name="_tabbox">null</field> + <property name="tabbox" readonly="true"> + <getter><![CDATA[ + // Memoize the result in a field rather than replacing this property, + // so that it can be reset along with the binding. + if (this._tabbox) { + return this._tabbox; + } + + let parent = this.parentNode; + while (parent) { + if (parent.localName == "tabbox") { + break; + } + parent = parent.parentNode; + } + + return this._tabbox = parent; + ]]></getter> + </property> + + <field name="_selectedPanel">this.childNodes.item(this.selectedIndex)</field> + + <property name="selectedIndex"> + <getter> + <![CDATA[ + var indexStr = this.getAttribute("selectedIndex"); + return indexStr ? parseInt(indexStr) : -1; + ]]> + </getter> + + <setter> + <![CDATA[ + if (val < 0 || val >= this.childNodes.length) + return val; + var panel = this._selectedPanel; + this._selectedPanel = this.childNodes[val]; + this.setAttribute("selectedIndex", val); + if (this._selectedPanel != panel) { + var event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + } + return val; + ]]> + </setter> + </property> + + <property name="selectedPanel"> + <getter> + <![CDATA[ + return this._selectedPanel; + ]]> + </getter> + + <setter> + <![CDATA[ + var selectedIndex = -1; + for (var panel = val; panel != null; panel = panel.previousSibling) + ++selectedIndex; + this.selectedIndex = selectedIndex; + return val; + ]]> + </setter> + </property> + </implementation> + </binding> + + <binding id="tab" display="xul:button" role="xul:tab" + extends="chrome://global/content/bindings/general.xml#control-item"> + <resources> + <stylesheet src="chrome://global/skin/tabbox.css"/> + </resources> + + <content> + <xul:hbox class="tab-middle box-inherit" xbl:inherits="align,dir,pack,orient,selected,visuallyselected" flex="1"> + <xul:image class="tab-icon" + xbl:inherits="validate,src=image" + role="presentation"/> + <xul:label class="tab-text" + xbl:inherits="value=label,accesskey,crop,disabled" + flex="1" + role="presentation"/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMXULSelectControlItemElement"> + <property name="control" readonly="true"> + <getter> + <![CDATA[ + var parent = this.parentNode; + if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement) + return parent; + return null; + ]]> + </getter> + </property> + + <property name="selected" readonly="true" + onget="return this.getAttribute('selected') == 'true';"/> + + <property name="_selected"> + <setter><![CDATA[ + if (val) { + this.setAttribute("selected", "true"); + this.setAttribute("visuallyselected", "true"); + } else { + this.removeAttribute("selected"); + this.removeAttribute("visuallyselected"); + } + + this._setPositionAttributes(val); + + return val; + ]]></setter> + </property> + + <method name="_setPositionAttributes"> + <parameter name="aSelected"/> + <body><![CDATA[ + if (this.previousSibling && this.previousSibling.localName == "tab") { + if (aSelected) + this.previousSibling.setAttribute("beforeselected", "true"); + else + this.previousSibling.removeAttribute("beforeselected"); + this.removeAttribute("first-tab"); + } else { + this.setAttribute("first-tab", "true"); + } + + if (this.nextSibling && this.nextSibling.localName == "tab") { + if (aSelected) + this.nextSibling.setAttribute("afterselected", "true"); + else + this.nextSibling.removeAttribute("afterselected"); + this.removeAttribute("last-tab"); + } else { + this.setAttribute("last-tab", "true"); + } + ]]></body> + </method> + + <property name="linkedPanel" onget="return this.getAttribute('linkedpanel')" + onset="this.setAttribute('linkedpanel', val); return val;"/> + + <field name="arrowKeysShouldWrap" readonly="true"> + /Mac/.test(navigator.platform) + </field> + <property name="TelemetryStopwatch" readonly="true"> + <getter><![CDATA[ + let module = {}; + Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", module); + Object.defineProperty(this, "TelemetryStopwatch", { + configurable: true, + enumerable: true, + writable: true, + value: module.TelemetryStopwatch + }); + return module.TelemetryStopwatch; + ]]></getter> + </property> + </implementation> + + <handlers> + <handler event="mousedown" button="0"> + <![CDATA[ + if (this.disabled) + return; + + if (this != this.parentNode.selectedItem) { // Not selected yet + let stopwatchid = this.parentNode.getAttribute("stopwatchid"); + if (stopwatchid) { + this.TelemetryStopwatch.start(stopwatchid); + } + + // Call this before setting the 'ignorefocus' attribute because this + // will pass on focus if the formerly selected tab was focused as well. + this.parentNode._selectNewTab(this); + + var isTabFocused = false; + try { + isTabFocused = (document.commandDispatcher.focusedElement == this); + } catch (e) {} + + // Set '-moz-user-focus' to 'ignore' so that PostHandleEvent() can't + // focus the tab; we only want tabs to be focusable by the mouse if + // they are already focused. After a short timeout we'll reset + // '-moz-user-focus' so that tabs can be focused by keyboard again. + if (!isTabFocused) { + this.setAttribute("ignorefocus", "true"); + setTimeout(tab => tab.removeAttribute("ignorefocus"), 0, this); + } + + if (stopwatchid) { + this.TelemetryStopwatch.finish(stopwatchid); + } + } + // Otherwise this tab is already selected and we will fall + // through to mousedown behavior which sets focus on the current tab, + // Only a click on an already selected tab should focus the tab itself. + ]]> + </handler> + + <handler event="keydown" keycode="VK_LEFT" group="system" preventdefault="true"> + <![CDATA[ + var direction = window.getComputedStyle(this.parentNode, null).direction; + this.parentNode.advanceSelectedTab(direction == 'ltr' ? -1 : 1, this.arrowKeysShouldWrap); + ]]> + </handler> + + <handler event="keydown" keycode="VK_RIGHT" group="system" preventdefault="true"> + <![CDATA[ + var direction = window.getComputedStyle(this.parentNode, null).direction; + this.parentNode.advanceSelectedTab(direction == 'ltr' ? 1 : -1, this.arrowKeysShouldWrap); + ]]> + </handler> + + <handler event="keydown" keycode="VK_UP" group="system" preventdefault="true"> + <![CDATA[ + this.parentNode.advanceSelectedTab(-1, this.arrowKeysShouldWrap); + ]]> + </handler> + + <handler event="keydown" keycode="VK_DOWN" group="system" preventdefault="true"> + <![CDATA[ + this.parentNode.advanceSelectedTab(1, this.arrowKeysShouldWrap); + ]]> + </handler> + + <handler event="keydown" keycode="VK_HOME" group="system" preventdefault="true"> + <![CDATA[ + this.parentNode._selectNewTab(this.parentNode.childNodes[0]); + ]]> + </handler> + + <handler event="keydown" keycode="VK_END" group="system" preventdefault="true"> + <![CDATA[ + var tabs = this.parentNode.childNodes; + this.parentNode._selectNewTab(tabs[tabs.length - 1], -1); + ]]> + </handler> + </handlers> + </binding> + +</bindings> + diff --git a/toolkit/content/widgets/text.xml b/toolkit/content/widgets/text.xml new file mode 100644 index 0000000000..ed998cee47 --- /dev/null +++ b/toolkit/content/widgets/text.xml @@ -0,0 +1,386 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="textBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <!-- bound to <description>s --> + <binding id="text-base" role="xul:text"> + <implementation implements="nsIDOMXULDescriptionElement"> + <property name="disabled" onset="if (val) this.setAttribute('disabled', 'true'); + else this.removeAttribute('disabled'); + return val;" + onget="return this.getAttribute('disabled') == 'true';"/> + <property name="value" onget="return this.getAttribute('value');" + onset="this.setAttribute('value', val); return val;"/> + <property name="crop" onget="return this.getAttribute('crop');" + onset="this.setAttribute('crop', val); return val;"/> + </implementation> + </binding> + + <binding id="text-label" extends="chrome://global/content/bindings/text.xml#text-base"> + <implementation implements="nsIDOMXULLabelElement"> + <property name="accessKey"> + <getter> + <![CDATA[ + var accessKey = this.getAttribute('accesskey'); + return accessKey ? accessKey[0] : null; + ]]> + </getter> + <setter> + <![CDATA[ + this.setAttribute('accesskey', val); + return val; + ]]> + </setter> + </property> + + <property name="control" onget="return getAttribute('control');"> + <setter> + <![CDATA[ + // After this gets set, the label will use the binding #label-control + this.setAttribute('control', val); + return val; + ]]> + </setter> + </property> + </implementation> + </binding> + + <binding id="label-control" extends="chrome://global/content/bindings/text.xml#text-label"> + <content> + <children/><html:span anonid="accessKeyParens"></html:span> + </content> + <implementation implements="nsIDOMXULLabelElement"> + <constructor> + <![CDATA[ + this.formatAccessKey(true); + ]]> + </constructor> + + <method name="formatAccessKey"> + <parameter name="firstTime"/> + <body> + <![CDATA[ + var control = this.labeledControlElement; + if (!control) { + var bindingParent = document.getBindingParent(this); + if (bindingParent instanceof Components.interfaces.nsIDOMXULLabeledControlElement) { + control = bindingParent; // For controls that make the <label> an anon child + } + } + if (control) { + control.labelElement = this; + } + + var accessKey = this.accessKey; + // No need to remove existing formatting the first time. + if (firstTime && !accessKey) + return; + + if (this.mInsertSeparator === undefined) { + try { + var prefs = Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch); + this.mUnderlineAccesskey = (prefs.getIntPref("ui.key.menuAccessKey") != 0); + + const nsIPrefLocalizedString = + Components.interfaces.nsIPrefLocalizedString; + + const prefNameInsertSeparator = + "intl.menuitems.insertseparatorbeforeaccesskeys"; + const prefNameAlwaysAppendAccessKey = + "intl.menuitems.alwaysappendaccesskeys"; + + var val = prefs.getComplexValue(prefNameInsertSeparator, + nsIPrefLocalizedString).data; + this.mInsertSeparator = (val == "true"); + + val = prefs.getComplexValue(prefNameAlwaysAppendAccessKey, + nsIPrefLocalizedString).data; + this.mAlwaysAppendAccessKey = (val == "true"); + } + catch (e) { + this.mInsertSeparator = true; + } + } + + if (!this.mUnderlineAccesskey) + return; + + var afterLabel = document.getAnonymousElementByAttribute(this, "anonid", "accessKeyParens"); + afterLabel.textContent = ""; + + var oldAccessKey = this.getElementsByAttribute('class', 'accesskey').item(0); + if (oldAccessKey) { // Clear old accesskey + this.mergeElement(oldAccessKey); + } + + var oldHiddenSpan = + this.getElementsByAttribute('class', 'hiddenColon').item(0); + if (oldHiddenSpan) { + this.mergeElement(oldHiddenSpan); + } + + var labelText = this.textContent; + if (!accessKey || !labelText || !control) { + return; + } + var accessKeyIndex = -1; + if (!this.mAlwaysAppendAccessKey) { + accessKeyIndex = labelText.indexOf(accessKey); + if (accessKeyIndex < 0) { // Try again in upper case + accessKeyIndex = + labelText.toUpperCase().indexOf(accessKey.toUpperCase()); + } + } + + const HTML_NS = "http://www.w3.org/1999/xhtml"; + var span = document.createElementNS(HTML_NS, "span"); + span.className = "accesskey"; + + // Note that if you change the following code, see the comment of + // nsTextBoxFrame::UpdateAccessTitle. + + // If accesskey is not in string, append in parentheses + if (accessKeyIndex < 0) { + // If end is colon, we should insert before colon. + // i.e., "label:" -> "label(X):" + var colonHidden = false; + if (/:$/.test(labelText)) { + labelText = labelText.slice(0, -1); + var hiddenSpan = document.createElementNS(HTML_NS, "span"); + hiddenSpan.className = "hiddenColon"; + hiddenSpan.style.display = "none"; + // Hide the last colon by using span element. + // I.e., label<span style="display:none;">:</span> + this.wrapChar(hiddenSpan, labelText.length); + colonHidden = true; + } + // If end is space(U+20), + // we should not add space before parentheses. + var endIsSpace = false; + if (/ $/.test(labelText)) { + endIsSpace = true; + } + if (this.mInsertSeparator && !endIsSpace) + afterLabel.textContent = " ("; + else + afterLabel.textContent = "("; + span.textContent = accessKey.toUpperCase(); + afterLabel.appendChild(span); + if (!colonHidden) + afterLabel.appendChild(document.createTextNode(")")); + else + afterLabel.appendChild(document.createTextNode("):")); + return; + } + this.wrapChar(span, accessKeyIndex); + ]]> + </body> + </method> + + <method name="wrapChar"> + <parameter name="element"/> + <parameter name="index"/> + <body> + <![CDATA[ + var treeWalker = document.createTreeWalker(this, + NodeFilter.SHOW_TEXT, + null); + var node = treeWalker.nextNode(); + while (index >= node.length) { + index -= node.length; + node = treeWalker.nextNode(); + } + if (index) { + node = node.splitText(index); + } + node.parentNode.insertBefore(element, node); + if (node.length > 1) { + node.splitText(1); + } + element.appendChild(node); + ]]> + </body> + </method> + + <method name="mergeElement"> + <parameter name="element"/> + <body> + <![CDATA[ + if (element.previousSibling instanceof Text) { + element.previousSibling.appendData(element.textContent) + } + else { + element.parentNode.insertBefore(element.firstChild, element); + } + element.parentNode.removeChild(element); + ]]> + </body> + </method> + + <field name="mUnderlineAccesskey"> + !/Mac/.test(navigator.platform) + </field> + <field name="mInsertSeparator"/> + <field name="mAlwaysAppendAccessKey">false</field> + + <property name="accessKey"> + <getter> + <![CDATA[ + var accessKey = null; + var labeledEl = this.labeledControlElement; + if (labeledEl) { + accessKey = labeledEl.getAttribute('accesskey'); + } + if (!accessKey) { + accessKey = this.getAttribute('accesskey'); + } + return accessKey ? accessKey[0] : null; + ]]> + </getter> + <setter> + <![CDATA[ + // If this label already has an accesskey attribute store it here as well + if (this.hasAttribute('accesskey')) { + this.setAttribute('accesskey', val); + } + var control = this.labeledControlElement; + if (control) { + control.setAttribute('accesskey', val); + } + this.formatAccessKey(false); + return val; + ]]> + </setter> + </property> + + <property name="labeledControlElement" readonly="true" + onget="var control = this.control; return control ? document.getElementById(control) : null;" /> + + <property name="control" onget="return this.getAttribute('control');"> + <setter> + <![CDATA[ + var control = this.labeledControlElement; + if (control) { + control.labelElement = null; // No longer pointed to be this label + } + this.setAttribute('control', val); + this.formatAccessKey(false); + return val; + ]]> + </setter> + </property> + + </implementation> + + <handlers> + <handler event="click" action="if (this.disabled) return; + var controlElement = this.labeledControlElement; + if(controlElement) + controlElement.focus(); + "/> + </handlers> + </binding> + + <binding id="text-link" extends="chrome://global/content/bindings/text.xml#text-label" role="xul:link"> + <implementation> + <property name="href" onget="return this.getAttribute('href');" + onset="this.setAttribute('href', val); return val;" /> + <method name="open"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var href = this.href; + if (!href || this.disabled || aEvent.defaultPrevented) + return; + + var uri = null; + try { + const nsISSM = Components.interfaces.nsIScriptSecurityManager; + const secMan = + Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(nsISSM); + + const ioService = + Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + uri = ioService.newURI(href, null, null); + + let principal; + if (this.getAttribute("useoriginprincipal") == "true") { + principal = this.nodePrincipal; + } else { + principal = secMan.createNullPrincipal({}); + } + try { + secMan.checkLoadURIWithPrincipal(principal, uri, + nsISSM.DISALLOW_INHERIT_PRINCIPAL); + } + catch (ex) { + var msg = "Error: Cannot open a " + uri.scheme + ": link using \ + the text-link binding."; + Components.utils.reportError(msg); + return; + } + + const cID = "@mozilla.org/uriloader/external-protocol-service;1"; + const nsIEPS = Components.interfaces.nsIExternalProtocolService; + var protocolSvc = Components.classes[cID].getService(nsIEPS); + + // if the scheme is not an exposed protocol, then opening this link + // should be deferred to the system's external protocol handler + if (!protocolSvc.isExposedProtocol(uri.scheme)) { + protocolSvc.loadUrl(uri); + aEvent.preventDefault() + return; + } + + } + catch (ex) { + Components.utils.reportError(ex); + } + + aEvent.preventDefault(); + href = uri ? uri.spec : href; + + // Try handing off the link to the host application, e.g. for + // opening it in a tabbed browser. + var linkHandled = Components.classes["@mozilla.org/supports-PRBool;1"] + .createInstance(Components.interfaces.nsISupportsPRBool); + linkHandled.data = false; + let {shiftKey, ctrlKey, metaKey, altKey, button} = aEvent; + let data = {shiftKey, ctrlKey, metaKey, altKey, button, href}; + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .notifyObservers(linkHandled, "handle-xul-text-link", JSON.stringify(data)); + if (linkHandled.data) + return; + + // otherwise, fall back to opening the anchor directly + var win = window; + if (window instanceof Components.interfaces.nsIDOMChromeWindow) { + while (win.opener && !win.opener.closed) + win = win.opener; + } + win.open(href); + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="click" phase="capturing" button="0" action="this.open(event)"/> + <handler event="click" phase="capturing" button="1" action="this.open(event)"/> + <handler event="keypress" preventdefault="true" keycode="VK_RETURN" action="this.click()" /> + </handlers> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/textbox.xml b/toolkit/content/widgets/textbox.xml new file mode 100644 index 0000000000..f166fb78a9 --- /dev/null +++ b/toolkit/content/widgets/textbox.xml @@ -0,0 +1,646 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<!DOCTYPE bindings [ + <!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" > + %textcontextDTD; +]> + +<bindings id="textboxBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="textbox" extends="xul:box" role="xul:textbox"> + <resources> + <stylesheet src="chrome://global/content/textbox.css"/> + <stylesheet src="chrome://global/skin/textbox.css"/> + </resources> + + <content> + <children/> + <xul:hbox class="textbox-input-box" flex="1" xbl:inherits="context,spellcheck"> + <html:input class="textbox-input" anonid="input" + xbl:inherits="value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,noinitialfocus,mozactionhint,spellcheck"/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMXULTextBoxElement, nsIDOMXULLabeledControlElement"> + <!-- nsIDOMXULLabeledControlElement --> + <field name="crop">""</field> + <field name="image">""</field> + <field name="command">""</field> + <field name="accessKey">""</field> + + <field name="mInputField">null</field> + <field name="mIgnoreClick">false</field> + <field name="mIgnoreFocus">false</field> + <field name="mEditor">null</field> + + <property name="inputField" readonly="true"> + <getter><![CDATA[ + if (!this.mInputField) + this.mInputField = document.getAnonymousElementByAttribute(this, "anonid", "input"); + return this.mInputField; + ]]></getter> + </property> + + <property name="value" onset="this.inputField.value = val; return val;" + onget="return this.inputField.value;"/> + <property name="defaultValue" onset="this.inputField.defaultValue = val; return val;" + onget="return this.inputField.defaultValue;"/> + <property name="label" onset="this.setAttribute('label', val); return val;" + onget="return this.getAttribute('label') || + (this.labelElement ? this.labelElement.value : + this.placeholder);"/> + <property name="placeholder" onset="this.inputField.placeholder = val; return val;" + onget="return this.inputField.placeholder;"/> + <property name="emptyText" onset="this.placeholder = val; return val;" + onget="return this.placeholder;"/> + <property name="type" onset="if (val) this.setAttribute('type', val); + else this.removeAttribute('type'); return val;" + onget="return this.getAttribute('type');"/> + <property name="maxLength" onset="this.inputField.maxLength = val; return val;" + onget="return this.inputField.maxLength;"/> + <property name="disabled" onset="this.inputField.disabled = val; + if (val) this.setAttribute('disabled', 'true'); + else this.removeAttribute('disabled'); return val;" + onget="return this.inputField.disabled;"/> + <property name="tabIndex" onget="return parseInt(this.getAttribute('tabindex'));" + onset="this.inputField.tabIndex = val; + if (val) this.setAttribute('tabindex', val); + else this.removeAttribute('tabindex'); return val;"/> + <property name="size" onset="this.inputField.size = val; return val;" + onget="return this.inputField.size;"/> + <property name="readOnly" onset="this.inputField.readOnly = val; + if (val) this.setAttribute('readonly', 'true'); + else this.removeAttribute('readonly'); return val;" + onget="return this.inputField.readOnly;"/> + <property name="clickSelectsAll" + onget="return this.getAttribute('clickSelectsAll') == 'true';" + onset="if (val) this.setAttribute('clickSelectsAll', 'true'); + else this.removeAttribute('clickSelectsAll'); return val;" /> + + <property name="editor" readonly="true"> + <getter><![CDATA[ + if (!this.mEditor) { + const nsIDOMNSEditableElement = Components.interfaces.nsIDOMNSEditableElement; + this.mEditor = this.inputField.QueryInterface(nsIDOMNSEditableElement).editor; + } + return this.mEditor; + ]]></getter> + </property> + + <method name="reset"> + <body><![CDATA[ + this.value = this.defaultValue; + try { + this.editor.transactionManager.clear(); + return true; + } + catch (e) {} + return false; + ]]></body> + </method> + + <method name="select"> + <body> + this.inputField.select(); + </body> + </method> + + <property name="controllers" readonly="true" onget="return this.inputField.controllers"/> + <property name="textLength" readonly="true" + onget="return this.inputField.textLength;"/> + <property name="selectionStart" onset="this.inputField.selectionStart = val; return val;" + onget="return this.inputField.selectionStart;"/> + <property name="selectionEnd" onset="this.inputField.selectionEnd = val; return val;" + onget="return this.inputField.selectionEnd;"/> + + <method name="setSelectionRange"> + <parameter name="aSelectionStart"/> + <parameter name="aSelectionEnd"/> + <body> + this.inputField.setSelectionRange( aSelectionStart, aSelectionEnd ); + </body> + </method> + + <method name="_setNewlineHandling"> + <body><![CDATA[ + var str = this.getAttribute("newlines"); + if (str && this.editor) { + const nsIPlaintextEditor = Components.interfaces.nsIPlaintextEditor; + for (var x in nsIPlaintextEditor) { + if (/^eNewlines/.test(x)) { + if (str == RegExp.rightContext.toLowerCase()) { + this.editor.QueryInterface(nsIPlaintextEditor) + .newlineHandling = nsIPlaintextEditor[x]; + break; + } + } + } + } + ]]></body> + </method> + + <method name="_maybeSelectAll"> + <body><![CDATA[ + if (!this.mIgnoreClick && this.clickSelectsAll && + document.activeElement == this.inputField && + this.inputField.selectionStart == this.inputField.selectionEnd) + this.editor.selectAll(); + ]]></body> + </method> + + <constructor><![CDATA[ + var str = this.boxObject.getProperty("value"); + if (str) { + this.inputField.value = str; + this.boxObject.removeProperty("value"); + } + + this._setNewlineHandling(); + + if (this.hasAttribute("emptytext")) + this.placeholder = this.getAttribute("emptytext"); + ]]></constructor> + + <destructor> + <![CDATA[ + var field = this.inputField; + if (field && field.value) + this.boxObject.setProperty('value', field.value); + this.mInputField = null; + ]]> + </destructor> + + </implementation> + + <handlers> + <handler event="focus" phase="capturing"> + <![CDATA[ + if (this.hasAttribute("focused")) + return; + + switch (event.originalTarget) { + case this: + // Forward focus to actual HTML input + this.inputField.focus(); + break; + case this.inputField: + if (this.mIgnoreFocus) { + this.mIgnoreFocus = false; + } else if (this.clickSelectsAll) { + try { + const nsIEditorIMESupport = + Components.interfaces.nsIEditorIMESupport; + let imeEditor = this.editor.QueryInterface(nsIEditorIMESupport); + if (!imeEditor || !imeEditor.composing) + this.editor.selectAll(); + } catch (e) {} + } + break; + default: + // Allow other children (e.g. URL bar buttons) to get focus + return; + } + this.setAttribute("focused", "true"); + ]]> + </handler> + + <handler event="blur" phase="capturing"> + <![CDATA[ + this.removeAttribute("focused"); + + // don't trigger clickSelectsAll when switching application windows + if (window == window.top && + window.constructor == ChromeWindow && + document.activeElement == this.inputField) + this.mIgnoreFocus = true; + ]]> + </handler> + + <handler event="mousedown"> + <![CDATA[ + this.mIgnoreClick = this.hasAttribute("focused"); + + if (!this.mIgnoreClick) { + this.mIgnoreFocus = true; + this.inputField.setSelectionRange(0, 0); + if (event.originalTarget == this || + event.originalTarget == this.inputField.parentNode) + this.inputField.focus(); + } + ]]> + </handler> + + <handler event="click" action="this._maybeSelectAll();"/> + +#ifndef XP_WIN + <handler event="contextmenu"> + // Only care about context clicks on the textbox itself. + if (event.target != this) + return; + + if (!event.button) // context menu opened via keyboard shortcut + return; + this._maybeSelectAll(); + // see bug 576135 comment 4 + let box = this.inputField.parentNode; + let menu = document.getAnonymousElementByAttribute(box, "anonid", "input-box-contextmenu"); + box._doPopupItemEnabling(menu); + </handler> +#endif + </handlers> + </binding> + + <binding id="timed-textbox" extends="chrome://global/content/bindings/textbox.xml#textbox"> + <implementation> + <constructor><![CDATA[ + try { + var consoleService = Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService); + var scriptError = Components.classes["@mozilla.org/scripterror;1"] + .createInstance(Components.interfaces.nsIScriptError); + scriptError.init("Timed textboxes are deprecated. Consider using type=\"search\" instead.", + this.ownerDocument.location.href, null, null, + null, scriptError.warningFlag, "XUL Widgets"); + consoleService.logMessage(scriptError); + } catch (e) {} + ]]></constructor> + <field name="_timer">null</field> + <property name="timeout" + onset="this.setAttribute('timeout', val); return val;" + onget="return parseInt(this.getAttribute('timeout')) || 0;"/> + <property name="value" + onget="return this.inputField.value;"> + <setter><![CDATA[ + this.inputField.value = val; + if (this._timer) + clearTimeout(this._timer); + return val; + ]]></setter> + </property> + <method name="_fireCommand"> + <parameter name="me"/> + <body> + <![CDATA[ + me._timer = null; + me.doCommand(); + ]]> + </body> + </method> + </implementation> + <handlers> + <handler event="input"> + <![CDATA[ + if (this._timer) + clearTimeout(this._timer); + this._timer = this.timeout && setTimeout(this._fireCommand, this.timeout, this); + ]]> + </handler> + <handler event="keypress" keycode="VK_RETURN"> + <![CDATA[ + if (this._timer) + clearTimeout(this._timer); + this._fireCommand(this); + event.preventDefault(); + ]]> + </handler> + </handlers> + </binding> + + <binding id="search-textbox" extends="chrome://global/content/bindings/textbox.xml#textbox"> + <content> + <children/> + <xul:hbox class="textbox-input-box" flex="1" xbl:inherits="context,spellcheck" align="center"> + <html:input class="textbox-input" anonid="input" mozactionhint="search" + xbl:inherits="value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint,spellcheck"/> + <xul:deck class="textbox-search-icons" anonid="search-icons"> + <xul:image class="textbox-search-icon" anonid="searchbutton-icon" + xbl:inherits="src=image,label=searchbuttonlabel,searchbutton,disabled"/> + <xul:image class="textbox-search-clear" + onclick="document.getBindingParent(this)._clearSearch();" + label="&searchTextBox.clear.label;" + xbl:inherits="disabled"/> + </xul:deck> + </xul:hbox> + </content> + <implementation> + <field name="_timer">null</field> + <field name="_searchIcons"> + document.getAnonymousElementByAttribute(this, "anonid", "search-icons"); + </field> + <field name="_searchButtonIcon"> + document.getAnonymousElementByAttribute(this, "anonid", "searchbutton-icon"); + </field> + <property name="timeout" + onset="this.setAttribute('timeout', val); return val;" + onget="return parseInt(this.getAttribute('timeout')) || 500;"/> + <property name="searchButton" + onget="return this.getAttribute('searchbutton') == 'true';"> + <setter><![CDATA[ + if (val) { + this.setAttribute("searchbutton", "true"); + this.removeAttribute("aria-autocomplete"); + // Hack for the button to get the right accessible: + this._searchButtonIcon.setAttribute("onclick", "true"); + } else { + this.removeAttribute("searchbutton"); + this._searchButtonIcon.removeAttribute("onclick"); + this.setAttribute("aria-autocomplete", "list"); + } + return val; + ]]></setter> + </property> + <property name="value" + onget="return this.inputField.value;"> + <setter><![CDATA[ + this.inputField.value = val; + + if (val) + this._searchIcons.selectedIndex = this.searchButton ? 0 : 1; + else + this._searchIcons.selectedIndex = 0; + + if (this._timer) + clearTimeout(this._timer); + + return val; + ]]></setter> + </property> + <constructor><![CDATA[ + // Ensure the button state is up to date: + this.searchButton = this.searchButton; + this._searchButtonIcon.addEventListener("click", (e) => this._iconClick(e), false); + ]]></constructor> + <method name="_fireCommand"> + <parameter name="me"/> + <body><![CDATA[ + if (me._timer) + clearTimeout(me._timer); + me._timer = null; + me.doCommand(); + ]]></body> + </method> + <method name="_iconClick"> + <body><![CDATA[ + if (this.searchButton) + this._enterSearch(); + else + this.focus(); + ]]></body> + </method> + <method name="_enterSearch"> + <body><![CDATA[ + if (this.disabled) + return; + if (this.searchButton && this.value && !this.readOnly) + this._searchIcons.selectedIndex = 1; + this._fireCommand(this); + ]]></body> + </method> + <method name="_clearSearch"> + <body><![CDATA[ + if (!this.disabled && !this.readOnly && this.value) { + this.value = ""; + this._fireCommand(this); + this._searchIcons.selectedIndex = 0; + return true; + } + return false; + ]]></body> + </method> + </implementation> + <handlers> + <handler event="input"> + <![CDATA[ + if (this.searchButton) { + this._searchIcons.selectedIndex = 0; + return; + } + if (this._timer) + clearTimeout(this._timer); + this._timer = this.timeout && setTimeout(this._fireCommand, this.timeout, this); + this._searchIcons.selectedIndex = this.value ? 1 : 0; + ]]> + </handler> + <handler event="keypress" keycode="VK_ESCAPE"> + <![CDATA[ + if (this._clearSearch()) { + event.preventDefault(); + event.stopPropagation(); + } + ]]> + </handler> + <handler event="keypress" keycode="VK_RETURN"> + <![CDATA[ + this._enterSearch(); + event.preventDefault(); + event.stopPropagation(); + ]]> + </handler> + </handlers> + </binding> + + <binding id="textarea" extends="chrome://global/content/bindings/textbox.xml#textbox"> + <content> + <xul:hbox class="textbox-input-box" flex="1" xbl:inherits="context,spellcheck"> + <html:textarea class="textbox-textarea" anonid="input" + xbl:inherits="xbl:text=value,disabled,tabindex,rows,cols,readonly,wrap,placeholder,mozactionhint,spellcheck"><children/></html:textarea> + </xul:hbox> + </content> + </binding> + + <binding id="input-box"> + <content context="_child"> + <children/> + <xul:menupopup anonid="input-box-contextmenu" + class="textbox-contextmenu" + onpopupshowing="var input = + this.parentNode.getElementsByAttribute('anonid', 'input')[0]; + if (document.commandDispatcher.focusedElement != input) + input.focus(); + this.parentNode._doPopupItemEnabling(this);" + oncommand="var cmd = event.originalTarget.getAttribute('cmd'); if(cmd) { this.parentNode.doCommand(cmd); event.stopPropagation(); }"> + <xul:menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"/> + <xul:menuseparator/> + <xul:menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"/> + <xul:menuitem label="©Cmd.label;" accesskey="©Cmd.accesskey;" cmd="cmd_copy"/> + <xul:menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" cmd="cmd_paste"/> + <xul:menuitem label="&deleteCmd.label;" accesskey="&deleteCmd.accesskey;" cmd="cmd_delete"/> + <xul:menuseparator/> + <xul:menuitem label="&selectAllCmd.label;" accesskey="&selectAllCmd.accesskey;" cmd="cmd_selectAll"/> + </xul:menupopup> + </content> + + <implementation> + <method name="_doPopupItemEnabling"> + <parameter name="popupNode"/> + <body> + <![CDATA[ + var children = popupNode.childNodes; + for (var i = 0; i < children.length; i++) { + var command = children[i].getAttribute("cmd"); + if (command) { + var controller = document.commandDispatcher.getControllerForCommand(command); + var enabled = controller.isCommandEnabled(command); + if (enabled) + children[i].removeAttribute("disabled"); + else + children[i].setAttribute("disabled", "true"); + } + } + ]]> + </body> + </method> + + <method name="_setMenuItemVisibility"> + <parameter name="anonid"/> + <parameter name="visible"/> + <body><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", anonid). + hidden = ! visible; + ]]></body> + </method> + + <method name="doCommand"> + <parameter name="command"/> + <body> + <![CDATA[ + var controller = document.commandDispatcher.getControllerForCommand(command); + controller.doCommand(command); + ]]> + </body> + </method> + </implementation> + </binding> + + <binding id="input-box-spell" extends="chrome://global/content/bindings/textbox.xml#input-box"> + <content context="_child"> + <children/> + <xul:menupopup anonid="input-box-contextmenu" + class="textbox-contextmenu" + onpopupshowing="var input = + this.parentNode.getElementsByAttribute('anonid', 'input')[0]; + if (document.commandDispatcher.focusedElement != input) + input.focus(); + this.parentNode._doPopupItemEnablingSpell(this);" + onpopuphiding="this.parentNode._doPopupItemDisabling(this);" + oncommand="var cmd = event.originalTarget.getAttribute('cmd'); if(cmd) { this.parentNode.doCommand(cmd); event.stopPropagation(); }"> + <xul:menuitem label="&spellNoSuggestions.label;" anonid="spell-no-suggestions" disabled="true"/> + <xul:menuitem label="&spellAddToDictionary.label;" accesskey="&spellAddToDictionary.accesskey;" anonid="spell-add-to-dictionary" + oncommand="this.parentNode.parentNode.spellCheckerUI.addToDictionary();"/> + <xul:menuitem label="&spellUndoAddToDictionary.label;" accesskey="&spellUndoAddToDictionary.accesskey;" anonid="spell-undo-add-to-dictionary" + oncommand="this.parentNode.parentNode.spellCheckerUI.undoAddToDictionary();"/> + <xul:menuseparator anonid="spell-suggestions-separator"/> + <xul:menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"/> + <xul:menuseparator/> + <xul:menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"/> + <xul:menuitem label="©Cmd.label;" accesskey="©Cmd.accesskey;" cmd="cmd_copy"/> + <xul:menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" cmd="cmd_paste"/> + <xul:menuitem label="&deleteCmd.label;" accesskey="&deleteCmd.accesskey;" cmd="cmd_delete"/> + <xul:menuseparator/> + <xul:menuitem label="&selectAllCmd.label;" accesskey="&selectAllCmd.accesskey;" cmd="cmd_selectAll"/> + <xul:menuseparator anonid="spell-check-separator"/> + <xul:menuitem label="&spellCheckToggle.label;" type="checkbox" accesskey="&spellCheckToggle.accesskey;" anonid="spell-check-enabled" + oncommand="this.parentNode.parentNode.spellCheckerUI.toggleEnabled();"/> + <xul:menu label="&spellDictionaries.label;" accesskey="&spellDictionaries.accesskey;" anonid="spell-dictionaries"> + <xul:menupopup anonid="spell-dictionaries-menu" + onpopupshowing="event.stopPropagation();" + onpopuphiding="event.stopPropagation();"/> + </xul:menu> + </xul:menupopup> + </content> + + <implementation> + <field name="_spellCheckInitialized">false</field> + <field name="_enabledCheckbox"> + document.getAnonymousElementByAttribute(this, "anonid", "spell-check-enabled"); + </field> + <field name="_suggestionsSeparator"> + document.getAnonymousElementByAttribute(this, "anonid", "spell-no-suggestions"); + </field> + <field name="_dictionariesMenu"> + document.getAnonymousElementByAttribute(this, "anonid", "spell-dictionaries-menu"); + </field> + + <property name="spellCheckerUI" readonly="true"> + <getter><![CDATA[ + if (!this._spellCheckInitialized) { + this._spellCheckInitialized = true; + + const CI = Components.interfaces; + if (!(document instanceof CI.nsIDOMXULDocument)) + return null; + + var textbox = document.getBindingParent(this); + if (!textbox || !(textbox instanceof CI.nsIDOMXULTextBoxElement)) + return null; + + try { + Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm", this); + this.InlineSpellCheckerUI = new this.InlineSpellChecker(textbox.editor); + } catch (ex) { } + } + + return this.InlineSpellCheckerUI; + ]]></getter> + </property> + + <method name="_doPopupItemEnablingSpell"> + <parameter name="popupNode"/> + <body> + <![CDATA[ + var spellui = this.spellCheckerUI; + if (!spellui || !spellui.canSpellCheck) { + this._setMenuItemVisibility("spell-no-suggestions", false); + this._setMenuItemVisibility("spell-check-enabled", false); + this._setMenuItemVisibility("spell-check-separator", false); + this._setMenuItemVisibility("spell-add-to-dictionary", false); + this._setMenuItemVisibility("spell-undo-add-to-dictionary", false); + this._setMenuItemVisibility("spell-suggestions-separator", false); + this._setMenuItemVisibility("spell-dictionaries", false); + return; + } + + spellui.initFromEvent(document.popupRangeParent, + document.popupRangeOffset); + + var enabled = spellui.enabled; + var showUndo = spellui.canSpellCheck && spellui.canUndo(); + this._enabledCheckbox.setAttribute("checked", enabled); + + var overMisspelling = spellui.overMisspelling; + this._setMenuItemVisibility("spell-add-to-dictionary", overMisspelling); + this._setMenuItemVisibility("spell-undo-add-to-dictionary", showUndo); + this._setMenuItemVisibility("spell-suggestions-separator", overMisspelling || showUndo); + + // suggestion list + var numsug = spellui.addSuggestionsToMenu(popupNode, this._suggestionsSeparator, 5); + this._setMenuItemVisibility("spell-no-suggestions", overMisspelling && numsug == 0); + + // dictionary list + var numdicts = spellui.addDictionaryListToMenu(this._dictionariesMenu, null); + this._setMenuItemVisibility("spell-dictionaries", enabled && numdicts > 1); + + this._doPopupItemEnabling(popupNode); + ]]> + </body> + </method> + <method name="_doPopupItemDisabling"> + <body><![CDATA[ + if (this.spellCheckerUI) { + this.spellCheckerUI.clearSuggestionsFromMenu(); + this.spellCheckerUI.clearDictionaryListFromMenu(); + } + ]]></body> + </method> + </implementation> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/timekeeper.js b/toolkit/content/widgets/timekeeper.js new file mode 100644 index 0000000000..2234c9e509 --- /dev/null +++ b/toolkit/content/widgets/timekeeper.js @@ -0,0 +1,418 @@ +/* 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/. */ + +"use strict"; + +/** + * TimeKeeper keeps track of the time states. Given min, max, step, and + * format (12/24hr), TimeKeeper will determine the ranges of possible + * selections, and whether or not the current time state is out of range + * or off step. + * + * @param {Object} props + * { + * {Date} min + * {Date} max + * {Number} stepInMs + * {String} format: Either "12" or "24" + * } + */ +function TimeKeeper(props) { + this.props = props; + this.state = { time: new Date(0), ranges: {} }; +} + +{ + const debug = 0 ? console.log.bind(console, '[timekeeper]') : function() {}; + + const DAY_PERIOD_IN_HOURS = 12, + SECOND_IN_MS = 1000, + MINUTE_IN_MS = 60000, + HOUR_IN_MS = 3600000, + DAY_PERIOD_IN_MS = 43200000, + DAY_IN_MS = 86400000, + TIME_FORMAT_24 = "24"; + + TimeKeeper.prototype = { + /** + * Getters for different time units. + * @return {Number} + */ + get hour() { + return this.state.time.getUTCHours(); + }, + get minute() { + return this.state.time.getUTCMinutes(); + }, + get second() { + return this.state.time.getUTCSeconds(); + }, + get millisecond() { + return this.state.time.getUTCMilliseconds(); + }, + get dayPeriod() { + // 0 stands for AM and 12 for PM + return this.state.time.getUTCHours() < DAY_PERIOD_IN_HOURS ? 0 : DAY_PERIOD_IN_HOURS; + }, + + /** + * Get the ranges of different time units. + * @return {Object} + * { + * {Array<Number>} dayPeriod + * {Array<Number>} hours + * {Array<Number>} minutes + * {Array<Number>} seconds + * {Array<Number>} milliseconds + * } + */ + get ranges() { + return this.state.ranges; + }, + + /** + * Set new time, check if the current state is valid, and set ranges. + * + * @param {Object} timeState: The new time + * { + * {Number} hour [optional] + * {Number} minute [optional] + * {Number} second [optional] + * {Number} millisecond [optional] + * } + */ + setState(timeState) { + const { min, max } = this.props; + const { hour, minute, second, millisecond } = timeState; + + if (hour != undefined) { + this.state.time.setUTCHours(hour); + } + if (minute != undefined) { + this.state.time.setUTCMinutes(minute); + } + if (second != undefined) { + this.state.time.setUTCSeconds(second); + } + if (millisecond != undefined) { + this.state.time.setUTCMilliseconds(millisecond); + } + + this.state.isOffStep = this._isOffStep(this.state.time); + this.state.isOutOfRange = (this.state.time < min || this.state.time > max); + this.state.isInvalid = this.state.isOutOfRange || this.state.isOffStep; + + this._setRanges(this.dayPeriod, this.hour, this.minute, this.second); + }, + + /** + * Set day-period (AM/PM) + * @param {Number} dayPeriod: 0 as AM, 12 as PM + */ + setDayPeriod(dayPeriod) { + if (dayPeriod == this.dayPeriod) { + return; + } + + if (dayPeriod == 0) { + this.setState({ hour: this.hour - DAY_PERIOD_IN_HOURS }); + } else { + this.setState({ hour: this.hour + DAY_PERIOD_IN_HOURS }); + } + }, + + /** + * Set hour in 24hr format (0 ~ 23) + * @param {Number} hour + */ + setHour(hour) { + this.setState({ hour }); + }, + + /** + * Set minute (0 ~ 59) + * @param {Number} minute + */ + setMinute(minute) { + this.setState({ minute }); + }, + + /** + * Set second (0 ~ 59) + * @param {Number} second + */ + setSecond(second) { + this.setState({ second }); + }, + + /** + * Set millisecond (0 ~ 999) + * @param {Number} millisecond + */ + setMillisecond(millisecond) { + this.setState({ millisecond }); + }, + + /** + * Calculate the range of possible choices for each time unit. + * Reuse the old result if the input has not changed. + * + * @param {Number} dayPeriod + * @param {Number} hour + * @param {Number} minute + * @param {Number} second + */ + _setRanges(dayPeriod, hour, minute, second) { + this.state.ranges.dayPeriod = + this.state.ranges.dayPeriod || this._getDayPeriodRange(); + + if (this.state.dayPeriod != dayPeriod) { + this.state.ranges.hours = this._getHoursRange(dayPeriod); + } + + if (this.state.hour != hour) { + this.state.ranges.minutes = this._getMinutesRange(hour); + } + + if (this.state.hour != hour || this.state.minute != minute) { + this.state.ranges.seconds = this._getSecondsRange(hour, minute); + } + + if (this.state.hour != hour || this.state.minute != minute || this.state.second != second) { + this.state.ranges.milliseconds = this._getMillisecondsRange(hour, minute, second); + } + + // Save the time states for comparison. + this.state.dayPeriod = dayPeriod; + this.state.hour = hour; + this.state.minute = minute; + this.state.second = second; + }, + + /** + * Get the AM/PM range. Return an empty array if in 24hr mode. + * + * @return {Array<Number>} + */ + _getDayPeriodRange() { + if (this.props.format == TIME_FORMAT_24) { + return []; + } + + const start = 0; + const end = DAY_IN_MS - 1; + const minStep = DAY_PERIOD_IN_MS; + const formatter = (time) => + new Date(time).getUTCHours() < DAY_PERIOD_IN_HOURS ? 0 : DAY_PERIOD_IN_HOURS; + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Get the hours range. + * + * @param {Number} dayPeriod + * @return {Array<Number>} + */ + _getHoursRange(dayPeriod) { + const { format } = this.props; + const start = format == "24" ? 0 : dayPeriod * HOUR_IN_MS; + const end = format == "24" ? DAY_IN_MS - 1 : start + DAY_PERIOD_IN_MS - 1; + const minStep = HOUR_IN_MS; + const formatter = (time) => new Date(time).getUTCHours(); + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Get the minutes range + * + * @param {Number} hour + * @return {Array<Number>} + */ + _getMinutesRange(hour) { + const start = hour * HOUR_IN_MS; + const end = start + HOUR_IN_MS - 1; + const minStep = MINUTE_IN_MS; + const formatter = (time) => new Date(time).getUTCMinutes(); + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Get the seconds range + * + * @param {Number} hour + * @param {Number} minute + * @return {Array<Number>} + */ + _getSecondsRange(hour, minute) { + const start = hour * HOUR_IN_MS + minute * MINUTE_IN_MS; + const end = start + MINUTE_IN_MS - 1; + const minStep = SECOND_IN_MS; + const formatter = (time) => new Date(time).getUTCSeconds(); + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Get the milliseconds range + * @param {Number} hour + * @param {Number} minute + * @param {Number} second + * @return {Array<Number>} + */ + _getMillisecondsRange(hour, minute, second) { + const start = hour * HOUR_IN_MS + minute * MINUTE_IN_MS + second * SECOND_IN_MS; + const end = start + SECOND_IN_MS - 1; + const minStep = 1; + const formatter = (time) => new Date(time).getUTCMilliseconds(); + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Calculate the range of possible steps. + * + * @param {Number} startValue: Start time in ms + * @param {Number} endValue: End time in ms + * @param {Number} minStep: Smallest step in ms for the time unit + * @param {Function} formatter: Outputs time in a particular format + * @return {Array<Object>} + * { + * {Number} value + * {Boolean} enabled + * } + */ + _getSteps(startValue, endValue, minStep, formatter) { + const { min, max, stepInMs } = this.props; + // The timeStep should be big enough so that there won't be + // duplications. Ex: minimum step for minute should be 60000ms, + // if smaller than that, next step might return the same minute. + const timeStep = Math.max(minStep, stepInMs); + + // Make sure the starting point and end point is not off step + let time = min.valueOf() + Math.ceil((startValue - min.valueOf()) / timeStep) * timeStep; + let maxValue = min.valueOf() + Math.floor((max.valueOf() - min.valueOf()) / stepInMs) * stepInMs; + let steps = []; + + // Increment by timeStep until reaching the end of the range. + while (time <= endValue) { + steps.push({ + value: formatter(time), + // Check if the value is within the min and max. If it's out of range, + // also check for the case when minStep is too large, and has stepped out + // of range when it should be enabled. + enabled: (time >= min.valueOf() && time <= max.valueOf()) || + (time > maxValue && startValue <= maxValue && + endValue >= maxValue && formatter(time) == formatter(maxValue)) + }); + time += timeStep; + } + + return steps; + }, + + /** + * A generic function for stepping up or down from a value of a range. + * It stops at the upper and lower limits. + * + * @param {Number} current: The current value + * @param {Number} offset: The offset relative to current value + * @param {Array<Object>} range: List of possible steps + * @return {Number} The new value + */ + _step(current, offset, range) { + const index = range.findIndex(step => step.value == current); + const newIndex = offset > 0 ? + Math.min(index + offset, range.length - 1) : + Math.max(index + offset, 0); + return range[newIndex].value; + }, + + /** + * Step up or down AM/PM + * + * @param {Number} offset + */ + stepDayPeriodBy(offset) { + const current = this.dayPeriod; + const dayPeriod = this._step(current, offset, this.state.ranges.dayPeriod); + + if (current != dayPeriod) { + this.hour < DAY_PERIOD_IN_HOURS ? + this.setState({ hour: this.hour + DAY_PERIOD_IN_HOURS }) : + this.setState({ hour: this.hour - DAY_PERIOD_IN_HOURS }); + } + }, + + /** + * Step up or down hours + * + * @param {Number} offset + */ + stepHourBy(offset) { + const current = this.hour; + const hour = this._step(current, offset, this.state.ranges.hours); + + if (current != hour) { + this.setState({ hour }); + } + }, + + /** + * Step up or down minutes + * + * @param {Number} offset + */ + stepMinuteBy(offset) { + const current = this.minute; + const minute = this._step(current, offset, this.state.ranges.minutes); + + if (current != minute) { + this.setState({ minute }); + } + }, + + /** + * Step up or down seconds + * + * @param {Number} offset + */ + stepSecondBy(offset) { + const current = this.second; + const second = this._step(current, offset, this.state.ranges.seconds); + + if (current != second) { + this.setState({ second }); + } + }, + + /** + * Step up or down milliseconds + * + * @param {Number} offset + */ + stepMillisecondBy(offset) { + const current = this.milliseconds; + const millisecond = this._step(current, offset, this.state.ranges.millisecond); + + if (current != millisecond) { + this.setState({ millisecond }); + } + }, + + /** + * Checks if the time state is off step. + * + * @param {Date} time + * @return {Boolean} + */ + _isOffStep(time) { + const { min, stepInMs } = this.props; + + return (time.valueOf() - min.valueOf()) % stepInMs != 0; + } + }; +} diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js new file mode 100644 index 0000000000..f438e9ec60 --- /dev/null +++ b/toolkit/content/widgets/timepicker.js @@ -0,0 +1,277 @@ +/* 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/. */ + +"use strict"; + +function TimePicker(context) { + this.context = context; + this._attachEventListeners(); +} + +{ + const debug = 0 ? console.log.bind(console, "[timepicker]") : function() {}; + + const DAY_PERIOD_IN_HOURS = 12, + SECOND_IN_MS = 1000, + MINUTE_IN_MS = 60000, + DAY_IN_MS = 86400000; + + TimePicker.prototype = { + /** + * Initializes the time picker. Set the default states and properties. + * @param {Object} props + * { + * {Number} hour [optional]: Hour in 24 hours format (0~23), default is current hour + * {Number} minute [optional]: Minute (0~59), default is current minute + * {String} min [optional]: Minimum time, in 24 hours format. ex: "05:45" + * {String} max [optional]: Maximum time, in 24 hours format. ex: "23:00" + * {Number} step [optional]: Step size in minutes. Default is 60. + * {String} format [optional]: "12" for 12 hours, "24" for 24 hours format + * {String} locale [optional]: User preferred locale + * } + */ + init(props) { + this.props = props || {}; + this._setDefaultState(); + this._createComponents(); + this._setComponentStates(); + }, + + /* + * Set initial time states. If there's no hour & minute, it will + * use the current time. The Time module keeps track of the time states, + * and calculates the valid options given the time, min, max, step, + * and format (12 or 24). + */ + _setDefaultState() { + const { hour, minute, min, max, step, format } = this.props; + const now = new Date(); + + let timerHour = hour == undefined ? now.getHours() : hour; + let timerMinute = minute == undefined ? now.getMinutes() : minute; + + // The spec defines 1 step == 1 second, need to convert to ms for timekeeper + let timeKeeper = new TimeKeeper({ + min: this._parseTimeString(min) || new Date(0), + max: this._parseTimeString(max) || new Date(DAY_IN_MS - 1), + stepInMs: step ? step * SECOND_IN_MS : MINUTE_IN_MS, + format: format || "12" + }); + timeKeeper.setState({ hour: timerHour, minute: timerMinute }); + + this.state = { timeKeeper }; + }, + + /** + * Convert a time string from DOM attribute to a date object. + * + * @param {String} timeString: (ex. "10:30", "23:55", "12:34:56.789") + * @return {Date/Boolean} Date object or false if date is invalid. + */ + _parseTimeString(timeString) { + let time = new Date("1970-01-01T" + timeString + "Z"); + return time.toString() == "Invalid Date" ? false : time; + }, + + /** + * Initalize the spinner components. + */ + _createComponents() { + const { locale, step, format } = this.props; + const { timeKeeper } = this.state; + + const wrapSetValueFn = (setTimeFunction) => { + return (value) => { + setTimeFunction(value); + this._setComponentStates(); + this._dispatchState(); + }; + }; + const numberFormat = new Intl.NumberFormat(locale).format; + + this.components = { + hour: new Spinner({ + setValue: wrapSetValueFn(value => { + timeKeeper.setHour(value); + this.state.isHourSet = true; + }), + getDisplayString: hour => { + if (format == "24") { + return numberFormat(hour); + } + // Hour 0 in 12 hour format is displayed as 12. + const hourIn12 = hour % DAY_PERIOD_IN_HOURS; + return hourIn12 == 0 ? numberFormat(12) + : numberFormat(hourIn12); + } + }, this.context), + minute: new Spinner({ + setValue: wrapSetValueFn(value => { + timeKeeper.setMinute(value); + this.state.isMinuteSet = true; + }), + getDisplayString: minute => numberFormat(minute) + }, this.context) + }; + + this._insertLayoutElement({ + tag: "div", + textContent: ":", + className: "colon", + insertBefore: this.components.minute.elements.container + }); + + // The AM/PM spinner is only available in 12hr mode + // TODO: Replace AM & PM string with localized string + if (format == "12") { + this.components.dayPeriod = new Spinner({ + setValue: wrapSetValueFn(value => { + timeKeeper.setDayPeriod(value); + this.state.isDayPeriodSet = true; + }), + getDisplayString: dayPeriod => dayPeriod == 0 ? "AM" : "PM", + hideButtons: true + }, this.context); + + this._insertLayoutElement({ + tag: "div", + className: "spacer", + insertBefore: this.components.dayPeriod.elements.container + }); + } + }, + + /** + * Insert element for layout purposes. + * + * @param {Object} + * { + * {String} tag: The tag to create + * {DOMElement} insertBefore: The DOM node to insert before + * {String} className [optional]: Class name + * {String} textContent [optional]: Text content + * } + */ + _insertLayoutElement({ tag, insertBefore, className, textContent }) { + let el = document.createElement(tag); + el.textContent = textContent; + el.className = className; + this.context.insertBefore(el, insertBefore); + }, + + /** + * Set component states. + */ + _setComponentStates() { + const { timeKeeper, isHourSet, isMinuteSet, isDayPeriodSet } = this.state; + const isInvalid = timeKeeper.state.isInvalid; + // Value is set to min if it's first opened and time state is invalid + const setToMinValue = !isHourSet && !isMinuteSet && !isDayPeriodSet && isInvalid; + + this.components.hour.setState({ + value: setToMinValue ? timeKeeper.ranges.hours[0].value : timeKeeper.hour, + items: timeKeeper.ranges.hours, + isInfiniteScroll: true, + isValueSet: isHourSet, + isInvalid + }); + + this.components.minute.setState({ + value: setToMinValue ? timeKeeper.ranges.minutes[0].value : timeKeeper.minute, + items: timeKeeper.ranges.minutes, + isInfiniteScroll: true, + isValueSet: isMinuteSet, + isInvalid + }); + + // The AM/PM spinner is only available in 12hr mode + if (this.props.format == "12") { + this.components.dayPeriod.setState({ + value: setToMinValue ? timeKeeper.ranges.dayPeriod[0].value : timeKeeper.dayPeriod, + items: timeKeeper.ranges.dayPeriod, + isInfiniteScroll: false, + isValueSet: isDayPeriodSet, + isInvalid + }); + } + }, + + /** + * Dispatch CustomEvent to pass the state of picker to the panel. + */ + _dispatchState() { + const { hour, minute } = this.state.timeKeeper; + const { isHourSet, isMinuteSet, isDayPeriodSet } = this.state; + // The panel is listening to window for postMessage event, so we + // do postMessage to itself to send data to input boxes. + window.postMessage({ + name: "TimePickerPopupChanged", + detail: { + hour, + minute, + isHourSet, + isMinuteSet, + isDayPeriodSet + } + }, "*"); + }, + _attachEventListeners() { + window.addEventListener("message", this); + }, + + /** + * Handle events. + * + * @param {Event} event + */ + handleEvent(event) { + switch (event.type) { + case "message": { + this.handleMessage(event); + break; + } + } + }, + + /** + * Handle postMessage events. + * + * @param {Event} event + */ + handleMessage(event) { + switch (event.data.name) { + case "TimePickerSetValue": { + this.set(event.data.detail); + break; + } + case "TimePickerInit": { + this.init(event.data.detail); + break; + } + } + }, + + /** + * Set the time state and update the components with the new state. + * + * @param {Object} timeState + * { + * {Number} hour [optional] + * {Number} minute [optional] + * {Number} second [optional] + * {Number} millisecond [optional] + * } + */ + set(timeState) { + if (timeState.hour != undefined) { + this.state.isHourSet = true; + } + if (timeState.minute != undefined) { + this.state.isMinuteSet = true; + } + this.state.timeKeeper.setState(timeState); + this._setComponentStates(); + } + }; +} diff --git a/toolkit/content/widgets/toolbar.xml b/toolkit/content/widgets/toolbar.xml new file mode 100644 index 0000000000..548504e24a --- /dev/null +++ b/toolkit/content/widgets/toolbar.xml @@ -0,0 +1,590 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="toolbarBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="toolbar-base"> + <resources> + <stylesheet src="chrome://global/skin/toolbar.css"/> + </resources> + </binding> + + <binding id="toolbox" extends="chrome://global/content/bindings/toolbar.xml#toolbar-base"> + <implementation> + <field name="palette"> + null + </field> + + <field name="toolbarset"> + null + </field> + + <field name="customToolbarCount"> + 0 + </field> + + <field name="externalToolbars"> + [] + </field> + + <!-- Set by customizeToolbar.js --> + <property name="customizing"> + <getter><![CDATA[ + return this.getAttribute("customizing") == "true"; + ]]></getter> + <setter><![CDATA[ + if (val) + this.setAttribute("customizing", "true"); + else + this.removeAttribute("customizing"); + return val; + ]]></setter> + </property> + + <constructor> + <![CDATA[ + // Look to see if there is a toolbarset. + this.toolbarset = this.firstChild; + while (this.toolbarset && this.toolbarset.localName != "toolbarset") + this.toolbarset = toolbarset.nextSibling; + + if (this.toolbarset) { + // Create each toolbar described by the toolbarset. + var index = 0; + while (toolbarset.hasAttribute("toolbar"+(++index))) { + var toolbarInfo = toolbarset.getAttribute("toolbar"+index); + var infoSplit = toolbarInfo.split(":"); + this.appendCustomToolbar(infoSplit[0], infoSplit[1]); + } + } + ]]> + </constructor> + + <method name="appendCustomToolbar"> + <parameter name="aName"/> + <parameter name="aCurrentSet"/> + <body> + <![CDATA[ + if (!this.toolbarset) + return null; + var toolbar = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "toolbar"); + toolbar.id = "__customToolbar_" + aName.replace(" ", "_"); + toolbar.setAttribute("customizable", "true"); + toolbar.setAttribute("customindex", ++this.customToolbarCount); + toolbar.setAttribute("toolbarname", aName); + toolbar.setAttribute("currentset", aCurrentSet); + toolbar.setAttribute("mode", this.getAttribute("mode")); + toolbar.setAttribute("iconsize", this.getAttribute("iconsize")); + toolbar.setAttribute("context", this.toolbarset.getAttribute("context")); + toolbar.setAttribute("class", "chromeclass-toolbar"); + + this.insertBefore(toolbar, this.toolbarset); + return toolbar; + ]]> + </body> + </method> + </implementation> + </binding> + + <binding id="toolbar" role="xul:toolbar" + extends="chrome://global/content/bindings/toolbar.xml#toolbar-base"> + <implementation> + <property name="toolbarName" + onget="return this.getAttribute('toolbarname');" + onset="this.setAttribute('toolbarname', val); return val;"/> + + <field name="_toolbox">null</field> + <property name="toolbox" readonly="true"> + <getter><![CDATA[ + if (this._toolbox) + return this._toolbox; + + let toolboxId = this.getAttribute("toolboxid"); + if (toolboxId) { + let toolbox = document.getElementById(toolboxId); + if (!toolbox) { + let tbName = this.toolbarName; + if (tbName) + tbName = " (" + tbName + ")"; + else + tbName = ""; + throw new Error(`toolbar ID ${this.id}${tbName}: toolboxid attribute '${toolboxId}' points to a toolbox that doesn't exist`); + } + + if (toolbox.externalToolbars.indexOf(this) == -1) + toolbox.externalToolbars.push(this); + + return this._toolbox = toolbox; + } + + return this._toolbox = (this.parentNode && + this.parentNode.localName == "toolbox") ? + this.parentNode : null; + ]]></getter> + </property> + + <constructor> + <![CDATA[ + if (document.readyState == "complete") { + this._init(); + } else { + // Need to wait until XUL overlays are loaded. See bug 554279. + let self = this; + document.addEventListener("readystatechange", function (event) { + if (document.readyState != "complete") + return; + document.removeEventListener("readystatechange", arguments.callee, false); + self._init(); + }, false); + } + ]]> + </constructor> + + <method name="_init"> + <body> + <![CDATA[ + // Searching for the toolbox palette in the toolbar binding because + // toolbars are constructed first. + var toolbox = this.toolbox; + if (!toolbox) + return; + + if (!toolbox.palette) { + // Look to see if there is a toolbarpalette. + var node = toolbox.firstChild; + while (node) { + if (node.localName == "toolbarpalette") + break; + node = node.nextSibling; + } + + if (!node) + return; + + // Hold on to the palette but remove it from the document. + toolbox.palette = node; + toolbox.removeChild(node); + } + + // Build up our contents from the palette. + var currentSet = this.getAttribute("currentset"); + if (!currentSet) + currentSet = this.getAttribute("defaultset"); + if (currentSet) + this.currentSet = currentSet; + ]]> + </body> + </method> + + <method name="_idFromNode"> + <parameter name="aNode"/> + <body> + <![CDATA[ + if (aNode.getAttribute("skipintoolbarset") == "true") + return ""; + + switch (aNode.localName) { + case "toolbarseparator": + return "separator"; + case "toolbarspring": + return "spring"; + case "toolbarspacer": + return "spacer"; + default: + return aNode.id; + } + ]]> + </body> + </method> + + <property name="currentSet"> + <getter> + <![CDATA[ + var node = this.firstChild; + var currentSet = []; + while (node) { + var id = this._idFromNode(node); + if (id) { + currentSet.push(id); + } + node = node.nextSibling; + } + + return currentSet.join(",") || "__empty"; + ]]> + </getter> + + <setter> + <![CDATA[ + if (val == this.currentSet) + return val; + + var ids = (val == "__empty") ? [] : val.split(","); + + var nodeidx = 0; + var paletteItems = { }, added = { }; + + var palette = this.toolbox ? this.toolbox.palette : null; + + // build a cache of items in the toolbarpalette + var paletteChildren = palette ? palette.childNodes : []; + for (let c = 0; c < paletteChildren.length; c++) { + let curNode = paletteChildren[c]; + paletteItems[curNode.id] = curNode; + } + + var children = this.childNodes; + + // iterate over the ids to use on the toolbar + for (let i = 0; i < ids.length; i++) { + let id = ids[i]; + // iterate over the existing nodes on the toolbar. nodeidx is the + // spot where we want to insert items. + let found = false; + for (let c = nodeidx; c < children.length; c++) { + let curNode = children[c]; + if (this._idFromNode(curNode) == id) { + // the node already exists. If c equals nodeidx, we haven't + // iterated yet, so the item is already in the right position. + // Otherwise, insert it here. + if (c != nodeidx) { + this.insertBefore(curNode, children[nodeidx]); + } + + added[curNode.id] = true; + nodeidx++; + found = true; + break; + } + } + if (found) { + // move on to the next id + continue; + } + + // the node isn't already on the toolbar, so add a new one. + var nodeToAdd = paletteItems[id] || this._getToolbarItem(id); + if (nodeToAdd && !(nodeToAdd.id in added)) { + added[nodeToAdd.id] = true; + this.insertBefore(nodeToAdd, children[nodeidx] || null); + nodeToAdd.setAttribute("removable", "true"); + nodeidx++; + } + } + + // remove any leftover removable nodes + for (let i = children.length - 1; i >= nodeidx; i--) { + let curNode = children[i]; + + let curNodeId = this._idFromNode(curNode); + // skip over fixed items + if (curNodeId && curNode.getAttribute("removable") == "true") { + if (palette) + palette.appendChild(curNode); + else + this.removeChild(curNode); + } + } + + return val; + ]]> + </setter> + </property> + + <field name="_newElementCount">0</field> + <method name="_getToolbarItem"> + <parameter name="aId"/> + <body> + <![CDATA[ + const XUL_NS = "http://www.mozilla.org/keymaster/" + + "gatekeeper/there.is.only.xul"; + + var newItem = null; + switch (aId) { + // Handle special cases + case "separator": + case "spring": + case "spacer": + newItem = document.createElementNS(XUL_NS, "toolbar" + aId); + // Due to timers resolution Date.now() can be the same for + // elements created in small timeframes. So ids are + // differentiated through a unique count suffix. + newItem.id = aId + Date.now() + (++this._newElementCount); + if (aId == "spring") + newItem.flex = 1; + break; + default: + var toolbox = this.toolbox; + if (!toolbox) + break; + + // look for an item with the same id, as the item may be + // in a different toolbar. + var item = document.getElementById(aId); + if (item && item.parentNode && + item.parentNode.localName == "toolbar" && + item.parentNode.toolbox == toolbox) { + newItem = item; + break; + } + + if (toolbox.palette) { + // Attempt to locate an item with a matching ID within + // the palette. + let paletteItem = this.toolbox.palette.firstChild; + while (paletteItem) { + if (paletteItem.id == aId) { + newItem = paletteItem; + break; + } + paletteItem = paletteItem.nextSibling; + } + } + break; + } + + return newItem; + ]]> + </body> + </method> + + <method name="insertItem"> + <parameter name="aId"/> + <parameter name="aBeforeElt"/> + <parameter name="aWrapper"/> + <body> + <![CDATA[ + var newItem = this._getToolbarItem(aId); + if (!newItem) + return null; + + var insertItem = newItem; + // make sure added items are removable + newItem.setAttribute("removable", "true"); + + // Wrap the item in another node if so inclined. + if (aWrapper) { + aWrapper.appendChild(newItem); + insertItem = aWrapper; + } + + // Insert the palette item into the toolbar. + if (aBeforeElt) + this.insertBefore(insertItem, aBeforeElt); + else + this.appendChild(insertItem); + + return newItem; + ]]> + </body> + </method> + + <method name="hasCustomInteractiveItems"> + <parameter name="aCurrentSet"/> + <body><![CDATA[ + if (aCurrentSet == "__empty") + return false; + + var defaultOrNoninteractive = (this.getAttribute("defaultset") || "") + .split(",") + .concat(["separator", "spacer", "spring"]); + return aCurrentSet.split(",").some(function (item) { + return defaultOrNoninteractive.indexOf(item) == -1; + }); + ]]></body> + </method> + </implementation> + </binding> + + <binding id="toolbar-menubar-autohide" + extends="chrome://global/content/bindings/toolbar.xml#toolbar"> + <implementation> + <constructor> + this._setInactive(); + </constructor> + <destructor> + this._setActive(); + </destructor> + + <field name="_inactiveTimeout">null</field> + + <field name="_contextMenuListener"><![CDATA[({ + toolbar: this, + contextMenu: null, + + get active () { + return !!this.contextMenu; + }, + + init: function (event) { + var node = event.target; + while (node != this.toolbar) { + if (node.localName == "menupopup") + return; + node = node.parentNode; + } + + var contextMenuId = this.toolbar.getAttribute("context"); + if (!contextMenuId) + return; + + this.contextMenu = document.getElementById(contextMenuId); + if (!this.contextMenu) + return; + + this.contextMenu.addEventListener("popupshown", this, false); + this.contextMenu.addEventListener("popuphiding", this, false); + this.toolbar.addEventListener("mousemove", this, false); + }, + handleEvent: function (event) { + switch (event.type) { + case "popupshown": + this.toolbar.removeEventListener("mousemove", this, false); + break; + case "popuphiding": + case "mousemove": + this.toolbar._setInactiveAsync(); + this.toolbar.removeEventListener("mousemove", this, false); + this.contextMenu.removeEventListener("popuphiding", this, false); + this.contextMenu.removeEventListener("popupshown", this, false); + this.contextMenu = null; + break; + } + } + })]]></field> + + <method name="_setInactive"> + <body><![CDATA[ + this.setAttribute("inactive", "true"); + ]]></body> + </method> + + <method name="_setInactiveAsync"> + <body><![CDATA[ + this._inactiveTimeout = setTimeout(function (self) { + if (self.getAttribute("autohide") == "true") { + self._inactiveTimeout = null; + self._setInactive(); + } + }, 0, this); + ]]></body> + </method> + + <method name="_setActive"> + <body><![CDATA[ + if (this._inactiveTimeout) { + clearTimeout(this._inactiveTimeout); + this._inactiveTimeout = null; + } + this.removeAttribute("inactive"); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="DOMMenuBarActive" action="this._setActive();"/> + <handler event="popupshowing" action="this._setActive();"/> + <handler event="mousedown" button="2" action="this._contextMenuListener.init(event);"/> + <handler event="DOMMenuBarInactive"><![CDATA[ + if (!this._contextMenuListener.active) + this._setInactiveAsync(); + ]]></handler> + </handlers> + </binding> + + <binding id="toolbar-drag" + extends="chrome://global/content/bindings/toolbar.xml#toolbar"> + <implementation> + <field name="_dragBindingAlive">true</field> + <constructor><![CDATA[ + if (!this._draggableStarted) { + this._draggableStarted = true; + try { + let tmp = {}; + Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp); + let draggableThis = new tmp.WindowDraggingElement(this); + draggableThis.mouseDownCheck = function(e) { + // Don't move while customizing. + return this._dragBindingAlive && + this.getAttribute("customizing") != "true"; + }; + } catch (e) {} + } + ]]></constructor> + </implementation> + </binding> + + <binding id="menubar" role="xul:menubar" + extends="chrome://global/content/bindings/toolbar.xml#toolbar-base" display="xul:menubar"> + <implementation> + <field name="_active">false</field> + <field name="_statusbar">null</field> + <field name="_originalStatusText">null</field> + <property name="statusbar" onget="return this.getAttribute('statusbar');" + onset="this.setAttribute('statusbar', val); return val;"/> + <method name="_updateStatusText"> + <parameter name="itemText"/> + <body> + <![CDATA[ + if (!this._active) + return; + var newText = itemText ? itemText : this._originalStatusText; + if (newText != this._statusbar.label) + this._statusbar.label = newText; + ]]> + </body> + </method> + </implementation> + <handlers> + <handler event="DOMMenuBarActive"> + <![CDATA[ + if (!this.statusbar) return; + this._statusbar = document.getElementById(this.statusbar); + if (!this._statusbar) + return; + this._active = true; + this._originalStatusText = this._statusbar.label; + ]]> + </handler> + <handler event="DOMMenuBarInactive"> + <![CDATA[ + if (!this._active) + return; + this._active = false; + this._statusbar.label = this._originalStatusText; + ]]> + </handler> + <handler event="DOMMenuItemActive">this._updateStatusText(event.target.statusText);</handler> + <handler event="DOMMenuItemInactive">this._updateStatusText("");</handler> + </handlers> + </binding> + + <binding id="toolbardecoration" role="xul:toolbarseparator" extends="chrome://global/content/bindings/toolbar.xml#toolbar-base"> + </binding> + + <binding id="toolbarpaletteitem" extends="chrome://global/content/bindings/toolbar.xml#toolbar-base" display="xul:button"> + <content> + <xul:hbox class="toolbarpaletteitem-box" flex="1" xbl:inherits="type,place"> + <children/> + </xul:hbox> + </content> + </binding> + + <binding id="toolbarpaletteitem-palette" extends="chrome://global/content/bindings/toolbar.xml#toolbarpaletteitem"> + <content> + <xul:hbox class="toolbarpaletteitem-box" xbl:inherits="type,place"> + <children/> + </xul:hbox> + <xul:label xbl:inherits="value=title"/> + </content> + </binding> + +</bindings> + diff --git a/toolkit/content/widgets/toolbarbutton.xml b/toolkit/content/widgets/toolbarbutton.xml new file mode 100644 index 0000000000..5de3f040d2 --- /dev/null +++ b/toolkit/content/widgets/toolbarbutton.xml @@ -0,0 +1,115 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<bindings id="toolbarbuttonBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="toolbarbutton" display="xul:button" role="xul:toolbarbutton" + extends="chrome://global/content/bindings/button.xml#button-base"> + <resources> + <stylesheet src="chrome://global/skin/toolbarbutton.css"/> + </resources> + + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/> + <xul:label class="toolbarbutton-text" crop="right" flex="1" + xbl:inherits="value=label,accesskey,crop,wrap"/> + <xul:label class="toolbarbutton-multiline-text" flex="1" + xbl:inherits="xbl:text=label,accesskey,wrap"/> + </content> + </binding> + + <binding id="menu" display="xul:menu" + extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"> + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,type,consumeanchor"/> + <xul:label class="toolbarbutton-text" crop="right" flex="1" + xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/> + <xul:label class="toolbarbutton-multiline-text" flex="1" + xbl:inherits="xbl:text=label,accesskey,wrap"/> + <xul:dropmarker anonid="dropmarker" type="menu" + class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/> + </content> + </binding> + + <binding id="menu-vertical" display="xul:menu" + extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"> + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:hbox flex="1" align="center"> + <xul:vbox flex="1" align="center"> + <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/> + <xul:label class="toolbarbutton-text" crop="right" flex="1" + xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/> + <xul:label class="toolbarbutton-multiline-text" flex="1" + xbl:inherits="xbl:text=label,accesskey,wrap"/> + </xul:vbox> + <xul:dropmarker anonid="dropmarker" type="menu" + class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/> + </xul:hbox> + </content> + </binding> + + <binding id="menu-button" display="xul:menu" + extends="chrome://global/content/bindings/button.xml#menu-button-base"> + <resources> + <stylesheet src="chrome://global/skin/toolbarbutton.css"/> + </resources> + + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:toolbarbutton class="box-inherit toolbarbutton-menubutton-button" + anonid="button" flex="1" allowevents="true" + xbl:inherits="disabled,crop,image,label,accesskey,command,wrap,badge, + align,dir,pack,orient,tooltiptext=buttontooltiptext"/> + <xul:dropmarker type="menu-button" class="toolbarbutton-menubutton-dropmarker" + anonid="dropmarker" xbl:inherits="align,dir,pack,orient,disabled,label,open,consumeanchor"/> + </content> + </binding> + + <binding id="toolbarbutton-image" + extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"> + <content> + <xul:image class="toolbarbutton-icon" xbl:inherits="src=image"/> + </content> + </binding> + + <binding id="toolbarbutton-badged" + extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"> + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:stack class="toolbarbutton-badge-stack"> + <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/> + <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0" crop="none"/> + </xul:stack> + <xul:label class="toolbarbutton-text" crop="right" flex="1" + xbl:inherits="value=label,accesskey,crop,wrap"/> + <xul:label class="toolbarbutton-multiline-text" flex="1" + xbl:inherits="xbl:text=label,accesskey,wrap"/> + </content> + </binding> + + <binding id="toolbarbutton-badged-menu" display="xul:menu" + extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"> + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:stack class="toolbarbutton-badge-stack"> + <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/> + <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0" crop="none"/> + </xul:stack> + <xul:label class="toolbarbutton-text" crop="right" flex="1" + xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/> + <xul:label class="toolbarbutton-multiline-text" flex="1" + xbl:inherits="xbl:text=label,accesskey,wrap"/> + <xul:dropmarker anonid="dropmarker" type="menu" + class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/> + </content> + </binding> +</bindings> diff --git a/toolkit/content/widgets/tree.xml b/toolkit/content/widgets/tree.xml new file mode 100644 index 0000000000..aa17172575 --- /dev/null +++ b/toolkit/content/widgets/tree.xml @@ -0,0 +1,1561 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<!DOCTYPE bindings [ +<!ENTITY % treeDTD SYSTEM "chrome://global/locale/tree.dtd"> +%treeDTD; +]> + +<bindings id="treeBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="tree-base" extends="chrome://global/content/bindings/general.xml#basecontrol"> + <resources> + <stylesheet src="chrome://global/skin/tree.css"/> + </resources> + <implementation> + <method name="_isAccelPressed"> + <parameter name="aEvent"/> + <body><![CDATA[ + return aEvent.getModifierState("Accel"); + ]]></body> + </method> + </implementation> + </binding> + + <binding id="tree" extends="chrome://global/content/bindings/tree.xml#tree-base" role="xul:tree"> + <content hidevscroll="true" hidehscroll="true" clickthrough="never"> + <children includes="treecols"/> + <xul:stack class="tree-stack" flex="1"> + <xul:treerows class="tree-rows" flex="1" xbl:inherits="hidevscroll"> + <children/> + </xul:treerows> + <xul:textbox anonid="input" class="tree-input" left="0" top="0" hidden="true"/> + </xul:stack> + <xul:hbox xbl:inherits="collapsed=hidehscroll"> + <xul:scrollbar orient="horizontal" flex="1" increment="16" style="position:relative; z-index:2147483647;"/> + <xul:scrollcorner xbl:inherits="collapsed=hidevscroll"/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMXULTreeElement, nsIDOMXULMultiSelectControlElement"> + + <!-- ///////////////// nsIDOMXULTreeElement ///////////////// --> + + <property name="columns" + onget="return this.treeBoxObject.columns;"/> + + <property name="view" + onget="return this.treeBoxObject.view ? + this.treeBoxObject.view.QueryInterface(Components.interfaces.nsITreeView) : + null;" + onset="return this.treeBoxObject.view = val;"/> + + <property name="body" + onget="return this.treeBoxObject.treeBody;"/> + + <property name="editable" + onget="return this.getAttribute('editable') == 'true';" + onset="if (val) this.setAttribute('editable', 'true'); + else this.removeAttribute('editable'); return val;"/> + + <!-- ///////////////// nsIDOMXULSelectControlElement ///////////////// --> + + <!-- ///////////////// nsIDOMXULMultiSelectControlElement ///////////////// --> + + <property name="selType" + onget="return this.getAttribute('seltype')" + onset="this.setAttribute('seltype', val); return val;"/> + + <property name="currentIndex" + onget="return this.view ? this.view.selection.currentIndex: - 1;" + onset="if (this.view) return this.view.selection.currentIndex = val; return val;"/> + + <property name="treeBoxObject" + onget="return this.boxObject;" + readonly="true"/> +# contentView is obsolete (see bug 202391) + <property name="contentView" + onget="return this.view; /*.QueryInterface(Components.interfaces.nsITreeContentView)*/" + readonly="true"/> +# builderView is obsolete (see bug 202393) + <property name="builderView" + onget="return this.view; /*.QueryInterface(Components.interfaces.nsIXULTreeBuilder)*/" + readonly="true"/> + <field name="pageUpOrDownMovesSelection"> + !/Mac/.test(navigator.platform) + </field> + <property name="keepCurrentInView" + onget="return (this.getAttribute('keepcurrentinview') == 'true');" + onset="if (val) this.setAttribute('keepcurrentinview', 'true'); + else this.removeAttribute('keepcurrentinview'); return val;"/> + + <property name="enableColumnDrag" + onget="return this.hasAttribute('enableColumnDrag');" + onset="if (val) this.setAttribute('enableColumnDrag', 'true'); + else this.removeAttribute('enableColumnDrag'); return val;"/> + + <field name="_inputField">null</field> + + <property name="inputField" readonly="true"> + <getter><![CDATA[ + if (!this._inputField) + this._inputField = document.getAnonymousElementByAttribute(this, "anonid", "input"); + return this._inputField; + ]]></getter> + </property> + + <property name="disableKeyNavigation" + onget="return this.hasAttribute('disableKeyNavigation');" + onset="if (val) this.setAttribute('disableKeyNavigation', 'true'); + else this.removeAttribute('disableKeyNavigation'); return val;"/> + + <field name="_editingRow">-1</field> + <field name="_editingColumn">null</field> + + <property name="editingRow" readonly="true" + onget="return this._editingRow;"/> + <property name="editingColumn" readonly="true" + onget="return this._editingColumn;"/> + + <property name="_selectDelay" + onset="this.setAttribute('_selectDelay', val);" + onget="return this.getAttribute('_selectDelay') || 50;"/> + <field name="_columnsDirty">true</field> + <field name="_lastKeyTime">0</field> + <field name="_incrementalString">""</field> + + <field name="_touchY">-1</field> + + <method name="_ensureColumnOrder"> + <body><![CDATA[ + if (!this._columnsDirty) + return; + + if (this.columns) { + // update the ordinal position of each column to assure that it is + // an odd number and 2 positions above its next sibling + var cols = []; + var i; + for (var col = this.columns.getFirstColumn(); col; col = col.getNext()) + cols.push(col.element); + for (i = 0; i < cols.length; ++i) + cols[i].setAttribute("ordinal", (i*2)+1); + + // update the ordinal positions of splitters to even numbers, so that + // they are in between columns + var splitters = this.getElementsByTagName("splitter"); + for (i = 0; i < splitters.length; ++i) + splitters[i].setAttribute("ordinal", (i+1)*2); + } + this._columnsDirty = false; + ]]></body> + </method> + + <method name="_reorderColumn"> + <parameter name="aColMove"/> + <parameter name="aColBefore"/> + <parameter name="aBefore"/> + <body><![CDATA[ + this._ensureColumnOrder(); + + var i; + var cols = []; + var col = this.columns.getColumnFor(aColBefore); + if (parseInt(aColBefore.ordinal) < parseInt(aColMove.ordinal)) { + if (aBefore) + cols.push(aColBefore); + for (col = col.getNext(); col.element != aColMove; + col = col.getNext()) + cols.push(col.element); + + aColMove.ordinal = cols[0].ordinal; + for (i = 0; i < cols.length; ++i) + cols[i].ordinal = parseInt(cols[i].ordinal) + 2; + } else if (aColBefore.ordinal != aColMove.ordinal) { + if (!aBefore) + cols.push(aColBefore); + for (col = col.getPrevious(); col.element != aColMove; + col = col.getPrevious()) + cols.push(col.element); + + aColMove.ordinal = cols[0].ordinal; + for (i = 0; i < cols.length; ++i) + cols[i].ordinal = parseInt(cols[i].ordinal) - 2; + } + ]]></body> + </method> + + <method name="_getColumnAtX"> + <parameter name="aX"/> + <parameter name="aThresh"/> + <parameter name="aPos"/> + <body><![CDATA[ + var isRTL = document.defaultView.getComputedStyle(this, "") + .direction == "rtl"; + + if (aPos) + aPos.value = isRTL ? "after" : "before"; + + var columns = []; + var col = this.columns.getFirstColumn(); + while (col) { + columns.push(col); + col = col.getNext(); + } + if (isRTL) + columns.reverse(); + var currentX = this.boxObject.x; + var adjustedX = aX + this.treeBoxObject.horizontalPosition; + for (var i = 0; i < columns.length; ++i) { + col = columns[i]; + var cw = col.element.boxObject.width; + if (cw > 0) { + currentX += cw; + if (currentX - (cw * aThresh) > adjustedX) + return col.element; + } + } + + if (aPos) + aPos.value = isRTL ? "before" : "after"; + return columns.pop().element; + ]]></body> + </method> + + <method name="changeOpenState"> + <parameter name="row"/> + <!-- Optional parameter openState == true or false to set. + No openState param == toggle --> + <parameter name="openState"/> + <body><![CDATA[ + if (row < 0 || !this.view.isContainer(row)) { + return false; + } + + if (this.view.isContainerOpen(row) != openState) { + this.view.toggleOpenState(row); + if (row == this.currentIndex) { + // Only fire event when current row is expanded or collapsed + // because that's all the assistive technology really cares about. + var event = document.createEvent('Events'); + event.initEvent('OpenStateChange', true, true); + this.dispatchEvent(event); + } + return true; + } + return false; + ]]></body> + </method> + + <property name="_cellSelType"> + <getter> + <![CDATA[ + var seltype = this.selType; + if (seltype == "cell" || seltype == "text") + return seltype; + return null; + ]]> + </getter> + </property> + + <method name="_getNextColumn"> + <parameter name="row"/> + <parameter name="left"/> + <body><![CDATA[ + var col = this.view.selection.currentColumn; + if (col) { + col = left ? col.getPrevious() : col.getNext(); + } + else { + col = this.columns.getKeyColumn(); + } + while (col && (col.width == 0 || !col.selectable || + !this.view.isSelectable(row, col))) + col = left ? col.getPrevious() : col.getNext(); + return col; + ]]></body> + </method> + + <method name="_keyNavigate"> + <parameter name="event"/> + <body><![CDATA[ + var key = String.fromCharCode(event.charCode).toLowerCase(); + if (event.timeStamp - this._lastKeyTime > 1000) + this._incrementalString = key; + else + this._incrementalString += key; + this._lastKeyTime = event.timeStamp; + + var length = this._incrementalString.length; + var incrementalString = this._incrementalString; + var charIndex = 1; + while (charIndex < length && incrementalString[charIndex] == incrementalString[charIndex - 1]) + charIndex++; + // If all letters in incremental string are same, just try to match the first one + if (charIndex == length) { + length = 1; + incrementalString = incrementalString.substring(0, length); + } + + var keyCol = this.columns.getKeyColumn(); + var rowCount = this.view.rowCount; + var start = 1; + + var c = this.currentIndex; + if (length > 1) { + start = 0; + if (c < 0) + c = 0; + } + + for (var i = 0; i < rowCount; i++) { + var l = (i + start + c) % rowCount; + var cellText = this.view.getCellText(l, keyCol); + cellText = cellText.substring(0, length).toLowerCase(); + if (cellText == incrementalString) + return l; + } + return -1; + ]]></body> + </method> + + <method name="startEditing"> + <parameter name="row"/> + <parameter name="column"/> + <body> + <![CDATA[ + if (!this.editable) + return false; + if (row < 0 || row >= this.view.rowCount || !column) + return false; + if (column.type != Components.interfaces.nsITreeColumn.TYPE_TEXT && + column.type != Components.interfaces.nsITreeColumn.TYPE_PASSWORD) + return false; + if (column.cycler || !this.view.isEditable(row, column)) + return false; + + // Beyond this point, we are going to edit the cell. + if (this._editingColumn) + this.stopEditing(); + + var input = this.inputField; + + var box = this.treeBoxObject; + box.ensureCellIsVisible(row, column); + + // Get the coordinates of the text inside the cell. + var textRect = box.getCoordsForCellItem(row, column, "text"); + + // Get the coordinates of the cell itself. + var cellRect = box.getCoordsForCellItem(row, column, "cell"); + + // Calculate the top offset of the textbox. + var style = window.getComputedStyle(input, ""); + var topadj = parseInt(style.borderTopWidth) + parseInt(style.paddingTop); + input.top = textRect.y - topadj; + + // The leftside of the textbox is aligned to the left side of the text + // in LTR mode, and left side of the cell in RTL mode. + var left, widthdiff; + if (style.direction == "rtl") { + left = cellRect.x; + widthdiff = cellRect.x - textRect.x; + } else { + left = textRect.x; + widthdiff = textRect.x - cellRect.x; + } + + input.left = left; + input.height = textRect.height + topadj + + parseInt(style.borderBottomWidth) + + parseInt(style.paddingBottom); + input.width = cellRect.width - widthdiff; + input.hidden = false; + + input.value = this.view.getCellText(row, column); + var selectText = function selectText() { + input.select(); + input.inputField.focus(); + } + setTimeout(selectText, 0); + + this._editingRow = row; + this._editingColumn = column; + this.setAttribute("editing", "true"); + + box.invalidateCell(row, column); + return true; + ]]> + </body> + </method> + + <method name="stopEditing"> + <parameter name="accept"/> + <body> + <![CDATA[ + if (!this._editingColumn) + return; + + var input = this.inputField; + var editingRow = this._editingRow; + var editingColumn = this._editingColumn; + this._editingRow = -1; + this._editingColumn = null; + + if (accept) { + var value = input.value; + this.view.setCellText(editingRow, editingColumn, value); + } + input.hidden = true; + input.value = ""; + this.removeAttribute("editing"); + ]]> + </body> + </method> + + <method name="_moveByOffset"> + <parameter name="offset"/> + <parameter name="edge"/> + <parameter name="event"/> + <body> + <![CDATA[ + event.preventDefault(); + + if (this.view.rowCount == 0) + return; + + if (this._isAccelPressed(event) && this.view.selection.single) { + this.treeBoxObject.scrollByLines(offset); + return; + } + + var c = this.currentIndex + offset; + if (offset > 0 ? c > edge : c < edge) { + if (this.view.selection.isSelected(edge) && this.view.selection.count <= 1) + return; + c = edge; + } + + var cellSelType = this._cellSelType; + if (cellSelType) { + var column = this.view.selection.currentColumn; + if (!column) + return; + + while ((offset > 0 ? c <= edge : c >= edge) && !this.view.isSelectable(c, column)) + c += offset; + if (offset > 0 ? c > edge : c < edge) + return; + } + + if (!this._isAccelPressed(event)) + this.view.selection.timedSelect(c, this._selectDelay); + else // Ctrl+Up/Down moves the anchor without selecting + this.currentIndex = c; + this.treeBoxObject.ensureRowIsVisible(c); + ]]> + </body> + </method> + + <method name="_moveByOffsetShift"> + <parameter name="offset"/> + <parameter name="edge"/> + <parameter name="event"/> + <body> + <![CDATA[ + event.preventDefault(); + + if (this.view.rowCount == 0) + return; + + if (this.view.selection.single) { + this.treeBoxObject.scrollByLines(offset); + return; + } + + if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) { + this.view.selection.timedSelect(0, this._selectDelay); + return; + } + + var c = this.currentIndex; + if (c == -1) + c = 0; + + if (c == edge) { + if (this.view.selection.isSelected(c)) + return; + } + + // Extend the selection from the existing pivot, if any + this.view.selection.rangedSelect(-1, c + offset, + this._isAccelPressed(event)); + this.treeBoxObject.ensureRowIsVisible(c + offset); + + ]]> + </body> + </method> + + <method name="_moveByPage"> + <parameter name="offset"/> + <parameter name="edge"/> + <parameter name="event"/> + <body> + <![CDATA[ + event.preventDefault(); + + if (this.view.rowCount == 0) + return; + + if (this.pageUpOrDownMovesSelection == this._isAccelPressed(event)) { + this.treeBoxObject.scrollByPages(offset); + return; + } + + if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) { + this.view.selection.timedSelect(0, this._selectDelay); + return; + } + + var c = this.currentIndex; + if (c == -1) + return; + + if (c == edge && this.view.selection.isSelected(c)) { + this.treeBoxObject.ensureRowIsVisible(c); + return; + } + var i = this.treeBoxObject.getFirstVisibleRow(); + var p = this.treeBoxObject.getPageLength(); + + if (offset > 0) { + i += p - 1; + if (c >= i) { + i = c + p; + this.treeBoxObject.ensureRowIsVisible(i > edge ? edge : i); + } + i = i > edge ? edge : i; + + } else if (c <= i) { + i = c <= p ? 0 : c - p; + this.treeBoxObject.ensureRowIsVisible(i); + } + this.view.selection.timedSelect(i, this._selectDelay); + ]]> + </body> + </method> + + <method name="_moveByPageShift"> + <parameter name="offset"/> + <parameter name="edge"/> + <parameter name="event"/> + <body> + <![CDATA[ + event.preventDefault(); + + if (this.view.rowCount == 0) + return; + + if (this.view.rowCount == 1 && !this.view.selection.isSelected(0) && + !(this.pageUpOrDownMovesSelection == this._isAccelPressed(event))) { + this.view.selection.timedSelect(0, this._selectDelay); + return; + } + + if (this.view.selection.single) + return; + + var c = this.currentIndex; + if (c == -1) + return; + if (c == edge && this.view.selection.isSelected(c)) { + this.treeBoxObject.ensureRowIsVisible(edge); + return; + } + var i = this.treeBoxObject.getFirstVisibleRow(); + var p = this.treeBoxObject.getPageLength(); + + if (offset > 0) { + i += p - 1; + if (c >= i) { + i = c + p; + this.treeBoxObject.ensureRowIsVisible(i > edge ? edge : i); + } + // Extend the selection from the existing pivot, if any + this.view.selection.rangedSelect(-1, i > edge ? edge : i, this._isAccelPressed(event)); + + } else { + + if (c <= i) { + i = c <= p ? 0 : c - p; + this.treeBoxObject.ensureRowIsVisible(i); + } + // Extend the selection from the existing pivot, if any + this.view.selection.rangedSelect(-1, i, this._isAccelPressed(event)); + } + + ]]> + </body> + </method> + + <method name="_moveToEdge"> + <parameter name="edge"/> + <parameter name="event"/> + <body> + <![CDATA[ + event.preventDefault(); + + if (this.view.rowCount == 0) + return; + + if (this.view.selection.isSelected(edge) && this.view.selection.count == 1) { + this.currentIndex = edge; + return; + } + + // Normal behaviour is to select the first/last row + if (!this._isAccelPressed(event)) + this.view.selection.timedSelect(edge, this._selectDelay); + + // In a multiselect tree Ctrl+Home/End moves the anchor + else if (!this.view.selection.single) + this.currentIndex = edge; + + this.treeBoxObject.ensureRowIsVisible(edge); + ]]> + </body> + </method> + + <method name="_moveToEdgeShift"> + <parameter name="edge"/> + <parameter name="event"/> + <body> + <![CDATA[ + event.preventDefault(); + + if (this.view.rowCount == 0) + return; + + if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) { + this.view.selection.timedSelect(0, this._selectDelay); + return; + } + + if (this.view.selection.single || + (this.view.selection.isSelected(edge)) && this.view.selection.isSelected(this.currentIndex)) + return; + + // Extend the selection from the existing pivot, if any. + // -1 doesn't work here, so using currentIndex instead + this.view.selection.rangedSelect(this.currentIndex, edge, this._isAccelPressed(event)); + + this.treeBoxObject.ensureRowIsVisible(edge); + ]]> + </body> + </method> + <method name="_handleEnter"> + <parameter name="event"/> + <body><![CDATA[ + if (this._editingColumn) { + this.stopEditing(true); + this.focus(); + return true; + } + + if (/Mac/.test(navigator.platform)) { + // See if we can edit the cell. + var row = this.currentIndex; + if (this._cellSelType) { + var column = this.view.selection.currentColumn; + var startedEditing = this.startEditing(row, column); + if (startedEditing) + return true; + } + } + return this.changeOpenState(this.currentIndex); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="touchstart"> + <![CDATA[ + if (event.touches.length > 1) { + // Multiple touch points detected, abort. In particular this aborts + // the panning gesture when the user puts a second finger down after + // already panning with one finger. Aborting at this point prevents + // the pan gesture from being resumed until all fingers are lifted + // (as opposed to when the user is back down to one finger). + this._touchY = -1; + } else { + this._touchY = event.touches[0].screenY; + } + ]]> + </handler> + <handler event="touchmove"> + <![CDATA[ + if (event.touches.length == 1 && + this._touchY >= 0) { + var deltaY = this._touchY - event.touches[0].screenY; + var lines = Math.trunc(deltaY / this.treeBoxObject.rowHeight); + if (Math.abs(lines) > 0) { + this.treeBoxObject.scrollByLines(lines); + deltaY -= lines * this.treeBoxObject.rowHeight; + this._touchY = event.touches[0].screenY + deltaY; + } + event.preventDefault(); + } + ]]> + </handler> + <handler event="touchend"> + <![CDATA[ + this._touchY = -1; + ]]> + </handler> + <handler event="MozMousePixelScroll" preventdefault="true"/> + <handler event="DOMMouseScroll" preventdefault="true"> + <![CDATA[ + if (this._editingColumn) + return; + if (event.axis == event.HORIZONTAL_AXIS) + return; + + var rows = event.detail; + if (rows == UIEvent.SCROLL_PAGE_UP) + this.treeBoxObject.scrollByPages(-1); + else if (rows == UIEvent.SCROLL_PAGE_DOWN) + this.treeBoxObject.scrollByPages(1); + else + this.treeBoxObject.scrollByLines(rows); + ]]> + </handler> + <handler event="MozSwipeGesture" preventdefault="true"> + <![CDATA[ + // Figure out which row to show + let targetRow = 0; + + // Only handle swipe gestures up and down + switch (event.direction) { + case event.DIRECTION_DOWN: + targetRow = this.view.rowCount - 1; + // Fall through for actual action + case event.DIRECTION_UP: + this.treeBoxObject.ensureRowIsVisible(targetRow); + break; + } + ]]> + </handler> + <handler event="select" phase="target" + action="if (event.originalTarget == this) this.stopEditing(true);"/> + <handler event="focus"> + <![CDATA[ + this.treeBoxObject.focused = true; + if (this.currentIndex == -1 && this.view.rowCount > 0) { + this.currentIndex = this.treeBoxObject.getFirstVisibleRow(); + } + if (this._cellSelType && !this.view.selection.currentColumn) { + var col = this._getNextColumn(this.currentIndex, false); + this.view.selection.currentColumn = col; + } + ]]> + </handler> + <handler event="blur" action="this.treeBoxObject.focused = false;"/> + <handler event="blur" phase="capturing" + action="if (event.originalTarget == this.inputField.inputField) this.stopEditing(true);"/> + <handler event="keydown" keycode="VK_RETURN"> + if (this._handleEnter(event)) { + event.stopPropagation(); + event.preventDefault(); + } + </handler> +#ifndef XP_MACOSX + <!-- Use F2 key to enter text editing. --> + <handler event="keydown" keycode="VK_F2"> + <![CDATA[ + if (!this._cellSelType) + return; + var row = this.currentIndex; + var column = this.view.selection.currentColumn; + if (this.startEditing(row, column)) + event.preventDefault(); + ]]> + </handler> +#endif // XP_MACOSX + + <handler event="keydown" keycode="VK_ESCAPE"> + <![CDATA[ + if (this._editingColumn) { + this.stopEditing(false); + this.focus(); + event.stopPropagation(); + event.preventDefault(); + } + ]]> + </handler> + <handler event="keydown" keycode="VK_LEFT"> + <![CDATA[ + if (this._editingColumn) + return; + + var row = this.currentIndex; + if (row < 0) + return; + + var cellSelType = this._cellSelType; + var checkContainers = true; + + var currentColumn; + if (cellSelType) { + currentColumn = this.view.selection.currentColumn; + if (currentColumn && !currentColumn.primary) + checkContainers = false; + } + + if (checkContainers) { + if (this.changeOpenState(this.currentIndex, false)) { + event.preventDefault(); + return; + } + var parentIndex = this.view.getParentIndex(this.currentIndex); + if (parentIndex >= 0) { + if (cellSelType && !this.view.isSelectable(parentIndex, currentColumn)) { + return; + } + this.view.selection.select(parentIndex); + this.treeBoxObject.ensureRowIsVisible(parentIndex); + event.preventDefault(); + return; + } + } + + if (cellSelType) { + var col = this._getNextColumn(row, true); + if (col) { + this.view.selection.currentColumn = col; + this.treeBoxObject.ensureCellIsVisible(row, col); + event.preventDefault(); + } + } + ]]> + </handler> + <handler event="keydown" keycode="VK_RIGHT"> + <![CDATA[ + if (this._editingColumn) + return; + + var row = this.currentIndex; + if (row < 0) + return; + + var cellSelType = this._cellSelType; + var checkContainers = true; + + var currentColumn; + if (cellSelType) { + currentColumn = this.view.selection.currentColumn; + if (currentColumn && !currentColumn.primary) + checkContainers = false; + } + + if (checkContainers) { + if (this.changeOpenState(row, true)) { + event.preventDefault(); + return; + } + var c = row + 1; + var view = this.view; + if (c < view.rowCount && + view.getParentIndex(c) == row) { + // If already opened, select the first child. + // The getParentIndex test above ensures that the children + // are already populated and ready. + if (cellSelType && !this.view.isSelectable(c, currentColumn)) { + let col = this._getNextColumn(c, false); + if (col) { + this.view.selection.currentColumn = col; + } + } + this.view.selection.timedSelect(c, this._selectDelay); + this.treeBoxObject.ensureRowIsVisible(c); + event.preventDefault(); + return; + } + } + + if (cellSelType) { + let col = this._getNextColumn(row, false); + if (col) { + this.view.selection.currentColumn = col; + this.treeBoxObject.ensureCellIsVisible(row, col); + event.preventDefault(); + } + } + ]]> + </handler> + <handler event="keydown" keycode="VK_UP" modifiers="accel any"> + <![CDATA[ + if (this._editingColumn) + return; + _moveByOffset(-1, 0, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_DOWN" modifiers="accel any"> + <![CDATA[ + if (this._editingColumn) + return; + _moveByOffset(1, this.view.rowCount - 1, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_UP" modifiers="accel any, shift"> + <![CDATA[ + if (this._editingColumn) + return; + _moveByOffsetShift(-1, 0, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_DOWN" modifiers="accel any, shift"> + <![CDATA[ + if (this._editingColumn) + return; + _moveByOffsetShift(1, this.view.rowCount - 1, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_PAGE_UP" modifiers="accel any"> + <![CDATA[ + if (this._editingColumn) + return; + _moveByPage(-1, 0, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_PAGE_DOWN" modifiers="accel any"> + <![CDATA[ + if (this._editingColumn) + return; + _moveByPage(1, this.view.rowCount - 1, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_PAGE_UP" modifiers="accel any, shift"> + <![CDATA[ + if (this._editingColumn) + return; + _moveByPageShift(-1, 0, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_PAGE_DOWN" modifiers="accel any, shift"> + <![CDATA[ + if (this._editingColumn) + return; + _moveByPageShift(1, this.view.rowCount - 1, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_HOME" modifiers="accel any"> + <![CDATA[ + if (this._editingColumn) + return; + _moveToEdge(0, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_END" modifiers="accel any"> + <![CDATA[ + if (this._editingColumn) + return; + _moveToEdge(this.view.rowCount - 1, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_HOME" modifiers="accel any, shift"> + <![CDATA[ + if (this._editingColumn) + return; + _moveToEdgeShift(0, event); + ]]> + </handler> + <handler event="keydown" keycode="VK_END" modifiers="accel any, shift"> + <![CDATA[ + if (this._editingColumn) + return; + _moveToEdgeShift(this.view.rowCount - 1, event); + ]]> + </handler> + <handler event="keypress"> + <![CDATA[ + if (this._editingColumn) + return; + + if (event.charCode == ' '.charCodeAt(0)) { + var c = this.currentIndex; + if (!this.view.selection.isSelected(c) || + (!this.view.selection.single && this._isAccelPressed(event))) { + this.view.selection.toggleSelect(c); + event.preventDefault(); + } + } + else if (!this.disableKeyNavigation && event.charCode > 0 && + !event.altKey && !this._isAccelPressed(event) && + !event.metaKey && !event.ctrlKey) { + var l = this._keyNavigate(event); + if (l >= 0) { + this.view.selection.timedSelect(l, this._selectDelay); + this.treeBoxObject.ensureRowIsVisible(l); + } + event.preventDefault(); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="treecols" role="xul:treecolumns"> + <resources> + <stylesheet src="chrome://global/skin/tree.css"/> + </resources> + <content orient="horizontal"> + <xul:hbox class="tree-scrollable-columns" flex="1"> + <children includes="treecol|splitter"/> + </xul:hbox> + <xul:treecolpicker class="treecol-image" fixed="true" xbl:inherits="tooltiptext=pickertooltiptext"/> + </content> + <implementation> + <constructor><![CDATA[ + // Set resizeafter="farthest" on the splitters if nothing else has been + // specified. + Array.forEach(this.getElementsByTagName("splitter"), function (splitter) { + if (!splitter.hasAttribute("resizeafter")) + splitter.setAttribute("resizeafter", "farthest"); + }); + ]]></constructor> + </implementation> + </binding> + + <binding id="treerows" extends="chrome://global/content/bindings/tree.xml#tree-base"> + <content> + <xul:hbox flex="1" class="tree-bodybox"> + <children/> + </xul:hbox> + <xul:scrollbar height="0" minwidth="0" minheight="0" orient="vertical" xbl:inherits="collapsed=hidevscroll" style="position:relative; z-index:2147483647;"/> + </content> + <handlers> + <handler event="underflow"> + <![CDATA[ + // Scrollport event orientation + // 0: vertical + // 1: horizontal + // 2: both (not used) + var tree = document.getBindingParent(this); + if (event.detail == 1) + tree.setAttribute("hidehscroll", "true"); + else if (event.detail == 0) + tree.setAttribute("hidevscroll", "true"); + event.stopPropagation(); + ]]> + </handler> + <handler event="overflow"> + <![CDATA[ + var tree = document.getBindingParent(this); + if (event.detail == 1) + tree.removeAttribute("hidehscroll"); + else if (event.detail == 0) + tree.removeAttribute("hidevscroll"); + event.stopPropagation(); + ]]> + </handler> + </handlers> + </binding> + + <binding id="treebody" extends="chrome://global/content/bindings/tree.xml#tree-base"> + <implementation> + <constructor> + if ("_ensureColumnOrder" in this.parentNode) + this.parentNode._ensureColumnOrder(); + </constructor> + + <field name="_lastSelectedRow"> + -1 + </field> + </implementation> + <handlers> + <!-- If there is no modifier key, we select on mousedown, not + click, so that drags work correctly. --> + <handler event="mousedown" clickcount="1"> + <![CDATA[ + if (this.parentNode.disabled) + return; + if (((!this._isAccelPressed(event) || + !this.parentNode.pageUpOrDownMovesSelection) && + !event.shiftKey && !event.metaKey) || + this.parentNode.view.selection.single) { + var b = this.parentNode.treeBoxObject; + var cell = b.getCellAt(event.clientX, event.clientY); + var view = this.parentNode.view; + + // save off the last selected row + this._lastSelectedRow = cell.row; + + if (cell.row == -1) + return; + + if (cell.childElt == "twisty") + return; + + if (cell.col && event.button == 0) { + if (cell.col.cycler) { + view.cycleCell(cell.row, cell.col); + return; + } else if (cell.col.type == Components.interfaces.nsITreeColumn.TYPE_CHECKBOX) { + if (this.parentNode.editable && cell.col.editable && + view.isEditable(cell.row, cell.col)) { + var value = view.getCellValue(cell.row, cell.col); + value = value == "true" ? "false" : "true"; + view.setCellValue(cell.row, cell.col, value); + return; + } + } + } + + var cellSelType = this.parentNode._cellSelType; + if (cellSelType == "text" && cell.childElt != "text" && cell.childElt != "image") + return; + + if (cellSelType) { + if (!cell.col.selectable || + !view.isSelectable(cell.row, cell.col)) { + return; + } + } + + if (!view.selection.isSelected(cell.row)) { + view.selection.select(cell.row); + b.ensureRowIsVisible(cell.row); + } + + if (cellSelType) { + view.selection.currentColumn = cell.col; + } + } + ]]> + </handler> + + <!-- On a click (up+down on the same item), deselect everything + except this item. --> + <handler event="click" button="0" clickcount="1"> + <![CDATA[ + if (this.parentNode.disabled) + return; + var b = this.parentNode.treeBoxObject; + var cell = b.getCellAt(event.clientX, event.clientY); + var view = this.parentNode.view; + + if (cell.row == -1) + return; + + if (cell.childElt == "twisty") { + if (view.selection.currentIndex >= 0 && + view.isContainerOpen(cell.row)) { + var parentIndex = view.getParentIndex(view.selection.currentIndex); + while (parentIndex >= 0 && parentIndex != cell.row) + parentIndex = view.getParentIndex(parentIndex); + if (parentIndex == cell.row) { + var parentSelectable = true; + if (this.parentNode._cellSelType) { + var currentColumn = view.selection.currentColumn; + if (!view.isSelectable(parentIndex, currentColumn)) + parentSelectable = false; + } + if (parentSelectable) + view.selection.select(parentIndex); + } + } + this.parentNode.changeOpenState(cell.row); + return; + } + + if (! view.selection.single) { + var augment = this._isAccelPressed(event); + if (event.shiftKey) { + view.selection.rangedSelect(-1, cell.row, augment); + b.ensureRowIsVisible(cell.row); + return; + } + if (augment) { + view.selection.toggleSelect(cell.row); + b.ensureRowIsVisible(cell.row); + view.selection.currentIndex = cell.row; + return; + } + } + + /* We want to deselect all the selected items except what was + clicked, UNLESS it was a right-click. We have to do this + in click rather than mousedown so that you can drag a + selected group of items */ + + if (!cell.col) return; + + // if the last row has changed in between the time we + // mousedown and the time we click, don't fire the select handler. + // see bug #92366 + if (!cell.col.cycler && this._lastSelectedRow == cell.row && + cell.col.type != Components.interfaces.nsITreeColumn.TYPE_CHECKBOX) { + + var cellSelType = this.parentNode._cellSelType; + if (cellSelType == "text" && cell.childElt != "text" && cell.childElt != "image") + return; + + if (cellSelType) { + if (!cell.col.selectable || + !view.isSelectable(cell.row, cell.col)) { + return; + } + } + + view.selection.select(cell.row); + b.ensureRowIsVisible(cell.row); + + if (cellSelType) { + view.selection.currentColumn = cell.col; + } + } + ]]> + </handler> + + <!-- double-click --> + <handler event="click" clickcount="2"> + <![CDATA[ + if (this.parentNode.disabled) + return; + var tbo = this.parentNode.treeBoxObject; + var view = this.parentNode.view; + var row = view.selection.currentIndex; + + if (row == -1) + return; + + var cell = tbo.getCellAt(event.clientX, event.clientY); + + if (cell.childElt != "twisty") { + view.selection.currentColumn = cell.col; + this.parentNode.startEditing(row, cell.col); + } + + if (this.parentNode._editingColumn || !view.isContainer(row)) + return; + + // Cyclers and twisties respond to single clicks, not double clicks + if (cell.col && !cell.col.cycler && cell.childElt != "twisty") + this.parentNode.changeOpenState(row); + ]]> + </handler> + + </handlers> + </binding> + + <binding id="treecol-base" role="xul:treecolumnitem" + extends="chrome://global/content/bindings/tree.xml#tree-base"> + <implementation> + <constructor> + this.parentNode.parentNode._columnsDirty = true; + </constructor> + + <property name="ordinal"> + <getter><![CDATA[ + var val = this.getAttribute("ordinal"); + if (val == "") + return "1"; + + return "" + (val == "0" ? 0 : parseInt(val)); + ]]></getter> + <setter><![CDATA[ + this.setAttribute("ordinal", val); + return val; + ]]></setter> + </property> + + <property name="_previousVisibleColumn"> + <getter><![CDATA[ + var sib = this.boxObject.previousSibling; + while (sib) { + if (sib.localName == "treecol" && sib.boxObject.width > 0 && sib.parentNode == this.parentNode) + return sib; + sib = sib.boxObject.previousSibling; + } + return null; + ]]></getter> + </property> + + <method name="_onDragMouseMove"> + <parameter name="aEvent"/> + <body><![CDATA[ + var col = document.treecolDragging; + if (!col) return; + + // determine if we have moved the mouse far enough + // to initiate a drag + if (col.mDragGesturing) { + if (Math.abs(aEvent.clientX - col.mStartDragX) < 5 && + Math.abs(aEvent.clientY - col.mStartDragY) < 5) { + return; + } + col.mDragGesturing = false; + col.setAttribute("dragging", "true"); + window.addEventListener("click", col._onDragMouseClick, true); + } + + var pos = {}; + var targetCol = col.parentNode.parentNode._getColumnAtX(aEvent.clientX, 0.5, pos); + + // bail if we haven't mousemoved to a different column + if (col.mTargetCol == targetCol && col.mTargetDir == pos.value) + return; + + var tree = col.parentNode.parentNode; + var sib; + var column; + if (col.mTargetCol) { + // remove previous insertbefore/after attributes + col.mTargetCol.removeAttribute("insertbefore"); + col.mTargetCol.removeAttribute("insertafter"); + column = tree.columns.getColumnFor(col.mTargetCol); + tree.treeBoxObject.invalidateColumn(column); + sib = col.mTargetCol._previousVisibleColumn; + if (sib) { + sib.removeAttribute("insertafter"); + column = tree.columns.getColumnFor(sib); + tree.treeBoxObject.invalidateColumn(column); + } + col.mTargetCol = null; + col.mTargetDir = null; + } + + if (targetCol) { + // set insertbefore/after attributes + if (pos.value == "after") { + targetCol.setAttribute("insertafter", "true"); + } else { + targetCol.setAttribute("insertbefore", "true"); + sib = targetCol._previousVisibleColumn; + if (sib) { + sib.setAttribute("insertafter", "true"); + column = tree.columns.getColumnFor(sib); + tree.treeBoxObject.invalidateColumn(column); + } + } + column = tree.columns.getColumnFor(targetCol); + tree.treeBoxObject.invalidateColumn(column); + col.mTargetCol = targetCol; + col.mTargetDir = pos.value; + } + ]]></body> + </method> + + <method name="_onDragMouseUp"> + <parameter name="aEvent"/> + <body><![CDATA[ + var col = document.treecolDragging; + if (!col) return; + + if (!col.mDragGesturing) { + if (col.mTargetCol) { + // remove insertbefore/after attributes + var before = col.mTargetCol.hasAttribute("insertbefore"); + col.mTargetCol.removeAttribute(before ? "insertbefore" : "insertafter"); + + var sib = col.mTargetCol._previousVisibleColumn; + if (before && sib) { + sib.removeAttribute("insertafter"); + } + + // Move the column only if it will result in a different column + // ordering + var move = true; + + // If this is a before move and the previous visible column is + // the same as the column we're moving, don't move + if (before && col == sib) { + move = false; + } + else if (!before && col == col.mTargetCol) { + // If this is an after move and the column we're moving is + // the same as the target column, don't move. + move = false; + } + + if (move) { + col.parentNode.parentNode._reorderColumn(col, col.mTargetCol, before); + } + + // repaint to remove lines + col.parentNode.parentNode.treeBoxObject.invalidate(); + + col.mTargetCol = null; + } + } else + col.mDragGesturing = false; + + document.treecolDragging = null; + col.removeAttribute("dragging"); + + window.removeEventListener("mousemove", col._onDragMouseMove, true); + window.removeEventListener("mouseup", col._onDragMouseUp, true); + // we have to wait for the click event to fire before removing + // cancelling handler + var clickHandler = function(handler) { + window.removeEventListener("click", handler, true); + }; + window.setTimeout(clickHandler, 0, col._onDragMouseClick); + ]]></body> + </method> + + <method name="_onDragMouseClick"> + <parameter name="aEvent"/> + <body><![CDATA[ + // prevent click event from firing after column drag and drop + aEvent.stopPropagation(); + aEvent.preventDefault(); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="mousedown" button="0"><![CDATA[ + if (this.parentNode.parentNode.enableColumnDrag) { + var xulns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var cols = this.parentNode.getElementsByTagNameNS(xulns, "treecol"); + + // only start column drag operation if there are at least 2 visible columns + var visible = 0; + for (var i = 0; i < cols.length; ++i) + if (cols[i].boxObject.width > 0) ++visible; + + if (visible > 1) { + window.addEventListener("mousemove", this._onDragMouseMove, true); + window.addEventListener("mouseup", this._onDragMouseUp, true); + document.treecolDragging = this; + this.mDragGesturing = true; + this.mStartDragX = event.clientX; + this.mStartDragY = event.clientY; + } + } + ]]></handler> + <handler event="click" button="0" phase="target"> + <![CDATA[ + if (event.target != event.originalTarget) + return; + + // On Windows multiple clicking on tree columns only cycles one time + // every 2 clicks. + if (/Win/.test(navigator.platform) && event.detail % 2 == 0) + return; + + var tree = this.parentNode.parentNode; + if (tree.columns) { + tree.view.cycleHeader(tree.columns.getColumnFor(this)); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="treecol" extends="chrome://global/content/bindings/tree.xml#treecol-base"> + <content> + <xul:label class="treecol-text" xbl:inherits="crop,value=label" flex="1" crop="right"/> + <xul:image class="treecol-sortdirection" xbl:inherits="sortDirection,hidden=hideheader"/> + </content> + </binding> + + <binding id="treecol-image" extends="chrome://global/content/bindings/tree.xml#treecol-base"> + <content> + <xul:image class="treecol-icon" xbl:inherits="src"/> + </content> + </binding> + + <binding id="columnpicker" display="xul:button" role="xul:button" + extends="chrome://global/content/bindings/tree.xml#tree-base"> + <content> + <xul:image class="tree-columnpicker-icon"/> + <xul:menupopup anonid="popup"> + <xul:menuseparator anonid="menuseparator"/> + <xul:menuitem anonid="menuitem" label="&restoreColumnOrder.label;"/> + </xul:menupopup> + </content> + + <implementation> + <method name="buildPopup"> + <parameter name="aPopup"/> + <body> + <![CDATA[ + // We no longer cache the picker content, remove the old content. + while (aPopup.childNodes.length > 2) + aPopup.removeChild(aPopup.firstChild); + + var refChild = aPopup.firstChild; + + var tree = this.parentNode.parentNode; + for (var currCol = tree.columns.getFirstColumn(); currCol; + currCol = currCol.getNext()) { + // Construct an entry for each column in the row, unless + // it is not being shown. + var currElement = currCol.element; + if (!currElement.hasAttribute("ignoreincolumnpicker")) { + var popupChild = document.createElement("menuitem"); + popupChild.setAttribute("type", "checkbox"); + var columnName = currElement.getAttribute("display") || + currElement.getAttribute("label"); + popupChild.setAttribute("label", columnName); + popupChild.setAttribute("colindex", currCol.index); + if (currElement.getAttribute("hidden") != "true") + popupChild.setAttribute("checked", "true"); + if (currCol.primary) + popupChild.setAttribute("disabled", "true"); + aPopup.insertBefore(popupChild, refChild); + } + } + + var hidden = !tree.enableColumnDrag; + const anonids = ["menuseparator", "menuitem"]; + for (var i = 0; i < anonids.length; i++) { + var element = document.getAnonymousElementByAttribute(this, "anonid", anonids[i]); + element.hidden = hidden; + } + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="command"> + <![CDATA[ + if (event.originalTarget == this) { + var popup = document.getAnonymousElementByAttribute(this, "anonid", "popup"); + this.buildPopup(popup); + popup.showPopup(this, -1, -1, "popup", "bottomright", "topright"); + } + else { + var tree = this.parentNode.parentNode; + tree.stopEditing(true); + var menuitem = document.getAnonymousElementByAttribute(this, "anonid", "menuitem"); + if (event.originalTarget == menuitem) { + tree.columns.restoreNaturalOrder(); + tree._ensureColumnOrder(); + } + else { + var colindex = event.originalTarget.getAttribute("colindex"); + var column = tree.columns[colindex]; + if (column) { + var element = column.element; + if (element.getAttribute("hidden") == "true") + element.setAttribute("hidden", "false"); + else + element.setAttribute("hidden", "true"); + } + } + } + ]]> + </handler> + </handlers> + </binding> +</bindings> diff --git a/toolkit/content/widgets/videocontrols.css b/toolkit/content/widgets/videocontrols.css new file mode 100644 index 0000000000..99dbf5a2ff --- /dev/null +++ b/toolkit/content/widgets/videocontrols.css @@ -0,0 +1,128 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace html url("http://www.w3.org/1999/xhtml"); + +.scrubber, +.volumeControl { + -moz-binding: url("chrome://global/content/bindings/videocontrols.xml#suppressChangeEvent"); +} + +.scrubber .scale-thumb { + -moz-binding: url("chrome://global/content/bindings/videocontrols.xml#timeThumb"); +} + +.playButton, +.muteButton, +.scrubber .scale-slider, +.volumeControl .scale-slider { + -moz-user-focus: none; +} + +.controlBar[fullscreen-unavailable] > .fullscreenButton { + display: none; +} + +.mediaControlsFrame { + direction: ltr; + /* Prevent unwanted style inheritance. See bug 554717. */ + text-align: left; + list-style-image: none !important; + font: normal normal normal 100%/normal sans-serif !important; + text-decoration: none !important; +} + +.controlsSpacer[hideCursor] { + cursor: none; +} + +.controlsOverlay[scaled] { + -moz-box-align: center; +} + +/* CSS Transitions + * + * These are overriden by the default theme; the rules here just + * provide a fallback to drive the required transitionend event + * (in case a 3rd party theme does not provide transitions). + */ +.controlBar:not([immediate]) { + transition-property: opacity; + transition-duration: 1ms; +} +.controlBar[fadeout] { + opacity: 0; +} +.volumeStack:not([immediate]) { + transition-property: opacity, margin-top; + transition-duration: 1ms, 1ms; +} +.volumeStack[fadeout] { + opacity: 0; + margin-top: 0; +} +.statusOverlay:not([immediate]) { + transition-property: opacity; + transition-duration: 1ms; + transition-delay: 750ms; +} +.statusOverlay[fadeout] { + opacity: 0; +} + +/* Statistics formatting */ +html|td.statLabel { + font-weight: bold; + max-width: 20%; + white-space: nowrap; +} +html|td.statValue { + max-width: 30%; +} +html|td.filename { + max-width: 80%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +html|span.statActivity > html|span { + display: none; +} +html|span.statActivity[activity="paused"] > html|span.statActivityPaused, +html|span.statActivity[activity="playing"] > html|span.statActivityPlaying, +html|span.statActivity[activity="ended"] > html|span.statActivityEnded, +html|span.statActivity[seeking] > html|span.statActivitySeeking { + display: inline; +} + +.controlBar[size="hidden"], +.controlBar[size="small"] .durationBox, +.controlBar[size="small"] .durationLabel, +.controlBar[size="small"] .positionLabel, +.controlBar[size="small"] .volumeStack { + visibility: collapse; +} + +.controlBar[size="small"] .scrubberStack, +.controlBar[size="small"] .backgroundBar, +.controlBar[size="small"] .bufferBar, +.controlBar[size="small"] .progressBar, +.controlBar[size="small"] .scrubber { + visibility: hidden; +} + +/* Error description formatting */ +.errorLabel { + display: none; +} + +[error="errorAborted"] > [anonid="errorAborted"], +[error="errorNetwork"] > [anonid="errorNetwork"], +[error="errorDecode"] > [anonid="errorDecode"], +[error="errorSrcNotSupported"] > [anonid="errorSrcNotSupported"], +[error="errorNoSource"] > [anonid="errorNoSource"], +[error="errorGeneric"] > [anonid="errorGeneric"] { + display: inline; +} diff --git a/toolkit/content/widgets/videocontrols.xml b/toolkit/content/widgets/videocontrols.xml new file mode 100644 index 0000000000..630f5a0226 --- /dev/null +++ b/toolkit/content/widgets/videocontrols.xml @@ -0,0 +1,2027 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<!DOCTYPE bindings [ + <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> + %videocontrolsDTD; +]> + +<bindings id="videoControlBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <binding id="timeThumb" + extends="chrome://global/content/bindings/scale.xml#scalethumb"> + <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <xbl:children/> + <hbox class="timeThumb" xbl:inherits="showhours"> + <label class="timeLabel"/> + </hbox> + </xbl:content> + <implementation> + + <constructor> + <![CDATA[ + this.timeLabel = document.getAnonymousElementByAttribute(this, "class", "timeLabel"); + this.timeLabel.setAttribute("value", "0:00"); + ]]> + </constructor> + + <property name="showHours"> + <getter> + <![CDATA[ + return this.getAttribute("showhours") == "true"; + ]]> + </getter> + <setter> + <![CDATA[ + this.setAttribute("showhours", val); + // If the duration becomes known while we're still showing the value + // for time=0, immediately update the value to show or hide the hours. + // It's less intrusive to do it now than when the user clicks play and + // is looking right next to the thumb. + var displayedTime = this.timeLabel.getAttribute("value"); + if (val && displayedTime == "0:00") + this.timeLabel.setAttribute("value", "0:00:00"); + else if (!val && displayedTime == "0:00:00") + this.timeLabel.setAttribute("value", "0:00"); + ]]> + </setter> + </property> + + <method name="setTime"> + <parameter name="time"/> + <body> + <![CDATA[ + var timeString; + time = Math.round(time / 1000); + var hours = Math.floor(time / 3600); + var mins = Math.floor((time % 3600) / 60); + var secs = Math.floor(time % 60); + if (secs < 10) + secs = "0" + secs; + if (hours || this.showHours) { + if (mins < 10) + mins = "0" + mins; + timeString = hours + ":" + mins + ":" + secs; + } else { + timeString = mins + ":" + secs; + } + + this.timeLabel.setAttribute("value", timeString); + ]]> + </body> + </method> + </implementation> + </binding> + + <binding id="suppressChangeEvent" + extends="chrome://global/content/bindings/scale.xml#scale"> + <implementation implements="nsIXBLAccessible"> + <!-- nsIXBLAccessible --> + <property name="accessibleName" readonly="true"> + <getter> + if (this.type != "scrubber") + return ""; + + var currTime = this.thumb.timeLabel.getAttribute("value"); + var totalTime = this.durationValue; + + return this.scrubberNameFormat.replace(/#1/, currTime). + replace(/#2/, totalTime); + </getter> + </property> + + <constructor> + <![CDATA[ + this.scrubberNameFormat = ]]>"&scrubberScale.nameFormat;"<![CDATA[; + this.durationValue = ""; + this.valueBar = null; + this.isDragging = false; + this.isPausedByDragging = false; + + this.thumb = document.getAnonymousElementByAttribute(this, "class", "scale-thumb"); + this.type = this.getAttribute("class"); + this.Utils = document.getBindingParent(this.parentNode).Utils; + if (this.type == "scrubber") + this.valueBar = this.Utils.progressBar; + ]]> + </constructor> + + <method name="valueChanged"> + <parameter name="which"/> + <parameter name="newValue"/> + <parameter name="userChanged"/> + <body> + <![CDATA[ + // This method is a copy of the base binding's valueChanged(), except that it does + // not dispatch a |change| event (to avoid exposing the event to web content), and + // just calls the videocontrol's seekToPosition() method directly. + switch (which) { + case "curpos": + if (this.type == "scrubber") { + // Update the time shown in the thumb. + this.thumb.setTime(newValue); + this.Utils.positionLabel.setAttribute("value", this.thumb.timeLabel.value); + // Update the value bar to match the thumb position. + let percent = newValue / this.max; + this.valueBar.value = Math.round(percent * 10000); // has max=10000 + } + + // The value of userChanged is true when changing the position with the mouse, + // but not when pressing an arrow key. However, the base binding sets + // ._userChanged in its keypress handlers, so we just need to check both. + if (!userChanged && !this._userChanged) + return; + this.setAttribute("value", newValue); + + if (this.type == "scrubber") + this.Utils.seekToPosition(newValue); + else if (this.type == "volumeControl") + this.Utils.setVolume(newValue / 100); + break; + + case "minpos": + this.setAttribute("min", newValue); + break; + + case "maxpos": + if (this.type == "scrubber") { + // Update the value bar to match the thumb position. + let percent = this.value / newValue; + this.valueBar.value = Math.round(percent * 10000); // has max=10000 + } + this.setAttribute("max", newValue); + break; + } + ]]> + </body> + </method> + + <method name="dragStateChanged"> + <parameter name="isDragging"/> + <body> + <![CDATA[ + if (this.type == "scrubber") { + this.Utils.log("--- dragStateChanged: " + isDragging + " ---"); + this.isDragging = isDragging; + if (this.isPausedByDragging && !isDragging) { + // After the drag ends, resume playing. + this.Utils.video.play(); + this.isPausedByDragging = false; + } + } + ]]> + </body> + </method> + + <method name="pauseVideoDuringDragging"> + <body> + <![CDATA[ + if (this.isDragging && + !this.Utils.video.paused && !this.isPausedByDragging) { + this.isPausedByDragging = true; + this.Utils.video.pause(); + } + ]]> + </body> + </method> + + </implementation> + </binding> + + <binding id="videoControls"> + + <resources> + <stylesheet src="chrome://global/content/bindings/videocontrols.css"/> + <stylesheet src="chrome://global/skin/media/videocontrols.css"/> + </resources> + + <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="mediaControlsFrame"> + <stack flex="1"> + <vbox flex="1" class="statusOverlay" hidden="true"> + <box class="statusIcon"/> + <label class="errorLabel" anonid="errorAborted">&error.aborted;</label> + <label class="errorLabel" anonid="errorNetwork">&error.network;</label> + <label class="errorLabel" anonid="errorDecode">&error.decode;</label> + <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label> + <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label> + <label class="errorLabel" anonid="errorGeneric">&error.generic;</label> + </vbox> + + <vbox class="controlsOverlay"> + <stack flex="1"> + <spacer class="controlsSpacer" flex="1"/> + <box class="clickToPlay" hidden="true" flex="1"/> + <vbox class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox> + </stack> + <hbox class="controlBar" hidden="true"> + <button class="playButton" + playlabel="&playButton.playLabel;" + pauselabel="&playButton.pauseLabel;"/> + <stack class="scrubberStack" flex="1"> + <box class="backgroundBar"/> + <progressmeter class="bufferBar"/> + <progressmeter class="progressBar" max="10000"/> + <scale class="scrubber" movetoclick="true"/> + </stack> + <vbox class="durationBox"> + <label class="positionLabel" role="presentation"/> + <label class="durationLabel" role="presentation"/> + </vbox> + <button class="muteButton" + mutelabel="&muteButton.muteLabel;" + unmutelabel="&muteButton.unmuteLabel;"/> + <stack class="volumeStack"> + <box class="volumeBackground"/> + <box class="volumeForeground" anonid="volumeForeground"/> + <scale class="volumeControl" movetoclick="true"/> + </stack> + <button class="closedCaptionButton"/> + <button class="fullscreenButton" + enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;" + exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/> + </hbox> + </vbox> + </stack> + </xbl:content> + + <implementation> + + <constructor> + <![CDATA[ + this.isTouchControl = false; + this.randomID = 0; + + this.Utils = { + debug : false, + video : null, + videocontrols : null, + controlBar : null, + playButton : null, + muteButton : null, + volumeControl : null, + durationLabel : null, + positionLabel : null, + scrubberThumb : null, + scrubber : null, + progressBar : null, + bufferBar : null, + statusOverlay : null, + controlsSpacer : null, + clickToPlay : null, + controlsOverlay : null, + fullscreenButton : null, + currentTextTrackIndex: 0, + + textTracksCount: 0, + randomID : 0, + videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata", + "loadstart", "timeupdate", "progress", + "playing", "waiting", "canplay", "canplaythrough", + "seeking", "seeked", "emptied", "loadedmetadata", + "error", "suspend", "stalled", + "mozinterruptbegin", "mozinterruptend" ], + + firstFrameShown : false, + timeUpdateCount : 0, + maxCurrentTimeSeen : 0, + _isAudioOnly : false, + get isAudioOnly() { return this._isAudioOnly; }, + set isAudioOnly(val) { + this._isAudioOnly = val; + this.setFullscreenButtonState(); + + if (!this.isTopLevelSyntheticDocument) + return; + if (this._isAudioOnly) { + this.video.style.height = this._controlBarHeight + "px"; + this.video.style.width = "66%"; + } else { + this.video.style.removeProperty("height"); + this.video.style.removeProperty("width"); + } + }, + suppressError : false, + + setupStatusFader : function(immediate) { + // Since the play button will be showing, we don't want to + // show the throbber behind it. The throbber here will + // only show if needed after the play button has been pressed. + if (!this.clickToPlay.hidden) { + this.startFadeOut(this.statusOverlay, true); + return; + } + + var show = false; + if (this.video.seeking || + (this.video.error && !this.suppressError) || + this.video.networkState == this.video.NETWORK_NO_SOURCE || + (this.video.networkState == this.video.NETWORK_LOADING && + (this.video.paused || this.video.ended + ? this.video.readyState < this.video.HAVE_CURRENT_DATA + : this.video.readyState < this.video.HAVE_FUTURE_DATA)) || + (this.timeUpdateCount <= 1 && !this.video.ended && + this.video.readyState < this.video.HAVE_FUTURE_DATA && + this.video.networkState == this.video.NETWORK_LOADING)) + show = true; + + // Explicitly hide the status fader if this + // is audio only until bug 619421 is fixed. + if (this.isAudioOnly) + show = false; + + this.log("Status overlay: seeking=" + this.video.seeking + + " error=" + this.video.error + " readyState=" + this.video.readyState + + " paused=" + this.video.paused + " ended=" + this.video.ended + + " networkState=" + this.video.networkState + + " timeUpdateCount=" + this.timeUpdateCount + + " --> " + (show ? "SHOW" : "HIDE")); + this.startFade(this.statusOverlay, show, immediate); + }, + + /* + * Set the initial state of the controls. The binding is normally created along + * with video element, but could be attached at any point (eg, if the video is + * removed from the document and then reinserted). Thus, some one-time events may + * have already fired, and so we'll need to explicitly check the initial state. + */ + setupInitialState : function() { + this.randomID = Math.random(); + this.videocontrols.randomID = this.randomID; + + this.setPlayButtonState(this.video.paused); + + this.setFullscreenButtonState(); + + var duration = Math.round(this.video.duration * 1000); // in ms + var currentTime = Math.round(this.video.currentTime * 1000); // in ms + this.log("Initial playback position is at " + currentTime + " of " + duration); + // It would be nice to retain maxCurrentTimeSeen, but it would be difficult + // to determine if the media source changed while we were detached. + this.maxCurrentTimeSeen = currentTime; + this.showPosition(currentTime, duration); + + // If we have metadata, check if this is a <video> without + // video data, or a video with no audio track. + if (this.video.readyState >= this.video.HAVE_METADATA) { + if (this.video instanceof HTMLVideoElement && + (this.video.videoWidth == 0 || this.video.videoHeight == 0)) + this.isAudioOnly = true; + + // We have to check again if the media has audio here, + // because of bug 718107: switching to fullscreen may + // cause the bindings to detach and reattach, hence + // unsetting the attribute. + if (!this.isAudioOnly && !this.video.mozHasAudio) { + this.muteButton.setAttribute("noAudio", "true"); + this.muteButton.setAttribute("disabled", "true"); + } + } + + if (this.isAudioOnly) + this.clickToPlay.hidden = true; + + // If the first frame hasn't loaded, kick off a throbber fade-in. + if (this.video.readyState >= this.video.HAVE_CURRENT_DATA) + this.firstFrameShown = true; + + // We can't determine the exact buffering status, but do know if it's + // fully loaded. (If it's still loading, it will fire a progress event + // and we'll figure out the exact state then.) + this.bufferBar.setAttribute("max", 100); + if (this.video.readyState >= this.video.HAVE_METADATA) + this.showBuffered(); + else + this.bufferBar.setAttribute("value", 0); + + // Set the current status icon. + if (this.hasError()) { + this.clickToPlay.hidden = true; + this.statusIcon.setAttribute("type", "error"); + this.updateErrorText(); + this.setupStatusFader(true); + } + + // An event handler for |onresize| should be added when bug 227495 is fixed. + this.controlBar.hidden = false; + this._playButtonWidth = this.playButton.clientWidth; + this._durationLabelWidth = this.durationLabel.clientWidth; + this._muteButtonWidth = this.muteButton.clientWidth; + this._volumeControlWidth = this.volumeControl.clientWidth; + this._closedCaptionButtonWidth = this.closedCaptionButton.clientWidth; + this._fullscreenButtonWidth = this.fullscreenButton.clientWidth; + this._controlBarHeight = this.controlBar.clientHeight; + this.controlBar.hidden = true; + this.adjustControlSize(); + + // Can only update the volume controls once we've computed + // _volumeControlWidth, since the volume slider implementation + // depends on it. + this.updateVolumeControls(); + }, + + setupNewLoadState : function() { + // videocontrols.css hides the control bar by default, because if script + // is disabled our binding's script is disabled too (bug 449358). Thus, + // the controls are broken and we don't want them shown. But if script is + // enabled, the code here will run and can explicitly unhide the controls. + // + // For videos with |autoplay| set, we'll leave the controls initially hidden, + // so that they don't get in the way of the playing video. Otherwise we'll + // go ahead and reveal the controls now, so they're an obvious user cue. + // + // (Note: the |controls| attribute is already handled via layout/style/html.css) + var shouldShow = !this.dynamicControls || + (this.video.paused && + !(this.video.autoplay && this.video.mozAutoplayEnabled)); + // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107. + this.startFade(this.clickToPlay, shouldShow && !this.isAudioOnly && + this.video.currentTime == 0 && !this.hasError(), true); + this.startFade(this.controlBar, shouldShow, true); + }, + + get dynamicControls() { + // Don't fade controls for <audio> elements. + var enabled = !this.isAudioOnly; + + // Allow tests to explicitly suppress the fading of controls. + if (this.video.hasAttribute("mozNoDynamicControls")) + enabled = false; + + // If the video hits an error, suppress controls if it + // hasn't managed to do anything else yet. + if (!this.firstFrameShown && this.hasError()) + enabled = false; + + return enabled; + }, + + updateVolumeControls() { + var volume = this.video.muted ? 0 : this.video.volume; + var volumePercentage = Math.round(volume * 100); + this.updateMuteButtonState(); + this.volumeControl.value = volumePercentage; + this.volumeForeground.style.paddingRight = (1 - volume) * this._volumeControlWidth + "px"; + }, + + handleEvent : function (aEvent) { + this.log("Got media event ----> " + aEvent.type); + + // If the binding is detached (or has been replaced by a + // newer instance of the binding), nuke our event-listeners. + if (this.videocontrols.randomID != this.randomID) { + this.terminateEventListeners(); + return; + } + + switch (aEvent.type) { + case "play": + this.setPlayButtonState(false); + this.setupStatusFader(); + if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControl) + this.startFadeOut(this.controlBar); + if (!this._triggeredByControls) + this.clickToPlay.hidden = true; + this._triggeredByControls = false; + break; + case "pause": + // Little white lie: if we've internally paused the video + // while dragging the scrubber, don't change the button state. + if (!this.scrubber.isDragging) + this.setPlayButtonState(true); + this.setupStatusFader(); + break; + case "ended": + this.setPlayButtonState(true); + // We throttle timechange events, so the thumb might not be + // exactly at the end when the video finishes. + this.showPosition(Math.round(this.video.currentTime * 1000), + Math.round(this.video.duration * 1000)); + this.startFadeIn(this.controlBar); + this.setupStatusFader(); + break; + case "volumechange": + this.updateVolumeControls(); + // Show the controls to highlight the changing volume, + // but only if the click-to-play overlay has already + // been hidden (we don't hide controls when the overlay is visible). + if (this.clickToPlay.hidden && !this.isAudioOnly) { + this.startFadeIn(this.controlBar); + clearTimeout(this._hideControlsTimeout); + this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS); + } + break; + case "loadedmetadata": + this.adjustControlSize(); + // If a <video> doesn't have any video data, treat it as <audio> + // and show the controls (they won't fade back out) + if (this.video instanceof HTMLVideoElement && + (this.video.videoWidth == 0 || this.video.videoHeight == 0)) { + this.isAudioOnly = true; + this.clickToPlay.hidden = true; + this.startFadeIn(this.controlBar); + this.setFullscreenButtonState(); + } + this.showDuration(Math.round(this.video.duration * 1000)); + if (!this.isAudioOnly && !this.video.mozHasAudio) { + this.muteButton.setAttribute("noAudio", "true"); + this.muteButton.setAttribute("disabled", "true"); + } + break; + case "loadeddata": + this.firstFrameShown = true; + this.setupStatusFader(); + break; + case "loadstart": + this.maxCurrentTimeSeen = 0; + this.controlsSpacer.removeAttribute("aria-label"); + this.statusOverlay.removeAttribute("error"); + this.statusIcon.setAttribute("type", "throbber"); + this.isAudioOnly = (this.video instanceof HTMLAudioElement); + this.setPlayButtonState(true); + this.setupNewLoadState(); + this.setupStatusFader(); + break; + case "progress": + this.statusIcon.removeAttribute("stalled"); + this.showBuffered(); + this.setupStatusFader(); + break; + case "stalled": + this.statusIcon.setAttribute("stalled", "true"); + this.statusIcon.setAttribute("type", "throbber"); + this.setupStatusFader(); + break; + case "suspend": + this.setupStatusFader(); + break; + case "timeupdate": + var currentTime = Math.round(this.video.currentTime * 1000); // in ms + var duration = Math.round(this.video.duration * 1000); // in ms + + // If playing/seeking after the video ended, we won't get a "play" + // event, so update the button state here. + if (!this.video.paused) + this.setPlayButtonState(false); + + this.timeUpdateCount++; + // Whether we show the statusOverlay sometimes depends + // on whether we've seen more than one timeupdate + // event (if we haven't, there hasn't been any + // "playback activity" and we may wish to show the + // statusOverlay while we wait for HAVE_ENOUGH_DATA). + // If we've seen more than 2 timeupdate events, + // the count is no longer relevant to setupStatusFader. + if (this.timeUpdateCount <= 2) + this.setupStatusFader(); + + // If the user is dragging the scrubber ignore the delayed seek + // responses (don't yank the thumb away from the user) + if (this.scrubber.isDragging) + return; + + this.showPosition(currentTime, duration); + break; + case "emptied": + this.bufferBar.value = 0; + this.showPosition(0, 0); + break; + case "seeking": + this.showBuffered(); + this.statusIcon.setAttribute("type", "throbber"); + this.setupStatusFader(); + break; + case "waiting": + this.statusIcon.setAttribute("type", "throbber"); + this.setupStatusFader(); + break; + case "seeked": + case "playing": + case "canplay": + case "canplaythrough": + this.setupStatusFader(); + break; + case "error": + // We'll show the error status icon when we receive an error event + // under either of the following conditions: + // 1. The video has its error attribute set; this means we're loading + // from our src attribute, and the load failed, or we we're loading + // from source children and the decode or playback failed after we + // determined our selected resource was playable. + // 2. The video's networkState is NETWORK_NO_SOURCE. This means we we're + // loading from child source elements, but we were unable to select + // any of the child elements for playback during resource selection. + if (this.hasError()) { + this.suppressError = false; + this.clickToPlay.hidden = true; + this.statusIcon.setAttribute("type", "error"); + this.updateErrorText(); + this.setupStatusFader(true); + // If video hasn't shown anything yet, disable the controls. + if (!this.firstFrameShown) + this.startFadeOut(this.controlBar); + this.controlsSpacer.removeAttribute("hideCursor"); + } + break; + case "mozinterruptbegin": + case "mozinterruptend": + // Nothing to do... + break; + default: + this.log("!!! event " + aEvent.type + " not handled!"); + } + }, + + terminateEventListeners : function () { + if (this.videoEvents) { + for (let event of this.videoEvents) { + this.video.removeEventListener(event, this, { + capture: true, + mozSystemGroup: true + }); + } + } + + if (this.controlListeners) { + for (let element of this.controlListeners) { + element.item.removeEventListener(element.event, element.func, + { mozSystemGroup: true }); + } + + delete this.controlListeners; + } + + this.log("--- videocontrols terminated ---"); + }, + + hasError : function () { + // We either have an explicit error, or the resource selection + // algorithm is running and we've tried to load something and failed. + // Note: we don't consider the case where we've tried to load but + // there's no sources to load as an error condition, as sites may + // do this intentionally to work around requires-user-interaction to + // play restrictions, and we don't want to display a debug message + // if that's the case. + return this.video.error != null || + (this.video.networkState == this.video.NETWORK_NO_SOURCE && + this.hasSources()); + }, + + hasSources : function() { + if (this.video.hasAttribute('src') && + this.video.getAttribute('src') !== "") { + return true; + } + for (var child = this.video.firstChild; + child !== null; + child = child.nextElementSibling) { + if (child instanceof HTMLSourceElement) { + return true; + } + } + return false; + }, + + updateErrorText : function () { + let error; + let v = this.video; + // It is possible to have both v.networkState == NETWORK_NO_SOURCE + // as well as v.error being non-null. In this case, we will show + // the v.error.code instead of the v.networkState error. + if (v.error) { + switch (v.error.code) { + case v.error.MEDIA_ERR_ABORTED: + error = "errorAborted"; + break; + case v.error.MEDIA_ERR_NETWORK: + error = "errorNetwork"; + break; + case v.error.MEDIA_ERR_DECODE: + error = "errorDecode"; + break; + case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED: + error = "errorSrcNotSupported"; + break; + default: + error = "errorGeneric"; + break; + } + } else if (v.networkState == v.NETWORK_NO_SOURCE) { + error = "errorNoSource"; + } else { + return; // No error found. + } + + let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error); + this.controlsSpacer.setAttribute("aria-label", label.textContent); + this.statusOverlay.setAttribute("error", error); + }, + + formatTime : function(aTime) { + // Format the duration as "h:mm:ss" or "m:ss" + aTime = Math.round(aTime / 1000); + let hours = Math.floor(aTime / 3600); + let mins = Math.floor((aTime % 3600) / 60); + let secs = Math.floor(aTime % 60); + let timeString; + if (secs < 10) + secs = "0" + secs; + if (hours) { + if (mins < 10) + mins = "0" + mins; + timeString = hours + ":" + mins + ":" + secs; + } else { + timeString = mins + ":" + secs; + } + return timeString; + }, + + showDuration : function (duration) { + let isInfinite = (duration == Infinity); + this.log("Duration is " + duration + "ms.\n"); + + if (isNaN(duration) || isInfinite) + duration = this.maxCurrentTimeSeen; + + // Format the duration as "h:mm:ss" or "m:ss" + let timeString = isInfinite ? "" : this.formatTime(duration); + this.durationLabel.setAttribute("value", timeString); + + // "durationValue" property is used by scale binding to + // generate accessible name. + this.scrubber.durationValue = timeString; + + // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss + this.scrubberThumb.showHours = (duration >= 3600000); + + this.scrubber.max = duration; + // XXX Can't set increment here, due to bug 473103. Also, doing so causes + // snapping when dragging with the mouse, so we can't just set a value for + // the arrow-keys. + this.scrubber.pageIncrement = Math.round(duration / 10); + }, + + seekToPosition : function(newPosition) { + newPosition /= 1000; // convert from ms + this.log("+++ seeking to " + newPosition); + if (this.videocontrols.isGonk) { + // We use fastSeek() on B2G, and an accurate (but slower) + // seek on other platforms (that are likely to be higher + // perf). + this.video.fastSeek(newPosition); + } else { + this.video.currentTime = newPosition; + } + }, + + setVolume : function(newVolume) { + this.log("*** setting volume to " + newVolume); + this.video.volume = newVolume; + this.video.muted = false; + }, + + showPosition : function(currentTime, duration) { + // If the duration is unknown (because the server didn't provide + // it, or the video is a stream), then we want to fudge the duration + // by using the maximum playback position that's been seen. + if (currentTime > this.maxCurrentTimeSeen) + this.maxCurrentTimeSeen = currentTime; + this.showDuration(duration); + + this.log("time update @ " + currentTime + "ms of " + duration + "ms"); + + this.positionLabel.setAttribute("value", this.formatTime(currentTime)); + this.scrubber.value = currentTime; + }, + + showBuffered : function() { + function bsearch(haystack, needle, cmp) { + var length = haystack.length; + var low = 0; + var high = length; + while (low < high) { + var probe = low + ((high - low) >> 1); + var r = cmp(haystack, probe, needle); + if (r == 0) { + return probe; + } else if (r > 0) { + low = probe + 1; + } else { + high = probe; + } + } + return -1; + } + + function bufferedCompare(buffered, i, time) { + if (time > buffered.end(i)) { + return 1; + } else if (time >= buffered.start(i)) { + return 0; + } + return -1; + } + + var duration = Math.round(this.video.duration * 1000); + if (isNaN(duration)) + duration = this.maxCurrentTimeSeen; + + // Find the range that the current play position is in and use that + // range for bufferBar. At some point we may support multiple ranges + // displayed in the bar. + var currentTime = this.video.currentTime; + var buffered = this.video.buffered; + var index = bsearch(buffered, currentTime, bufferedCompare); + var endTime = 0; + if (index >= 0) { + endTime = Math.round(buffered.end(index) * 1000); + } + this.bufferBar.max = duration; + this.bufferBar.value = endTime; + }, + + _controlsHiddenByTimeout : false, + _showControlsTimeout : 0, + SHOW_CONTROLS_TIMEOUT_MS: 500, + _showControlsFn : function () { + if (Utils.video.matches("video:hover")) { + Utils.startFadeIn(Utils.controlBar, false); + Utils._showControlsTimeout = 0; + Utils._controlsHiddenByTimeout = false; + } + }, + + _hideControlsTimeout : 0, + _hideControlsFn : function () { + if (!Utils.scrubber.isDragging) { + Utils.startFade(Utils.controlBar, false); + Utils._hideControlsTimeout = 0; + Utils._controlsHiddenByTimeout = true; + } + }, + HIDE_CONTROLS_TIMEOUT_MS : 2000, + onMouseMove : function (event) { + // Pause playing video when the mouse is dragging over the control bar. + if (this.scrubber.isDragging) { + this.scrubber.pauseVideoDuringDragging(); + } + + // If the controls are static, don't change anything. + if (!this.dynamicControls) + return; + + clearTimeout(this._hideControlsTimeout); + + // Suppress fading out the controls until the video has rendered + // its first frame. But since autoplay videos start off with no + // controls, let them fade-out so the controls don't get stuck on. + if (!this.firstFrameShown && + !(this.video.autoplay && this.video.mozAutoplayEnabled)) + return; + + if (this._controlsHiddenByTimeout) + this._showControlsTimeout = setTimeout(this._showControlsFn, this.SHOW_CONTROLS_TIMEOUT_MS); + else + this.startFade(this.controlBar, true); + + // Hide the controls if the mouse cursor is left on top of the video + // but above the control bar and if the click-to-play overlay is hidden. + if ((this._controlsHiddenByTimeout || + event.clientY < this.controlBar.getBoundingClientRect().top) && + this.clickToPlay.hidden) { + this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS); + } + }, + + onMouseInOut : function (event) { + // If the controls are static, don't change anything. + if (!this.dynamicControls) + return; + + clearTimeout(this._hideControlsTimeout); + + // Ignore events caused by transitions between child nodes. + // Note that the videocontrols element is the same + // size as the *content area* of the video element, + // but this is not the same as the video element's + // border area if the video has border or padding. + if (this.isEventWithin(event, this.videocontrols)) + return; + + var isMouseOver = (event.type == "mouseover"); + + var controlRect = this.controlBar.getBoundingClientRect(); + var isMouseInControls = event.clientY > controlRect.top && + event.clientY < controlRect.bottom && + event.clientX > controlRect.left && + event.clientX < controlRect.right; + + // Suppress fading out the controls until the video has rendered + // its first frame. But since autoplay videos start off with no + // controls, let them fade-out so the controls don't get stuck on. + if (!this.firstFrameShown && !isMouseOver && + !(this.video.autoplay && this.video.mozAutoplayEnabled)) + return; + + if (!isMouseOver && !isMouseInControls) { + this.adjustControlSize(); + + // Keep the controls visible if the click-to-play is visible. + if (!this.clickToPlay.hidden) + return; + + this.startFadeOut(this.controlBar, false); + this.textTrackList.setAttribute("hidden", "true"); + clearTimeout(this._showControlsTimeout); + Utils._controlsHiddenByTimeout = false; + } + }, + + startFadeIn : function (element, immediate) { + this.startFade(element, true, immediate); + }, + + startFadeOut : function (element, immediate) { + this.startFade(element, false, immediate); + }, + + startFade : function (element, fadeIn, immediate) { + if (element.classList.contains("controlBar") && fadeIn) { + // Bug 493523, the scrubber doesn't call valueChanged while hidden, + // so our dependent state (eg, timestamp in the thumb) will be stale. + // As a workaround, update it manually when it first becomes unhidden. + if (element.hidden) + this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false); + } + + if (immediate) + element.setAttribute("immediate", true); + else + element.removeAttribute("immediate"); + + if (fadeIn) { + element.hidden = false; + // force style resolution, so that transition begins + // when we remove the attribute. + element.clientTop; + element.removeAttribute("fadeout"); + if (element.classList.contains("controlBar")) + this.controlsSpacer.removeAttribute("hideCursor"); + } else { + element.setAttribute("fadeout", true); + if (element.classList.contains("controlBar") && !this.hasError() && + document.mozFullScreenElement == this.video) + this.controlsSpacer.setAttribute("hideCursor", true); + + } + }, + + onTransitionEnd : function (event) { + // Ignore events for things other than opacity changes. + if (event.propertyName != "opacity") + return; + + var element = event.originalTarget; + + // Nothing to do when a fade *in* finishes. + if (!element.hasAttribute("fadeout")) + return; + + this.scrubber.dragStateChanged(false); + element.hidden = true; + }, + + _triggeredByControls: false, + + startPlay : function () { + this._triggeredByControls = true; + this.hideClickToPlay(); + this.video.play(); + }, + + togglePause : function () { + if (this.video.paused || this.video.ended) { + this.startPlay(); + } else { + this.video.pause(); + } + + // We'll handle style changes in the event listener for + // the "play" and "pause" events, same as if content + // script was controlling video playback. + }, + + isVideoWithoutAudioTrack : function() { + return this.video.readyState >= this.video.HAVE_METADATA && + !this.isAudioOnly && + !this.video.mozHasAudio; + }, + + toggleMute : function () { + if (this.isVideoWithoutAudioTrack()) { + return; + } + this.video.muted = !this.isEffectivelyMuted(); + if (this.video.volume === 0) { + this.video.volume = 0.5; + } + + // We'll handle style changes in the event listener for + // the "volumechange" event, same as if content script was + // controlling volume. + }, + + isVideoInFullScreen : function () { + return document.mozFullScreenElement == this.video; + }, + + toggleFullscreen : function () { + this.isVideoInFullScreen() ? + document.mozCancelFullScreen() : + this.video.mozRequestFullScreen(); + }, + + setFullscreenButtonState : function () { + if (this.isAudioOnly || !document.mozFullScreenEnabled) { + this.controlBar.setAttribute("fullscreen-unavailable", true); + this.adjustControlSize(); + return; + } + this.controlBar.removeAttribute("fullscreen-unavailable"); + this.adjustControlSize(); + + var attrName = this.isVideoInFullScreen() ? "exitfullscreenlabel" : "enterfullscreenlabel"; + var value = this.fullscreenButton.getAttribute(attrName); + this.fullscreenButton.setAttribute("aria-label", value); + + if (this.isVideoInFullScreen()) + this.fullscreenButton.setAttribute("fullscreened", "true"); + else + this.fullscreenButton.removeAttribute("fullscreened"); + }, + + onFullscreenChange: function () { + if (this.isVideoInFullScreen()) { + Utils._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS); + } + this.setFullscreenButtonState(); + }, + + clickToPlayClickHandler : function(e) { + if (e.button != 0) + return; + if (this.hasError() && !this.suppressError) { + // Errors that can be dismissed should be placed here as we discover them. + if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED) + return; + this.statusOverlay.hidden = true; + this.suppressError = true; + return; + } + if (e.defaultPrevented) + return; + if (this.playButton.hasAttribute("paused")) { + this.startPlay(); + } else { + this.video.pause(); + } + }, + hideClickToPlay : function () { + let videoHeight = this.video.clientHeight; + let videoWidth = this.video.clientWidth; + + // The play button will animate to 3x its size. This + // shows the animation unless the video is too small + // to show 2/3 of the animation. + let animationScale = 2; + if (this._overlayPlayButtonHeight * animationScale > (videoHeight - this._controlBarHeight)|| + this._overlayPlayButtonWidth * animationScale > videoWidth) { + this.clickToPlay.setAttribute("immediate", "true"); + this.clickToPlay.hidden = true; + } else { + this.clickToPlay.removeAttribute("immediate"); + } + this.clickToPlay.setAttribute("fadeout", "true"); + }, + + setPlayButtonState : function(aPaused) { + if (aPaused) + this.playButton.setAttribute("paused", "true"); + else + this.playButton.removeAttribute("paused"); + + var attrName = aPaused ? "playlabel" : "pauselabel"; + var value = this.playButton.getAttribute(attrName); + this.playButton.setAttribute("aria-label", value); + }, + + isEffectivelyMuted : function() { + return this.video.muted || !this.video.volume; + }, + + updateMuteButtonState : function() { + var muted = this.isEffectivelyMuted(); + + if (muted) + this.muteButton.setAttribute("muted", "true"); + else + this.muteButton.removeAttribute("muted"); + + var attrName = muted ? "unmutelabel" : "mutelabel"; + var value = this.muteButton.getAttribute(attrName); + this.muteButton.setAttribute("aria-label", value); + }, + + _getComputedPropertyValueAsInt : function(element, property) { + let value = getComputedStyle(element, null).getPropertyValue(property); + return parseInt(value, 10); + }, + + keyHandler : function(event) { + // Ignore keys when content might be providing its own. + if (!this.video.hasAttribute("controls")) + return; + + var keystroke = ""; + if (event.altKey) + keystroke += "alt-"; + if (event.shiftKey) + keystroke += "shift-"; + if (navigator.platform.startsWith("Mac")) { + if (event.metaKey) + keystroke += "accel-"; + if (event.ctrlKey) + keystroke += "control-"; + } else { + if (event.metaKey) + keystroke += "meta-"; + if (event.ctrlKey) + keystroke += "accel-"; + } + switch (event.keyCode) { + case KeyEvent.DOM_VK_UP: + keystroke += "upArrow"; + break; + case KeyEvent.DOM_VK_DOWN: + keystroke += "downArrow"; + break; + case KeyEvent.DOM_VK_LEFT: + keystroke += "leftArrow"; + break; + case KeyEvent.DOM_VK_RIGHT: + keystroke += "rightArrow"; + break; + case KeyEvent.DOM_VK_HOME: + keystroke += "home"; + break; + case KeyEvent.DOM_VK_END: + keystroke += "end"; + break; + } + + if (String.fromCharCode(event.charCode) == ' ') + keystroke += "space"; + + this.log("Got keystroke: " + keystroke); + var oldval, newval; + + try { + switch (keystroke) { + case "space": /* Play */ + this.togglePause(); + break; + case "downArrow": /* Volume decrease */ + oldval = this.video.volume; + this.video.volume = (oldval < 0.1 ? 0 : oldval - 0.1); + this.video.muted = false; + break; + case "upArrow": /* Volume increase */ + oldval = this.video.volume; + this.video.volume = (oldval > 0.9 ? 1 : oldval + 0.1); + this.video.muted = false; + break; + case "accel-downArrow": /* Mute */ + this.video.muted = true; + break; + case "accel-upArrow": /* Unmute */ + this.video.muted = false; + break; + case "leftArrow": /* Seek back 15 seconds */ + case "accel-leftArrow": /* Seek back 10% */ + oldval = this.video.currentTime; + if (keystroke == "leftArrow") + newval = oldval - 15; + else + newval = oldval - (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10; + this.video.currentTime = (newval >= 0 ? newval : 0); + break; + case "rightArrow": /* Seek forward 15 seconds */ + case "accel-rightArrow": /* Seek forward 10% */ + oldval = this.video.currentTime; + var maxtime = (this.video.duration || this.maxCurrentTimeSeen / 1000); + if (keystroke == "rightArrow") + newval = oldval + 15; + else + newval = oldval + maxtime / 10; + this.video.currentTime = (newval <= maxtime ? newval : maxtime); + break; + case "home": /* Seek to beginning */ + this.video.currentTime = 0; + break; + case "end": /* Seek to end */ + if (this.video.currentTime != this.video.duration) + this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000); + break; + default: + return; + } + } catch (e) { /* ignore any exception from setting .currentTime */ } + + event.preventDefault(); // Prevent page scrolling + }, + + isSupportedTextTrack : function(textTrack) { + return textTrack.kind == "subtitles" || + textTrack.kind == "captions"; + }, + + get overlayableTextTracks() { + return Array.prototype.filter.call(this.video.textTracks, this.isSupportedTextTrack); + }, + + isClosedCaptionOn : function () { + for (let tt of this.overlayableTextTracks) { + if (tt.mode === "showing") { + return true; + } + } + + return false; + }, + + setClosedCaptionButtonState : function () { + if (!this.overlayableTextTracks.length || this.videocontrols.isTouchControl) { + this.closedCaptionButton.setAttribute("hidden", "true"); + return; + } + + this.closedCaptionButton.removeAttribute("hidden"); + + if (this.isClosedCaptionOn()) { + this.closedCaptionButton.setAttribute("enabled", "true"); + } else { + this.closedCaptionButton.removeAttribute("enabled"); + } + + let ttItems = this.textTrackList.childNodes; + + for (let tti of ttItems) { + const idx = +tti.getAttribute("index"); + + if (idx == this.currentTextTrackIndex) { + tti.setAttribute("on", "true"); + } else { + tti.removeAttribute("on"); + } + } + }, + + addNewTextTrack : function (tt) { + if (!this.isSupportedTextTrack(tt)) { + return; + } + + if (tt.index && tt.index < this.textTracksCount) { + // Don't create items for initialized tracks. However, we + // still need to care about mode since TextTrackManager would + // turn on the first available track automatically. + if (tt.mode === "showing") { + this.changeTextTrack(tt.index); + } + return; + } + + tt.index = this.textTracksCount++; + + const label = tt.label || ""; + const ttText = document.createTextNode(label); + const ttBtn = document.createElement("button"); + + ttBtn.classList.add("textTrackItem"); + ttBtn.setAttribute("index", tt.index); + + ttBtn.addEventListener("click", function (event) { + event.stopPropagation(); + + this.changeTextTrack(tt.index); + }.bind(this)); + + ttBtn.appendChild(ttText); + + this.textTrackList.appendChild(ttBtn); + + if (tt.mode === "showing" && tt.index) { + this.changeTextTrack(tt.index); + } + }, + + changeTextTrack : function (index) { + for (let tt of this.overlayableTextTracks) { + if (tt.index === index) { + tt.mode = "showing"; + + this.currentTextTrackIndex = tt.index; + } else { + tt.mode = "disabled"; + } + } + + // should fallback to off + if (this.currentTextTrackIndex !== index) { + this.currentTextTrackIndex = 0; + } + + this.textTrackList.setAttribute("hidden", "true"); + this.setClosedCaptionButtonState(); + }, + + onControlBarTransitioned : function () { + this.textTrackList.setAttribute("hidden", "true"); + this.video.dispatchEvent(new CustomEvent("controlbarchange")); + }, + + toggleClosedCaption : function () { + if (this.overlayableTextTracks.length === 1) { + const lastTTIdx = this.overlayableTextTracks[0].index; + this.changeTextTrack(this.isClosedCaptionOn() ? 0 : lastTTIdx); + return; + } + + if (this.textTrackList.hasAttribute("hidden")) { + this.textTrackList.removeAttribute("hidden"); + } else { + this.textTrackList.setAttribute("hidden", "true"); + } + + let maxButtonWidth = 0; + + for (let tti of this.textTrackList.childNodes) { + if (tti.clientWidth > maxButtonWidth) { + maxButtonWidth = tti.clientWidth; + } + } + + if (maxButtonWidth > this.video.clientWidth) { + maxButtonWidth = this.video.clientWidth; + } + + for (let tti of this.textTrackList.childNodes) { + tti.style.width = maxButtonWidth + "px"; + } + }, + + onTextTrackAdd : function (trackEvent) { + this.addNewTextTrack(trackEvent.track); + this.setClosedCaptionButtonState(); + }, + + onTextTrackRemove : function (trackEvent) { + const toRemoveIndex = trackEvent.track.index; + const ttItems = this.textTrackList.childNodes; + + if (!ttItems) { + return; + } + + for (let tti of ttItems) { + const idx = +tti.getAttribute("index"); + + if (idx === toRemoveIndex) { + tti.remove(); + this.textTracksCount--; + } + + if (idx === this.currentTextTrackIndex) { + this.currentTextTrackIndex = 0; + + this.video.dispatchEvent(new CustomEvent("texttrackchange")); + } + } + + this.setClosedCaptionButtonState(); + }, + + initTextTracks : function () { + // add 'off' button anyway as new text track might be + // dynamically added after initialization. + const offLabel = this.textTrackList.getAttribute("offlabel"); + + this.addNewTextTrack({ + label: offLabel, + kind: "subtitles" + }); + + for (let tt of this.overlayableTextTracks) { + this.addNewTextTrack(tt); + } + + this.setClosedCaptionButtonState(); + }, + + isEventWithin : function (event, parent1, parent2) { + function isDescendant (node) { + while (node) { + if (node == parent1 || node == parent2) + return true; + node = node.parentNode; + } + return false; + } + return isDescendant(event.target) && isDescendant(event.relatedTarget); + }, + + log : function (msg) { + if (this.debug) + console.log("videoctl: " + msg + "\n"); + }, + + get isTopLevelSyntheticDocument() { + let doc = this.video.ownerDocument; + let win = doc.defaultView; + return doc.mozSyntheticDocument && win === win.top; + }, + + _playButtonWidth : 0, + _durationLabelWidth : 0, + _muteButtonWidth : 0, + _volumeControlWidth : 0, + _closedCaptionButtonWidth : 0, + _fullscreenButtonWidth : 0, + _controlBarHeight : 0, + _overlayPlayButtonHeight : 64, + _overlayPlayButtonWidth : 64, + _controlBarPaddingEnd: 8, + adjustControlSize : function adjustControlSize() { + let doc = this.video.ownerDocument; + + // The scrubber has |flex=1|, therefore |minScrubberWidth| + // was generated by empirical testing. + let minScrubberWidth = 25; + let minWidthAllControls = this._playButtonWidth + + minScrubberWidth + + this._durationLabelWidth + + this._muteButtonWidth + + this._volumeControlWidth + + this._closedCaptionButtonWidth + + this._fullscreenButtonWidth; + + let isFullscreenUnavailable = this.controlBar.hasAttribute("fullscreen-unavailable"); + if (isFullscreenUnavailable) { + // When the fullscreen button is hidden we add margin-end to the volume stack. + minWidthAllControls -= this._fullscreenButtonWidth - this._controlBarPaddingEnd; + } + + let minHeightForControlBar = this._controlBarHeight; + let minWidthOnlyPlayPause = this._playButtonWidth + this._muteButtonWidth; + + let isAudioOnly = this.isAudioOnly; + let videoHeight = isAudioOnly ? minHeightForControlBar : this.video.clientHeight; + let videoWidth = isAudioOnly ? minWidthAllControls : this.video.clientWidth; + + // Adapt the size of the controls to the size of the video + if (this.video.readyState >= this.video.HAVE_METADATA) { + if (!this.isAudioOnly && this.video.videoWidth && this.video.videoHeight) { + var rect = this.video.getBoundingClientRect(); + var widthRatio = rect.width / this.video.videoWidth; + var heightRatio = rect.height / this.video.videoHeight; + var width = this.video.videoWidth * Math.min(widthRatio, heightRatio); + + this.controlsOverlay.setAttribute("scaled", true); + this.controlsOverlay.style.width = width + "px"; + this.controlsSpacer.style.width = width + "px"; + this.controlBar.style.width = width + "px"; + } else { + this.controlsOverlay.removeAttribute("scaled"); + this.controlsOverlay.style.width = ""; + this.controlsSpacer.style.width = ""; + this.controlBar.style.width = ""; + } + } + + if ((this._overlayPlayButtonHeight + this._controlBarHeight) > videoHeight || + this._overlayPlayButtonWidth > videoWidth) { + this.clickToPlay.hidden = true; + } else if (this.clickToPlay.hidden && + !this.video.played.length && + this.video.paused) { + // Check this.video.paused to handle when a video is + // playing but hasn't processed any frames yet + this.clickToPlay.hidden = false; + } + + let size = "normal"; + if (videoHeight < minHeightForControlBar) + size = "hidden"; + else if (videoWidth < minWidthOnlyPlayPause) + size = "hidden"; + else if (videoWidth < minWidthAllControls) + size = "small"; + this.controlBar.setAttribute("size", size); + }, + + init : function (binding) { + this.video = binding.parentNode; + this.videocontrols = binding; + + this.statusIcon = document.getAnonymousElementByAttribute(binding, "class", "statusIcon"); + this.controlBar = document.getAnonymousElementByAttribute(binding, "class", "controlBar"); + this.playButton = document.getAnonymousElementByAttribute(binding, "class", "playButton"); + this.muteButton = document.getAnonymousElementByAttribute(binding, "class", "muteButton"); + this.volumeControl = document.getAnonymousElementByAttribute(binding, "class", "volumeControl"); + this.progressBar = document.getAnonymousElementByAttribute(binding, "class", "progressBar"); + this.bufferBar = document.getAnonymousElementByAttribute(binding, "class", "bufferBar"); + this.scrubber = document.getAnonymousElementByAttribute(binding, "class", "scrubber"); + this.scrubberThumb = document.getAnonymousElementByAttribute(this.scrubber, "class", "scale-thumb"); + this.durationLabel = document.getAnonymousElementByAttribute(binding, "class", "durationLabel"); + this.positionLabel = document.getAnonymousElementByAttribute(binding, "class", "positionLabel"); + this.statusOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay"); + this.controlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "controlsOverlay"); + this.controlsSpacer = document.getAnonymousElementByAttribute(binding, "class", "controlsSpacer"); + this.clickToPlay = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay"); + this.fullscreenButton = document.getAnonymousElementByAttribute(binding, "class", "fullscreenButton"); + this.volumeForeground = document.getAnonymousElementByAttribute(binding, "anonid", "volumeForeground"); + this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "class", "closedCaptionButton"); + this.textTrackList = document.getAnonymousElementByAttribute(binding, "class", "textTrackList"); + + this.isAudioOnly = (this.video instanceof HTMLAudioElement); + this.setupInitialState(); + this.setupNewLoadState(); + this.initTextTracks(); + + // Use the handleEvent() callback for all media events. + // Only the "error" event listener must capture, so that it can trap error + // events from <source> children, which don't bubble. But we use capture + // for all events in order to simplify the event listener add/remove. + for (let event of this.videoEvents) { + this.video.addEventListener(event, this, { + capture: true, + mozSystemGroup: true + }); + } + + var self = this; + this.controlListeners = []; + + // Helper function to add an event listener to the given element + function addListener(elem, eventName, func) { + let boundFunc = func.bind(self); + self.controlListeners.push({ item: elem, event: eventName, func: boundFunc }); + elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true }); + } + + addListener(this.muteButton, "command", this.toggleMute); + addListener(this.closedCaptionButton, "command", this.toggleClosedCaption); + addListener(this.playButton, "click", this.clickToPlayClickHandler); + addListener(this.fullscreenButton, "command", this.toggleFullscreen); + addListener(this.clickToPlay, "click", this.clickToPlayClickHandler); + addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler); + addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen); + + addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize); + addListener(this.videocontrols, "transitionend", this.onTransitionEnd); + addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange); + addListener(this.videocontrols, "transitionend", this.onControlBarTransitioned); + addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange); + addListener(this.video, "keypress", this.keyHandler); + addListener(this.video.textTracks, "addtrack", this.onTextTrackAdd); + addListener(this.video.textTracks, "removetrack", this.onTextTrackRemove); + + addListener(this.videocontrols, "dragstart", function(event) { + event.preventDefault(); // prevent dragging of controls image (bug 517114) + }); + + this.log("--- videocontrols initialized ---"); + } + }; + this.Utils.init(this); + ]]> + </constructor> + <destructor> + <![CDATA[ + this.Utils.terminateEventListeners(); + // randomID used to be a <field>, which meant that the XBL machinery + // undefined the property when the element was unbound. The code in + // this file actually depends on this, so now that randomID is an + // expando, we need to make sure to explicitly delete it. + delete this.randomID; + ]]> + </destructor> + + </implementation> + + <handlers> + <handler event="mouseover"> + if (!this.isTouchControl) + this.Utils.onMouseInOut(event); + </handler> + <handler event="mouseout"> + if (!this.isTouchControl) + this.Utils.onMouseInOut(event); + </handler> + <handler event="mousemove"> + if (!this.isTouchControl) + this.Utils.onMouseMove(event); + </handler> + </handlers> + </binding> + + <binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls"> + + <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame"> + <stack flex="1"> + <vbox flex="1" class="statusOverlay" hidden="true"> + <box class="statusIcon"/> + <label class="errorLabel" anonid="errorAborted">&error.aborted;</label> + <label class="errorLabel" anonid="errorNetwork">&error.network;</label> + <label class="errorLabel" anonid="errorDecode">&error.decode;</label> + <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label> + <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label> + <label class="errorLabel" anonid="errorGeneric">&error.generic;</label> + </vbox> + + <vbox class="controlsOverlay"> + <spacer class="controlsSpacer" flex="1"/> + <box flex="1" hidden="true"> + <box class="clickToPlay" hidden="true" flex="1"/> + <vbox class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox> + </box> + <vbox class="controlBar" hidden="true"> + <hbox class="buttonsBar"> + <button class="playButton" + playlabel="&playButton.playLabel;" + pauselabel="&playButton.pauseLabel;"/> + <label class="positionLabel" role="presentation"/> + <stack class="scrubberStack"> + <box class="backgroundBar"/> + <progressmeter class="flexibleBar" value="100"/> + <progressmeter class="bufferBar"/> + <progressmeter class="progressBar" max="10000"/> + <scale class="scrubber" movetoclick="true"/> + </stack> + <label class="durationLabel" role="presentation"/> + <button class="muteButton" + mutelabel="&muteButton.muteLabel;" + unmutelabel="&muteButton.unmuteLabel;"/> + <stack class="volumeStack"> + <box class="volumeBackground"/> + <box class="volumeForeground" anonid="volumeForeground"/> + <scale class="volumeControl" movetoclick="true"/> + </stack> + <button class="castingButton" hidden="true" + aria-label="&castingButton.castingLabel;"/> + <button class="closedCaptionButton" hidden="true"/> + <button class="fullscreenButton" + enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;" + exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/> + </hbox> + </vbox> + </vbox> + </stack> + </xbl:content> + + <implementation> + + <constructor> + <![CDATA[ + this.isTouchControl = true; + this.TouchUtils = { + videocontrols: null, + video: null, + controlsTimer: null, + controlsTimeout: 5000, + positionLabel: null, + castingButton: null, + + get Utils() { + return this.videocontrols.Utils; + }, + + get visible() { + return !this.Utils.controlBar.hasAttribute("fadeout") && + !(this.Utils.controlBar.getAttribute("hidden") == "true"); + }, + + _firstShow: false, + get firstShow() { return this._firstShow; }, + set firstShow(val) { + this._firstShow = val; + this.Utils.controlBar.setAttribute("firstshow", val); + }, + + toggleControls: function() { + if (!this.Utils.dynamicControls || !this.visible) + this.showControls(); + else + this.delayHideControls(0); + }, + + showControls : function() { + if (this.Utils.dynamicControls) { + this.Utils.startFadeIn(this.Utils.controlBar); + this.delayHideControls(this.controlsTimeout); + } + }, + + clearTimer: function() { + if (this.controlsTimer) { + clearTimeout(this.controlsTimer); + this.controlsTimer = null; + } + }, + + delayHideControls : function(aTimeout) { + this.clearTimer(); + let self = this; + this.controlsTimer = setTimeout(function() { + self.hideControls(); + }, aTimeout); + }, + + hideControls : function() { + if (!this.Utils.dynamicControls) + return; + this.Utils.startFadeOut(this.Utils.controlBar); + if (this.firstShow) + this.videocontrols.addEventListener("transitionend", this, false); + }, + + handleEvent : function (aEvent) { + if (aEvent.type == "transitionend") { + this.firstShow = false; + this.videocontrols.removeEventListener("transitionend", this, false); + return; + } + + if (this.videocontrols.randomID != this.Utils.randomID) + this.terminateEventListeners(); + + }, + + terminateEventListeners : function () { + for (var event of this.videoEvents) + this.Utils.video.removeEventListener(event, this, false); + }, + + isVideoCasting : function () { + if (this.video.mozIsCasting) + return true; + return false; + }, + + updateCasting : function (eventDetail) { + let castingData = JSON.parse(eventDetail); + if ("allow" in castingData) { + this.video.mozAllowCasting = !!castingData.allow; + } + + if ("active" in castingData) { + this.video.mozIsCasting = !!castingData.active; + } + this.setCastButtonState(); + }, + + startCasting : function () { + this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast")); + }, + + setCastButtonState : function () { + if (this.isAudioOnly || !this.video.mozAllowCasting) { + this.castingButton.hidden = true; + return; + } + + if (this.video.mozIsCasting) { + this.castingButton.setAttribute("active", "true"); + } else { + this.castingButton.removeAttribute("active"); + } + + this.castingButton.hidden = false; + }, + + init : function (binding) { + this.videocontrols = binding; + this.video = binding.parentNode; + + let self = this; + this.Utils.playButton.addEventListener("command", function() { + if (!self.video.paused) + self.delayHideControls(0); + else + self.showControls(); + }, false); + this.Utils.scrubber.addEventListener("touchstart", function() { + self.clearTimer(); + }, false); + this.Utils.scrubber.addEventListener("touchend", function() { + self.delayHideControls(self.controlsTimeout); + }, false); + this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false); + + this.castingButton = document.getAnonymousElementByAttribute(binding, "class", "castingButton"); + this.castingButton.addEventListener("command", function() { + self.startCasting(); + }, false); + + this.video.addEventListener("media-videoCasting", function (e) { + if (!e.isTrusted) + return; + self.updateCasting(e.detail); + }, false, true); + + // The first time the controls appear we want to just display + // a play button that does not fade away. The firstShow property + // makes that happen. But because of bug 718107 this init() method + // may be called again when we switch in or out of fullscreen + // mode. So we only set firstShow if we're not autoplaying and + // if we are at the beginning of the video and not already playing + if (!this.video.autoplay && this.Utils.dynamicControls && this.video.paused && + this.video.currentTime === 0) + this.firstShow = true; + + // If the video is not at the start, then we probably just + // transitioned into or out of fullscreen mode, and we don't want + // the controls to remain visible. this.controlsTimeout is a full + // 5s, which feels too long after the transition. + if (this.video.currentTime !== 0) { + this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS); + } + } + }; + this.TouchUtils.init(this); + this.dispatchEvent(new CustomEvent("VideoBindingAttached")); + ]]> + </constructor> + <destructor> + <![CDATA[ + // XBL destructors don't appear to be inherited properly, so we need + // to do this here in addition to the videoControls destructor. :-( + delete this.randomID; + ]]> + </destructor> + + </implementation> + + <handlers> + <handler event="mouseup"> + if (event.originalTarget.nodeName == "vbox") { + if (this.TouchUtils.firstShow) + this.Utils.video.play(); + this.TouchUtils.toggleControls(); + } + </handler> + </handlers> + + </binding> + + <binding id="touchControlsGonk" extends="chrome://global/content/bindings/videoControls.xml#touchControls"> + <implementation> + <constructor> + this.isGonk = true; + </constructor> + </implementation> + </binding> + + <binding id="noControls"> + + <resources> + <stylesheet src="chrome://global/content/bindings/videocontrols.css"/> + <stylesheet src="chrome://global/skin/media/videocontrols.css"/> + </resources> + + <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame"> + <vbox flex="1" class="statusOverlay" hidden="true"> + <box flex="1"> + <box class="clickToPlay" flex="1"/> + </box> + </vbox> + </xbl:content> + + <implementation> + <constructor> + <![CDATA[ + this.randomID = 0; + this.Utils = { + randomID : 0, + videoEvents : ["play", + "playing"], + controlListeners: [], + terminateEventListeners : function () { + for (let event of this.videoEvents) + this.video.removeEventListener(event, this, { mozSystemGroup: true }); + + for (let element of this.controlListeners) { + element.item.removeEventListener(element.event, element.func, + { mozSystemGroup: true }); + } + + delete this.controlListeners; + }, + + hasError : function () { + return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE); + }, + + handleEvent : function (aEvent) { + // If the binding is detached (or has been replaced by a + // newer instance of the binding), nuke our event-listeners. + if (this.binding.randomID != this.randomID) { + this.terminateEventListeners(); + return; + } + + switch (aEvent.type) { + case "play": + this.noControlsOverlay.hidden = true; + break; + case "playing": + this.noControlsOverlay.hidden = true; + break; + } + }, + + blockedVideoHandler : function () { + if (this.binding.randomID != this.randomID) { + this.terminateEventListeners(); + return; + } else if (this.hasError()) { + this.noControlsOverlay.hidden = true; + return; + } + this.noControlsOverlay.hidden = false; + }, + + clickToPlayClickHandler : function (e) { + if (this.binding.randomID != this.randomID) { + this.terminateEventListeners(); + return; + } else if (e.button != 0) { + return; + } + + this.noControlsOverlay.hidden = true; + this.video.play(); + }, + + init : function (binding) { + this.binding = binding; + this.randomID = Math.random(); + this.binding.randomID = this.randomID; + this.video = binding.parentNode; + this.clickToPlay = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay"); + this.noControlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay"); + + let self = this; + function addListener(elem, eventName, func) { + let boundFunc = func.bind(self); + self.controlListeners.push({ item: elem, event: eventName, func: boundFunc }); + elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true }); + } + addListener(this.clickToPlay, "click", this.clickToPlayClickHandler); + addListener(this.video, "MozNoControlsBlockedVideo", this.blockedVideoHandler); + + for (let event of this.videoEvents) { + this.video.addEventListener(event, this, { mozSystemGroup: true }); + } + + if (this.video.autoplay && !this.video.mozAutoplayEnabled) { + this.blockedVideoHandler(); + } + } + }; + this.Utils.init(this); + this.Utils.video.dispatchEvent(new CustomEvent("MozNoControlsVideoBindingAttached")); + ]]> + </constructor> + <destructor> + <![CDATA[ + this.Utils.terminateEventListeners(); + // randomID used to be a <field>, which meant that the XBL machinery + // undefined the property when the element was unbound. The code in + // this file actually depends on this, so now that randomID is an + // expando, we need to make sure to explicitly delete it. + delete this.randomID; + ]]> + </destructor> + </implementation> + </binding> + +</bindings> diff --git a/toolkit/content/widgets/wizard.xml b/toolkit/content/widgets/wizard.xml new file mode 100644 index 0000000000..3a8ec2cfef --- /dev/null +++ b/toolkit/content/widgets/wizard.xml @@ -0,0 +1,607 @@ +<?xml version="1.0"?> +<!-- 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/. --> + + +<!DOCTYPE bindings [ + <!ENTITY % wizardDTD SYSTEM "chrome://global/locale/wizard.dtd"> + %wizardDTD; +]> + +<bindings id="wizardBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="wizard-base"> + <resources> + <stylesheet src="chrome://global/skin/wizard.css"/> + </resources> + </binding> + + <binding id="wizard" extends="chrome://global/content/bindings/general.xml#root-element"> + <resources> + <stylesheet src="chrome://global/skin/wizard.css"/> + </resources> + <content> + <xul:hbox class="wizard-header" anonid="Header"/> + + <xul:deck class="wizard-page-box" flex="1" anonid="Deck"> + <children includes="wizardpage"/> + </xul:deck> + <children/> + + <xul:hbox class="wizard-buttons" anonid="Buttons" xbl:inherits="pagestep,firstpage,lastpage"/> + </content> + + <implementation> + <property name="title" onget="return document.title;" + onset="return document.title = val;"/> + + <property name="canAdvance" onget="return this._canAdvance;" + onset="this._nextButton.disabled = !val; return this._canAdvance = val;"/> + <property name="canRewind" onget="return this._canRewind;" + onset="this._backButton.disabled = !val; return this._canRewind = val;"/> + + <property name="pageStep" readonly="true" onget="return this._pageStack.length"/> + + <field name="pageCount">0</field> + + <field name="_accessMethod">null</field> + <field name="_pageStack">null</field> + <field name="_currentPage">null</field> + + <property name="wizardPages"> + <getter> + <![CDATA[ + var xulns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + return this.getElementsByTagNameNS(xulns, "wizardpage"); + ]]> + </getter> + </property> + + <property name="currentPage" onget="return this._currentPage"> + <setter> + <![CDATA[ + if (!val) + return val; + + this._currentPage = val; + + // Setting this attribute allows wizard's clients to dynamically + // change the styles of each page based on purpose of the page. + this.setAttribute("currentpageid", val.pageid); + if (this.onFirstPage) { + this.canRewind = false; + this.setAttribute("firstpage", "true"); + if (/Linux/.test(navigator.platform)) { + this._backButton.setAttribute('hidden', 'true'); + } + } else { + this.canRewind = true; + this.setAttribute("firstpage", "false"); + if (/Linux/.test(navigator.platform)) { + this._backButton.setAttribute('hidden', 'false'); + } + } + + if (this.onLastPage) { + this.canAdvance = true; + this.setAttribute("lastpage", "true"); + } else { + this.setAttribute("lastpage", "false"); + } + + this._deck.setAttribute("selectedIndex", val.pageIndex); + this._advanceFocusToPage(val); + + this._adjustWizardHeader(); + this._wizardButtons.onPageChange(); + + this._fireEvent(val, "pageshow"); + + return val; + ]]> + </setter> + </property> + + <property name="pageIndex" + onget="return this._currentPage ? this._currentPage.pageIndex : -1;"> + <setter> + <![CDATA[ + if (val < 0 || val >= this.pageCount) + return val; + + var page = this.wizardPages[val]; + this._pageStack[this._pageStack.length-1] = page; + this.currentPage = page; + + return val; + ]]> + </setter> + </property> + + <property name="onFirstPage" readonly="true" + onget="return this._pageStack.length == 1;"/> + + <property name="onLastPage" readonly="true"> + <getter><![CDATA[ + var cp = this.currentPage; + return cp && ((this._accessMethod == "sequential" && cp.pageIndex == this.pageCount-1) || + (this._accessMethod == "random" && cp.next == "")); + ]]></getter> + </property> + + <method name="getButton"> + <parameter name="aDlgType"/> + <body> + <![CDATA[ + var btns = this.getElementsByAttribute("dlgtype", aDlgType); + return btns.item(0) ? btns[0] : document.getAnonymousElementByAttribute(this._wizardButtons, "dlgtype", aDlgType); + ]]> + </body> + </method> + + <field name="_canAdvance"/> + <field name="_canRewind"/> + <field name="_wizardHeader"/> + <field name="_wizardButtons"/> + <field name="_deck"/> + <field name="_backButton"/> + <field name="_nextButton"/> + <field name="_cancelButton"/> + + <!-- functions to be added as oncommand listeners to the wizard buttons --> + <field name="_backFunc">(function() { document.documentElement.rewind(); })</field> + <field name="_nextFunc">(function() { document.documentElement.advance(); })</field> + <field name="_finishFunc">(function() { document.documentElement.advance(); })</field> + <field name="_cancelFunc">(function() { document.documentElement.cancel(); })</field> + <field name="_extra1Func">(function() { document.documentElement.extra1(); })</field> + <field name="_extra2Func">(function() { document.documentElement.extra2(); })</field> + + <field name="_closeHandler">(function(event) { + if (document.documentElement.cancel()) + event.preventDefault(); + })</field> + + <constructor><![CDATA[ + this._canAdvance = true; + this._canRewind = false; + this._hasLoaded = false; + + this._pageStack = []; + + try { + // need to create string bundle manually instead of using <xul:stringbundle/> + // see bug 63370 for details + this._bundle = Components.classes["@mozilla.org/intl/stringbundle;1"] + .getService(Components.interfaces.nsIStringBundleService) + .createBundle("chrome://global/locale/wizard.properties"); + } catch (e) { + // This fails in remote XUL, which has to provide titles for all pages + // see bug 142502 + } + + // get anonymous content references + this._wizardHeader = document.getAnonymousElementByAttribute(this, "anonid", "Header"); + this._wizardButtons = document.getAnonymousElementByAttribute(this, "anonid", "Buttons"); + this._deck = document.getAnonymousElementByAttribute(this, "anonid", "Deck"); + + this._initWizardButton("back"); + this._initWizardButton("next"); + this._initWizardButton("finish"); + this._initWizardButton("cancel"); + this._initWizardButton("extra1"); + this._initWizardButton("extra2"); + + this._initPages(); + + window.addEventListener("close", this._closeHandler, false); + + // start off on the first page + this.pageCount = this.wizardPages.length; + this.advance(); + + // give focus to the first focusable element in the dialog + window.addEventListener("load", this._setInitialFocus, false); + ]]></constructor> + + <method name="getPageById"> + <parameter name="aPageId"/> + <body><![CDATA[ + var els = this.getElementsByAttribute("pageid", aPageId); + return els.item(0); + ]]></body> + </method> + + <method name="extra1"> + <body><![CDATA[ + if (this.currentPage) + this._fireEvent(this.currentPage, "extra1"); + ]]></body> + </method> + + <method name="extra2"> + <body><![CDATA[ + if (this.currentPage) + this._fireEvent(this.currentPage, "extra2"); + ]]></body> + </method> + + <method name="rewind"> + <body><![CDATA[ + if (!this.canRewind) + return; + + if (this.currentPage && !this._fireEvent(this.currentPage, "pagehide")) + return; + + if (this.currentPage && !this._fireEvent(this.currentPage, "pagerewound")) + return; + + if (!this._fireEvent(this, "wizardback")) + return; + + + this._pageStack.pop(); + this.currentPage = this._pageStack[this._pageStack.length-1]; + this.setAttribute("pagestep", this._pageStack.length); + ]]></body> + </method> + + <method name="advance"> + <parameter name="aPageId"/> + <body><![CDATA[ + if (!this.canAdvance) + return; + + if (this.currentPage && !this._fireEvent(this.currentPage, "pagehide")) + return; + + if (this.currentPage && !this._fireEvent(this.currentPage, "pageadvanced")) + return; + + if (this.onLastPage && !aPageId) { + if (this._fireEvent(this, "wizardfinish")) + window.setTimeout(function() {window.close();}, 1); + } else { + if (!this._fireEvent(this, "wizardnext")) + return; + + var page; + if (aPageId) + page = this.getPageById(aPageId); + else { + if (this.currentPage) { + if (this._accessMethod == "random") + page = this.getPageById(this.currentPage.next); + else + page = this.wizardPages[this.currentPage.pageIndex+1]; + } else + page = this.wizardPages[0]; + } + + if (page) { + this._pageStack.push(page); + this.setAttribute("pagestep", this._pageStack.length); + + this.currentPage = page; + } + } + ]]></body> + </method> + + <method name="goTo"> + <parameter name="aPageId"/> + <body><![CDATA[ + var page = this.getPageById(aPageId); + if (page) { + this._pageStack[this._pageStack.length-1] = page; + this.currentPage = page; + } + ]]></body> + </method> + + <method name="cancel"> + <body><![CDATA[ + if (!this._fireEvent(this, "wizardcancel")) + return true; + + window.close(); + window.setTimeout(function() {window.close();}, 1); + return false; + ]]></body> + </method> + + <method name="_setInitialFocus"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + document.documentElement._hasLoaded = true; + var focusInit = + function() { + // give focus to the first focusable element in the dialog + if (!document.commandDispatcher.focusedElement) + document.commandDispatcher.advanceFocusIntoSubtree(document.documentElement); + + try { + var button = + document.documentElement._wizardButtons.defaultButton; + if (button) + window.notifyDefaultButtonLoaded(button); + } catch (e) { } + }; + + // Give focus after onload completes, see bug 103197. + setTimeout(focusInit, 0); + ]]> + </body> + </method> + + <method name="_advanceFocusToPage"> + <parameter name="aPage"/> + <body> + <![CDATA[ + if (!this._hasLoaded) + return; + + document.commandDispatcher.advanceFocusIntoSubtree(aPage); + + // if advanceFocusIntoSubtree tries to focus one of our + // dialog buttons, then remove it and put it on the root + var focused = document.commandDispatcher.focusedElement; + if (focused && focused.hasAttribute("dlgtype")) + this.focus(); + ]]> + </body> + </method> + + <method name="_initPages"> + <body><![CDATA[ + var meth = "sequential"; + var pages = this.wizardPages; + for (var i = 0; i < pages.length; ++i) { + var page = pages[i]; + page.pageIndex = i; + if (page.next != "") + meth = "random"; + } + this._accessMethod = meth; + ]]></body> + </method> + + <method name="_initWizardButton"> + <parameter name="aName"/> + <body><![CDATA[ + var btn = document.getAnonymousElementByAttribute(this._wizardButtons, "dlgtype", aName); + if (btn) { + btn.addEventListener("command", this["_"+aName+"Func"], false); + this["_"+aName+"Button"] = btn; + } + return btn; + ]]></body> + </method> + + <method name="_adjustWizardHeader"> + <body><![CDATA[ + var label = this.currentPage.getAttribute("label"); + if (!label && this.onFirstPage && this._bundle) { + if (/Mac/.test(navigator.platform)) { + label = this._bundle.GetStringFromName("default-first-title-mac"); + } else { + label = this._bundle.formatStringFromName("default-first-title", [this.title], 1); + } + } else if (!label && this.onLastPage && this._bundle) { + if (/Mac/.test(navigator.platform)) { + label = this._bundle.GetStringFromName("default-last-title-mac"); + } else { + label = this._bundle.formatStringFromName("default-last-title", [this.title], 1); + } + } + this._wizardHeader.setAttribute("label", label); + this._wizardHeader.setAttribute("description", this.currentPage.getAttribute("description")); + ]]></body> + </method> + + <method name="_hitEnter"> + <parameter name="evt"/> + <body> + <![CDATA[ + if (!evt.defaultPrevented) + this.advance(); + ]]> + </body> + </method> + + <method name="_fireEvent"> + <parameter name="aTarget"/> + <parameter name="aType"/> + <body> + <![CDATA[ + var event = document.createEvent("Events"); + event.initEvent(aType, true, true); + + // handle dom event handlers + var noCancel = aTarget.dispatchEvent(event); + + // handle any xml attribute event handlers + var handler = aTarget.getAttribute("on"+aType); + if (handler != "") { + var fn = new Function("event", handler); + var returned = fn.apply(aTarget, [event]); + if (returned == false) + noCancel = false; + } + + return noCancel; + ]]> + </body> + </method> + + </implementation> + + <handlers> + <handler event="keypress" keycode="VK_RETURN" + group="system" action="this._hitEnter(event)"/> + <handler event="keypress" keycode="VK_ESCAPE" group="system"> + if (!event.defaultPrevented) + this.cancel(); + </handler> + </handlers> + </binding> + + <binding id="wizardpage" extends="chrome://global/content/bindings/wizard.xml#wizard-base"> + <implementation> + <field name="pageIndex">-1</field> + + <property name="pageid" onget="return this.getAttribute('pageid');" + onset="this.setAttribute('pageid', val);"/> + + <property name="next" onget="return this.getAttribute('next');" + onset="this.setAttribute('next', val); + this.parentNode._accessMethod = 'random'; + return val;"/> + </implementation> + </binding> + +#ifdef XP_MACOSX + <binding id="wizard-header" extends="chrome://global/content/bindings/wizard.xml#wizard-base"> + <content> + <xul:stack class="wizard-header-stack" flex="1"> + <xul:vbox class="wizard-header-box-1"> + <xul:vbox class="wizard-header-box-text"> + <xul:label class="wizard-header-label" xbl:inherits="xbl:text=label"/> + </xul:vbox> + </xul:vbox> + <xul:hbox class="wizard-header-box-icon"> + <xul:spacer flex="1"/> + <xul:image class="wizard-header-icon" xbl:inherits="src=iconsrc"/> + </xul:hbox> + </xul:stack> + </content> + </binding> + + <binding id="wizard-buttons" extends="chrome://global/content/bindings/wizard.xml#wizard-base"> + <content> + <xul:vbox flex="1"> + <xul:hbox class="wizard-buttons-btm"> + <xul:button class="wizard-button" dlgtype="extra1" hidden="true"/> + <xul:button class="wizard-button" dlgtype="extra2" hidden="true"/> + <xul:button label="&button-cancel-mac.label;" class="wizard-button" dlgtype="cancel"/> + <xul:spacer flex="1"/> + <xul:button label="&button-back-mac.label;" accesskey="&button-back-mac.accesskey;" + class="wizard-button wizard-nav-button" dlgtype="back"/> + <xul:button label="&button-next-mac.label;" accesskey="&button-next-mac.accesskey;" + class="wizard-button wizard-nav-button" dlgtype="next" + default="true" xbl:inherits="hidden=lastpage" /> + <xul:button label="&button-finish-mac.label;" class="wizard-button" + dlgtype="finish" default="true" xbl:inherits="hidden=hidefinishbutton" /> + </xul:hbox> + </xul:vbox> + </content> + + <implementation> + <method name="onPageChange"> + <body><![CDATA[ + this.setAttribute("hidefinishbutton", !(this.getAttribute("lastpage") == "true")); + ]]></body> + </method> + </implementation> + + </binding> + +#else + + <binding id="wizard-header" extends="chrome://global/content/bindings/wizard.xml#wizard-base"> + <content> + <xul:hbox class="wizard-header-box-1" flex="1"> + <xul:vbox class="wizard-header-box-text" flex="1"> + <xul:label class="wizard-header-label" xbl:inherits="xbl:text=label"/> + <xul:label class="wizard-header-description" xbl:inherits="xbl:text=description"/> + </xul:vbox> + <xul:image class="wizard-header-icon" xbl:inherits="src=iconsrc"/> + </xul:hbox> + </content> + </binding> + + <binding id="wizard-buttons" extends="chrome://global/content/bindings/wizard.xml#wizard-base"> + <content> + <xul:vbox class="wizard-buttons-box-1" flex="1"> + <xul:separator class="wizard-buttons-separator groove"/> + <xul:hbox class="wizard-buttons-box-2"> + <xul:button class="wizard-button" dlgtype="extra1" hidden="true"/> + <xul:button class="wizard-button" dlgtype="extra2" hidden="true"/> + <xul:spacer flex="1" anonid="spacer"/> +#ifdef XP_UNIX + <xul:button label="&button-cancel-unix.label;" class="wizard-button" + dlgtype="cancel" icon="cancel"/> + <xul:spacer style="width: 24px"/> + <xul:button label="&button-back-unix.label;" accesskey="&button-back-unix.accesskey;" + class="wizard-button" dlgtype="back" icon="go-back"/> + <xul:deck class="wizard-next-deck" anonid="WizardButtonDeck"> + <xul:hbox> + <xul:button label="&button-finish-unix.label;" class="wizard-button" + dlgtype="finish" default="true" flex="1"/> + </xul:hbox> + <xul:hbox> + <xul:button label="&button-next-unix.label;" accesskey="&button-next-unix.accesskey;" + class="wizard-button" dlgtype="next" icon="go-forward" + default="true" flex="1"/> + </xul:hbox> + </xul:deck> +#else + <xul:button label="&button-back-win.label;" accesskey="&button-back-win.accesskey;" + class="wizard-button" dlgtype="back" icon="go-back"/> + <xul:deck class="wizard-next-deck" anonid="WizardButtonDeck"> + <xul:hbox> + <xul:button label="&button-finish-win.label;" class="wizard-button" + dlgtype="finish" default="true" flex="1"/> + </xul:hbox> + <xul:hbox> + <xul:button label="&button-next-win.label;" accesskey="&button-next-win.accesskey;" + class="wizard-button" dlgtype="next" icon="go-forward" + default="true" flex="1"/> + </xul:hbox> + </xul:deck> + <xul:button label="&button-cancel-win.label;" class="wizard-button" + dlgtype="cancel" icon="cancel"/> +#endif + </xul:hbox> + </xul:vbox> + </content> + + <implementation> + <field name="_wizardButtonDeck" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "WizardButtonDeck"); + </field> + + <method name="onPageChange"> + <body><![CDATA[ + if (this.getAttribute("lastpage") == "true") { + this._wizardButtonDeck.setAttribute("selectedIndex", 0); + } else { + this._wizardButtonDeck.setAttribute("selectedIndex", 1); + } + ]]></body> + </method> + + <property name="defaultButton" readonly="true"> + <getter><![CDATA[ + const kXULNS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var buttons = this._wizardButtonDeck.selectedPanel + .getElementsByTagNameNS(kXULNS, "button"); + for (var i = 0; i < buttons.length; i++) { + if (buttons[i].getAttribute("default") == "true" && + !buttons[i].hidden && !buttons[i].disabled) + return buttons[i]; + } + return null; + ]]></getter> + </property> + </implementation> + </binding> +#endif + +</bindings> diff --git a/toolkit/content/xul.css b/toolkit/content/xul.css new file mode 100644 index 0000000000..24a6713f9b --- /dev/null +++ b/toolkit/content/xul.css @@ -0,0 +1,1210 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * A minimal set of rules for the XUL elements that may be implicitly created + * as part of HTML/SVG documents (e.g. scrollbars) can be found over in + * minimal-xul.css. Rules for everything else related to XUL can be found in + * this file. Make sure you choose the correct style sheet when adding new + * rules. (This split of the XUL rules is to minimize memory use and improve + * performance in HTML/SVG documents.) + * + * This file should also not contain any app specific styling. Defaults for + * widgets of a particular application should be in that application's style + * sheet. For example, style definitions for navigator can be found in + * navigator.css. + * + * THIS FILE IS LOCKED DOWN. YOU ARE NOT ALLOWED TO MODIFY IT WITHOUT FIRST + * HAVING YOUR CHANGES REVIEWED BY enndeakin@gmail.com + */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ +@namespace html url("http://www.w3.org/1999/xhtml"); /* namespace for HTML elements */ +@namespace xbl url("http://www.mozilla.org/xbl"); /* namespace for XBL elements */ + +/* :::::::::: + :: Rules for 'hiding' portions of the chrome for special + :: kinds of windows (not JUST browser windows) with toolbars + ::::: */ + +window[chromehidden~="menubar"] .chromeclass-menubar, +window[chromehidden~="directories"] .chromeclass-directories, +window[chromehidden~="status"] .chromeclass-status, +window[chromehidden~="extrachrome"] .chromeclass-extrachrome, +window[chromehidden~="location"] .chromeclass-location, +window[chromehidden~="location"][chromehidden~="toolbar"] .chromeclass-toolbar, +window[chromehidden~="toolbar"] .chromeclass-toolbar-additional { + display: none; +} + +/* :::::::::: + :: Rules for forcing direction for entry and display of URIs + :: or URI elements + ::::: */ + +.uri-element { + direction: ltr !important; +} + +/****** elements that have no visual representation ******/ + +script, data, +xbl|children, +commands, commandset, command, +broadcasterset, broadcaster, observes, +keyset, key, toolbarpalette, toolbarset, +template, rule, conditions, action, +bindings, binding, content, member, triple, +treechildren, treeitem, treeseparator, treerow, treecell { + display: none; +} + +/********** focus rules **********/ + +button, +checkbox, +colorpicker[type="button"], +datepicker[type="grid"], +menulist, +radiogroup, +tree, +browser, +editor, +iframe { + -moz-user-focus: normal; +} + +menulist[editable="true"] { + -moz-user-focus: ignore; +} + +/******** window & page ******/ + +window, +page { + overflow: -moz-hidden-unscrollable; + -moz-box-orient: vertical; +} + +/******** box *******/ + +vbox { + -moz-box-orient: vertical; +} + +bbox { + -moz-box-align: baseline; +} + +/********** button **********/ + +button { + -moz-binding: url("chrome://global/content/bindings/button.xml#button"); +} + +button[type="repeat"] { + -moz-binding: url("chrome://global/content/bindings/button.xml#button-repeat"); +} + +button[type="menu"], button[type="panel"] { + -moz-binding: url("chrome://global/content/bindings/button.xml#menu"); +} + +button[type="menu-button"] { + -moz-binding: url("chrome://global/content/bindings/button.xml#menu-button"); +} + +/********** toolbarbutton **********/ + +toolbarbutton { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"); +} + +toolbarbutton.badged-button > toolbarbutton, +toolbarbutton.badged-button { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-badged"); +} + +.toolbarbutton-badge:not([value]), +.toolbarbutton-badge[value=""] { + display: none; +} + +toolbarbutton[type="menu"], +toolbarbutton[type="panel"] { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#menu"); +} + +toolbarbutton.badged-button[type="menu"], +toolbarbutton.badged-button[type="panel"] { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-badged-menu"); +} + +toolbarbutton[type="menu-button"] { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#menu-button"); +} + +toolbar[mode="icons"] .toolbarbutton-text, +toolbar[mode="icons"] .toolbarbutton-multiline-text, +toolbar[mode="text"] .toolbarbutton-icon { + display: none; +} + +.toolbarbutton-multiline-text:not([wrap="true"]), +.toolbarbutton-text[wrap="true"] { + display: none; +} + +/******** browser, editor, iframe ********/ + +browser, +editor, +iframe { + display: inline; +} + +browser { + -moz-binding: url("chrome://global/content/bindings/browser.xml#browser"); +} + +editor { + -moz-binding: url("chrome://global/content/bindings/editor.xml#editor"); +} + +iframe { + -moz-binding: url("chrome://global/content/bindings/general.xml#iframe"); +} + +/********** notifications **********/ + +notificationbox { + -moz-binding: url("chrome://global/content/bindings/notification.xml#notificationbox"); + -moz-box-orient: vertical; +} + +.notificationbox-stack { + overflow: -moz-hidden-unscrollable; +} + +notification { + -moz-binding: url("chrome://global/content/bindings/notification.xml#notification"); + transition: margin-top 300ms, opacity 300ms; +} + +/*********** popup notification ************/ +popupnotification { + -moz-binding: url("chrome://global/content/bindings/notification.xml#popup-notification"); +} + +.popup-notification-menubutton:not([label]) { + display: none; +} + +/********** image **********/ + +image { + -moz-binding: url("chrome://global/content/bindings/general.xml#image"); +} + +/********** checkbox **********/ + +checkbox { + -moz-binding: url("chrome://global/content/bindings/checkbox.xml#checkbox"); +} + +/********** radio **********/ + +radiogroup { + -moz-binding: url("chrome://global/content/bindings/radio.xml#radiogroup"); + -moz-box-orient: vertical; +} + +radio { + -moz-binding: url("chrome://global/content/bindings/radio.xml#radio"); +} + +/******** groupbox *********/ + +groupbox { + -moz-binding: url("chrome://global/content/bindings/groupbox.xml#groupbox"); + display: -moz-groupbox; +} + +caption { + -moz-binding: url("chrome://global/content/bindings/groupbox.xml#caption"); +} + +.groupbox-body { + -moz-box-pack: inherit; + -moz-box-align: inherit; + -moz-box-orient: vertical; +} + +/******** draggable elements *********/ + +%ifdef XP_MACOSX +titlebar, +toolbar:not([nowindowdrag="true"]):not([customizing="true"]), +statusbar:not([nowindowdrag="true"]), +%endif +windowdragbox { + -moz-window-dragging: drag; +} + +/* The list below is non-comprehensive and will probably need some tweaking. */ +toolbaritem, +toolbarbutton, +button, +textbox, +tab, +radio, +splitter, +scale, +menulist { + -moz-window-dragging: no-drag; +} + +/******* toolbar *******/ + +toolbox { + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbox"); + -moz-box-orient: vertical; +} + +toolbar { + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbar"); +} + +toolbar[customizing="true"][collapsed="true"] { + /* Some apps, e.g. Firefox, use 'collapsed' to hide toolbars. + Override it while customizing. */ + visibility: visible; +} + +toolbar[customizing="true"][hidden="true"] { + /* Some apps, e.g. SeaMonkey, use 'hidden' to hide toolbars. + Override it while customizing. */ + display: -moz-box; +} + +%ifdef XP_MACOSX +toolbar[type="menubar"] { + min-height: 0 !important; + border: 0 !important; +} +%else +toolbar[type="menubar"][autohide="true"] { + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbar-menubar-autohide"); + overflow: hidden; +} + +toolbar[type="menubar"][autohide="true"][inactive="true"]:not([customizing="true"]) { + min-height: 0 !important; + height: 0 !important; + -moz-appearance: none !important; + border-style: none !important; +} +%endif + +toolbarseparator { + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbardecoration"); +} + +toolbarspacer { + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbardecoration"); +} + +toolbarspring { + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbardecoration"); + -moz-box-flex: 1000; +} + +toolbarpaletteitem { + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbarpaletteitem"); +} + +toolbarpaletteitem[place="palette"] { + -moz-box-orient: vertical; + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbarpaletteitem-palette"); +} + +/********* menubar ***********/ + +menubar { + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#menubar"); +} + +/********* menu ***********/ + +menubar > menu { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menu-menubar"); +} + +menubar > menu.menu-iconic { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menu-menubar-iconic"); +} + +menu { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menu"); +} + +menu.menu-iconic { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menu-iconic"); +} + +menubar > menu:empty { + visibility: collapse; +} + +/********* menuitem ***********/ + +menuitem { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem"); +} + +menuitem.menuitem-iconic { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic"); +} + +menuitem[description] { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic-desc-noaccel"); +} + +menuitem[type="checkbox"], +menuitem[type="radio"] { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic"); +} + +menuitem.menuitem-non-iconic { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menubutton-item"); +} + +menucaption { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menucaption"); +} + +.menu-text { + -moz-box-flex: 1; +} + +/********* menuseparator ***********/ + +menuseparator { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuseparator"); +} + +/********* popup & menupopup ***********/ + +/* <popup> is deprecated. Only <menupopup> and <tooltip> are still valid. */ + +popup, +menupopup { + -moz-binding: url("chrome://global/content/bindings/popup.xml#popup"); + -moz-box-orient: vertical; +} + +panel { + -moz-binding: url("chrome://global/content/bindings/popup.xml#panel"); + -moz-box-orient: vertical; +} + +popup, +menupopup, +panel, +tooltip { + display: -moz-popup; + z-index: 2147483647; + text-shadow: none; +} + +tooltip { + -moz-binding: url("chrome://global/content/bindings/popup.xml#tooltip"); + -moz-box-orient: vertical; + white-space: pre-wrap; + margin-top: 21px; +} + +panel[type="arrow"] { + -moz-binding: url("chrome://global/content/bindings/popup.xml#arrowpanel"); +} + +%ifndef MOZ_WIDGET_GTK + +panel[type="arrow"]:not([animate="false"]) { + transform: scale(.4); + opacity: 0; + transition-property: transform, opacity; + transition-duration: 0.15s; + transition-timing-function: ease-out; +} + +panel[type="arrow"][animate="open"] { + transform: none; + opacity: 1.0; +} + +panel[type="arrow"][animate="cancel"] { + transform: none; +} + +panel[arrowposition="after_start"]:-moz-locale-dir(ltr), +panel[arrowposition="after_end"]:-moz-locale-dir(rtl) { + transform-origin: 20px top; +} + +panel[arrowposition="after_end"]:-moz-locale-dir(ltr), +panel[arrowposition="after_start"]:-moz-locale-dir(rtl) { + transform-origin: calc(100% - 20px) top; +} + +panel[arrowposition="before_start"]:-moz-locale-dir(ltr), +panel[arrowposition="before_end"]:-moz-locale-dir(rtl) { + transform-origin: 20px bottom; +} + +panel[arrowposition="before_end"]:-moz-locale-dir(ltr), +panel[arrowposition="before_start"]:-moz-locale-dir(rtl) { + transform-origin: calc(100% - 20px) bottom; +} + +panel[arrowposition="start_before"]:-moz-locale-dir(ltr), +panel[arrowposition="end_before"]:-moz-locale-dir(rtl) { + transform-origin: right 20px; +} + +panel[arrowposition="start_after"]:-moz-locale-dir(ltr), +panel[arrowposition="end_after"]:-moz-locale-dir(rtl) { + transform-origin: right calc(100% - 20px); +} + +panel[arrowposition="end_before"]:-moz-locale-dir(ltr), +panel[arrowposition="start_before"]:-moz-locale-dir(rtl) { + transform-origin: left 20px; +} + +panel[arrowposition="end_after"]:-moz-locale-dir(ltr), +panel[arrowposition="start_after"]:-moz-locale-dir(rtl) { + transform-origin: left calc(100% - 20px); +} + +%endif + +%ifdef XP_MACOSX +.statusbar-resizerpanel { + display: none; +} +%else +window[sizemode="maximized"] statusbarpanel.statusbar-resizerpanel { + visibility: collapse; +} +%endif + +/******** grid **********/ + +grid { + display: -moz-grid; +} + +rows, +columns { + display: -moz-grid-group; +} + +row, +column { + display: -moz-grid-line; +} + +rows { + -moz-box-orient: vertical; +} + +column { + -moz-box-orient: vertical; +} + +/******** listbox **********/ + +listbox { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listbox"); +} + +listhead { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listhead"); +} + +listrows { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listrows"); +} + +listitem { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listitem"); +} + +listitem[type="checkbox"] { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listitem-checkbox"); +} + +listheader { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listheader"); + -moz-box-ordinal-group: 2147483646; +} + +listcell { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listcell"); +} + +listcell[type="checkbox"] { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listcell-checkbox"); +} + +.listitem-iconic { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listitem-iconic"); +} + +listitem[type="checkbox"].listitem-iconic { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listitem-checkbox-iconic"); +} + +.listcell-iconic { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listcell-iconic"); +} + +listcell[type="checkbox"].listcell-iconic { + -moz-binding: url("chrome://global/content/bindings/listbox.xml#listcell-checkbox-iconic"); +} + +listbox { + display: -moz-grid; +} + +listbox[rows] { + height: auto; +} + +listcols, listhead, listrows, listboxbody { + display: -moz-grid-group; +} + +listcol, listitem, listheaditem { + display: -moz-grid-line; +} + +listbox { + -moz-user-focus: normal; + -moz-box-orient: vertical; + min-width: 0px; + min-height: 0px; + width: 200px; + height: 200px; +} + +listhead { + -moz-box-orient: vertical; +} + +listrows { + -moz-box-orient: vertical; + -moz-box-flex: 1; +} + +listboxbody { + -moz-box-orient: vertical; + -moz-box-flex: 1; + /* Don't permit a horizontal scrollbar. See bug 285449 */ + overflow-x: hidden !important; + overflow-y: auto; + min-height: 0px; +} + +listcol { + -moz-box-orient: vertical; + min-width: 16px; +} + +listcell { + -moz-box-align: center; +} + +/******** tree ******/ + +tree { + -moz-binding: url("chrome://global/content/bindings/tree.xml#tree"); +} + +treecols { + -moz-binding: url("chrome://global/content/bindings/tree.xml#treecols"); +} + +treecol { + -moz-binding: url("chrome://global/content/bindings/tree.xml#treecol"); + -moz-box-ordinal-group: 2147483646; +} + +treecol.treecol-image { + -moz-binding: url("chrome://global/content/bindings/tree.xml#treecol-image"); +} + +tree > treechildren { + display: -moz-box; + -moz-binding: url("chrome://global/content/bindings/tree.xml#treebody"); + -moz-user-select: none; + -moz-box-flex: 1; +} + +treerows { + -moz-binding: url("chrome://global/content/bindings/tree.xml#treerows"); +} + +treecolpicker { + -moz-binding: url("chrome://global/content/bindings/tree.xml#columnpicker"); +} + +tree { + -moz-box-orient: vertical; + min-width: 0px; + min-height: 0px; + width: 10px; + height: 10px; +} + +tree[hidecolumnpicker="true"] > treecols > treecolpicker { + display: none; +} + +treecol { + min-width: 16px; +} + +treecol[hidden="true"] { + visibility: collapse; + display: -moz-box; +} + +.tree-scrollable-columns { + /* Yes, Virginia, this makes it scrollable */ + overflow: hidden; +} + +/* ::::: lines connecting cells ::::: */ +tree:not([treelines="true"]) > treechildren::-moz-tree-line { + visibility: hidden; +} + +treechildren::-moz-tree-cell(ltr) { + direction: ltr !important; +} + +/********** deck & stack *********/ + +deck { + display: -moz-deck; + -moz-binding: url("chrome://global/content/bindings/general.xml#deck"); +} + +stack, bulletinboard { + display: -moz-stack; +} + +/********** tabbox *********/ + +tabbox { + -moz-binding: url("chrome://global/content/bindings/tabbox.xml#tabbox"); + -moz-box-orient: vertical; +} + +tabs { + -moz-binding: url("chrome://global/content/bindings/tabbox.xml#tabs"); + -moz-box-orient: horizontal; +} + +tab { + -moz-binding: url("chrome://global/content/bindings/tabbox.xml#tab"); + -moz-box-align: center; + -moz-box-pack: center; +} + +tab[selected="true"]:not([ignorefocus="true"]) { + -moz-user-focus: normal; +} + +tabpanels { + -moz-binding: url("chrome://global/content/bindings/tabbox.xml#tabpanels"); + display: -moz-deck; +} + +/********** progressmeter **********/ + +progressmeter { + -moz-binding: url("chrome://global/content/bindings/progressmeter.xml#progressmeter"); +} + +/********** basic rule for anonymous content that needs to pass box properties through + ********** to an insertion point parent that holds the real kids **************/ + +.box-inherit { + -moz-box-orient: inherit; + -moz-box-pack: inherit; + -moz-box-align: inherit; + -moz-box-direction: inherit; +} + +/********** textbox **********/ + +textbox { + -moz-binding: url("chrome://global/content/bindings/textbox.xml#textbox"); + -moz-user-select: text; + text-shadow: none; +} + +textbox[multiline="true"] { + -moz-binding: url("chrome://global/content/bindings/textbox.xml#textarea"); +} + +.textbox-input-box { + -moz-binding: url("chrome://global/content/bindings/textbox.xml#input-box"); +} + +html|textarea.textbox-textarea { + resize: none; +} + +textbox[resizable="true"] > .textbox-input-box > html|textarea.textbox-textarea { + resize: both; +} + +.textbox-input-box[spellcheck="true"] { + -moz-binding: url("chrome://global/content/bindings/textbox.xml#input-box-spell"); +} + +textbox[type="timed"] { + -moz-binding: url("chrome://global/content/bindings/textbox.xml#timed-textbox"); +} + +textbox[type="search"] { + -moz-binding: url("chrome://global/content/bindings/textbox.xml#search-textbox"); +} + +textbox[type="number"] { + -moz-binding: url("chrome://global/content/bindings/numberbox.xml#numberbox"); +} + +.textbox-contextmenu:-moz-locale-dir(rtl) { + direction: rtl; +} + +/********** autocomplete textbox **********/ + +/* SeaMonkey does not use the new toolkit's autocomplete widget */ +%ifdef MOZ_SUITE + +textbox[type="autocomplete"] { + -moz-binding: url("chrome://global/content/autocomplete.xml#autocomplete"); +} + +panel[type="autocomplete"] { + -moz-binding: url("chrome://global/content/autocomplete.xml#autocomplete-result-popup"); +} + +.autocomplete-history-popup { + -moz-binding: url("chrome://global/content/autocomplete.xml#autocomplete-history-popup"); +} + +.autocomplete-treebody { + -moz-binding: url("chrome://global/content/autocomplete.xml#autocomplete-treebody"); +} + +.autocomplete-history-dropmarker { + -moz-binding: url("chrome://global/content/autocomplete.xml#history-dropmarker"); +} + +panel[type="autocomplete-richlistbox"] { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup"); +} + +.autocomplete-richlistbox { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistbox"); + -moz-user-focus: ignore; +} + +.autocomplete-richlistbox > scrollbox { + overflow-x: hidden !important; +} + +.autocomplete-richlistitem { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem"); + -moz-box-orient: vertical; + overflow: -moz-hidden-unscrollable; +} + +%else + +textbox[type="autocomplete"] { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete"); +} + +panel[type="autocomplete"] { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-result-popup"); +} + +panel[type="autocomplete-richlistbox"] { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup"); +} + +/* FIXME: bug 616258 */ + +.autocomplete-tree { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-tree"); + -moz-user-focus: ignore; +} + +.autocomplete-treebody { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-treebody"); +} + +.autocomplete-richlistbox { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistbox"); + -moz-user-focus: ignore; +} + +.autocomplete-richlistbox > scrollbox { + overflow-x: hidden !important; +} + +.autocomplete-richlistitem { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem"); + -moz-box-orient: vertical; + overflow: -moz-hidden-unscrollable; +} + +.autocomplete-treerows { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-treerows"); +} + +.autocomplete-history-dropmarker { + display: none; +} + +.autocomplete-history-dropmarker[enablehistory="true"] { + display: -moz-box; + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#history-dropmarker"); +} + +%endif + +/* the C++ implementation of widgets is too eager to make popups visible. + this causes problems (bug 120155 and others), thus this workaround: */ +popup[type="autocomplete"][hidden="true"] { + visibility: hidden; +} + +/* The following rule is here to fix bug 96899 (and now 117952). + Somehow trees create a situation + in which a popupset flows itself as if its popup child is directly within it + instead of the placeholder child that should actually be inside the popupset. + This is a stopgap measure, and it does not address the real bug. */ +.autocomplete-result-popupset { + max-width: 0px; + width: 0 !important; + min-width: 0%; + min-height: 0%; +} + +/********** colorpicker **********/ + +colorpicker { + -moz-binding: url("chrome://global/content/bindings/colorpicker.xml#colorpicker"); +} + +colorpicker[type="button"] { + -moz-binding: url("chrome://global/content/bindings/colorpicker.xml#colorpicker-button"); +} + +.colorpickertile { + -moz-binding: url("chrome://global/content/bindings/colorpicker.xml#colorpickertile"); +} + +/********** menulist **********/ + +menulist { + -moz-binding: url("chrome://global/content/bindings/menulist.xml#menulist"); +} + +menulist[popuponly="true"] { + -moz-binding: url("chrome://global/content/bindings/menulist.xml#menulist-popuponly"); + -moz-appearance: none !important; + margin: 0 !important; + height: 0 !important; + border: 0 !important; +} + +menulist[editable="true"] { + -moz-binding: url("chrome://global/content/bindings/menulist.xml#menulist-editable"); +} + +menulist[type="description"] { + -moz-binding: url("chrome://global/content/bindings/menulist.xml#menulist-description"); +} + +menulist > menupopup > menuitem { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic-noaccel"); +} + +menulist > menupopup > menucaption { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menucaption-inmenulist"); +} + +dropmarker { + -moz-binding: url("chrome://global/content/bindings/general.xml#dropmarker"); +} + +/********** splitter **********/ + +splitter { + -moz-binding: url("chrome://global/content/bindings/splitter.xml#splitter"); +} + +grippy { + -moz-binding: url("chrome://global/content/bindings/splitter.xml#grippy"); +} + +.tree-splitter { + width: 0px; + max-width: 0px; + min-width: 0% ! important; + min-height: 0% ! important; + -moz-box-ordinal-group: 2147483646; +} + +/******** scrollbar ********/ + +slider { + /* This is a hint to layerization that the scrollbar thumb can never leave + the scrollbar track. */ + overflow: hidden; +} + +/******** scrollbox ********/ + +scrollbox { + -moz-binding: url("chrome://global/content/bindings/scrollbox.xml#scrollbox"); + /* This makes it scrollable! */ + overflow: hidden; +} + +arrowscrollbox { + -moz-binding: url("chrome://global/content/bindings/scrollbox.xml#arrowscrollbox"); +} + +arrowscrollbox[clicktoscroll="true"] { + -moz-binding: url("chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll"); +} + +autorepeatbutton { + -moz-binding: url("chrome://global/content/bindings/scrollbox.xml#autorepeatbutton"); +} + +/********** statusbar **********/ + +statusbar { + -moz-binding: url("chrome://global/content/bindings/general.xml#statusbar"); +%ifdef XP_MACOSX + padding-right: 14px; +%endif +} + +statusbarpanel { + -moz-binding: url("chrome://global/content/bindings/general.xml#statusbarpanel"); +} + +.statusbarpanel-iconic { + -moz-binding: url("chrome://global/content/bindings/general.xml#statusbarpanel-iconic"); +} + +.statusbarpanel-iconic-text { + -moz-binding: url("chrome://global/content/bindings/general.xml#statusbarpanel-iconic-text"); +} + +.statusbarpanel-menu-iconic { + -moz-binding: url("chrome://global/content/bindings/general.xml#statusbarpanel-menu-iconic"); +} + +/********** spinbuttons ***********/ + +spinbuttons { + -moz-binding: url("chrome://global/content/bindings/spinbuttons.xml#spinbuttons"); +} + +.spinbuttons-button { + -moz-user-focus: ignore; +} + +/********** stringbundle **********/ + +stringbundleset { + -moz-binding: url("chrome://global/content/bindings/stringbundle.xml#stringbundleset"); + visibility: collapse; +} + +stringbundle { + -moz-binding: url("chrome://global/content/bindings/stringbundle.xml#stringbundle"); + visibility: collapse; +} + +/********** dialog **********/ + +dialog, +dialog:root /* override :root from above */ { + -moz-binding: url("chrome://global/content/bindings/dialog.xml#dialog"); + -moz-box-orient: vertical; +} + +dialogheader { + -moz-binding: url("chrome://global/content/bindings/dialog.xml#dialogheader"); +} + +/********* page ************/ + +page { + -moz-box-orient: vertical; +} + +/********** wizard **********/ + +wizard, +wizard:root /* override :root from above */ { + -moz-binding: url("chrome://global/content/bindings/wizard.xml#wizard"); + -moz-box-orient: vertical; + width: 40em; + height: 30em; +} + +wizardpage { + -moz-binding: url("chrome://global/content/bindings/wizard.xml#wizardpage"); + -moz-box-orient: vertical; + overflow: auto; +} + +.wizard-header { + -moz-binding: url("chrome://global/content/bindings/wizard.xml#wizard-header"); +} + +.wizard-buttons { + -moz-binding: url("chrome://global/content/bindings/wizard.xml#wizard-buttons"); +} + +/********** preferences ********/ + +prefwindow, +prefwindow:root /* override :root from above */ { + -moz-binding: url("chrome://global/content/bindings/preferences.xml#prefwindow"); + -moz-box-orient: vertical; +} + +prefpane { + -moz-binding: url("chrome://global/content/bindings/preferences.xml#prefpane"); + -moz-box-orient: vertical; +} + +prefwindow > .paneDeckContainer { + overflow: hidden; +} + +prefpane > .content-box { + overflow: hidden; +} + +prefwindow[type="child"] > .paneDeckContainer { + overflow: -moz-hidden-unscrollable; +} + +prefwindow[type="child"] > prefpane > .content-box { + -moz-box-flex: 1; + overflow: -moz-hidden-unscrollable; +} + +preferences { + -moz-binding: url("chrome://global/content/bindings/preferences.xml#preferences"); + visibility: collapse; +} + +preference { + -moz-binding: url("chrome://global/content/bindings/preferences.xml#preference"); + visibility: collapse; +} + +radio[pane] { + -moz-binding: url("chrome://global/content/bindings/preferences.xml#panebutton") !important; + -moz-box-orient: vertical; + -moz-box-align: center; +} + +prefwindow[chromehidden~="toolbar"] .chromeclass-toolbar { + display: none; +} + +/********** expander ********/ + +expander { + -moz-binding: url("chrome://global/content/bindings/expander.xml#expander"); + -moz-box-orient: vertical; +} + + +/********** Rich Listbox ********/ + +richlistbox { + -moz-binding: url('chrome://global/content/bindings/richlistbox.xml#richlistbox'); + -moz-user-focus: normal; + -moz-box-orient: vertical; +} + +richlistitem { + -moz-binding: url('chrome://global/content/bindings/richlistbox.xml#richlistitem'); +} + +richlistbox > listheader { + -moz-box-ordinal-group: 1; +} + +/********** datepicker and timepicker ********/ + +datepicker { + -moz-binding: url('chrome://global/content/bindings/datetimepicker.xml#datepicker'); +} + +datepicker[type="popup"] { + -moz-binding: url('chrome://global/content/bindings/datetimepicker.xml#datepicker-popup'); +} + +datepicker[type="grid"] { + -moz-binding: url('chrome://global/content/bindings/datetimepicker.xml#datepicker-grid'); +} + +timepicker { + -moz-binding: url('chrome://global/content/bindings/datetimepicker.xml#timepicker'); +} + + +/*********** findbar ************/ +findbar { + -moz-binding: url('chrome://global/content/bindings/findbar.xml#findbar'); +} + +.findbar-textbox { + -moz-binding: url("chrome://global/content/bindings/findbar.xml#findbar-textbox"); +} + + +/*********** filefield ************/ +filefield { + -moz-binding: url("chrome://global/content/bindings/filefield.xml#filefield"); +} + +/*********** tabmodalprompt ************/ +tabmodalprompt { + -moz-binding: url("chrome://global/content/tabprompts.xml#tabmodalprompt"); + overflow: hidden; + text-shadow: none; +} |