diff options
Diffstat (limited to 'browser/base/content')
73 files changed, 33981 insertions, 0 deletions
diff --git a/browser/base/content/aboutDialog.css b/browser/base/content/aboutDialog.css new file mode 100644 index 000000000..d96eba5a2 --- /dev/null +++ b/browser/base/content/aboutDialog.css @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#aboutPMDialogContainer { + width: 700px; + height: 410px; +} + +#aboutVersionBox { + font-family: Arial, helvetica; + height: 38 px; +} + +#aboutVersion { + text-align: center; + font-size: 18px; + font-weight: bold; + margin: 4px; +} + +#distribution, +#distributionId { + text-align: center; + font-size: 14px; + font-weight: bold; + display: none; + margin-top: 0; + margin-bottom: 0; +} + +#aboutTextBox { + font-family: Arial, helvetica; + font-size: 14px; + margin: 5px 20px; + padding: 4px 10px 0px; + border-radius: 3px; + color: black; + background-color: rgba(240, 240, 240, .6); +} + +#aboutLinkBox { + font-family: Arial, helvetica; +} + +.text-credits { + margin: 5px 0px; +} + +.text-center { + text-align: center; +} + +.text-link, +.text-link:focus { + margin: 0px; + padding: 0px; +} + +.bottom-link, +.bottom-link:focus { + text-align: center; + text-decoration: none !important; + padding: 4px; + border-radius: 3px; + color: #244C8A; + background-color: rgba(240, 240, 240, .7); + margin: 0 40px; + transition: background-color 0.5s ease-out; +} + +.bottom-link:hover { + background-color: rgba(240, 240, 255, .95); +} diff --git a/browser/base/content/aboutDialog.js b/browser/base/content/aboutDialog.js new file mode 100644 index 000000000..7568de726 --- /dev/null +++ b/browser/base/content/aboutDialog.js @@ -0,0 +1,55 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Services = object with smart getters for common XPCOM services +Components.utils.import("resource://gre/modules/Services.jsm"); + +function init(aEvent) +{ + if (aEvent.target != document) { + return; + } + + try { + var distroId = Services.prefs.getCharPref("distribution.id"); + if (distroId) { + var distroVersion = Services.prefs.getCharPref("distribution.version"); + + var distroIdField = document.getElementById("distributionId"); + distroIdField.value = distroId + " - " + distroVersion; + distroIdField.style.display = "block"; + + try { + // This is in its own try catch due to bug 895473 and bug 900925. + var distroAbout = Services.prefs.getComplexValue("distribution.about", + Components.interfaces.nsISupportsString); + var distroField = document.getElementById("distribution"); + distroField.value = distroAbout; + distroField.style.display = "block"; + } catch (ex) { + // Pref is unset + Components.utils.reportError(ex); + } + } + } catch(e) { + // Pref is unset + } + + // Include the build ID if this is an "a#" or "b#" build + let version = Services.appinfo.version; + if (/[ab]\d+$/.test(version)) { + let buildID = Services.appinfo.appBuildID; + let buildDate = buildID.slice(0,4) + "-" + buildID.slice(4,6) + "-" + buildID.slice(6,8); + document.getElementById("aboutVersion").textContent += " (" + buildDate + ")"; + } + +// get release notes 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); + } +} diff --git a/browser/base/content/aboutDialog.xul b/browser/base/content/aboutDialog.xul new file mode 100644 index 000000000..e4e9db15e --- /dev/null +++ b/browser/base/content/aboutDialog.xul @@ -0,0 +1,77 @@ +<?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"?> +<?xml-stylesheet href="chrome://browser/content/aboutDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://branding/content/aboutDialog.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % aboutDialogDTD SYSTEM "chrome://browser/locale/aboutDialog.dtd" > +%aboutDialogDTD; +]> + +<window xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="PMaboutDialog" + windowtype="Browser:About" + onload="init(event);" + title="&aboutDialog.title;" + role="dialog" + aria-describedby="version distribution distributionId communityDesc contributeDesc trademark" + > + + <script type="application/javascript" src="chrome://browser/content/aboutDialog.js"/> + + <vbox id="aboutPMDialogContainer" flex="1"> + <vbox id="aboutHeaderBox" /> + <vbox id="aboutVersionBox" flex="3"> +#ifdef HAVE_64BIT_BUILD +#expand <label id="aboutVersion">Version: __MOZ_APP_VERSION__ (64-bit)</label> +#else +#expand <label id="aboutVersion">Version: __MOZ_APP_VERSION__ (32-bit)</label> +#endif + <label id="distribution" class="text-blurb"/> + <label id="distributionId" class="text-blurb"/> + + </vbox> + <vbox id="aboutTextBox" flex="1"> + <description class="text-credits text-center"> +#if defined(MOZ_OFFICIAL_BRANDING) || defined(MC_OFFICIAL) +#ifdef MC_PRIVATE_BUILD + This is a private build of Pale Moon. If you did not manually build this copy from source yourself, then please download an official version from the <label class="text-link" href="http://www.palemoon.org/">Pale Moon website</label>. +#else + <label class="text-link" href="http://www.palemoon.org">Pale Moon</label> is released by <label class="text-link" href="http://www.moonchildproductions.info">Moonchild Productions</label>. + </description> + <description class="text-credits text-center"> + Special thanks to all our supporters and donors for making this browser possible! + </description> + <description class="text-credits"> + If you wish to contribute, please consider helping out by providing support to other users on the <label class="text-link" href="https://forum.palemoon.org/">Pale Moon forum</label>. +#endif +#else + &brandFullName; is released by &vendorShortName;. + </description> + <description class="text-credits"> + This is an unofficial build of Pale Moon. For official builds, please go to <label class="text-link" href="http://www.palemoon.org/">the Pale Moon website</label>. +#endif + </description> + </vbox> + <vbox id="aboutLinkBox"> + <hbox pack="center"> + <label class="text-link bottom-link" href="about:rights">End-user rights</label> + <label class="text-link bottom-link" href="about:license">Licensing information</label> + <label class="text-link bottom-link" id="releaseNotesURL">Release notes</label> + </hbox> + <description id="aboutPMtrademark">&trademarkInfo.part1;</description> + </vbox> + </vbox> + + <keyset> + <key keycode="VK_ESCAPE" oncommand="window.close();"/> + </keyset> +</window> diff --git a/browser/base/content/autocomplete.css b/browser/base/content/autocomplete.css new file mode 100644 index 000000000..960bdc456 --- /dev/null +++ b/browser/base/content/autocomplete.css @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 > .ac-title-box > .ac-title > .ac-comment:not([selected]) > html|span.ac-selected-text { + display: none; +} diff --git a/browser/base/content/autocomplete.xml b/browser/base/content/autocomplete.xml new file mode 100644 index 000000000..b3250199d --- /dev/null +++ b/browser/base/content/autocomplete.xml @@ -0,0 +1,2112 @@ +<?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="privateAutocompleteBindings" + 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="private-autocomplete" role="xul:combobox" + extends="chrome://global/content/bindings/textbox.xml#textbox"> + <resources> + <stylesheet src="chrome://browser/content/autocomplete.css"/> + <stylesheet src="chrome://browser/skin/autocomplete.css"/> + </resources> + + <content sizetopopup="pref"> + <xul:hbox class="private-autocomplete-textbox-container" flex="1" xbl:inherits="focused"> + <children includes="image|deck|stack|box"> + <xul:image class="private-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="private-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="private-autocomplete-history-dropmarker" + allowevents="true" + xbl:inherits="open,enablehistory,parentfocused=focused"/> + + <xul:popupset anonid="popupset" class="private-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="mEnterEvent">null</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"><![CDATA[ + // Wrap in a block so that the let statements don't + // create properties on 'this' (bug 635252). + { + 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; + popup; + } + ]]></field> + + <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> + + <field name="PrivateBrowsingUtils" readonly="true"> + { + let utils = {}; + Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", utils); + utils.PrivateBrowsingUtils + } + </field> + + <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> + + <property name="textValue" + onget="return this.value;"> + <setter><![CDATA[ + // Completing a result should simulate the user typing the result, + // so fire an input event. + // Trim popup selected values, but never trim results coming from + // autofill. + if (this.popup.selectedIndex == -1) + this._disableTrim = true; + this.value = val; + this._disableTrim = false; + + var 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"> + <body><![CDATA[ + let rv = false; + if (this._textEnteredHandler) + rv = this._textEnteredHandler(this.mEnterEvent); + this.mEnterEvent = null; + 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. + let metaKey = 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: + this.mEnterEvent = aEvent; + if (this.mController.selection) { + this._selectionDetails = { + index: this.mController.selection.currentIndex, + kind: "key" + }; + } + cancel = this.handleEnter(); + break; + case KeyEvent.DOM_VK_DELETE: + cancel = this.handleDelete(); + break; + case KeyEvent.DOM_VK_BACK_SPACE: + break; + case KeyEvent.DOM_VK_DOWN: + case KeyEvent.DOM_VK_UP: + if (aEvent.altKey) + this.toggleHistoryPopup(); + break; + case KeyEvent.DOM_VK_F4: + this.toggleHistoryPopup(); + break; + } + + if (cancel) { + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + + return true; + ]]></body> + </method> + + <method name="handleEnter"> + <body><![CDATA[ + return this.mController.handleEnter(false); + ]]></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" + action="this.attachController();"/> + + <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="private-autocomplete-result-popup" extends="chrome://browser/content/autocomplete.xml#private-autocomplete-base-popup"> + <resources> + <stylesheet src="chrome://browser/content/autocomplete.css"/> + <stylesheet src="chrome://global/skin/tree.css"/> + <stylesheet src="chrome://browser/skin/autocomplete.css"/> + </resources> + + <content ignorekeys="true" level="top" consumeoutsideclicks="never"> + <xul:tree anonid="tree" class="private-autocomplete-tree plain" hidecolumnpicker="true" flex="1" seltype="single"> + <xul:treecols anonid="treecols"> + <xul:treecol id="treecolAutoCompleteValue" class="private-autocomplete-treecol" flex="1" overflow="true"/> + </xul:treecols> + <xul:treechildren class="private-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", "private-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="private-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); + ]]></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="private-autocomplete-rich-result-popup" extends="chrome://browser/content/autocomplete.xml#private-autocomplete-base-popup"> + <resources> + <stylesheet src="chrome://browser/content/autocomplete.css"/> + <stylesheet src="chrome://browser/skin/autocomplete.css"/> + </resources> + + <content ignorekeys="true" level="top" consumeoutsideclicks="never"> + <xul:richlistbox anonid="richlistbox" class="private-autocomplete-richlistbox" flex="1"/> + <xul:hbox> + <children/> + </xul:hbox> + </content> + + <implementation implements="nsIAutoCompletePopup"> + <field name="_currentIndex">0</field> + <field name="_rowHeight">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; + ]]></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; + // 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); + } + this._adjustHeightTimeout = setTimeout(() => this.adjustHeight(), 0); + + 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) { + if (!this._rowHeight) { + let firstRowRect = rows[0].getBoundingClientRect(); + this._rowHeight = firstRowRect.height; + + let transition = + window.getComputedStyle(this.richlistbox).transitionProperty; + this._rlbAnimated = transition && transition != "none"; + + // Set a fixed max-height to avoid flicker when growing the panel. + this.richlistbox.style.maxHeight = (this._rowHeight * this.maxRows) + "px"; + } + + // Calculate the height to have the first row to last row shown + height = this._rowHeight * numRows; + } + + 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]; + + // 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)) { + item.collapsed = false; + this._currentIndex++; + continue; + } + } + else { + // need to create a new item + item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem"); + } + + // 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("type", 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 = "private-autocomplete-richlistitem"; + this.richlistbox.appendChild(item); + } + + 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> + + <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> + </binding> + + <binding id="private-autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:hbox align="center" class="ac-title-box"> + <xul:image xbl:inherits="src=image" class="ac-site-icon"/> + <xul:hbox anonid="title-box" class="ac-title" flex="1" + onunderflow="_doUnderflow('_title');" + onoverflow="_doOverflow('_title');"> + <xul:description anonid="title" class="ac-normal-text ac-comment" xbl:inherits="selected"/> + </xul:hbox> + <xul:label anonid="title-overflow-ellipsis" xbl:inherits="selected" + class="ac-ellipsis-after ac-comment"/> + <xul:hbox anonid="extra-box" class="ac-extra" align="center" hidden="true"> + <xul:image class="ac-result-type-tag"/> + <xul:label class="ac-normal-text ac-comment" xbl:inherits="selected" value=":"/> + <xul:description anonid="extra" class="ac-normal-text ac-comment" xbl:inherits="selected"/> + </xul:hbox> + <xul:image anonid="type-image" class="ac-type-icon" xbl:inherits="selected"/> + </xul:hbox> + <xul:hbox align="center" class="ac-url-box"> + <xul:spacer class="ac-site-icon"/> + <xul:image class="ac-action-icon"/> + <xul:hbox anonid="url-box" class="ac-url" flex="1" + onunderflow="_doUnderflow('_url');" + onoverflow="_doOverflow('_url');"> + <xul:description anonid="url" class="ac-normal-text ac-url-text" + xbl:inherits="selected type"/> + <xul:description anonid="action" class="ac-normal-text ac-action-text" + xbl:inherits="selected type"/> + </xul:hbox> + <xul:label anonid="url-overflow-ellipsis" xbl:inherits="selected" + class="ac-ellipsis-after ac-url-text"/> + <xul:spacer class="ac-type-icon"/> + </xul:hbox> + </content> + <implementation implements="nsIDOMXULSelectControlItemElement"> + <constructor> + <![CDATA[ + let ellipsis = "\u2026"; + try { + ellipsis = Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch). + getComplexValue("intl.ellipsis", + Components.interfaces.nsIPrefLocalizedString).data; + } catch (ex) { + // Do nothing.. we already have a default + } + + this._urlOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "url-overflow-ellipsis"); + this._titleOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "title-overflow-ellipsis"); + + this._urlOverflowEllipsis.value = ellipsis; + this._titleOverflowEllipsis.value = ellipsis; + + this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image"); + + this._urlBox = document.getAnonymousElementByAttribute(this, "anonid", "url-box"); + this._url = document.getAnonymousElementByAttribute(this, "anonid", "url"); + this._action = document.getAnonymousElementByAttribute(this, "anonid", "action"); + + this._titleBox = document.getAnonymousElementByAttribute(this, "anonid", "title-box"); + this._title = document.getAnonymousElementByAttribute(this, "anonid", "title"); + + this._extraBox = document.getAnonymousElementByAttribute(this, "anonid", "extra-box"); + this._extra = document.getAnonymousElementByAttribute(this, "anonid", "extra"); + + 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> + + <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 + 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); + + 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 text = aText.substr(start, next - start); + start = next; + + if (i % 2 == 0) { + // Emphasize the text for even indices + let span = aDescriptionElement.appendChild( + document.createElementNS("http://www.w3.org/1999/xhtml", "span")); + span.className = "ac-emphasize-text"; + span.textContent = text; + } else { + // Otherwise, it's plain text + aDescriptionElement.appendChild(document.createTextNode(text)); + } + } + ]]> + </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": + span.className = "ac-emphasize-text"; + break; + case "selected": + span.className = "ac-selected-text"; + 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 originalUrl = this.getAttribute("url"); + let title = this.getAttribute("title"); + let type = this.getAttribute("type"); + + let displayUrl; + let emphasiseTitle = true; + let emphasiseUrl = true; + + // Hide the title's extra box by default, until we find out later if + // we need extra stuff. + this._extraBox.hidden = true; + this._titleBox.flex = 1; + this._typeImage.hidden = false; + + this.removeAttribute("actiontype"); + this.classList.remove("overridable-action"); + + // The ellipses are hidden via their visibility so that they always + // take up space and don't pop in on top of text when shown. For + // keyword searches, however, the title ellipsis should not take up + // space when hidden. Setting the hidden property accomplishes that. + this._titleOverflowEllipsis.hidden = false; + + let types = new Set(type.split(/\s+/)); + + // Remove types that should ultimately not be in the `type` string. + let initialTypes = new Set(types); + types.delete("action"); + types.delete("autofill"); + types.delete("heuristic"); + types.delete("search"); + + type = [...types][0] || ""; + + // If the type includes an action, set up the item appropriately. + if (initialTypes.has("action")) { + let 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("switchToTab"); + this._setUpDescription(this._action, desc, true); + } else if (action.type == "remotetab") { + displayUrl = this._unescapeUrl(action.params.url); + let desc = action.params.deviceName; + this._setUpDescription(this._action, 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; + let engineStr = " - " + + this._stringBundle.formatStringFromName("searchWithEngine", + [engineName], 1); + + // 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, ""], + ]; + } + pairs.push([engineStr, "selected"]); + 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) { + this.removeAttribute("image"); + } + } else if (action.type == "visiturl") { + emphasiseUrl = false; + displayUrl = this._unescapeUrl(action.params.url); + let sourceStr = this._stringBundle.GetStringFromName("visitURL"); + title = this._generateEmphasisPairs(sourceStr, [ + [displayUrl, "match"], + ]); + } + } + + // Check if we have a search engine name + if (initialTypes.has("search")) { + emphasiseUrl = false; + + const TITLE_SEARCH_ENGINE_SEPARATOR = " \u00B7\u2013\u00B7 "; + + let searchEngine = ""; + [title, searchEngine] = title.split(TITLE_SEARCH_ENGINE_SEPARATOR); + displayUrl = this._stringBundle.formatStringFromName("searchWithEngine", [searchEngine], 1); + } + + if (!displayUrl) { + let input = this.parentNode.parentNode.input; + let url = typeof(input.trimValue) == "function" ? + input.trimValue(originalUrl) : + originalUrl; + displayUrl = this._unescapeUrl(url); + } + this.setAttribute("displayurl", displayUrl); + + // Check if we have an auto-fill URL + if (initialTypes.has("autofill")) { + emphasiseUrl = false; + + let sourceStr = this._stringBundle.GetStringFromName("visitURL"); + title = this._generateEmphasisPairs(sourceStr, [ + [displayUrl, "match"], + ]); + } + + // If we have a tag match, show the tags and icon + if (type == "tag" || type == "bookmark-tag") { + // Configure the extra box for tags display + this._extraBox.hidden = false; + this._extraBox.childNodes[0].hidden = false; + this._extraBox.childNodes[1].hidden = true; + this._extraBox.pack = "end"; + this._titleBox.flex = 1; + + // 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(",").sort().join(", "); + + // Emphasize the matching text in the tags + this._setUpDescription(this._extra, sortedTags); + + // If we're suggesting bookmarks, then treat tagged matches as + // bookmarks for the star. + if (type == "bookmark-tag") { + type = "bookmark"; + } else { + this._typeImage.hidden = true; + } + // keyword and favicon type results for search engines + // have an extra magnifying glass icon after them + } else if (type == "keyword" || (initialTypes.has("search") && + initialTypes.has("favicon"))) { + // Configure the extra box for keyword display + this._extraBox.hidden = false; + this._extraBox.childNodes[0].hidden = true; + // The second child node is ":" and it should be hidden for non keyword types + this._extraBox.childNodes[1].hidden = type == "keyword" ? false : true; + this._extraBox.pack = "start"; + this._titleBox.flex = 0; + + // Hide the ellipsis so it doesn't take up space. + this._titleOverflowEllipsis.hidden = true; + + if (type == "keyword") { + // Put the parameters next to the title if we have any + let search = this.getAttribute("text"); + let params = ""; + let paramsIndex = search.indexOf(" "); + if (paramsIndex != -1) + params = search.substr(paramsIndex + 1); + + // Emphasize the keyword parameters + this._setUpDescription(this._extra, params); + + // Don't emphasize keyword searches in the title or url + emphasiseUrl = false; + emphasiseTitle = false; + } else { + // Don't show any description for non keyword types. + this._setUpDescription(this._extra, "", true); + } + // If the result has the type favicon and a known search provider, + // customize it the same way as a keyword result. + type = "keyword"; + } + + // Give the image the icon style and a special one for the type + this._typeImage.className = "ac-type-icon" + + (type ? " ac-result-type-" + type : ""); + + // Show the domain as the title if we don't have a title. + if (title == "") { + title = displayUrl; + try { + let uri = Services.io.newURI(originalUrl, null, null); + // Not all valid URLs have a domain. + if (uri.host) + title = uri.host; + } catch (e) {} + } + + // Emphasize the matching search terms for the description + if (Array.isArray(title)) + this._setUpEmphasisedSections(this._title, title); + else + this._setUpDescription(this._title, title, !emphasiseTitle); + + this._setUpDescription(this._url, displayUrl, !emphasiseUrl); + + // Set up overflow on a timeout because the contents of the box + // might not have a width yet even though we just changed them + setTimeout(this._setUpOverflow, 0, this._titleBox, this._titleOverflowEllipsis); + setTimeout(this._setUpOverflow, 0, this._urlBox, this._urlOverflowEllipsis); + ]]> + </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 will happen when + // UnifiedComplete is disabled - in which case, the param is always + // a URL. + action.params = { + url: params, + } + } + + return action; + ]]></body> + </method> + + <method name="_setUpOverflow"> + <parameter name="aParentBox"/> + <parameter name="aEllipsis"/> + <body> + <![CDATA[ + // Hide the ellipsis incase there's just enough to not underflow + aEllipsis.style.visibility = "hidden"; + + // Start with the parent's width and subtract off its children + let tooltip = []; + let children = aParentBox.childNodes; + let widthDiff = aParentBox.boxObject.width; + + for (let i = 0; i < children.length; i++) { + // Only consider a child if it actually takes up space + let childWidth = children[i].boxObject.width; + if (childWidth > 0) { + // Subtract a little less to account for subpixel rounding + widthDiff -= childWidth - .5; + + // Add to the tooltip if it's not hidden and has text + let childText = children[i].textContent; + if (childText) + tooltip.push(childText); + } + } + + // If the children take up more space than the parent.. overflow! + if (widthDiff < 0) { + // Re-show the ellipsis now that we know it's needed + aEllipsis.style.visibility = "visible"; + + // Separate text components with a ndash -- + aParentBox.tooltipText = tooltip.join(" \u2013 "); + } + ]]> + </body> + </method> + + <method name="_doUnderflow"> + <parameter name="aName"/> + <body> + <![CDATA[ + // Hide the ellipsis right when we know we're underflowing instead of + // waiting for the timeout to trigger the _setUpOverflow calculations + this[aName + "Box"].tooltipText = ""; + this[aName + "OverflowEllipsis"].style.visibility = "hidden"; + ]]> + </body> + </method> + + <method name="_doOverflow"> + <parameter name="aName"/> + <body> + <![CDATA[ + this._setUpOverflow(this[aName + "Box"], + this[aName + "OverflowEllipsis"]); + ]]> + </body> + </method> + + </implementation> + </binding> + + <binding id="private-autocomplete-tree" extends="chrome://global/content/bindings/tree.xml#tree"> + <content> + <children includes="treecols"/> + <xul:treerows class="private-autocomplete-treerows tree-rows" xbl:inherits="hidescrollbar" flex="1"> + <children/> + </xul:treerows> + </content> + </binding> + + <binding id="private-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="private-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="private-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="private-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/browser/base/content/autorecovery.js b/browser/base/content/autorecovery.js new file mode 100644 index 000000000..78c7a5bd8 --- /dev/null +++ b/browser/base/content/autorecovery.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Auto-recovery module. + * This module aims to catch fatal browser initialization errors and either + * automatically correct likely causes from them, or automatically restarting + * the browser in safe mode. This is hooked into the browser's "onload" + * event because it can be assumed that at that point, everything must + * have been properly initialized already. + */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +// Services = object with smart getters for common XPCOM services +Cu.import("resource://gre/modules/Services.jsm"); + +var browser_autoRecovery = { + onLoad: function() { + var nsIAS = Ci.nsIAppStartup; // Application startup interface + + if (typeof gBrowser === "undefined") { + // gBrowser should always be defined at this point, but if it is not, then most likely + // it is due to an incompatible or outdated language pack being installed and selected. + // In this case, we reset "general.useragent.locale" to try to recover browser startup. + if (Services.prefs.prefHasUserValue("general.useragent.locale")) { + // Restart automatically in en-US. + Services.prefs.clearUserPref("general.useragent.locale"); + Cc["@mozilla.org/toolkit/app-startup;1"].getService(nsIAS).quit(nsIAS.eRestart | nsIAS.eAttemptQuit); + } else if (!Services.appinfo.inSafeMode) { + // gBrowser isn't defined, and we're not using a custom locale. Most likely + // a user-installed add-on causes issues here, so we restart in Safe Mode. + let RISM = Services.prompt.confirm(null, "Error", + "The Browser didn't start properly!\n"+ + "This is usually caused by an add-on or misconfiguration.\n\n"+ + "Restart in Safe Mode?"); + if (RISM) { + Cc["@mozilla.org/toolkit/app-startup;1"].getService(nsIAS).restartInSafeMode(nsIAS.eRestart | nsIAS.eAttemptQuit); + } else { + // Force quit application + Cc["@mozilla.org/toolkit/app-startup;1"].getService(nsIAS).quit(nsIAS.eForceQuit); + } + } + // Something else caused this issue and we're already in Safe Mode, so we return + // without doing anything else, and let normal error handling take place. + return; + } // gBrowser undefined + + // Other checks than gBrowser undefined can go here! + + // Remove our listener, since we don't want this to fire on every load. + window.removeEventListener("load", browser_autoRecovery.onLoad, false); + } +}; + +window.addEventListener("load", browser_autoRecovery.onLoad, false); diff --git a/browser/base/content/autorecovery.xul b/browser/base/content/autorecovery.xul new file mode 100644 index 000000000..866bdf288 --- /dev/null +++ b/browser/base/content/autorecovery.xul @@ -0,0 +1,12 @@ +<?xml version="1.0"?> + +<overlay + id="autorecovery" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/x-javascript" src="chrome://browser/content/autorecovery.js"/> + +<!-- This is an empty overlay to allow separation of the script into its + own context (needed for locale issues preventing browser start) --> + +</overlay> diff --git a/browser/base/content/baseMenuOverlay.xul b/browser/base/content/baseMenuOverlay.xul new file mode 100644 index 000000000..9922ca880 --- /dev/null +++ b/browser/base/content/baseMenuOverlay.xul @@ -0,0 +1,78 @@ +<?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 overlay [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % baseMenuOverlayDTD SYSTEM "chrome://browser/locale/baseMenuOverlay.dtd"> +%baseMenuOverlayDTD; +]> +<overlay id="baseMenuOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + +#ifdef XP_WIN + <menu id="helpMenu" + label="&helpMenuWin.label;" + accesskey="&helpMenuWin.accesskey;"> +#else + <menu id="helpMenu" + label="&helpMenu.label;" + accesskey="&helpMenu.accesskey;"> +#endif + <menupopup id="menu_HelpPopup" onpopupshowing="buildHelpMenu();"> + <menuitem id="menu_openHelp" + oncommand="openHelpLink('pale-moon-help')" + onclick="checkForMiddleClick(this, event);" + label="&productHelp.label;" + accesskey="&productHelp.accesskey;" + /> + <menuitem id="troubleShooting" + accesskey="&helpTroubleshootingInfo.accesskey;" + label="&helpTroubleshootingInfo.label;" + oncommand="openTroubleshootingPage()" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="helpSafeMode" + accesskey="&helpSafeMode.accesskey;" + label="&helpSafeMode.label;" + oncommand="restart(true);"/> + <menuseparator/> + <menuitem id="releaseNotes" + accesskey="&helpReleaseNotes.accesskey;" + label="&helpReleaseNotes.label;" + oncommand="openReleaseNotes();" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="feedbackPage" + accesskey="&helpFeedbackPage.accesskey;" + label="&helpFeedbackPage.label;" + oncommand="openFeedbackPage()" + onclick="checkForMiddleClick(this, event);"/> + <menuseparator id="updatesSeparator"/> + <menuitem id="checkForUpdates" class="menuitem-iconic" +#ifdef MOZ_UPDATER + label="&updateCmd.label;" + oncommand="checkForUpdates();"/> +#else + hidden="true"/> +#endif + <menuseparator id="aboutSeparator"/> + <menuitem id="aboutName" + accesskey="&aboutProduct.accesskey;" + label="&aboutProduct.label;" + oncommand="openAboutDialog();"/> + </menupopup> + </menu> + + <keyset id="baseMenuKeyset"> + </keyset> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/> + <stringbundle id="bundle_browser_region" src="chrome://browser-region/locale/region.properties"/> + </stringbundleset> +</overlay> diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js new file mode 100644 index 000000000..df4c2fb6a --- /dev/null +++ b/browser/base/content/browser-addons.js @@ -0,0 +1,562 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Removes a doorhanger notification if all of the installs it was notifying +// about have ended in some way. +function removeNotificationOnEnd(notification, installs) { + let count = installs.length; + + function maybeRemove(install) { + install.removeListener(this); + + if (--count == 0) { + // Check that the notification is still showing + let current = PopupNotifications.getNotification(notification.id, notification.browser); + if (current === notification) + notification.remove(); + } + } + + for (let install of installs) { + install.addListener({ + onDownloadCancelled: maybeRemove, + onDownloadFailed: maybeRemove, + onInstallFailed: maybeRemove, + onInstallEnded: maybeRemove + }); + } +} + +const gXPInstallObserver = { + _findChildShell: function (aDocShell, aSoughtShell) { + if (aDocShell == aSoughtShell) { + 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, aSoughtShell); + if (docShell == aSoughtShell) { + return docShell; + } + } + return null; + }, + + _getBrowser: function (aDocShell) { + for (let browser of gBrowser.browsers) { + if (this._findChildShell(browser.docShell, aDocShell)) { + return browser; + } + } + return null; + }, + + observe: function (aSubject, aTopic, aData) { + var brandBundle = document.getElementById("bundle_brand"); + var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo); + var browser = installInfo.browser; + + // Make sure the browser is still alive. + if (!browser || gBrowser.browsers.indexOf(browser) == -1) { + return; + } + + const anchorID = "addons-notification-icon"; + var messageString, action; + var brandShortName = brandBundle.getString("brandShortName"); + + var notificationID = aTopic; + // Make notifications persist a minimum of 30 seconds + var options = { timeout: Date.now() + 30000 }; + + switch (aTopic) { + case "addon-install-disabled": { + notificationID = "xpinstall-disabled" + + if (gPrefService.prefIsLocked("xpinstall.enabled")) { + messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked"); + buttons = []; + } else { + messageString = gNavigatorBundle.getString("xpinstallDisabledMessage"); + + action = { + label: gNavigatorBundle.getString("xpinstallDisabledButton"), + accessKey: gNavigatorBundle.getString("xpinstallDisabledButton.accesskey"), + callback: function editPrefs() { + gPrefService.setBoolPref("xpinstall.enabled", true); + } + }; + } + + PopupNotifications.show(browser, notificationID, messageString, anchorID, + action, null, options); + break; + } + case "addon-install-origin-blocked": { + messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarningOrigin", + [brandShortName]); + + let popup = PopupNotifications.show(browser, notificationID, + messageString, anchorID, + null, null, options); + removeNotificationOnEnd(popup, installInfo.installs); + break; + } + case "addon-install-blocked": { + let originatingHost; + try { + originatingHost = installInfo.originatingURI.host; + } catch(ex) { + // Need to deal with missing originatingURI and with about:/data: URIs more gracefully, + // see bug 1063418 - but for now, bail: + return; + } + messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarning", + [brandShortName, originatingHost]); + + action = { + label: gNavigatorBundle.getString("xpinstallPromptAllowButton"), + accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"), + callback: function() { + installInfo.install(); + } + }; + + let popup = PopupNotifications.show(browser, notificationID, messageString, + anchorID, action, null, options); + removeNotificationOnEnd(popup, installInfo.installs); + break; + } + case "addon-install-started": { + var needsDownload = function needsDownload(aInstall) { + return aInstall.state != AddonManager.STATE_DOWNLOADED; + } + // If all installs have already been downloaded then there is no need to + // show the download progress + if (!installInfo.installs.some(needsDownload)) { + return; + } + notificationID = "addon-progress"; + messageString = gNavigatorBundle.getString("addonDownloading"); + messageString = PluralForm.get(installInfo.installs.length, messageString); + options.installs = installInfo.installs; + options.contentWindow = browser.contentWindow; + options.sourceURI = browser.currentURI; + options.eventCallback = function(aEvent) { + if (aEvent != "removed") { + return; + } + options.contentWindow = null; + options.sourceURI = null; + }; + PopupNotifications.show(browser, notificationID, messageString, anchorID, + null, null, options); + break; + } + case "addon-install-failed": { + // TODO This isn't terribly ideal for the multiple failure case + for (let install of installInfo.installs) { + let host = (installInfo.originatingURI instanceof Ci.nsIStandardURL) && + installInfo.originatingURI.host; + if (!host) { + host = (install.sourceURI instanceof Ci.nsIStandardURL) && + install.sourceURI.host; + } + + let error = (host || install.error == 0) ? "addonError" : "addonLocalError"; + if (install.error != 0) { + error += install.error; + } else if (install.addon.jetsdk) { + error += "JetSDK"; + } else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { + error += "Blocklisted"; + } else { + error += "Incompatible"; + } + + messageString = gNavigatorBundle.getString(error); + messageString = messageString.replace("#1", install.name); + if (host) { + messageString = messageString.replace("#2", host); + } + messageString = messageString.replace("#3", brandShortName); + messageString = messageString.replace("#4", Services.appinfo.version); + + PopupNotifications.show(browser, notificationID, messageString, anchorID, + action, null, options); + } + break; + } + case "addon-install-complete": { + var needsRestart = installInfo.installs.some(function(i) { + return i.addon.pendingOperations != AddonManager.PENDING_NONE; + }); + + if (needsRestart) { + messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart"); + action = { + label: gNavigatorBundle.getString("addonInstallRestartButton"), + accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"), + callback: function() { + Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); + } + }; + } else { + messageString = gNavigatorBundle.getString("addonsInstalled"); + action = null; + } + + messageString = PluralForm.get(installInfo.installs.length, messageString); + messageString = messageString.replace("#1", installInfo.installs[0].name); + messageString = messageString.replace("#2", installInfo.installs.length); + messageString = messageString.replace("#3", brandShortName); + + // Remove notificaion on dismissal, since it's possible to cancel the + // install through the addons manager UI, making the "restart" prompt + // irrelevant. + options.removeOnDismissal = true; + + PopupNotifications.show(browser, notificationID, messageString, anchorID, + action, null, options); + break; + } + } + } +}; + +/* + * When addons are installed/uninstalled, check and see if the number of items + * on the add-on bar changed: + * - If an add-on was installed, incrementing the count, show the bar. + * - If an add-on was uninstalled, and no more items are left, hide the bar. + */ +var AddonsMgrListener = { + get addonBar() document.getElementById("addon-bar"), + get statusBar() document.getElementById("status-bar"), + getAddonBarItemCount: function() { + // Take into account the contents of the status bar shim for the count. + var itemCount = this.statusBar.childNodes.length; + + var defaultOrNoninteractive = this.addonBar.getAttribute("defaultset") + .split(",") + .concat(["separator", "spacer", "spring"]); + for (let item of this.addonBar.currentSet.split(",")) { + if (defaultOrNoninteractive.indexOf(item) == -1) { + itemCount++; + } + } + + return itemCount; + }, + onInstalling: function(aAddon) { + this.lastAddonBarCount = this.getAddonBarItemCount(); + }, + onInstalled: function(aAddon) { + if (this.getAddonBarItemCount() > this.lastAddonBarCount) { + setToolbarVisibility(this.addonBar, true); + } + }, + onUninstalling: function(aAddon) { + this.lastAddonBarCount = this.getAddonBarItemCount(); + }, + onUninstalled: function(aAddon) { + if (this.getAddonBarItemCount() == 0) { + setToolbarVisibility(this.addonBar, false); + } + }, + onEnabling: function(aAddon) { + return this.onInstalling(); + }, + onEnabled: function(aAddon) { + return this.onInstalled(); + }, + onDisabling: function(aAddon) { + return this.onUninstalling(); + }, + onDisabled: function(aAddon) { + return this.onUninstalled(); + } +}; + +var LightWeightThemeWebInstaller = { + handleEvent: function (event) { + switch (event.type) { + case "InstallBrowserTheme": + case "PreviewBrowserTheme": + case "ResetBrowserThemePreview": + // ignore requests from background tabs + if (event.target.ownerDocument.defaultView.top != content) { + return; + } + } + switch (event.type) { + case "InstallBrowserTheme": + this._installRequest(event); + break; + case "PreviewBrowserTheme": + this._preview(event); + break; + case "ResetBrowserThemePreview": + this._resetPreview(event); + break; + case "pagehide": + case "TabSelect": + this._resetPreview(); + break; + } + }, + + get _manager () { + var temp = {}; + Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); + delete this._manager; + return this._manager = temp.LightweightThemeManager; + }, + + _installRequest: function (event) { + var node = event.target; + var data = this._getThemeFromNode(node); + if (!data) { + return; + } + + if (this._isAllowed(node)) { + this._install(data); + return; + } + + var allowButtonText = gNavigatorBundle.getString("lwthemeInstallRequest.allowButton"); + var allowButtonAccesskey = gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey"); + var message = gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message", + [node.ownerDocument.location.host]); + var buttons = [{ + label: allowButtonText, + accessKey: allowButtonAccesskey, + callback: function () { + LightWeightThemeWebInstaller._install(data); + } + }]; + + this._removePreviousNotifications(); + + var notificationBox = gBrowser.getNotificationBox(); + var notificationBar = + notificationBox.appendNotification(message, "lwtheme-install-request", "", + notificationBox.PRIORITY_INFO_MEDIUM, + buttons); + notificationBar.persistence = 1; + }, + + _install: function (newLWTheme) { + var previousLWTheme = this._manager.currentTheme; + + var listener = { + onEnabling: function(aAddon, aRequiresRestart) { + if (!aRequiresRestart) { + return; + } + + let messageString = gNavigatorBundle.getFormattedString("lwthemeNeedsRestart.message", + [aAddon.name], 1); + + let action = { + label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"), + accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"), + callback: function () { + Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); + } + }; + + let options = { timeout: Date.now() + 30000 }; + + PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change", + messageString, "addons-notification-icon", + action, null, options); + }, + + onEnabled: function(aAddon) { + LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme); + } + }; + + AddonManager.addAddonListener(listener); + this._manager.currentTheme = newLWTheme; + AddonManager.removeAddonListener(listener); + }, + + _postInstallNotification: function (newTheme, previousTheme) { + function text(id) { + return gNavigatorBundle.getString("lwthemePostInstallNotification." + id); + } + + var buttons = [{ + label: text("undoButton"), + accessKey: text("undoButton.accesskey"), + callback: function () { + LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id); + LightWeightThemeWebInstaller._manager.currentTheme = previousTheme; + } + }, { + label: text("manageButton"), + accessKey: text("manageButton.accesskey"), + callback: function () { + BrowserOpenAddonsMgr("addons://list/theme"); + } + }]; + + this._removePreviousNotifications(); + + var notificationBox = gBrowser.getNotificationBox(); + var notificationBar = + notificationBox.appendNotification(text("message"), + "lwtheme-install-notification", "", + notificationBox.PRIORITY_INFO_MEDIUM, + buttons); + notificationBar.persistence = 1; + notificationBar.timeout = Date.now() + 20000; // 20 seconds + }, + + _removePreviousNotifications: function () { + var box = gBrowser.getNotificationBox(); + + ["lwtheme-install-request", + "lwtheme-install-notification"].forEach(function (value) { + var notification = box.getNotificationWithValue(value); + if (notification) + box.removeNotification(notification); + }); + }, + + _previewWindow: null, + _preview: function (event) { + if (!this._isAllowed(event.target)) { + return; + } + + var data = this._getThemeFromNode(event.target); + if (!data) { + return; + } + + this._resetPreview(); + + this._previewWindow = event.target.ownerDocument.defaultView; + this._previewWindow.addEventListener("pagehide", this, true); + gBrowser.tabContainer.addEventListener("TabSelect", this, false); + + this._manager.previewTheme(data); + }, + + _resetPreview: function (event) { + if (!this._previewWindow || + (event && !this._isAllowed(event.target))) { + return; + } + + this._previewWindow.removeEventListener("pagehide", this, true); + this._previewWindow = null; + gBrowser.tabContainer.removeEventListener("TabSelect", this, false); + + this._manager.resetPreview(); + }, + + _isAllowed: function (node) { + var pm = Services.perms; + + var uri = node.ownerDocument.documentURIObject; + return pm.testPermission(uri, "install") == pm.ALLOW_ACTION; + }, + + _getThemeFromNode: function (node) { + return this._manager.parseTheme(node.getAttribute("data-browsertheme"), + node.baseURI); + } +} + +/* + * Listen for Lightweight Theme styling changes and update the browser's theme accordingly. + */ +var LightweightThemeListener = { + _modifiedStyles: [], + + init: function () { + XPCOMUtils.defineLazyGetter(this, "styleSheet", function() { + for (let i = document.styleSheets.length - 1; i >= 0; i--) { + let sheet = document.styleSheets[i]; + if (sheet.href == "chrome://browser/skin/browser-lightweightTheme.css") + return sheet; + } + }); + + Services.obs.addObserver(this, "lightweight-theme-styling-update", false); + Services.obs.addObserver(this, "lightweight-theme-optimized", false); + if (document.documentElement.hasAttribute("lwtheme")) { + this.updateStyleSheet(document.documentElement.style.backgroundImage); + } + }, + + uninit: function () { + Services.obs.removeObserver(this, "lightweight-theme-styling-update"); + Services.obs.removeObserver(this, "lightweight-theme-optimized"); + }, + + /** + * Append the headerImage to the background-image property of all rulesets in + * browser-lightweightTheme.css. + * + * @param headerImage - a string containing a CSS image for the lightweight theme header. + */ + updateStyleSheet: function(headerImage) { + if (!this.styleSheet) { + return; + } + this.substituteRules(this.styleSheet.cssRules, headerImage); + }, + + substituteRules: function(ruleList, headerImage, existingStyleRulesModified = 0) { + let styleRulesModified = 0; + for (let i = 0; i < ruleList.length; i++) { + let rule = ruleList[i]; + if (rule instanceof Ci.nsIDOMCSSGroupingRule) { + // Add the number of modified sub-rules to the modified count + styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified); + } else if (rule instanceof Ci.nsIDOMCSSStyleRule) { + if (!rule.style.backgroundImage) { + continue; + } + let modifiedIndex = existingStyleRulesModified + styleRulesModified; + if (!this._modifiedStyles[modifiedIndex]) { + this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage }; + } + + rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage; + styleRulesModified++; + } else { + Cu.reportError("Unsupported rule encountered"); + } + } + return styleRulesModified; + }, + + // nsIObserver + observe: function (aSubject, aTopic, aData) { + if ((aTopic != "lightweight-theme-styling-update" && aTopic != "lightweight-theme-optimized") || + !this.styleSheet) { + return; + } + + if (aTopic == "lightweight-theme-optimized" && aSubject != window) { + return; + } + + let themeData = JSON.parse(aData); + if (!themeData) { + return; + } + this.updateStyleSheet("url(" + themeData.headerURL + ")"); + }, +}; diff --git a/browser/base/content/browser-appmenu.inc b/browser/base/content/browser-appmenu.inc new file mode 100644 index 000000000..17d103049 --- /dev/null +++ b/browser/base/content/browser-appmenu.inc @@ -0,0 +1,381 @@ +# -*- 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/. + +<menupopup id="appmenu-popup" + onpopupshowing="if (event.target == this) { + updateEditUIVisibility(); +#ifdef MOZ_SERVICES_SYNC + gSyncUI.updateUI(); +#endif + return; + } + updateCharacterEncodingMenuState(); + if (event.target.parentNode.parentNode.parentNode.parentNode == this) + this._currentPopup = event.target;"> + <hbox> + <vbox id="appmenuPrimaryPane"> + <menuitem id="appmenu_newTab_popup" + label="&tabCmd.label;" + command="cmd_newNavigatorTab" + key="key_newNavigatorTab"/> + <menuitem id="appmenu_newNavigator" + label="&newNavigatorCmd.label;" + command="cmd_newNavigator" + key="key_newNavigator"/> + <menuitem id="appmenu_newPrivateWindow" + class="menuitem-iconic menuitem-iconic-tooltip" + label="&newPrivateWindow.label;" + command="Tools:PrivateBrowsing" + key="key_privatebrowsing"/> + <menuitem label="&goOfflineCmd.label;" + id="appmenu_offlineModeRecovery" + type="checkbox" + observes="workOfflineMenuitemState" + oncommand="BrowserOffline.toggleOfflineStatus();"/> + <menuseparator class="appmenu-menuseparator"/> + <hbox> + <menuitem id="appmenu-edit-label" + label="&appMenuEdit.label;" + disabled="true"/> + <toolbarbutton id="appmenu-cut" + class="appmenu-edit-button" + command="cmd_cut" + onclick="if (!this.disabled) hidePopup();" + tooltiptext="&cutButton.tooltip;"/> + <toolbarbutton id="appmenu-copy" + class="appmenu-edit-button" + command="cmd_copy" + onclick="if (!this.disabled) hidePopup();" + tooltiptext="©Button.tooltip;"/> + <toolbarbutton id="appmenu-paste" + class="appmenu-edit-button" + command="cmd_paste" + onclick="if (!this.disabled) hidePopup();" + tooltiptext="&pasteButton.tooltip;"/> + <spacer flex="1"/> + <menu id="appmenu-editmenu"> + <menupopup id="appmenu-editmenu-menupopup"> + <menuitem id="appmenu-editmenu-cut" + class="menuitem-iconic" + label="&cutCmd.label;" + key="key_cut" + command="cmd_cut"/> + <menuitem id="appmenu-editmenu-copy" + class="menuitem-iconic" + label="©Cmd.label;" + key="key_copy" + command="cmd_copy"/> + <menuitem id="appmenu-editmenu-paste" + class="menuitem-iconic" + label="&pasteCmd.label;" + key="key_paste" + command="cmd_paste"/> + <menuseparator/> + <menuitem id="appmenu-editmenu-undo" + label="&undoCmd.label;" + key="key_undo" + command="cmd_undo"/> + <menuitem id="appmenu-editmenu-redo" + label="&redoCmd.label;" + key="key_redo" + command="cmd_redo"/> + <menuseparator/> + <menuitem id="appmenu-editmenu-selectAll" + label="&selectAllCmd.label;" + key="key_selectAll" + command="cmd_selectAll"/> + <menuseparator/> + <menuitem id="appmenu-editmenu-delete" + label="&deleteCmd.label;" + key="key_delete" + command="cmd_delete"/> + </menupopup> + </menu> + </hbox> + <menuitem id="appmenu_find" + class="menuitem-tooltip" + label="&appMenuFind.label;" + command="cmd_find" + key="key_find"/> + <menuseparator class="appmenu-menuseparator"/> + <menuitem id="appmenu_openFile" + label="&openFileCmd.label;" + command="Browser:OpenFile" + key="openFileKb"/> + <menuitem id="appmenu_savePage" + class="menuitem-tooltip" + label="&savePageCmd.label;" + command="Browser:SavePage" + key="key_savePage"/> + <menuitem id="appmenu_sendLink" + label="&emailPageCmd.label;" + command="Browser:SendLink"/> + <splitmenu id="appmenu_print" + iconic="true" + label="&printCmd.label;" + command="cmd_print"> + <menupopup> + <menuitem id="appmenu_print_popup" + class="menuitem-iconic" + label="&printCmd.label;" + command="cmd_print" + key="printKb"/> + <menuitem id="appmenu_printPreview" + label="&printPreviewCmd.label;" + command="cmd_printPreview"/> + <menuitem id="appmenu_printSetup" + label="&printSetupCmd.label;" + command="cmd_pageSetup"/> + </menupopup> + </splitmenu> + <menuseparator class="appmenu-menuseparator"/> + <splitmenu id="appmenu_webDeveloper" + label="&appMenuWebDeveloper.label;"> + <menupopup id="appmenu_webDeveloper_popup"> +#define ID_PREFIX appmenu_developer_ +#define OMIT_ACCESSKEYS +#include browser-charsetmenu.inc +#undef ID_PREFIX +#undef OMIT_ACCESSKEYS + <menuitem label="&goOfflineCmd.label;" + type="checkbox" + observes="workOfflineMenuitemState" + oncommand="BrowserOffline.toggleOfflineStatus();"/> + <menuseparator/> + <menuitem id="appmenu_pageSource" + observes="devtoolsMenuBroadcaster_PageSource"/> + <menuitem id="appmenu_javascriptConsole" + observes="devtoolsMenuBroadcaster_ErrorConsole"/> + </menupopup> + </splitmenu> + <menuseparator class="appmenu-menuseparator"/> +#define ID_PREFIX appmenu_ +#define OMIT_ACCESSKEYS +#include browser-charsetmenu.inc +#undef ID_PREFIX +#undef OMIT_ACCESSKEYS + <menuitem id="appmenu_fullScreen" + class="menuitem-tooltip" + label="&fullScreenCmd.label;" + type="checkbox" + observes="View:FullScreen" + key="key_fullScreen"/> + <menuitem id="appmenu_restart" + class="menuitem-iconic" + label="&appMenuRestart.label;" + command="cmd_restartApplication"/> + <menuitem id="appmenu-quit" + class="menuitem-iconic" +#ifdef XP_WIN + label="&quitApplicationCmdWin.label;" +#else + label="&quitApplicationCmd.label;" +#endif + command="cmd_quitApplication"/> + </vbox> + <vbox id="appmenuSecondaryPane"> + <splitmenu id="appmenu_bookmarks" + iconic="true" + label="&bookmarksMenu.label;" + command="Browser:ShowAllBookmarks"> + <menupopup id="appmenu_bookmarksPopup" + placespopup="true" + context="placesContext" + openInTabs="children" + oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);" + onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);" + onpopupshowing="BookmarkingUI.onPopupShowing(event); + if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=BOOKMARKS_MENU');" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <menuitem id="appmenu_showAllBookmarks" + class="menuitem-iconic" + label="&organizeBookmarks.label;" + command="Browser:ShowAllBookmarks" + context="" + key="manBookmarkKb"/> + <menuseparator/> + <menuitem id="appmenu_bookmarkThisPage" + class="menuitem-iconic" + label="&bookmarkThisPageCmd.label;" + command="Browser:AddBookmarkAs" + key="addBookmarkAsKb"/> + <menuitem id="appmenu_subscribeToPage" + class="menuitem-iconic" + label="&subscribeToPageMenuitem.label;" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);" + observes="singleFeedMenuitemState"/> + <menu id="appmenu_subscribeToPageMenu" + class="menu-iconic" + label="&subscribeToPageMenupopup.label;" + observes="multipleFeedsMenuState"> + <menupopup id="appmenu_subscribeToPageMenupopup" + onpopupshowing="return FeedHandler.buildFeedList(event.target);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </menu> + <menuseparator/> + <menu id="appmenu_bookmarksToolbar" + placesanonid="toolbar-autohide" + class="menu-iconic bookmark-item" + label="&personalbarCmd.label;" + container="true"> + <menupopup id="appmenu_bookmarksToolbarPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=TOOLBAR');"/> + </menu> + <menuseparator/> + <!-- Bookmarks menu items --> + <menuseparator builder="end" + class="hide-if-empty-places-result"/> + <menuitem id="appmenu_unsortedBookmarks" + class="menuitem-iconic" + label="&appMenuUnsorted.label;" + oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/> + </menupopup> + </splitmenu> + <splitmenu id="appmenu_history" + iconic="true" + label="&historyMenu.label;" + command="Browser:ShowAllHistory"> + <menupopup id="appmenu_historyMenupopup" + placespopup="true" + context="placesContext" + oncommand="this.parentNode._placesView._onCommand(event);" + onclick="checkForMiddleClick(this, event);" + onpopupshowing="if (!this.parentNode._placesView) + new HistoryMenu(event);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <menuitem id="appmenu_showAllHistory" + class="menuitem-iconic" + label="&showAllHistoryCmd2.label;" + command="Browser:ShowAllHistory" + key="showAllHistoryKb"/> + <menuseparator/> + <menuitem id="appmenu_sanitizeHistory" + class="menuitem-iconic" + label="&clearRecentHistory.label;" + key="key_sanitize" + command="Tools:Sanitize"/> + <menuseparator class="hide-if-empty-places-result"/> +#ifdef MOZ_SERVICES_SYNC + <menuitem id="appmenu_sync-tabs" + class="syncTabsMenuItem" + label="&syncTabsMenu2.label;" + oncommand="BrowserOpenSyncTabs();" + disabled="true"/> +#endif + <menuitem id="appmenu_restoreLastSession" + label="&historyRestoreLastSession.label;" + command="Browser:RestoreLastSession"/> + <menu id="appmenu_recentlyClosedTabsMenu" + class="recentlyClosedTabsMenu" + label="&historyUndoMenu.label;" + disabled="true"> + <menupopup id="appmenu_recentlyClosedTabsMenupopup" + onpopupshowing="document.getElementById('appmenu_history')._placesView.populateUndoSubmenu();"/> + </menu> + <menu id="appmenu_recentlyClosedWindowsMenu" + class="recentlyClosedWindowsMenu" + label="&historyUndoWindowMenu.label;" + disabled="true"> + <menupopup id="appmenu_recentlyClosedWindowsMenupopup" + onpopupshowing="document.getElementById('appmenu_history')._placesView.populateUndoWindowSubmenu();"/> + </menu> + <menuseparator/> + </menupopup> + </splitmenu> + <menuitem id="appmenu_downloads" + class="menuitem-tooltip" + label="&downloads.label;" + command="Tools:Downloads" + key="key_openDownloads"/> + <spacer id="appmenuSecondaryPane-spacer"/> + <menuitem id="appmenu_addons" + class="menuitem-iconic menuitem-iconic-tooltip" + label="&addons.label;" + command="Tools:Addons" + key="key_openAddons"/> + <menuitem id="appmenu_permissions" + class="menuitem-iconic" + label="&permissions.label;" + command="Tools:Permissions" + key="key_openPermissions"/> +#ifdef MOZ_SERVICES_SYNC + <!-- only one of sync-setup or sync-syncnow will be showing at once --> + <menuitem id="sync-setup-appmenu" + label="&syncSetup.label;" + observes="sync-setup-state" + oncommand="gSyncUI.openSetup()"/> + <menuitem id="sync-syncnowitem-appmenu" + label="&syncSyncNowItem.label;" + observes="sync-syncnow-state" + oncommand="gSyncUI.doSync(event);"/> +#endif + <splitmenu id="appmenu_customize" + label="&preferencesCmd2.label;" + oncommand="openPreferences();"> + <menupopup id="appmenu_customizeMenu" + onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('appmenu_toggleToolbarsSeparator'));"> + <menuitem id="appmenu_preferences" + label="&preferencesCmd2.label;" + oncommand="openPreferences();"/> + <menuseparator/> + <menuseparator id="appmenu_toggleToolbarsSeparator"/> + <menuitem id="appmenu_toggleTabsOnTop" + label="&viewTabsOnTop.label;" + type="checkbox" + command="cmd_ToggleTabsOnTop"/> + <menuitem id="appmenu_toolbarLayout" + label="&appMenuToolbarLayout.label;" + command="cmd_CustomizeToolbars"/> + </menupopup> + </splitmenu> + <splitmenu id="appmenu_help" + label="&helpMenu.label;" + oncommand="openHelpLink('pale-moon-help')"> + <menupopup id="appmenu_helpMenupopup" onpopupshowing="buildHelpMenu();"> + <menuitem id="appmenu_openHelp" + label="&helpMenu.label;" + oncommand="openHelpLink('pale-moon-help')" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="appmenu_troubleshootingInfo" + label="&helpTroubleshootingInfo.label;" + oncommand="openTroubleshootingPage()" + onclick="checkForMiddleClick(this,event);"/> + <menuitem id="appmenu_safeMode" + label="&appMenuSafeMode.label;" + oncommand="restart(true);"/> + <menuseparator/> + <menuitem id="appmenu_releaseNotes" + accesskey="&helpReleaseNotes.accesskey;" + label="&helpReleaseNotes.label;" + oncommand="openReleaseNotes();" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="appmenu_feedbackPage" + label="&helpFeedbackPage.label;" + oncommand="openFeedbackPage()" + onclick="checkForMiddleClick(this, event);"/> +#ifdef MOZ_UPDATER + <menuseparator/> + <menuitem id="appmenu_checkForUpdates" + class="menuitem-iconic" + label="&updateCmd.label;" + oncommand="checkForUpdates();"/> +#endif + <menuseparator/> + <menuitem id="appmenu_about" + label="&aboutProduct.label;" + oncommand="openAboutDialog();"/> + </menupopup> + </splitmenu> + </vbox> + </hbox> +</menupopup> diff --git a/browser/base/content/browser-charsetmenu.inc b/browser/base/content/browser-charsetmenu.inc new file mode 100644 index 000000000..628de1341 --- /dev/null +++ b/browser/base/content/browser-charsetmenu.inc @@ -0,0 +1,62 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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 + +#expand <menu id="__ID_PREFIX__charsetMenu" + label="&charsetMenu.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenu.accesskey;" +#endif + oncommand="MultiplexHandler(event)" +#ifdef OMIT_ACCESSKEYS +#expand onpopupshowing="CharsetMenu.build(event, '__ID_PREFIX__');" +#else +#expand onpopupshowing="CharsetMenu.build(event, '__ID_PREFIX__', true);" +#endif + onpopupshown="UpdateMenus(event);"> + <menupopup> + <menu label="&charsetMenuAutodet.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.accesskey;" +#endif + > + <menupopup> + <menuitem type="radio" + name="detectorGroup" +#expand id="__ID_PREFIX__chardet.off" + label="&charsetMenuAutodet.off.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.off.accesskey;" +#endif + /> + <menuitem type="radio" + name="detectorGroup" +#expand id="__ID_PREFIX__chardet.ja_parallel_state_machine" + label="&charsetMenuAutodet.ja.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.ja.accesskey;" +#endif + /> + <menuitem type="radio" + name="detectorGroup" +#expand id="__ID_PREFIX__chardet.ruprob" + label="&charsetMenuAutodet.ru.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.ru.accesskey;" +#endif + /> + <menuitem type="radio" + name="detectorGroup" +#expand id="__ID_PREFIX__chardet.ukprob" + label="&charsetMenuAutodet.uk.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.uk.accesskey;" +#endif + /> + </menupopup> + </menu> + <menuseparator/> + </menupopup> +</menu> diff --git a/browser/base/content/browser-context.inc b/browser/base/content/browser-context.inc new file mode 100644 index 000000000..33ea7b135 --- /dev/null +++ b/browser/base/content/browser-context.inc @@ -0,0 +1,390 @@ +# -*- 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/. + + <menuseparator id="page-menu-separator"/> + <menuitem id="spell-no-suggestions" + disabled="true" + label="&spellNoSuggestions.label;"/> + <menuitem id="spell-add-to-dictionary" + label="&spellAddToDictionary.label;" + accesskey="&spellAddToDictionary.accesskey;" + oncommand="InlineSpellCheckerUI.addToDictionary();"/> + <menuitem id="spell-undo-add-to-dictionary" + label="&spellUndoAddToDictionary.label;" + accesskey="&spellUndoAddToDictionary.accesskey;" + oncommand="InlineSpellCheckerUI.undoAddToDictionary();" /> + <menuseparator id="spell-suggestions-separator"/> + <menuitem id="context-openlinkintab" + label="&openLinkCmdInTab.label;" + accesskey="&openLinkCmdInTab.accesskey;" + oncommand="gContextMenu.openLinkInTab();"/> + <menuitem id="context-openlink" + label="&openLinkCmd.label;" + accesskey="&openLinkCmd.accesskey;" + oncommand="gContextMenu.openLink();"/> + <menuitem id="context-openlinkprivate" + label="&openLinkInPrivateWindowCmd.label;" + accesskey="&openLinkInPrivateWindowCmd.accesskey;" + oncommand="gContextMenu.openLinkInPrivateWindow();"/> + <menuitem id="context-openlinkincurrent" + label="&openLinkCmdInCurrent.label;" + accesskey="&openLinkCmdInCurrent.accesskey;" + oncommand="gContextMenu.openLinkInCurrent();"/> + <menuseparator id="context-sep-open"/> + <menuitem id="context-bookmarklink" + label="&bookmarkThisLinkCmd.label;" + accesskey="&bookmarkThisLinkCmd.accesskey;" + oncommand="gContextMenu.bookmarkLink();"/> + <menuitem id="context-savelink" + label="&saveLinkCmd.label;" + accesskey="&saveLinkCmd.accesskey;" + oncommand="gContextMenu.saveLink();"/> + <menuitem id="context-sendlink" + label="&sendLinkCmd.label;" + accesskey="&sendLinkCmd.accesskey;" + oncommand="gContextMenu.sendLink();"/> + <menuitem id="context-copyemail" + label="©EmailCmd.label;" + accesskey="©EmailCmd.accesskey;" + oncommand="gContextMenu.copyEmail();"/> + <menuitem id="context-copylink" + label="©LinkCmd.label;" + accesskey="©LinkCmd.accesskey;" + oncommand="goDoCommand('cmd_copyLink');"/> + <menuseparator id="context-sep-copylink"/> + <menuitem id="context-media-play" + label="&mediaPlay.label;" + accesskey="&mediaPlay.accesskey;" + oncommand="gContextMenu.mediaCommand('play');"/> + <menuitem id="context-media-pause" + label="&mediaPause.label;" + accesskey="&mediaPause.accesskey;" + oncommand="gContextMenu.mediaCommand('pause');"/> + <menuitem id="context-media-mute" + label="&mediaMute.label;" + accesskey="&mediaMute.accesskey;" + oncommand="gContextMenu.mediaCommand('mute');"/> + <menuitem id="context-media-unmute" + label="&mediaUnmute.label;" + accesskey="&mediaUnmute.accesskey;" + oncommand="gContextMenu.mediaCommand('unmute');"/> + <menu id="context-media-playbackrate" label="&mediaPlaybackRate2.label;" accesskey="&mediaPlaybackRate2.accesskey;"> + <menupopup> + <menuitem id="context-media-playbackrate-050x" + label="&mediaPlaybackRate050x.label;" + accesskey="&mediaPlaybackRate050x.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 0.5);"/> + <menuitem id="context-media-playbackrate-100x" + label="&mediaPlaybackRate100x.label;" + accesskey="&mediaPlaybackRate100x.accesskey;" + type="radio" + name="playbackrate" + checked="true" + oncommand="gContextMenu.mediaCommand('playbackRate', 1.0);"/> + <menuitem id="context-media-playbackrate-125x" + label="&mediaPlaybackRate125x.label;" + accesskey="&mediaPlaybackRate125x.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 1.25);"/> + <menuitem id="context-media-playbackrate-150x" + label="&mediaPlaybackRate150x.label;" + accesskey="&mediaPlaybackRate150x.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 1.5);"/> + <menuitem id="context-media-playbackrate-200x" + label="&mediaPlaybackRate200x.label;" + accesskey="&mediaPlaybackRate200x.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 2.0);"/> + </menupopup> + </menu> + <menuitem id="context-media-loop" + label="&mediaLoop.label;" + accesskey="&mediaLoop.accesskey;" + type="checkbox" + oncommand="gContextMenu.mediaCommand('loop');"/> + <menuitem id="context-media-showcontrols" + label="&mediaShowControls.label;" + accesskey="&mediaShowControls.accesskey;" + oncommand="gContextMenu.mediaCommand('showcontrols');"/> + <menuitem id="context-media-hidecontrols" + label="&mediaHideControls.label;" + accesskey="&mediaHideControls.accesskey;" + oncommand="gContextMenu.mediaCommand('hidecontrols');"/> + <menuitem id="context-video-showstats" + accesskey="&videoShowStats.accesskey;" + label="&videoShowStats.label;" + oncommand="gContextMenu.mediaCommand('showstats');"/> + <menuitem id="context-video-hidestats" + accesskey="&videoHideStats.accesskey;" + label="&videoHideStats.label;" + oncommand="gContextMenu.mediaCommand('hidestats');"/> + <menuitem id="context-video-fullscreen" + accesskey="&videoFullScreen.accesskey;" + label="&videoFullScreen.label;" + oncommand="gContextMenu.fullScreenVideo();"/> + <menuitem id="context-leave-dom-fullscreen" + accesskey="&leaveDOMFullScreen.accesskey;" + label="&leaveDOMFullScreen.label;" + oncommand="gContextMenu.leaveDOMFullScreen();"/> + <menuseparator id="context-media-sep-commands"/> + <menuitem id="context-reloadimage" + label="&reloadImageCmd.label;" + accesskey="&reloadImageCmd.accesskey;" + oncommand="gContextMenu.reloadImage();"/> + <menuitem id="context-viewimage" + label="&viewImageCmd.label;" + accesskey="&viewImageCmd.accesskey;" + oncommand="gContextMenu.viewMedia(event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-viewvideo" + label="&viewVideoCmd.label;" + accesskey="&viewVideoCmd.accesskey;" + oncommand="gContextMenu.viewMedia(event);" + onclick="checkForMiddleClick(this, event);"/> +#ifdef CONTEXT_COPY_IMAGE_CONTENTS + <menuitem id="context-copyimage-contents" + label="©ImageContentsCmd.label;" + accesskey="©ImageContentsCmd.accesskey;" + oncommand="goDoCommand('cmd_copyImage');"/> +#endif + <menuitem id="context-copyimage" + label="©ImageCmd.label;" + accesskey="©ImageCmd.accesskey;" + oncommand="gContextMenu.copyMediaLocation();"/> + <menuitem id="context-copyvideourl" + label="©VideoURLCmd.label;" + accesskey="©VideoURLCmd.accesskey;" + oncommand="gContextMenu.copyMediaLocation();"/> + <menuitem id="context-copyaudiourl" + label="©AudioURLCmd.label;" + accesskey="©AudioURLCmd.accesskey;" + oncommand="gContextMenu.copyMediaLocation();"/> + <menuseparator id="context-sep-copyimage"/> + <menuitem id="context-saveimage" + label="&saveImageCmd.label;" + accesskey="&saveImageCmd.accesskey;" + oncommand="gContextMenu.saveMedia();"/> + <menuitem id="context-sendimage" + label="&emailImageCmd.label;" + accesskey="&emailImageCmd.accesskey;" + oncommand="gContextMenu.sendMedia();"/> + <menuitem id="context-setDesktopBackground" + label="&setDesktopBackgroundCmd.label;" + accesskey="&setDesktopBackgroundCmd.accesskey;" + oncommand="gContextMenu.setDesktopBackground();"/> + <menuitem id="context-viewimageinfo" + label="&viewImageInfoCmd.label;" + accesskey="&viewImageInfoCmd.accesskey;" + oncommand="gContextMenu.viewImageInfo();"/> + <menuitem id="context-savevideo" + label="&saveVideoCmd.label;" + accesskey="&saveVideoCmd.accesskey;" + oncommand="gContextMenu.saveMedia();"/> + <menuitem id="context-saveaudio" + label="&saveAudioCmd.label;" + accesskey="&saveAudioCmd.accesskey;" + oncommand="gContextMenu.saveMedia();"/> + <menuitem id="context-video-saveimage" + accesskey="&videoSaveImage.accesskey;" + label="&videoSaveImage.label;" + oncommand="gContextMenu.saveVideoFrameAsImage();"/> + <menuitem id="context-sendvideo" + label="&emailVideoCmd.label;" + accesskey="&emailVideoCmd.accesskey;" + oncommand="gContextMenu.sendMedia();"/> + <menuitem id="context-sendaudio" + label="&emailAudioCmd.label;" + accesskey="&emailAudioCmd.accesskey;" + oncommand="gContextMenu.sendMedia();"/> + <menuitem id="context-ctp-play" + label="&playPluginCmd.label;" + accesskey="&playPluginCmd.accesskey;" + oncommand="gContextMenu.playPlugin();"/> + <menuitem id="context-ctp-hide" + label="&hidePluginCmd.label;" + accesskey="&hidePluginCmd.accesskey;" + oncommand="gContextMenu.hidePlugin();"/> + <menuseparator id="context-sep-ctp"/> + <menuitem id="context-back" + label="&backCmd.label;" + accesskey="&backCmd.accesskey;" + command="Browser:BackOrBackDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-forward" + label="&forwardCmd.label;" + accesskey="&forwardCmd.accesskey;" + command="Browser:ForwardOrForwardDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-reload" + label="&reloadCmd.label;" + accesskey="&reloadCmd.accesskey;" + oncommand="gContextMenu.reload(event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-stop" + label="&stopCmd.label;" + accesskey="&stopCmd.accesskey;" + command="Browser:Stop"/> + <menuseparator id="context-sep-stop"/> + <menuitem id="context-bookmarkpage" + label="&bookmarkPageCmd2.label;" + accesskey="&bookmarkPageCmd2.accesskey;" + oncommand="gContextMenu.bookmarkThisPage();"/> + <menuitem id="context-savepage" + label="&savePageCmd.label;" + accesskey="&savePageCmd.accesskey2;" + oncommand="gContextMenu.savePageAs();"/> + <menuitem id="context-sendpage" + label="&sendPageCmd.label;" + accesskey="&sendPageCmd.accesskey;" + oncommand="gContextMenu.sendPage();"/> + <menuseparator id="context-sep-viewbgimage"/> + <menuitem id="context-viewbgimage" + label="&viewBGImageCmd.label;" + accesskey="&viewBGImageCmd.accesskey;" + oncommand="gContextMenu.viewBGImage(event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-undo" + label="&undoCmd.label;" + accesskey="&undoCmd.accesskey;" + command="cmd_undo"/> + <menuseparator id="context-sep-undo"/> + <menuitem id="context-cut" + label="&cutCmd.label;" + accesskey="&cutCmd.accesskey;" + command="cmd_cut"/> + <menuitem id="context-copy" + label="©Cmd.label;" + accesskey="©Cmd.accesskey;" + command="cmd_copy"/> + <menuitem id="context-paste" + label="&pasteCmd.label;" + accesskey="&pasteCmd.accesskey;" + command="cmd_paste"/> + <menuitem id="context-delete" + label="&deleteCmd.label;" + accesskey="&deleteCmd.accesskey;" + command="cmd_delete"/> + <menuseparator id="context-sep-paste"/> + <menuitem id="context-selectall" + label="&selectAllCmd.label;" + accesskey="&selectAllCmd.accesskey;" + command="cmd_selectAll"/> + <menuseparator id="context-sep-selectall"/> + <menuitem id="context-keywordfield" + label="&keywordfield.label;" + accesskey="&keywordfield.accesskey;" + oncommand="AddKeywordForSearchField();"/> + <menuitem id="context-searchselect" + oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/> + <menuseparator id="frame-sep"/> + <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;"> + <menupopup> + <menuitem id="context-showonlythisframe" + label="&showOnlyThisFrameCmd.label;" + accesskey="&showOnlyThisFrameCmd.accesskey;" + oncommand="gContextMenu.showOnlyThisFrame();"/> + <menuitem id="context-openframeintab" + label="&openFrameCmdInTab.label;" + accesskey="&openFrameCmdInTab.accesskey;" + oncommand="gContextMenu.openFrameInTab();"/> + <menuitem id="context-openframe" + label="&openFrameCmd.label;" + accesskey="&openFrameCmd.accesskey;" + oncommand="gContextMenu.openFrame();"/> + <menuseparator id="open-frame-sep"/> + <menuitem id="context-reloadframe" + label="&reloadFrameCmd.label;" + accesskey="&reloadFrameCmd.accesskey;" + oncommand="gContextMenu.reloadFrame();"/> + <menuseparator/> + <menuitem id="context-bookmarkframe" + label="&bookmarkThisFrameCmd.label;" + accesskey="&bookmarkThisFrameCmd.accesskey;" + oncommand="gContextMenu.addBookmarkForFrame();"/> + <menuitem id="context-saveframe" + label="&saveFrameCmd.label;" + accesskey="&saveFrameCmd.accesskey;" + oncommand="gContextMenu.saveFrame();"/> + <menuseparator/> + <menuitem id="context-printframe" + label="&printFrameCmd.label;" + accesskey="&printFrameCmd.accesskey;" + oncommand="gContextMenu.printFrame();"/> + <menuseparator/> + <menuitem id="context-viewframesource" + label="&viewFrameSourceCmd.label;" + accesskey="&viewFrameSourceCmd.accesskey;" + oncommand="gContextMenu.viewFrameSource();" + observes="isFrameImage"/> + <menuitem id="context-viewframeinfo" + label="&viewFrameInfoCmd.label;" + accesskey="&viewFrameInfoCmd.accesskey;" + oncommand="gContextMenu.viewFrameInfo();"/> + </menupopup> + </menu> + <menuitem id="context-viewpartialsource-selection" + label="&viewPartialSourceForSelectionCmd.label;" + accesskey="&viewPartialSourceCmd.accesskey;" + oncommand="gContextMenu.viewPartialSource('selection');" + observes="isImage"/> + <menuitem id="context-viewpartialsource-mathml" + label="&viewPartialSourceForMathMLCmd.label;" + accesskey="&viewPartialSourceCmd.accesskey;" + oncommand="gContextMenu.viewPartialSource('mathml');" + observes="isImage"/> + <menuseparator id="context-sep-viewsource"/> + <menuitem id="context-viewsource" + label="&viewPageSourceCmd.label;" + accesskey="&viewPageSourceCmd.accesskey;" + oncommand="BrowserViewSourceOfDocument(gContextMenu.browser.contentDocument);" + observes="isImage"/> + <menuitem id="context-viewinfo" + label="&viewPageInfoCmd.label;" + accesskey="&viewPageInfoCmd.accesskey;" + oncommand="gContextMenu.viewInfo();"/> + <menuseparator id="spell-separator"/> + <menuitem id="spell-check-enabled" + label="&spellCheckToggle.label;" + type="checkbox" + accesskey="&spellCheckToggle.accesskey;" + oncommand="InlineSpellCheckerUI.toggleEnabled();"/> + <menuitem id="spell-add-dictionaries-main" + label="&spellAddDictionaries.label;" + accesskey="&spellAddDictionaries.accesskey;" + oncommand="gContextMenu.addDictionaries();"/> + <menu id="spell-dictionaries" + label="&spellDictionaries.label;" + accesskey="&spellDictionaries.accesskey;"> + <menupopup id="spell-dictionaries-menu"> + <menuseparator id="spell-language-separator"/> + <menuitem id="spell-add-dictionaries" + label="&spellAddDictionaries.label;" + accesskey="&spellAddDictionaries.accesskey;" + oncommand="gContextMenu.addDictionaries();"/> + </menupopup> + </menu> + <menuseparator hidden="true" id="context-sep-bidi"/> + <menuitem hidden="true" id="context-bidi-text-direction-toggle" + label="&bidiSwitchTextDirectionItem.label;" + accesskey="&bidiSwitchTextDirectionItem.accesskey;" + command="cmd_switchTextDirection"/> + <menuitem hidden="true" id="context-bidi-page-direction-toggle" + label="&bidiSwitchPageDirectionItem.label;" + accesskey="&bidiSwitchPageDirectionItem.accesskey;" + oncommand="gContextMenu.switchPageDirection();"/> +#ifdef MOZ_DEVTOOLS + <menuseparator id="inspect-separator" hidden="true"/> + <menuitem id="context-inspect" + hidden="true" + label="&inspectContextMenu.label;" + accesskey="&inspectContextMenu.accesskey;" + oncommand="gContextMenu.inspectNode();"/> +#endif diff --git a/browser/base/content/browser-devtools-theme.js b/browser/base/content/browser-devtools-theme.js new file mode 100644 index 000000000..7b21ddedc --- /dev/null +++ b/browser/base/content/browser-devtools-theme.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Listeners for the DevTools theme. + */ +var DevToolsTheme = { + _devtoolsThemePrefName: "devtools.theme", + styleSheet: null, + initialized: false, + + get isStyleSheetEnabled() { + return this.styleSheet && !this.styleSheet.sheet.disabled; + }, + + init: function () { + this.initialized = true; + Services.prefs.addObserver(this._devtoolsThemePrefName, this, false); + Services.obs.addObserver(this, "lightweight-theme-styling-update", false); + Services.obs.addObserver(this, "lightweight-theme-window-updated", false); + this._updateDevtoolsThemeAttribute(); + }, + + observe: function (subject, topic, data) { + if (topic == "lightweight-theme-styling-update") { + let newTheme = JSON.parse(data); + this._toggleStyleSheet(); + } + + if (topic == "nsPref:changed" && data == this._devtoolsThemePrefName) { + this._updateDevtoolsThemeAttribute(); + } + }, + + _inferBrightness: function() { + ToolbarIconColor.inferFromText(); + // Get an inverted full screen button if the dark theme is applied. + if (this.isStyleSheetEnabled && + document.documentElement.getAttribute("devtoolstheme") == "dark") { + document.documentElement.setAttribute("brighttitlebarforeground", "true"); + } else { + document.documentElement.removeAttribute("brighttitlebarforeground"); + } + }, + + _updateDevtoolsThemeAttribute: function() { + // Set an attribute on root element to make it possible + // to change colors based on the selected devtools theme. + let devtoolsTheme = Services.prefs.getCharPref(this._devtoolsThemePrefName); + if (devtoolsTheme != "dark") { + devtoolsTheme = "light"; + } + document.documentElement.setAttribute("devtoolstheme", devtoolsTheme); + this._inferBrightness(); + }, + + handleEvent: function(e) { + if (e.type === "load") { + this.styleSheet.removeEventListener("load", this); + this.refreshBrowserDisplay(); + } + }, + + refreshBrowserDisplay: function() { + // Don't touch things on the browser if gBrowserInit.onLoad hasn't + // yet fired. + if (this.initialized) { + gBrowser.tabContainer._positionPinnedTabs(); + this._inferBrightness(); + } + }, + + _toggleStyleSheet: function() { + let wasEnabled = this.isStyleSheetEnabled; + if (wasEnabled) { + this.styleSheet.sheet.disabled = true; + this.refreshBrowserDisplay(); + } + }, + + uninit: function () { + Services.prefs.removeObserver(this._devtoolsThemePrefName, this); + Services.obs.removeObserver(this, "lightweight-theme-styling-update", false); + Services.obs.removeObserver(this, "lightweight-theme-window-updated", false); + if (this.styleSheet) { + this.styleSheet.removeEventListener("load", this); + } + this.styleSheet = null; + } +}; diff --git a/browser/base/content/browser-doctype.inc b/browser/base/content/browser-doctype.inc new file mode 100644 index 000000000..6ee6384b6 --- /dev/null +++ b/browser/base/content/browser-doctype.inc @@ -0,0 +1,19 @@ +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" > +%browserDTD; +<!ENTITY % baseMenuDTD SYSTEM "chrome://browser/locale/baseMenuOverlay.dtd" > +%baseMenuDTD; +<!ENTITY % charsetDTD SYSTEM "chrome://browser/locale/charsetMenu.dtd" > +%charsetDTD; +<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" > +%textcontextDTD; +<!ENTITY % customizeToolbarDTD SYSTEM "chrome://global/locale/customizeToolbar.dtd"> + %customizeToolbarDTD; +<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd"> +%placesDTD; +<!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd"> +%aboutHomeDTD; +]> + diff --git a/browser/base/content/browser-feeds.js b/browser/base/content/browser-feeds.js new file mode 100644 index 000000000..a93f446b9 --- /dev/null +++ b/browser/base/content/browser-feeds.js @@ -0,0 +1,236 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/** + * The Feed Handler object manages discovery of RSS/ATOM feeds in web pages + * and shows UI when they are discovered. + */ +var FeedHandler = { + + /* Pale Moon: Address Bar: Feeds + * The click handler for the Feed icon in the location bar. Opens the + * subscription page if user is not given a choice of feeds. + * (Otherwise the list of available feeds will be presented to the + * user in a popup menu.) + */ + onFeedButtonPMClick: function(event) { + event.stopPropagation(); + + if (event.target.hasAttribute("feed") && + event.eventPhase == Event.AT_TARGET && + (event.button == 0 || event.button == 1)) { + this.subscribeToFeed(null, event); + } + }, + + /** + * The click handler for the Feed icon in the toolbar. Opens the + * subscription page if user is not given a choice of feeds. + * (Otherwise the list of available feeds will be presented to the + * user in a popup menu.) + */ + onFeedButtonClick: function(event) { + event.stopPropagation(); + + let feeds = gBrowser.selectedBrowser.feeds || []; + // If there are multiple feeds, the menu will open, so no need to do + // anything. If there are no feeds, nothing to do either. + if (feeds.length != 1) { + return; + } + + if (event.eventPhase == Event.AT_TARGET && + (event.button == 0 || event.button == 1)) { + this.subscribeToFeed(feeds[0].href, event); + } + }, + + /** Called when the user clicks on the Subscribe to This Page... menu item. + * Builds a menu of unique feeds associated with the page, and if there + * is only one, shows the feed inline in the browser window. + * @param menuPopup + * The feed list menupopup to be populated. + * @returns true if the menu should be shown, false if there was only + * one feed and the feed should be shown inline in the browser + * window (do not show the menupopup). + */ + buildFeedList: function(menuPopup) { + var feeds = gBrowser.selectedBrowser.feeds; + if (feeds == null) { + // XXX hack -- menu opening depends on setting of an "open" + // attribute, and the menu refuses to open if that attribute is + // set (because it thinks it's already open). onpopupshowing gets + // called after the attribute is unset, and it doesn't get unset + // if we return false. so we unset it here; otherwise, the menu + // refuses to work past this point. + menuPopup.parentNode.removeAttribute("open"); + return false; + } + + while (menuPopup.firstChild) { + menuPopup.removeChild(menuPopup.firstChild); + } + + if (feeds.length == 1) { + var feedButtonPM = document.getElementById("ub-feed-button"); + if (feedButtonPM) { + feedButtonPM.setAttribute("feed", feeds[0].href); + } + return false; + } + + if (feeds.length <= 1) { + return false; + } + + // Build the menu showing the available feed choices for viewing. + for (let feedInfo of feeds) { + var menuItem = document.createElement("menuitem"); + var baseTitle = feedInfo.title || feedInfo.href; + var labelStr = gNavigatorBundle.getFormattedString("feedShowFeedNew", [baseTitle]); + menuItem.setAttribute("class", "feed-menuitem"); + menuItem.setAttribute("label", labelStr); + menuItem.setAttribute("feed", feedInfo.href); + menuItem.setAttribute("tooltiptext", feedInfo.href); + menuItem.setAttribute("crop", "center"); + menuPopup.appendChild(menuItem); + } + return true; + }, + + /** + * Subscribe to a given feed. Called when + * 1. Page has a single feed and user clicks feed icon in location bar + * 2. Page has a single feed and user selects Subscribe menu item + * 3. Page has multiple feeds and user selects from feed icon popup + * 4. Page has multiple feeds and user selects from Subscribe submenu + * @param href + * The feed to subscribe to. May be null, in which case the + * event target's feed attribute is examined. + * @param event + * The event this method is handling. Used to decide where + * to open the preview UI. (Optional, unless href is null) + */ + subscribeToFeed: function(href, event) { + // Just load the feed in the content area to either subscribe or show the + // preview UI + if (!href) { + href = event.target.getAttribute("feed"); + } + urlSecurityCheck(href, gBrowser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + var feedURI = makeURI(href, document.characterSet); + // Use the feed scheme so X-Moz-Is-Feed will be set + // The value doesn't matter + if (/^https?$/.test(feedURI.scheme)) { + href = "feed:" + href; + } + this.loadFeed(href, event); + }, + + loadFeed: function(href, event) { + var feeds = gBrowser.selectedBrowser.feeds; + try { + openUILink(href, event, { ignoreAlt: true }); + } finally { + // We might default to a livebookmarks modal dialog, + // so reset that if the user happens to click it again + gBrowser.selectedBrowser.feeds = feeds; + } + }, + + get _feedMenuitem() { + delete this._feedMenuitem; + return this._feedMenuitem = document.getElementById("singleFeedMenuitemState"); + }, + + get _feedMenupopup() { + delete this._feedMenupopup; + return this._feedMenupopup = document.getElementById("multipleFeedsMenuState"); + }, + + /** + * Update the browser UI to show whether or not feeds are available when + * a page is loaded or the user switches tabs to a page that has feeds. + */ + updateFeeds: function() { + if (this._updateFeedTimeout) { + clearTimeout(this._updateFeedTimeout); + } + + var feeds = gBrowser.selectedBrowser.feeds; + var haveFeeds = feeds && feeds.length > 0; + + var feedButtonPM = document.getElementById("ub-feed-button"); + + var feedButton = document.getElementById("feed-button"); + + if (feedButton) { + feedButton.disabled = !haveFeeds; + } + + if (feedButtonPM) { + if (!haveFeeds) { + feedButtonPM.collapsed = true; + feedButtonPM.removeAttribute("feed"); + } else { + feedButtonPM.collapsed = !gPrefService.getBoolPref("browser.urlbar.rss"); + } + } + + if (!haveFeeds) { + this._feedMenuitem.setAttribute("disabled", "true"); + this._feedMenuitem.removeAttribute("hidden"); + this._feedMenupopup.setAttribute("hidden", "true"); + return; + } + + if (feeds.length > 1) { + if (feedButtonPM) { + feedButtonPM.removeAttribute("feed"); + } + this._feedMenuitem.setAttribute("hidden", "true"); + this._feedMenupopup.removeAttribute("hidden"); + } else { + if (feedButtonPM) { + feedButtonPM.setAttribute("feed", feeds[0].href); + } + this._feedMenuitem.setAttribute("feed", feeds[0].href); + this._feedMenuitem.removeAttribute("disabled"); + this._feedMenuitem.removeAttribute("hidden"); + this._feedMenupopup.setAttribute("hidden", "true"); + } + }, + + addFeed: function(link, targetDoc) { + // find which tab this is for, and set the attribute on the browser + var browserForLink = gBrowser.getBrowserForDocument(targetDoc); + if (!browserForLink) { + // ignore feeds loaded in subframes (see bug 305472) + return; + } + + if (!browserForLink.feeds) { + browserForLink.feeds = []; + } + + browserForLink.feeds.push({ href: link.href, title: link.title }); + + // If this addition was for the current browser, update the UI. For + // background browsers, we'll update on tab switch. + if (browserForLink == gBrowser.selectedBrowser) { + var feedButtonPM = document.getElementById("ub-feed-button"); + if (feedButtonPM) { + feedButtonPM.collapsed = !gPrefService.getBoolPref("browser.urlbar.rss"); + } + // Batch updates to avoid updating the UI for multiple onLinkAdded events + // fired within 100ms of each other. + if (this._updateFeedTimeout) { + clearTimeout(this._updateFeedTimeout); + } + this._updateFeedTimeout = setTimeout(this.updateFeeds.bind(this), 100); + } + } +}; diff --git a/browser/base/content/browser-fullScreen.js b/browser/base/content/browser-fullScreen.js new file mode 100644 index 000000000..8ccfcb667 --- /dev/null +++ b/browser/base/content/browser-fullScreen.js @@ -0,0 +1,454 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +var FullScreen = { + _XULNS: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + + toggle: function () { + var enterFS = window.fullScreen; + + // Toggle the View:FullScreen command, which controls elements like the + // fullscreen menuitem, menubars, and the appmenu. + let fullscreenCommand = document.getElementById("View:FullScreen"); + if (enterFS) { + fullscreenCommand.setAttribute("checked", enterFS); + } else { + fullscreenCommand.removeAttribute("checked"); + } + + if (!this._fullScrToggler) { + this._fullScrToggler = document.getElementById("fullscr-toggler"); + this._fullScrToggler.addEventListener("mouseover", this._expandCallback, false); + this._fullScrToggler.addEventListener("dragenter", this._expandCallback, false); + } + + // On OS X Lion we don't want to hide toolbars when entering fullscreen, unless + // we're entering DOM fullscreen, in which case we should hide the toolbars. + // If we're leaving fullscreen, then we'll go through the exit code below to + // make sure toolbars are made visible in the case of DOM fullscreen. + if (enterFS && this.useLionFullScreen) { + if (document.mozFullScreen) { + this.showXULChrome("toolbar", false); + } else { + gNavToolbox.setAttribute("inFullscreen", true); + document.documentElement.setAttribute("inFullscreen", true); + } + return; + } + + // show/hide menubars, toolbars (except the full screen toolbar) + this.showXULChrome("toolbar", !enterFS); + + if (enterFS) { + document.addEventListener("keypress", this._keyToggleCallback, false); + document.addEventListener("popupshown", this._setPopupOpen, false); + document.addEventListener("popuphidden", this._setPopupOpen, false); + this._shouldAnimate = true; + if (gPrefService.getBoolPref("browser.fullscreen.autohide")) { + gBrowser.mPanelContainer.addEventListener("mousemove", + this._collapseCallback, false); + } + // We don't animate the toolbar collapse if in DOM full-screen mode, + // as the size of the content area would still be changing after the + // mozfullscreenchange event fired, which could confuse content script. + this.hideNavToolbox(document.mozFullScreen); + } else { + this.showNavToolbox(false); + // This is needed if they use the context menu to quit fullscreen + this._isPopupOpen = false; + + document.documentElement.removeAttribute("inDOMFullscreen"); + + this.cleanup(); + } + }, + + exitDomFullScreen : function() { + document.mozCancelFullScreen(); + }, + + handleEvent: function (event) { + switch (event.type) { + case "activate": + if (document.mozFullScreen) { + this.showWarning(this.fullscreenDoc); + } + break; + case "transitionend": + if (event.propertyName == "opacity") { + this.cancelWarning(); + } + break; + } + }, + + enterDomFullscreen : function(event) { + if (!document.mozFullScreen) { + return; + } + + // However, if we receive a "MozDOMFullscreen:NewOrigin" event for a document + // which is not a subdocument of a currently active (ie. visible) browser + // or iframe, we know that we've switched to a different frame since the + // request to enter full-screen was made, so we should exit full-screen + // since the "full-screen document" isn't acutally visible. + if (!event.target.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell).isActive) { + document.mozCancelFullScreen(); + return; + } + + let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + if (focusManager.activeWindow != window) { + // The top-level window has lost focus since the request to enter + // full-screen was made. Cancel full-screen. + document.mozCancelFullScreen(); + return; + } + + document.documentElement.setAttribute("inDOMFullscreen", true); + + if (gFindBarInitialized) { + gFindBar.close(); + } + + this.showWarning(event.target); + + // Exit DOM full-screen mode upon open, close, or change tab. + gBrowser.tabContainer.addEventListener("TabOpen", this.exitDomFullScreen); + gBrowser.tabContainer.addEventListener("TabClose", this.exitDomFullScreen); + gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen); + + // Add listener to detect when the fullscreen window is re-focused. + // If a fullscreen window loses focus, we show a warning when the + // fullscreen window is refocused. + if (!this.useLionFullScreen) { + window.addEventListener("activate", this); + } + + // Cancel any "hide the toolbar" animation which is in progress, and make + // the toolbar hide immediately. + this.hideNavToolbox(true); + }, + + cleanup: function () { + if (!window.fullScreen) { + gBrowser.mPanelContainer.removeEventListener("mousemove", + this._collapseCallback, false); + document.removeEventListener("keypress", this._keyToggleCallback, false); + document.removeEventListener("popupshown", this._setPopupOpen, false); + document.removeEventListener("popuphidden", this._setPopupOpen, false); + + this.cancelWarning(); + gBrowser.tabContainer.removeEventListener("TabOpen", this.exitDomFullScreen); + gBrowser.tabContainer.removeEventListener("TabClose", this.exitDomFullScreen); + gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen); + if (!this.useLionFullScreen) { + window.removeEventListener("activate", this); + } + this.fullscreenDoc = null; + } + }, + + // Event callbacks + _expandCallback: function() { + FullScreen.showNavToolbox(); + }, + _collapseCallback: function() { + FullScreen.hideNavToolbox(); + }, + _keyToggleCallback: function(aEvent) { + // if we can use the keyboard (eg Ctrl+L or Ctrl+E) to open the toolbars, we + // should provide a way to collapse them too. + if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { + FullScreen.hideNavToolbox(true); + } else if (aEvent.keyCode == aEvent.DOM_VK_F6) { + // F6 is another shortcut to the address bar, but its not covered in OpenLocation() + FullScreen.showNavToolbox(); + } + }, + + // Checks whether we are allowed to collapse the chrome + _isPopupOpen: false, + _isChromeCollapsed: false, + _safeToCollapse: function(forceHide) { + if (!gPrefService.getBoolPref("browser.fullscreen.autohide")) { + return false; + } + + // a popup menu is open in chrome: don't collapse chrome + if (!forceHide && this._isPopupOpen) { + return false; + } + + // a textbox in chrome is focused (location bar anyone?): don't collapse chrome + if (document.commandDispatcher.focusedElement && + document.commandDispatcher.focusedElement.ownerDocument == document && + document.commandDispatcher.focusedElement.localName == "input") { + if (forceHide) { + // hidden textboxes that still have focus are bad bad bad + document.commandDispatcher.focusedElement.blur(); + } else { + return false; + } + } + return true; + }, + + _setPopupOpen: function(aEvent) + { + // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed. + // Otherwise, they would not affect chrome and the user would expect the chrome to go away. + // e.g. we wouldn't want the autoscroll icon firing this event, so when the user + // toggles chrome when moving mouse to the top, it doesn't go away again. + if (aEvent.type == "popupshown" && !FullScreen._isChromeCollapsed && + aEvent.target.localName != "tooltip" && aEvent.target.localName != "window") { + FullScreen._isPopupOpen = true; + } else if (aEvent.type == "popuphidden" && aEvent.target.localName != "tooltip" && + aEvent.target.localName != "window") { + FullScreen._isPopupOpen = false; + } + }, + + // Autohide helpers for the context menu item + getAutohide: function(aItem) { + aItem.setAttribute("checked", gPrefService.getBoolPref("browser.fullscreen.autohide")); + }, + setAutohide: function() { + gPrefService.setBoolPref("browser.fullscreen.autohide", !gPrefService.getBoolPref("browser.fullscreen.autohide")); + }, + + // Animate the toolbars disappearing + _shouldAnimate: true, + + cancelWarning: function(event) { + if (!this.warningBox) { + return; + } + this.warningBox.removeEventListener("transitionend", this); + if (this.warningFadeOutTimeout) { + clearTimeout(this.warningFadeOutTimeout); + this.warningFadeOutTimeout = null; + } + + // Ensure focus switches away from the (now hidden) warning box. If the user + // clicked buttons in the fullscreen key authorization UI, it would have been + // focused, and any key events would be directed at the (now hidden) chrome + // document instead of the target document. + gBrowser.selectedBrowser.focus(); + + this.warningBox.setAttribute("hidden", true); + this.warningBox.removeAttribute("fade-warning-out"); + this.warningBox = null; + }, + + warningBox: null, + warningFadeOutTimeout: null, + fullscreenDoc: null, + + // Shows a warning that the site has entered fullscreen for a short duration. + showWarning: function(targetDoc) { + let timeout = gPrefService.getIntPref("full-screen-api.warning.timeout"); + if (!document.mozFullScreen || timeout <= 0) { + return; + } + + // Set the strings on the fullscreen warning UI. + this.fullscreenDoc = targetDoc; + let uri = this.fullscreenDoc.nodePrincipal.URI; + let host = null; + try { + host = uri.host; + } catch(e) {} + let hostLabel = document.getElementById("full-screen-domain-text"); + if (host) { + // Document's principal's URI has a host. Display a warning including the hostname. + let utils = {}; + Cu.import("resource://gre/modules/DownloadUtils.jsm", utils); + let displayHost = utils.DownloadUtils.getURIHost(uri.spec)[0]; + let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); + + hostLabel.textContent = bundle.formatStringFromName("fullscreen.entered", [displayHost], 1); + hostLabel.removeAttribute("hidden"); + } else { + hostLabel.setAttribute("hidden", "true"); + } + + // Note: the warning box can be non-null if the warning box from the previous request + // wasn't hidden before another request was made. + if (!this.warningBox) { + this.warningBox = document.getElementById("full-screen-warning-container"); + // Add a listener to clean up state after the warning is hidden. + this.warningBox.addEventListener("transitionend", this); + this.warningBox.removeAttribute("hidden"); + } else { + if (this.warningFadeOutTimeout) { + clearTimeout(this.warningFadeOutTimeout); + this.warningFadeOutTimeout = null; + } + this.warningBox.removeAttribute("fade-warning-out"); + } + + // Set a timeout to fade the warning out after a few moments. + this.warningFadeOutTimeout = setTimeout(() => { + if (this.warningBox) { + this.warningBox.setAttribute("fade-warning-out", "true"); + } + }, timeout); + }, + + showNavToolbox: function(trackMouse = true) { + this._fullScrToggler.hidden = true; + gNavToolbox.removeAttribute("fullscreenShouldAnimate"); + gNavToolbox.style.marginTop = ""; + + if (!this._isChromeCollapsed) { + return; + } + + // Track whether mouse is near the toolbox + this._isChromeCollapsed = false; + if (trackMouse) { + gBrowser.mPanelContainer.addEventListener("mousemove", + this._collapseCallback, false); + } + }, + + hideNavToolbox: function(forceHide = false) { + this._fullScrToggler.hidden = document.mozFullScreen; + if (this._isChromeCollapsed) { + if (forceHide) { + gNavToolbox.removeAttribute("fullscreenShouldAnimate"); + } + return; + } + if (!this._safeToCollapse(forceHide)) { + this._fullScrToggler.hidden = true; + return; + } + + // browser.fullscreen.animateUp + // 0 - never animate up + // 1 - animate only for first collapse after entering fullscreen (default for perf's sake) + // 2 - animate every time it collapses + let animateUp = gPrefService.getIntPref("browser.fullscreen.animateUp"); + if (animateUp == 0) { + this._shouldAnimate = false; + } else if (animateUp == 2) { + this._shouldAnimate = true; + } + if (this._shouldAnimate && !forceHide) { + gNavToolbox.setAttribute("fullscreenShouldAnimate", true); + this._shouldAnimate = false; + // Hide the fullscreen toggler until the transition ends. + let listener = () => { + gNavToolbox.removeEventListener("transitionend", listener, true); + if (this._isChromeCollapsed) + this._fullScrToggler.hidden = false; + }; + gNavToolbox.addEventListener("transitionend", listener, true); + this._fullScrToggler.hidden = true; + } + + gNavToolbox.style.marginTop = + -gNavToolbox.getBoundingClientRect().height + "px"; + this._isChromeCollapsed = true; + gBrowser.mPanelContainer.removeEventListener("mousemove", + this._collapseCallback, false); + }, + + showXULChrome: function(aTag, aShow) + { + var els = document.getElementsByTagNameNS(this._XULNS, aTag); + + for (let el of els) { + // XXX don't interfere with previously collapsed toolbars + if (el.getAttribute("fullscreentoolbar") == "true") { + if (!aShow) { + + var toolbarMode = el.getAttribute("mode"); + if (toolbarMode != "text") { + el.setAttribute("saved-mode", toolbarMode); + el.setAttribute("saved-iconsize", el.getAttribute("iconsize")); + el.setAttribute("mode", "icons"); + el.setAttribute("iconsize", "small"); + } + + // Give the main nav bar and the tab bar the fullscreen context menu, + // otherwise remove context menu to prevent breakage + el.setAttribute("saved-context", el.getAttribute("context")); + if (el.id == "nav-bar" || el.id == "TabsToolbar") { + el.setAttribute("context", "autohide-context"); + } else { + el.removeAttribute("context"); + } + + // Set the inFullscreen attribute to allow specific styling + // in fullscreen mode + el.setAttribute("inFullscreen", true); + } else { + var restoreAttr = function restoreAttr(attrName) { + var savedAttr = "saved-" + attrName; + if (el.hasAttribute(savedAttr)) { + el.setAttribute(attrName, el.getAttribute(savedAttr)); + el.removeAttribute(savedAttr); + } + } + + restoreAttr("mode"); + restoreAttr("iconsize"); + restoreAttr("context"); + + el.removeAttribute("inFullscreen"); + } + } else { + // use moz-collapsed so it doesn't persist hidden/collapsed, + // so that new windows don't have missing toolbars + if (aShow) { + el.removeAttribute("moz-collapsed"); + } else { + el.setAttribute("moz-collapsed", "true"); + } + } + } + + if (aShow) { + gNavToolbox.removeAttribute("inFullscreen"); + document.documentElement.removeAttribute("inFullscreen"); + } else { + gNavToolbox.setAttribute("inFullscreen", true); + document.documentElement.setAttribute("inFullscreen", true); + } + + // In tabs-on-top mode, move window controls to the tab bar, + // and in tabs-on-bottom mode, move them back to the navigation toolbar. + // When there is a chance the tab bar may be collapsed, put window + // controls on nav bar. + var fullscreenctls = document.getElementById("window-controls"); + var navbar = document.getElementById("nav-bar"); + var ctlsOnTabbar = window.toolbar.visible && + (navbar.collapsed || + (TabsOnTop.enabled && + !gPrefService.getBoolPref("browser.tabs.autoHide"))); + if (fullscreenctls.parentNode == navbar && ctlsOnTabbar) { + fullscreenctls.removeAttribute("flex"); + document.getElementById("TabsToolbar").appendChild(fullscreenctls); + } else if (fullscreenctls.parentNode.id == "TabsToolbar" && !ctlsOnTabbar) { + fullscreenctls.setAttribute("flex", "1"); + navbar.appendChild(fullscreenctls); + } + fullscreenctls.hidden = aShow; + + ToolbarIconColor.inferFromText(); + } +}; +XPCOMUtils.defineLazyGetter(FullScreen, "useLionFullScreen", function() { + // We'll only use OS X Lion full screen if we're + // * on OS X + // * on Lion or higher (Darwin 11+) + // * have fullscreenbutton="true" + return false; +}); diff --git a/browser/base/content/browser-fullZoom.js b/browser/base/content/browser-fullZoom.js new file mode 100644 index 000000000..b1861a14e --- /dev/null +++ b/browser/base/content/browser-fullZoom.js @@ -0,0 +1,539 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Controls the "full zoom" setting and its site-specific preferences. + */ +var FullZoom = { + // Identifies the setting in the content prefs database. + name: "browser.content.full-zoom", + + // browser.zoom.siteSpecific preference cache + _siteSpecificPref: undefined, + + // browser.zoom.updateBackgroundTabs preference cache + updateBackgroundTabs: undefined, + + // This maps the browser to monotonically increasing integer + // tokens. _browserTokenMap[browser] is increased each time the zoom is + // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses. + _browserTokenMap: new WeakMap(), + + // Stores initial locations if we receive onLocationChange + // events before we're initialized. + _initialLocations: new WeakMap(), + + get siteSpecific() { + return this._siteSpecificPref; + }, + + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener, + Ci.nsIObserver, + Ci.nsIContentPrefObserver, + Ci.nsISupportsWeakReference, + Ci.nsISupports]), + + // Initialization & Destruction + + init: function() { + gBrowser.addEventListener("ZoomChangeUsingMouseWheel", this); + + // Register ourselves with the service so we know when our pref changes. + this._cps2 = Cc["@mozilla.org/content-pref/service;1"]. + getService(Ci.nsIContentPrefService2); + this._cps2.addObserverForName(this.name, this); + + this._siteSpecificPref = + gPrefService.getBoolPref("browser.zoom.siteSpecific"); + this.updateBackgroundTabs = + gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs"); + // Listen for changes to the browser.zoom branch so we can enable/disable + // updating background tabs and per-site saving and restoring of zoom levels. + gPrefService.addObserver("browser.zoom.", this, true); + + // If we received onLocationChange events for any of the current browsers + // before we were initialized we want to replay those upon initialization. + for (let browser of gBrowser.browsers) { + if (this._initialLocations.has(browser)) { + this.onLocationChange(...this._initialLocations.get(browser), browser); + } + } + + // This should be nulled after initialization. + this._initialLocations = null; + }, + + destroy: function() { + gPrefService.removeObserver("browser.zoom.", this); + this._cps2.removeObserverForName(this.name, this); + gBrowser.removeEventListener("ZoomChangeUsingMouseWheel", this); + }, + + + // Event Handlers + + // nsIDOMEventListener + + handleEvent: function(event) { + switch (event.type) { + case "ZoomChangeUsingMouseWheel": + let browser = this._getTargetedBrowser(event); + this._ignorePendingZoomAccesses(browser); + this._applyZoomToPref(browser); + break; + } + }, + + // nsIObserver + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "nsPref:changed": + switch (aData) { + case "browser.zoom.siteSpecific": + this._siteSpecificPref = + gPrefService.getBoolPref("browser.zoom.siteSpecific"); + break; + case "browser.zoom.updateBackgroundTabs": + this.updateBackgroundTabs = + gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs"); + break; + } + break; + } + }, + + // nsIContentPrefObserver + + onContentPrefSet: function(aGroup, aName, aValue, aIsPrivate) { + this._onContentPrefChanged(aGroup, aValue, aIsPrivate); + }, + + onContentPrefRemoved: function(aGroup, aName, aIsPrivate) { + this._onContentPrefChanged(aGroup, undefined, aIsPrivate); + }, + + /** + * Appropriately updates the zoom level after a content preference has + * changed. + * + * @param aGroup The group of the changed preference. + * @param aValue The new value of the changed preference. Pass undefined to + * indicate the preference's removal. + */ + _onContentPrefChanged: function(aGroup, aValue, aIsPrivate) { + if (this._isNextContentPrefChangeInternal) { + // Ignore changes that FullZoom itself makes. This works because the + // content pref service calls callbacks before notifying observers, and it + // does both in the same turn of the event loop. + delete this._isNextContentPrefChangeInternal; + return; + } + + let browser = gBrowser.selectedBrowser; + if (!browser.currentURI) { + return; + } + + let ctxt = this._loadContextFromBrowser(browser); + let domain = this._cps2.extractDomain(browser.currentURI.spec); + if (aGroup) { + if (aGroup == domain && ctxt.usePrivateBrowsing == aIsPrivate) { + this._applyPrefToZoom(aValue, browser); + } + return; + } + + this._globalValue = aValue === undefined ? + aValue : + this._ensureValid(aValue); + + // If the current page doesn't have a site-specific preference, then its + // zoom should be set to the new global preference now that the global + // preference has changed. + let hasPref = false; + let token = this._getBrowserToken(browser); + this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, { + handleResult: function() { hasPref = true; }, + handleCompletion: function() { + if (!hasPref && token.isCurrent) { + this._applyPrefToZoom(undefined, browser); + } + }.bind(this) + }); + }, + + // location change observer + + /** + * Called when the location of a tab changes. + * When that happens, we need to update the current zoom level if appropriate. + * + * @param aURI + * A URI object representing the new location. + * @param aIsTabSwitch + * Whether this location change has happened because of a tab switch. + * @param aBrowser + * (optional) browser object displaying the document + */ + onLocationChange: function(aURI, aIsTabSwitch, aBrowser) { + let browser = aBrowser || gBrowser.selectedBrowser; + + // If we haven't been initialized yet but receive an onLocationChange + // notification then let's store and replay it upon initialization. + if (this._initialLocations) { + this._initialLocations.set(browser, [aURI, aIsTabSwitch]); + return; + } + + // Ignore all pending async zoom accesses in the browser. Pending accesses + // that started before the location change will be prevented from applying + // to the new location. + this._ignorePendingZoomAccesses(browser); + + if (!aURI || (aIsTabSwitch && !this.siteSpecific)) { + this._notifyOnLocationChange(browser); + return; + } + + // Avoid the cps roundtrip and apply the default/global pref. + if (aURI.spec == "about:blank") { + this._applyPrefToZoom(undefined, browser, + this._notifyOnLocationChange.bind(this, browser)); + return; + } + + // Media documents should always start at 1, and are not affected by prefs. + if (!aIsTabSwitch && browser.isSyntheticDocument) { + ZoomManager.setZoomForBrowser(browser, 1); + // _ignorePendingZoomAccesses already called above, so no need here. + this._notifyOnLocationChange(browser); + return; + } + + // See if the zoom pref is cached. + let ctxt = this._loadContextFromBrowser(browser); + let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt); + if (pref) { + this._applyPrefToZoom(pref.value, browser, + this._notifyOnLocationChange.bind(this, browser)); + return; + } + + // It's not cached, so we have to asynchronously fetch it. + let value = undefined; + let token = this._getBrowserToken(browser); + this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, { + handleResult: function(resultPref) { value = resultPref.value; }, + handleCompletion: function() { + if (!token.isCurrent) { + this._notifyOnLocationChange(browser); + return; + } + this._applyPrefToZoom(value, browser, + this._notifyOnLocationChange.bind(this, browser)); + }.bind(this) + }); + }, + + // update state of zoom type menu item + + updateMenu: function() { + var menuItem = document.getElementById("toggle_zoom"); + + menuItem.setAttribute("checked", !ZoomManager.useFullZoom); + }, + + // Setting & Pref Manipulation + + /** + * Reduces the zoom level of the page in the current browser. + */ + reduce: function() { + ZoomManager.reduce(); + let browser = gBrowser.selectedBrowser; + this._ignorePendingZoomAccesses(browser); + this._applyZoomToPref(browser); + }, + + /** + * Enlarges the zoom level of the page in the current browser. + */ + enlarge: function() { + ZoomManager.enlarge(); + let browser = gBrowser.selectedBrowser; + this._ignorePendingZoomAccesses(browser); + this._applyZoomToPref(browser); + }, + + /** + * Sets the zoom level for the given browser to the given floating + * point value, where 1 is the default zoom level. + */ + setZoom: function(value, browser = gBrowser.selectedBrowser) { + ZoomManager.setZoomForBrowser(browser, value); + this._ignorePendingZoomAccesses(browser); + this._applyZoomToPref(browser); + }, + + /** + * Sets the zoom level of the page in the given browser to the global zoom + * level. + * + * @return A promise which resolves when the zoom reset has been applied. + */ + reset: function(browser = gBrowser.selectedBrowser) { + let token = this._getBrowserToken(browser); + let result = this._getGlobalValue(browser).then(value => { + if (token.isCurrent) { + ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value); + this._ignorePendingZoomAccesses(browser); + Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", ""); + } + }); + this._removePref(browser); + return result; + }, + + /** + * Set the zoom level for a given browser. + * + * Per nsPresContext::setFullZoom, we can set the zoom to its current value + * without significant impact on performance, as the setting is only applied + * if it differs from the current setting. In fact getting the zoom and then + * checking ourselves if it differs costs more. + * + * And perhaps we should always set the zoom even if it was more expensive, + * since nsDocumentViewer::SetTextZoom claims that child documents can have + * a different text zoom (although it would be unusual), and it implies that + * those child text zooms should get updated when the parent zoom gets set, + * and perhaps the same is true for full zoom + * (although nsDocumentViewer::SetFullZoom doesn't mention it). + * + * So when we apply new zoom values to the browser, we simply set the zoom. + * We don't check first to see if the new value is the same as the current + * one. + * + * @param aValue The zoom level value. + * @param aBrowser The zoom is set in this browser. Required. + * @param aCallback If given, it's asynchronously called when complete. + */ + _applyPrefToZoom: function(aValue, aBrowser, aCallback) { + if (!this.siteSpecific || gInPrintPreviewMode) { + this._executeSoon(aCallback); + return; + } + + // The browser is sometimes half-destroyed because this method is called + // by content pref service callbacks, which themselves can be called at any + // time, even after browsers are closed. + if (!aBrowser.parentNode || aBrowser.isSyntheticDocument) { + this._executeSoon(aCallback); + return; + } + + if (aValue !== undefined) { + ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue)); + this._ignorePendingZoomAccesses(aBrowser); + this._executeSoon(aCallback); + return; + } + + let token = this._getBrowserToken(aBrowser); + this._getGlobalValue(aBrowser).then(value => { + if (token.isCurrent) { + ZoomManager.setZoomForBrowser(aBrowser, value === undefined ? 1 : value); + this._ignorePendingZoomAccesses(aBrowser); + } + this._executeSoon(aCallback); + }); + }, + + /** + * Saves the zoom level of the page in the given browser to the content + * prefs store. + * + * @param browser The zoom of this browser will be saved. Required. + */ + _applyZoomToPref: function(browser) { + Services.obs.notifyObservers(browser, "browser-fullZoom:zoomChange", ""); + if (!this.siteSpecific || + gInPrintPreviewMode || + browser.isSyntheticDocument) { + return; + } + + this._cps2.set(browser.currentURI.spec, this.name, + ZoomManager.getZoomForBrowser(browser), + this._loadContextFromBrowser(browser), { + handleCompletion: function() { + this._isNextContentPrefChangeInternal = true; + }.bind(this), + }); + }, + + /** + * Removes from the content prefs store the zoom level of the given browser. + * + * @param browser The zoom of this browser will be removed. Required. + */ + _removePref: function(browser) { + Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", ""); + if (browser.isSyntheticDocument) { + return; + } + let ctxt = this._loadContextFromBrowser(browser); + this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, { + handleCompletion: function() { + this._isNextContentPrefChangeInternal = true; + }.bind(this), + }); + }, + + // Utilities + + /** + * Returns the zoom change token of the given browser. Asynchronous + * operations that access the given browser's zoom should use this method to + * capture the token before starting and use token.isCurrent to determine if + * it's safe to access the zoom when done. If token.isCurrent is false, then + * after the async operation started, either the browser's zoom was changed or + * the browser was destroyed, and depending on what the operation is doing, it + * may no longer be safe to set and get its zoom. + * + * @param browser The token of this browser will be returned. + * @return An object with an "isCurrent" getter. + */ + _getBrowserToken: function(browser) { + let map = this._browserTokenMap; + if (!map.has(browser)) { + map.set(browser, 0); + } + return { + token: map.get(browser), + get isCurrent() { + // At this point, the browser may have been destructed and unbound but + // its outer ID not removed from the map because outer-window-destroyed + // hasn't been received yet. In that case, the browser is unusable, it + // has no properties, so return false. Check for this case by getting a + // property, say, docShell. + return map.get(browser) === this.token && browser.parentNode; + }, + }; + }, + + /** + * Returns the browser that the supplied zoom event is associated with. + * @param event The ZoomChangeUsingMouseWheel event. + * @return The associated browser element, if one exists, otherwise null. + */ + _getTargetedBrowser: function(event) { + let target = event.originalTarget; + + // With remote content browsers, the event's target is the browser + // we're looking for. + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + if (target instanceof window.XULElement && + target.localName == "browser" && + target.namespaceURI == XUL_NS) { + return target; + } + + // With in-process content browsers, the event's target is the content + // document. + if (target.nodeType == Node.DOCUMENT_NODE) { + return gBrowser.getBrowserForDocument(target); + } + + throw new Error("Unexpected ZoomChangeUsingMouseWheel event source"); + }, + + /** + * Increments the zoom change token for the given browser so that pending + * async operations know that it may be unsafe to access they zoom when they + * finish. + * + * @param browser Pending accesses in this browser will be ignored. + */ + _ignorePendingZoomAccesses: function(browser) { + let map = this._browserTokenMap; + map.set(browser, (map.get(browser) || 0) + 1); + }, + + _ensureValid: function(aValue) { + // Note that undefined is a valid value for aValue that indicates a known- + // not-to-exist value. + if (isNaN(aValue)) { + return 1; + } + + if (aValue < ZoomManager.MIN) { + return ZoomManager.MIN; + } + + if (aValue > ZoomManager.MAX) { + return ZoomManager.MAX; + } + + return aValue; + }, + + /** + * Gets the global browser.content.full-zoom content preference. + * + * @param browser The browser pertaining to the zoom. + * @returns Promise<prefValue> + * Resolves to the preference value when done. + */ + _getGlobalValue: function(browser) { + // * !("_globalValue" in this) => global value not yet cached. + // * this._globalValue === undefined => global value known not to exist. + // * Otherwise, this._globalValue is a number, the global value. + return new Promise(resolve => { + if ("_globalValue" in this) { + resolve(this._globalValue); + return; + } + let value = undefined; + this._cps2.getGlobal(this.name, this._loadContextFromBrowser(browser), { + handleResult: function(pref) { value = pref.value; }, + handleCompletion: (reason) => { + this._globalValue = this._ensureValid(value); + resolve(this._globalValue); + } + }); + }); + }, + + /** + * Gets the load context from the given Browser. + * + * @param Browser The Browser whose load context will be returned. + * @return The nsILoadContext of the given Browser. + */ + _loadContextFromBrowser: function(browser) { + return browser.loadContext; + }, + + /** + * Asynchronously broadcasts "browser-fullZoom:location-change" so that + * listeners can be notified when the zoom levels on those pages change. + * The notification is always asynchronous so that observers are guaranteed a + * consistent behavior. + */ + _notifyOnLocationChange: function(browser) { + this._executeSoon(function() { + Services.obs.notifyObservers(browser, "browser-fullZoom:location-change", ""); + }); + }, + + _executeSoon: function(callback) { + if (!callback) { + return; + } + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); + }, +}; diff --git a/browser/base/content/browser-gestureSupport.js b/browser/base/content/browser-gestureSupport.js new file mode 100644 index 000000000..afd050462 --- /dev/null +++ b/browser/base/content/browser-gestureSupport.js @@ -0,0 +1,1189 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Simple gestures support +// +// As per bug #412486, web content must not be allowed to receive any +// simple gesture events. Multi-touch gesture APIs are in their +// infancy and we do NOT want to be forced into supporting an API that +// will probably have to change in the future. (The current Mac OS X +// API is undocumented and was reverse-engineered.) Until support is +// implemented in the event dispatcher to keep these events as +// chrome-only, we must listen for the simple gesture events during +// the capturing phase and call stopPropagation on every event. + +var gGestureSupport = { + _currentRotation: 0, + _lastRotateDelta: 0, + _rotateMomentumThreshold: .75, + + /** + * Add or remove mouse gesture event listeners + * + * @param aAddListener + * True to add/init listeners and false to remove/uninit + */ + init: function(aAddListener) { + const gestureEvents = ["SwipeGestureMayStart", "SwipeGestureStart", + "SwipeGestureUpdate", "SwipeGestureEnd", "SwipeGesture", + "MagnifyGestureStart", "MagnifyGestureUpdate", "MagnifyGesture", + "RotateGestureStart", "RotateGestureUpdate", "RotateGesture", + "TapGesture", "PressTapGesture"]; + + let addRemove = aAddListener ? + window.addEventListener : + window.removeEventListener; + + for (let event of gestureEvents) { + addRemove("Moz" + event, this, true); + } + }, + + /** + * Dispatch events based on the type of mouse gesture event. For now, make + * sure to stop propagation of every gesture event so that web content cannot + * receive gesture events. + * + * @param aEvent + * The gesture event to handle + */ + handleEvent: function(aEvent) { + if (!Services.prefs.getBoolPref("dom.debug.propagate_gesture_events_through_content")) { + aEvent.stopPropagation(); + } + + // Create a preference object with some defaults + let def = (aThreshold, aLatched) => ({ threshold: aThreshold, latched: !!aLatched }); + + switch (aEvent.type) { + case "MozSwipeGestureMayStart": + if (this._shouldDoSwipeGesture(aEvent)) { + aEvent.preventDefault(); + } + break; + case "MozSwipeGestureStart": + aEvent.preventDefault(); + this._setupSwipeGesture(); + break; + case "MozSwipeGestureUpdate": + aEvent.preventDefault(); + this._doUpdate(aEvent); + break; + case "MozSwipeGestureEnd": + aEvent.preventDefault(); + this._doEnd(aEvent); + break; + case "MozSwipeGesture": + aEvent.preventDefault(); + this.onSwipe(aEvent); + break; + case "MozMagnifyGestureStart": + aEvent.preventDefault(); +#ifdef XP_WIN + this._setupGesture(aEvent, "pinch", def(25, 0), "out", "in"); +#else + this._setupGesture(aEvent, "pinch", def(150, 1), "out", "in"); +#endif + break; + case "MozRotateGestureStart": + aEvent.preventDefault(); + this._setupGesture(aEvent, "twist", def(25, 0), "right", "left"); + break; + case "MozMagnifyGestureUpdate": + case "MozRotateGestureUpdate": + aEvent.preventDefault(); + this._doUpdate(aEvent); + break; + case "MozTapGesture": + aEvent.preventDefault(); + this._doAction(aEvent, ["tap"]); + break; + case "MozRotateGesture": + aEvent.preventDefault(); + this._doAction(aEvent, ["twist", "end"]); + break; + /* case "MozPressTapGesture": + break; */ + } + }, + + /** + * Called at the start of "pinch" and "twist" gestures to setup all of the + * information needed to process the gesture + * + * @param aEvent + * The continual motion start event to handle + * @param aGesture + * Name of the gesture to handle + * @param aPref + * Preference object with the names of preferences and defaults + * @param aInc + * Command to trigger for increasing motion (without gesture name) + * @param aDec + * Command to trigger for decreasing motion (without gesture name) + */ + _setupGesture: function(aEvent, aGesture, aPref, aInc, aDec) { + // Try to load user-set values from preferences + for (let [pref, def] in Iterator(aPref)) { + aPref[pref] = this._getPref(aGesture + "." + pref, def); + } + + // Keep track of the total deltas and latching behavior + let offset = 0; + let latchDir = aEvent.delta > 0 ? 1 : -1; + let isLatched = false; + + // Create the update function here to capture closure state + this._doUpdate = function(aEvent) { + // Update the offset with new event data + offset += aEvent.delta; + + // Check if the cumulative deltas exceed the threshold + if (Math.abs(offset) > aPref["threshold"]) { + // Trigger the action if we don't care about latching; otherwise, make + // sure either we're not latched and going the same direction of the + // initial motion; or we're latched and going the opposite way + let sameDir = (latchDir ^ offset) >= 0; + if (!aPref["latched"] || (isLatched ^ sameDir)) { + this._doAction(aEvent, [aGesture, offset > 0 ? aInc : aDec]); + + // We must be getting latched or leaving it, so just toggle + isLatched = !isLatched; + } + + // Reset motion counter to prepare for more of the same gesture + offset = 0; + } + }; + + // The start event also contains deltas, so handle an update right away + this._doUpdate(aEvent); + }, + + /** + * Checks whether a swipe gesture event can navigate the browser history or + * not. + * + * @param aEvent + * The swipe gesture event. + * @return true if the swipe event may navigate the history, false othwerwise. + */ + _swipeNavigatesHistory: function(aEvent) { + return this._getCommand(aEvent, ["swipe", "left"]) == "Browser:BackOrBackDuplicate" && + this._getCommand(aEvent, ["swipe", "right"]) == "Browser:ForwardOrForwardDuplicate"; + }, + + /** + * Checks whether we want to start a swipe for aEvent and sets + * aEvent.allowedDirections to the right values. + * + * @param aEvent + * The swipe gesture "MayStart" event. + * @return true if we're willing to start a swipe for this event, false + * otherwise. + */ + _shouldDoSwipeGesture: function(aEvent) { + if (!this._swipeNavigatesHistory(aEvent)) { + return false; + } + + let canGoBack = gHistorySwipeAnimation.canGoBack(); + let canGoForward = gHistorySwipeAnimation.canGoForward(); + let isLTR = gHistorySwipeAnimation.isLTR; + + if (canGoBack) { + aEvent.allowedDirections |= isLTR ? + aEvent.DIRECTION_LEFT : + aEvent.DIRECTION_RIGHT; + } + if (canGoForward) { + aEvent.allowedDirections |= isLTR ? + aEvent.DIRECTION_RIGHT : + aEvent.DIRECTION_LEFT; + } + + return true; + }, + + /** + * Sets up swipe gestures. This includes setting up swipe animations for the + * gesture, if enabled. + * + * @param aEvent + * The swipe gesture start event. + * @return true if swipe gestures could successfully be set up, false + * othwerwise. + */ + _setupSwipeGesture: function() { + gHistorySwipeAnimation.startAnimation(false); + + this._doUpdate = function(aEvent) { + gHistorySwipeAnimation.updateAnimation(aEvent.delta); + }; + + this._doEnd = function(aEvent) { + gHistorySwipeAnimation.swipeEndEventReceived(); + + this._doUpdate = function(aEvent) {}; + this._doEnd = function(aEvent) {}; + } + }, + + /** + * Generator producing the powerset of the input array where the first result + * is the complete set and the last result (before StopIteration) is empty. + * + * @param aArray + * Source array containing any number of elements + * @yield Array that is a subset of the input array from full set to empty + */ + _power: function(aArray) { + // Create a bitmask based on the length of the array + let num = 1 << aArray.length; + while (--num >= 0) { + // Only select array elements where the current bit is set + yield aArray.reduce(function(aPrev, aCurr, aIndex) { + if (num & 1 << aIndex) { + aPrev.push(aCurr); + } + return aPrev; + }, []); + } + }, + + /** + * Determine what action to do for the gesture based on which keys are + * pressed and which commands are set, and execute the command. + * + * @param aEvent + * The original gesture event to convert into a fake click event + * @param aGesture + * Array of gesture name parts (to be joined by periods) + * @return Name of the executed command. Returns null if no command is + * found. + */ + _doAction: function(aEvent, aGesture) { + let command = this._getCommand(aEvent, aGesture); + return command && this._doCommand(aEvent, command); + }, + + /** + * Determine what action to do for the gesture based on which keys are + * pressed and which commands are set + * + * @param aEvent + * The original gesture event to convert into a fake click event + * @param aGesture + * Array of gesture name parts (to be joined by periods) + */ + _getCommand: function(aEvent, aGesture) { + // Create an array of pressed keys in a fixed order so that a command for + // "meta" is preferred over "ctrl" when both buttons are pressed (and a + // command for both don't exist) + let keyCombos = []; + ["shift", "alt", "ctrl", "meta"].forEach(function(key) { + if (aEvent[key + "Key"]) { + keyCombos.push(key); + } + }); + + // Try each combination of key presses in decreasing order for commands + for (let subCombo of this._power(keyCombos)) { + // Convert a gesture and pressed keys into the corresponding command + // action where the preference has the gesture before "shift" before + // "alt" before "ctrl" before "meta" all separated by periods + let command; + try { + command = this._getPref(aGesture.concat(subCombo).join(".")); + } catch(e) {} + + if (command) { + return command; + } + } + return null; + }, + + /** + * Execute the specified command. + * + * @param aEvent + * The original gesture event to convert into a fake click event + * @param aCommand + * Name of the command found for the event's keys and gesture. + */ + _doCommand: function(aEvent, aCommand) { + let node = document.getElementById(aCommand); + if (node) { + if (node.getAttribute("disabled") != "true") { + let cmdEvent = document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent("command", true, true, window, 0, + aEvent.ctrlKey, aEvent.altKey, + aEvent.shiftKey, aEvent.metaKey, aEvent); + node.dispatchEvent(cmdEvent); + } + } else { + goDoCommand(aCommand); + } + }, + + /** + * Handle continual motion events. This function will be set by + * _setupGesture or _setupSwipe. + * + * @param aEvent + * The continual motion update event to handle + */ + _doUpdate: function(aEvent) {}, + + /** + * Handle gesture end events. This function will be set by _setupSwipe. + * + * @param aEvent + * The gesture end event to handle + */ + _doEnd: function(aEvent) {}, + + /** + * Convert the swipe gesture into a browser action based on the direction. + * + * @param aEvent + * The swipe event to handle + */ + onSwipe: function(aEvent) { + // Figure out which one (and only one) direction was triggered + for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) { + if (aEvent.direction == aEvent["DIRECTION_" + dir]) { + this._coordinateSwipeEventWithAnimation(aEvent, dir); + break; + } + } + }, + + /** + * Process a swipe event based on the given direction. + * + * @param aEvent + * The swipe event to handle + * @param aDir + * The direction for the swipe event + */ + processSwipeEvent: function(aEvent, aDir) { + this._doAction(aEvent, ["swipe", aDir.toLowerCase()]); + }, + + /** + * Coordinates the swipe event with the swipe animation, if any. + * If an animation is currently running, the swipe event will be + * processed once the animation stops. This will guarantee a fluid + * motion of the animation. + * + * @param aEvent + * The swipe event to handle + * @param aDir + * The direction for the swipe event + */ + _coordinateSwipeEventWithAnimation: + function(aEvent, aDir) { + if ((gHistorySwipeAnimation.isAnimationRunning()) && + (aDir == "RIGHT" || aDir == "LEFT")) { + gHistorySwipeAnimation.processSwipeEvent(aEvent, aDir); + } else { + this.processSwipeEvent(aEvent, aDir); + } + }, + + /** + * Get a gesture preference or use a default if it doesn't exist + * + * @param aPref + * Name of the preference to load under the gesture branch + * @param aDef + * Default value if the preference doesn't exist + */ + _getPref: function(aPref, aDef) { + // Preferences branch under which all gestures preferences are stored + const branch = "browser.gesture."; + + try { + // Determine what type of data to load based on default value's type + let type = typeof aDef; + let getFunc = "get" + (type == "boolean" ? "Bool" : + type == "number" ? "Int" : "Char") + "Pref"; + return gPrefService[getFunc](branch + aPref); + } catch(e) { + return aDef; + } + }, + + /** + * Perform rotation for ImageDocuments + * + * @param aEvent + * The MozRotateGestureUpdate event triggering this call + */ + rotate: function(aEvent) { + if (!(content.document instanceof ImageDocument)) { + return; + } + + let contentElement = content.document.body.firstElementChild; + if (!contentElement) { + return; + } + // If we're currently snapping, cancel that snap + if (contentElement.classList.contains("completeRotation")) { + this._clearCompleteRotation(); + } + + this.rotation = Math.round(this.rotation + aEvent.delta); + contentElement.style.transform = "rotate(" + this.rotation + "deg)"; + this._lastRotateDelta = aEvent.delta; + }, + + /** + * Perform a rotation end for ImageDocuments + */ + rotateEnd: function() { + if (!(content.document instanceof ImageDocument)) { + return; + } + + let contentElement = content.document.body.firstElementChild; + if (!contentElement) { + return; + } + + let transitionRotation = 0; + + // The reason that 360 is allowed here is because when rotating between + // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong + // direction around--spinning wildly. + if (this.rotation <= 45) { + transitionRotation = 0; + } else if (this.rotation > 45 && this.rotation <= 135) { + transitionRotation = 90; + } else if (this.rotation > 135 && this.rotation <= 225) { + transitionRotation = 180; + } else if (this.rotation > 225 && this.rotation <= 315) { + transitionRotation = 270; + } else { + transitionRotation = 360; + } + + // If we're going fast enough, and we didn't already snap ahead of rotation, + // then snap ahead of rotation to simulate momentum + if (this._lastRotateDelta > this._rotateMomentumThreshold && + this.rotation > transitionRotation) { + transitionRotation += 90; + } else if (this._lastRotateDelta < -1 * this._rotateMomentumThreshold && + this.rotation < transitionRotation) { + transitionRotation -= 90; + } + + // Only add the completeRotation class if it is is necessary + if (transitionRotation != this.rotation) { + contentElement.classList.add("completeRotation"); + contentElement.addEventListener("transitionend", this._clearCompleteRotation); + } + + contentElement.style.transform = "rotate(" + transitionRotation + "deg)"; + this.rotation = transitionRotation; + }, + + /** + * Gets the current rotation for the ImageDocument + */ + get rotation() { + return this._currentRotation; + }, + + /** + * Sets the current rotation for the ImageDocument + * + * @param aVal + * The new value to take. Can be any value, but it will be bounded to + * 0 inclusive to 360 exclusive. + */ + set rotation(aVal) { + this._currentRotation = aVal % 360; + if (this._currentRotation < 0) { + this._currentRotation += 360; + } + return this._currentRotation; + }, + + /** + * When the location/tab changes, need to reload the current rotation for the + * image + */ + restoreRotationState: function() { + if (!(content.document instanceof ImageDocument)) { + return; + } + + let contentElement = content.document.body.firstElementChild; + let transformValue = content.window.getComputedStyle(contentElement, null) + .transform; + + if (transformValue == "none") { + this.rotation = 0; + return; + } + + // transformValue is a rotation matrix--split it and do mathemagic to + // obtain the real rotation value + transformValue = transformValue.split("(")[1] + .split(")")[0] + .split(","); + this.rotation = Math.round(Math.atan2(transformValue[1], transformValue[0]) * + (180 / Math.PI)); + }, + + /** + * Removes the transition rule by removing the completeRotation class + */ + _clearCompleteRotation: function() { + let contentElement = content.document && + content.document instanceof ImageDocument && + content.document.body && + content.document.body.firstElementChild; + if (!contentElement) { + return; + } + contentElement.classList.remove("completeRotation"); + contentElement.removeEventListener("transitionend", this._clearCompleteRotation); + }, +}; + +// History Swipe Animation Support (bug 678392) +var gHistorySwipeAnimation = { + + active: false, + isLTR: false, + + /** + * Initializes the support for history swipe animations, if it is supported + * by the platform/configuration. + */ + init: function() { + if (!this._isSupported()) { + return; + } + + this.active = false; + this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)"); + this._trackedSnapshots = []; + this._historyIndex = -1; + this._boxWidth = -1; + this._maxSnapshots = this._getMaxSnapshots(); + this._lastSwipeDir = ""; + + // We only want to activate history swipe animations if we store snapshots. + // If we don't store any, we handle horizontal swipes without animations. + if (this._maxSnapshots > 0) { + this.active = true; + gBrowser.addEventListener("pagehide", this, false); + gBrowser.addEventListener("pageshow", this, false); + gBrowser.addEventListener("popstate", this, false); + gBrowser.addEventListener("DOMModalDialogClosed", this, false); + gBrowser.tabContainer.addEventListener("TabClose", this, false); + } + }, + + /** + * Uninitializes the support for history swipe animations. + */ + uninit: function() { + gBrowser.removeEventListener("pagehide", this, false); + gBrowser.removeEventListener("pageshow", this, false); + gBrowser.removeEventListener("popstate", this, false); + gBrowser.removeEventListener("DOMModalDialogClosed", this, false); + gBrowser.tabContainer.removeEventListener("TabClose", this, false); + + this.active = false; + this.isLTR = false; + }, + + /** + * Starts the swipe animation and handles fast swiping (i.e. a swipe animation + * is already in progress when a new one is initiated). + */ + startAnimation: function() { + if (this.isAnimationRunning()) { + gBrowser.stop(); + this._lastSwipeDir = "RELOAD"; // just ensure that != "" + this._canGoBack = this.canGoBack(); + this._canGoForward = this.canGoForward(); + this._handleFastSwiping(); + } else { + this._historyIndex = gBrowser.webNavigation.sessionHistory.index; + this._canGoBack = this.canGoBack(); + this._canGoForward = this.canGoForward(); + if (this.active) { + this._takeSnapshot(); + this._installPrevAndNextSnapshots(); + this._addBoxes(); + this._lastSwipeDir = ""; + } + } + this.updateAnimation(0); + }, + + /** + * Stops the swipe animation. + */ + stopAnimation: function() { + gHistorySwipeAnimation._removeBoxes(); + this._historyIndex = gBrowser.webNavigation.sessionHistory.index; + }, + + /** + * Updates the animation between two pages in history. + * + * @param aVal + * A floating point value that represents the progress of the + * swipe gesture. + */ + updateAnimation: function(aVal) { + if (!this.isAnimationRunning()) { + return; + } + + if ((aVal >= 0 && this.isLTR) || + (aVal <= 0 && !this.isLTR)) { + if (aVal > 1) { + // Cap value to avoid sliding the page further than allowed. + aVal = 1; + } + + if (this._canGoBack) { + this._prevBox.collapsed = false; + } else { + this._prevBox.collapsed = true; + } + + // The current page is pushed to the right (LTR) or left (RTL), + // the intention is to go back. + // If there is a page to go back to, it should show in the background. + this._positionBox(this._curBox, aVal); + + // The forward page should be pushed offscreen all the way to the right. + this._positionBox(this._nextBox, 1); + } + else { + if (aVal < -1) + aVal = -1; // Cap value to avoid sliding the page further than allowed. + // The intention is to go forward. If there is a page to go forward to, + // it should slide in from the right (LTR) or left (RTL). + // Otherwise, the current page should slide to the left (LTR) or + // right (RTL) and the backdrop should appear in the background. + // For the backdrop to be visible in that case, the previous page needs + // to be hidden (if it exists). + if (this._canGoForward) { + this._nextBox.collapsed = false; + let offset = this.isLTR ? 1 : -1; + this._positionBox(this._curBox, 0); + this._positionBox(this._nextBox, offset + aVal); // aval is negative + } else { + this._prevBox.collapsed = true; + this._positionBox(this._curBox, aVal); + } + } + }, + + /** + * Event handler for events relevant to the history swipe animation. + * + * @param aEvent + * An event to process. + */ + handleEvent: function(aEvent) { + let browser = gBrowser.selectedBrowser; + switch (aEvent.type) { + case "TabClose": + let browserForTab = gBrowser.getBrowserForTab(aEvent.target); + this._removeTrackedSnapshot(-1, browserForTab); + break; + case "DOMModalDialogClosed": + this.stopAnimation(); + break; + case "pageshow": + if (aEvent.target == browser.contentDocument) { + this.stopAnimation(); + } + break; + case "popstate": + if (aEvent.target == browser.contentDocument.defaultView) { + this.stopAnimation(); + } + break; + case "pagehide": + if (aEvent.target == browser.contentDocument) { + // Take and compress a snapshot of a page whenever it's about to be + // navigated away from. We already have a snapshot of the page if an + // animation is running, so we're left with compressing it. + if (!this.isAnimationRunning()) { + this._takeSnapshot(); + } + this._compressSnapshotAtCurrentIndex(); + } + break; + } + }, + + /** + * Checks whether the history swipe animation is currently running or not. + * + * @return true if the animation is currently running, false otherwise. + */ + isAnimationRunning: function() { + return !!this._container; + }, + + /** + * Process a swipe event based on the given direction. + * + * @param aEvent + * The swipe event to handle + * @param aDir + * The direction for the swipe event + */ + processSwipeEvent: function(aEvent, aDir) { + if (aDir == "RIGHT") { + this._historyIndex += this.isLTR ? 1 : -1; + } else if (aDir == "LEFT") { + this._historyIndex += this.isLTR ? -1 : 1; + } else { + return; + } + this._lastSwipeDir = aDir; + }, + + /** + * Checks if there is a page in the browser history to go back to. + * + * @return true if there is a previous page in history, false otherwise. + */ + canGoBack: function() { + if (this.isAnimationRunning()) { + return this._doesIndexExistInHistory(this._historyIndex - 1); + } + return gBrowser.webNavigation.canGoBack; + }, + + /** + * Checks if there is a page in the browser history to go forward to. + * + * @return true if there is a next page in history, false otherwise. + */ + canGoForward: function() { + if (this.isAnimationRunning()) { + return this._doesIndexExistInHistory(this._historyIndex + 1); + } + return gBrowser.webNavigation.canGoForward; + }, + + /** + * Used to notify the history swipe animation that the OS sent a swipe end + * event and that we should navigate to the page that the user swiped to, if + * any. This will also result in the animation overlay to be torn down. + */ + swipeEndEventReceived: function() { + if (this._lastSwipeDir != "") { + this._navigateToHistoryIndex(); + } else { + this.stopAnimation(); + } + }, + + /** + * Checks whether a particular index exists in the browser history or not. + * + * @param aIndex + * The index to check for availability for in the history. + * @return true if the index exists in the browser history, false otherwise. + */ + _doesIndexExistInHistory: function(aIndex) { + try { + gBrowser.webNavigation.sessionHistory.getEntryAtIndex(aIndex, false); + } catch(ex) { + return false; + } + return true; + }, + + /** + * Navigates to the index in history that is currently being tracked by + * |this|. + */ + _navigateToHistoryIndex: function() { + if (this._doesIndexExistInHistory(this._historyIndex)) { + gBrowser.webNavigation.gotoIndex(this._historyIndex); + } else { + this.stopAnimation(); + } + }, + + /** + * Checks to see if history swipe animations are supported by this + * platform/configuration. + * + * return true if supported, false otherwise. + */ + _isSupported: function() { + return window.matchMedia("(-moz-swipe-animation-enabled)").matches; + }, + + /** + * Handle fast swiping (i.e. a swipe animation is already in + * progress when a new one is initiated). This will swap out the snapshots + * used in the previous animation with the appropriate new ones. + */ + _handleFastSwiping: function() { + this._installCurrentPageSnapshot(null); + this._installPrevAndNextSnapshots(); + }, + + /** + * Adds the boxes that contain the snapshots used during the swipe animation. + */ + _addBoxes: function() { + let browserStack = + document.getAnonymousElementByAttribute(gBrowser.getNotificationBox(), + "class", "browserStack"); + this._container = this._createElement("historySwipeAnimationContainer", + "stack"); + browserStack.appendChild(this._container); + + this._prevBox = this._createElement("historySwipeAnimationPreviousPage", + "box"); + this._container.appendChild(this._prevBox); + + this._curBox = this._createElement("historySwipeAnimationCurrentPage", + "box"); + this._container.appendChild(this._curBox); + + this._nextBox = this._createElement("historySwipeAnimationNextPage", + "box"); + this._container.appendChild(this._nextBox); + + this._boxWidth = this._curBox.getBoundingClientRect().width; // cache width + }, + + /** + * Removes the boxes. + */ + _removeBoxes: function() { + this._curBox = null; + this._prevBox = null; + this._nextBox = null; + if (this._container) { + this._container.parentNode.removeChild(this._container); + } + this._container = null; + this._boxWidth = -1; + }, + + /** + * Creates an element with a given identifier and tag name. + * + * @param aID + * An identifier to create the element with. + * @param aTagName + * The name of the tag to create the element for. + * @return the newly created element. + */ + _createElement: function(aID, aTagName) { + let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let element = document.createElementNS(XULNS, aTagName); + element.id = aID; + return element; + }, + + /** + * Moves a given box to a given X coordinate position. + * + * @param aBox + * The box element to position. + * @param aPosition + * The position (in X coordinates) to move the box element to. + */ + _positionBox: function(aBox, aPosition) { + aBox.style.transform = "translateX(" + this._boxWidth * aPosition + "px)"; + let transform = ""; + + aBox.style.transform = transform; + }, + + /** + * Verifies that we're ready to take snapshots based on the global pref and + * the current index in history. + * + * @return true if we're ready to take snapshots, false otherwise. + */ + _readyToTakeSnapshots: function() { + if ((this._maxSnapshots < 1) || + (gBrowser.webNavigation.sessionHistory.index < 0)) { + return false; + } + return true; + }, + + /** + * Takes a snapshot of the page the browser is currently on. + */ + _takeSnapshot: function() { + if (!this._readyToTakeSnapshots()) { + return; + } + + let canvas = null; + + let browser = gBrowser.selectedBrowser; + let r = browser.getBoundingClientRect(); + canvas = document.createElementNS("http://www.w3.org/1999/xhtml", + "canvas"); + canvas.mozOpaque = true; + let scale = window.devicePixelRatio; + canvas.width = r.width * scale; + canvas.height = r.height * scale; + let ctx = canvas.getContext("2d"); + let zoom = browser.markupDocumentViewer.fullZoom * scale; + ctx.scale(zoom, zoom); + ctx.drawWindow(browser.contentWindow, + 0, 0, canvas.width / zoom, canvas.height / zoom, "white", + ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW | + ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES | + ctx.DRAWWINDOW_USE_WIDGET_LAYERS); + + this._installCurrentPageSnapshot(canvas); + this._assignSnapshotToCurrentBrowser(canvas); + }, + + /** + * Retrieves the maximum number of snapshots that should be kept in memory. + * This limit is a global limit and is valid across all open tabs. + */ + _getMaxSnapshots: function() { + return gPrefService.getIntPref("browser.snapshots.limit"); + }, + + /** + * Adds a snapshot to the list and initiates the compression of said snapshot. + * Once the compression is completed, it will replace the uncompressed + * snapshot in the list. + * + * @param aCanvas + * The snapshot to add to the list and compress. + */ + _assignSnapshotToCurrentBrowser: + function(aCanvas) { + let browser = gBrowser.selectedBrowser; + let currIndex = browser.webNavigation.sessionHistory.index; + + this._removeTrackedSnapshot(currIndex, browser); + this._addSnapshotRefToArray(currIndex, browser); + + if (!("snapshots" in browser)) { + browser.snapshots = []; + } + let snapshots = browser.snapshots; + // Temporarily store the canvas as the compressed snapshot. + // This avoids a blank page if the user swipes quickly + // between pages before the compression could complete. + snapshots[currIndex] = { + image: aCanvas, + scale: window.devicePixelRatio + }; + }, + + /** + * Compresses the HTMLCanvasElement that's stored at the current history + * index in the snapshot array and stores the compressed image in its place. + */ + _compressSnapshotAtCurrentIndex: + function() { + if (!this._readyToTakeSnapshots()) { + // We didn't take a snapshot earlier because we weren't ready to, so + // there's nothing to compress. + return; + } + + let browser = gBrowser.selectedBrowser; + let snapshots = browser.snapshots; + let currIndex = browser.webNavigation.sessionHistory.index; + + // Kick off snapshot compression. + let canvas = snapshots[currIndex].image; + canvas.toBlob(function(aBlob) { + if (snapshots[currIndex]) { + snapshots[currIndex].image = aBlob; + } + }, "image/png" + ); + }, + + /** + * Removes a snapshot identified by the browser and index in the array of + * snapshots for that browser, if present. If no snapshot could be identified + * the method simply returns without taking any action. If aIndex is negative, + * all snapshots for a particular browser will be removed. + * + * @param aIndex + * The index in history of the new snapshot, or negative value if all + * snapshots for a browser should be removed. + * @param aBrowser + * The browser the new snapshot was taken in. + */ + _removeTrackedSnapshot: function(aIndex, aBrowser) { + let arr = this._trackedSnapshots; + let requiresExactIndexMatch = aIndex >= 0; + for (let i = 0; i < arr.length; i++) { + if ((arr[i].browser == aBrowser) && + (aIndex < 0 || aIndex == arr[i].index)) { + delete aBrowser.snapshots[arr[i].index]; + arr.splice(i, 1); + if (requiresExactIndexMatch) { + // Found and removed the only element. + return; + } + // Make sure to revisit the index that we just removed an element at. + i--; + } + } + }, + + /** + * Adds a new snapshot reference for a given index and browser to the array + * of references to tracked snapshots. + * + * @param aIndex + * The index in history of the new snapshot. + * @param aBrowser + * The browser the new snapshot was taken in. + */ + _addSnapshotRefToArray: + function(aIndex, aBrowser) { + let id = { index: aIndex, + browser: aBrowser }; + let arr = this._trackedSnapshots; + arr.unshift(id); + + while (arr.length > this._maxSnapshots) { + let lastElem = arr[arr.length - 1]; + delete lastElem.browser.snapshots[lastElem.index].image; + delete lastElem.browser.snapshots[lastElem.index]; + arr.splice(-1, 1); + } + }, + + /** + * Converts a compressed blob to an Image object. In some situations + * (especially during fast swiping) aBlob may still be a canvas, not a + * compressed blob. In this case, we simply return the canvas. + * + * @param aBlob + * The compressed blob to convert, or a canvas if a blob compression + * couldn't complete before this method was called. + * @return A new Image object representing the converted blob. + */ + _convertToImg: function(aBlob) { + if (!aBlob) { + return null; + } + + // Return aBlob if it's still a canvas and not a compressed blob yet. + if (aBlob instanceof HTMLCanvasElement) { + return aBlob; + } + + let img = new Image(); + let url = ""; + try { + url = URL.createObjectURL(aBlob); + img.onload = function() { + URL.revokeObjectURL(url); + }; + } finally { + img.src = url; + return img; + } + }, + + /** + * Scales the background of a given box element (which uses a given snapshot + * as background) based on a given scale factor. + * @param aSnapshot + * The snapshot that is used as background of aBox. + * @param aScale + * The scale factor to use. + * @param aBox + * The box element that uses aSnapshot as background. + */ + _scaleSnapshot: function(aSnapshot, aScale, aBox) { + if (aSnapshot && aScale != 1 && aBox) { + if (aSnapshot instanceof HTMLCanvasElement) { + aBox.style.backgroundSize = + aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px"; + } else { + // snapshot is instanceof HTMLImageElement + aSnapshot.addEventListener("load", function() { + aBox.style.backgroundSize = + aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px"; + }); + } + } + }, + + /** + * Sets the snapshot of the current page to the snapshot passed as parameter, + * or to the one previously stored for the current index in history if the + * parameter is null. + * + * @param aCanvas + * The snapshot to set the current page to. If this parameter is null, + * the previously stored snapshot for this index (if any) will be used. + */ + _installCurrentPageSnapshot: + function(aCanvas) { + let currSnapshot = aCanvas; + let scale = window.devicePixelRatio; + if (!currSnapshot) { + let snapshots = gBrowser.selectedBrowser.snapshots || {}; + let currIndex = this._historyIndex; + if (currIndex in snapshots) { + currSnapshot = this._convertToImg(snapshots[currIndex].image); + scale = snapshots[currIndex].scale; + } + } + this._scaleSnapshot(currSnapshot, scale, this._curBox ? this._curBox : null); + document.mozSetImageElement("historySwipeAnimationCurrentPageSnapshot", currSnapshot); + }, + + /** + * Sets the snapshots of the previous and next pages to the snapshots + * previously stored for their respective indeces. + */ + _installPrevAndNextSnapshots: + function() { + let snapshots = gBrowser.selectedBrowser.snapshots || []; + let currIndex = this._historyIndex; + let prevIndex = currIndex - 1; + let prevSnapshot = null; + if (prevIndex in snapshots) { + prevSnapshot = this._convertToImg(snapshots[prevIndex].image); + this._scaleSnapshot(prevSnapshot, snapshots[prevIndex].scale, this._prevBox); + } + document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot", prevSnapshot); + + let nextIndex = currIndex + 1; + let nextSnapshot = null; + if (nextIndex in snapshots) { + nextSnapshot = this._convertToImg(snapshots[nextIndex].image); + this._scaleSnapshot(nextSnapshot, snapshots[nextIndex].scale, this._nextBox); + } + document.mozSetImageElement("historySwipeAnimationNextPageSnapshot", nextSnapshot); + }, +}; diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc new file mode 100644 index 000000000..8a93e3a55 --- /dev/null +++ b/browser/base/content/browser-menubar.inc @@ -0,0 +1,504 @@ +# -*- 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/. + + <menubar id="main-menubar" + onpopupshowing="if (event.target.parentNode.parentNode == this && +#ifdef MOZ_WIDGET_GTK + document.documentElement.getAttribute('shellshowingmenubar') != 'true') +#else + !('@mozilla.org/widget/nativemenuservice;1' in Cc)) +#endif + this.setAttribute('openedwithkey', + event.target.parentNode.openedWithKey);" + style="border:0px;padding:0px;margin:0px;-moz-appearance:none"> + <menu id="file-menu" label="&fileMenu.label;" + accesskey="&fileMenu.accesskey;"> + <menupopup id="menu_FilePopup"> + <menuitem id="menu_newNavigatorTab" + label="&tabCmd.label;" + command="cmd_newNavigatorTab" + key="key_newNavigatorTab" + accesskey="&tabCmd.accesskey;"/> + <menuitem id="menu_newNavigator" + label="&newNavigatorCmd.label;" + accesskey="&newNavigatorCmd.accesskey;" + key="key_newNavigator" + command="cmd_newNavigator"/> + <menuitem id="menu_newPrivateWindow" + label="&newPrivateWindow.label;" + accesskey="&newPrivateWindow.accesskey;" + command="Tools:PrivateBrowsing" + key="key_privatebrowsing"/> + <menuitem id="menu_openLocation" + class="show-only-for-keyboard" + label="&openLocationCmd.label;" + command="Browser:OpenLocation" + key="focusURLBar" + accesskey="&openLocationCmd.accesskey;"/> + <menuitem id="menu_openFile" + label="&openFileCmd.label;" + command="Browser:OpenFile" + key="openFileKb" + accesskey="&openFileCmd.accesskey;"/> + <menuitem id="menu_close" + class="show-only-for-keyboard" + label="&closeCmd.label;" + key="key_close" + accesskey="&closeCmd.accesskey;" + command="cmd_close"/> + <menuitem id="menu_closeWindow" + class="show-only-for-keyboard" + hidden="true" + command="cmd_closeWindow" + key="key_closeWindow" + label="&closeWindow.label;" + accesskey="&closeWindow.accesskey;"/> + <menuseparator/> + <menuitem id="menu_savePage" + label="&savePageCmd.label;" + accesskey="&savePageCmd.accesskey;" + key="key_savePage" + command="Browser:SavePage"/> + <menuitem id="menu_sendLink" + label="&emailPageCmd.label;" + accesskey="&emailPageCmd.accesskey;" + command="Browser:SendLink"/> + <menuseparator/> + <menuitem id="menu_printSetup" + label="&printSetupCmd.label;" + accesskey="&printSetupCmd.accesskey;" + command="cmd_pageSetup"/> + <menuitem id="menu_printPreview" + label="&printPreviewCmd.label;" + accesskey="&printPreviewCmd.accesskey;" + command="cmd_printPreview"/> + <menuitem id="menu_print" + label="&printCmd.label;" + accesskey="&printCmd.accesskey;" + key="printKb" + command="cmd_print"/> + <menuseparator/> + <menuitem id="goOfflineMenuitem" + label="&goOfflineCmd.label;" + accesskey="&goOfflineCmd.accesskey;" + type="checkbox" + observes="workOfflineMenuitemState" + oncommand="BrowserOffline.toggleOfflineStatus();"/> + <menuitem id="menu_restart" + label="&restartCmd.label;" + accesskey="&restartCmd.accesskey;" + command="cmd_restartApplication"/> + <menuitem id="menu_FileQuitItem" +#ifdef XP_WIN + label="&quitApplicationCmdWin.label;" + accesskey="&quitApplicationCmdWin.accesskey;" +#else + label="&quitApplicationCmd.label;" + accesskey="&quitApplicationCmd.accesskey;" +#ifdef XP_UNIX + key="key_quitApplication" +#endif +#endif + command="cmd_quitApplication"/> + </menupopup> + </menu> + + <menu id="edit-menu" label="&editMenu.label;" + accesskey="&editMenu.accesskey;"> + <menupopup id="menu_EditPopup" + onpopupshowing="updateEditUIVisibility()" + onpopuphidden="updateEditUIVisibility()"> + <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"/> + <menuseparator/> + <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"/> + <menuseparator/> + <menuitem id="menu_selectAll" + label="&selectAllCmd.label;" + key="key_selectAll" + accesskey="&selectAllCmd.accesskey;" + command="cmd_selectAll"/> + <menuseparator/> + <menuitem id="menu_find" + label="&findOnCmd.label;" + accesskey="&findOnCmd.accesskey;" + key="key_find" + command="cmd_find"/> + <menuitem id="menu_findAgain" + class="show-only-for-keyboard" + label="&findAgainCmd.label;" + accesskey="&findAgainCmd.accesskey;" + key="key_findAgain" + command="cmd_findAgain"/> + <menuseparator hidden="true" id="textfieldDirection-separator"/> + <menuitem id="textfieldDirection-swap" + command="cmd_switchTextDirection" + key="key_switchTextDirection" + label="&bidiSwitchTextDirectionItem.label;" + accesskey="&bidiSwitchTextDirectionItem.accesskey;" + hidden="true"/> + </menupopup> + </menu> + + <menu id="view-menu" label="&viewMenu.label;" + accesskey="&viewMenu.accesskey;"> + <menupopup id="menu_viewPopup" + onpopupshowing="updateCharacterEncodingMenuState();"> + <menu id="viewToolbarsMenu" + label="&viewToolbarsMenu.label;" + accesskey="&viewToolbarsMenu.accesskey;"> + <menupopup onpopupshowing="onViewToolbarsPopupShowing(event);"> + <menuseparator/> + <menuitem id="menu_tabsOnTop" + command="cmd_ToggleTabsOnTop" + type="checkbox" + label="&viewTabsOnTop.label;" + accesskey="&viewTabsOnTop.accesskey;"/> + <menuitem id="menu_customizeToolbars" + label="&viewCustomizeToolbar.label;" + accesskey="&viewCustomizeToolbar.accesskey;" + command="cmd_CustomizeToolbars"/> + </menupopup> + </menu> + <menu id="viewSidebarMenuMenu" + label="&viewSidebarMenu.label;" + accesskey="&viewSidebarMenu.accesskey;"> + <menupopup id="viewSidebarMenu"> + <menuitem id="menu_bookmarksSidebar" + key="viewBookmarksSidebarKb" + observes="viewBookmarksSidebar" + label="&bookmarksButton.label;"/> + <menuitem id="menu_historySidebar" + key="key_gotoHistory" + observes="viewHistorySidebar" + label="&historyButton.label;"/> + </menupopup> + </menu> + <menuseparator/> + <menuitem id="menu_stop" + class="show-only-for-keyboard" + label="&stopCmd.label;" + accesskey="&stopCmd.accesskey;" + command="Browser:Stop" + key="key_stop"/> + <menuitem id="menu_reload" + class="show-only-for-keyboard" + label="&reloadCmd.label;" + accesskey="&reloadCmd.accesskey;" + key="key_reload" + command="Browser:ReloadOrDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuseparator class="show-only-for-keyboard"/> + <menu id="viewFullZoomMenu" label="&fullZoom.label;" + accesskey="&fullZoom.accesskey;" + onpopupshowing="FullZoom.updateMenu();"> + <menupopup> + <menuitem id="menu_zoomEnlarge" + key="key_fullZoomEnlarge" + label="&fullZoomEnlargeCmd.label;" + accesskey="&fullZoomEnlargeCmd.accesskey;" + command="cmd_fullZoomEnlarge"/> + <menuitem id="menu_zoomReduce" + key="key_fullZoomReduce" + label="&fullZoomReduceCmd.label;" + accesskey="&fullZoomReduceCmd.accesskey;" + command="cmd_fullZoomReduce"/> + <menuseparator/> + <menuitem id="menu_zoomReset" + key="key_fullZoomReset" + label="&fullZoomResetCmd.label;" + accesskey="&fullZoomResetCmd.accesskey;" + command="cmd_fullZoomReset"/> + <menuseparator/> + <menuitem id="toggle_zoom" + label="&fullZoomToggleCmd.label;" + accesskey="&fullZoomToggleCmd.accesskey;" + type="checkbox" + command="cmd_fullZoomToggle" + checked="false"/> + </menupopup> + </menu> + <menu id="pageStyleMenu" label="&pageStyleMenu.label;" + accesskey="&pageStyleMenu.accesskey;" observes="isImage"> + <menupopup onpopupshowing="gPageStyleMenu.fillPopup(this);"> + <menuitem id="menu_pageStyleNoStyle" + label="&pageStyleNoStyle.label;" + accesskey="&pageStyleNoStyle.accesskey;" + oncommand="gPageStyleMenu.disableStyle();" + type="radio"/> + <menuitem id="menu_pageStylePersistentOnly" + label="&pageStylePersistentOnly.label;" + accesskey="&pageStylePersistentOnly.accesskey;" + oncommand="gPageStyleMenu.switchStyleSheet('');" + type="radio" + checked="true"/> + <menuseparator/> + </menupopup> + </menu> +#include browser-charsetmenu.inc + <menuseparator/> + <menuitem id="fullScreenItem" + accesskey="&fullScreenCmd.accesskey;" + label="&fullScreenCmd.label;" + key="key_fullScreen" + type="checkbox" + observes="View:FullScreen"/> + <menuitem id="menu_showAllTabs" + hidden="true" + accesskey="&showAllTabsCmd.accesskey;" + label="&showAllTabsCmd.label;" + command="Browser:ShowAllTabs" + key="key_showAllTabs"/> + <menuseparator hidden="true" id="documentDirection-separator"/> + <menuitem id="documentDirection-swap" + hidden="true" + label="&bidiSwitchPageDirectionItem.label;" + accesskey="&bidiSwitchPageDirectionItem.accesskey;" + oncommand="SwitchDocumentDirection(window.content)"/> + </menupopup> + </menu> + + <menu id="history-menu" + label="&historyMenu.label;" + accesskey="&historyMenu.accesskey;"> + <menupopup id="goPopup" + placespopup="true" + context="placesContext" + oncommand="this.parentNode._placesView._onCommand(event);" + onclick="checkForMiddleClick(this, event);" + onpopupshowing="if (!this.parentNode._placesView) + new HistoryMenu(event);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <menuitem id="historyMenuBack" + class="show-only-for-keyboard" + label="&backCmd.label;" + key="goBackKb" + command="Browser:BackOrBackDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="historyMenuForward" + class="show-only-for-keyboard" + label="&forwardCmd.label;" + key="goForwardKb" + command="Browser:ForwardOrForwardDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="historyMenuHome" + class="show-only-for-keyboard" + label="&historyHomeCmd.label;" + oncommand="BrowserGoHome(event);" + onclick="checkForMiddleClick(this, event);" + key="goHome"/> + <menuseparator id="historyMenuHomeSeparator" + class="show-only-for-keyboard"/> + <menuitem id="menu_showAllHistory" + label="&showAllHistoryCmd2.label;" + class="menuitem-iconic" + key="showAllHistoryKb" + command="Browser:ShowAllHistory"/> + <menuitem id="sanitizeItem" + class="menuitem-iconic" + label="&clearRecentHistory.label;" + key="key_sanitize" + command="Tools:Sanitize"/> + <menuseparator id="sanitizeSeparator"/> +#ifdef MOZ_SERVICES_SYNC + <menuitem id="sync-tabs-menuitem" + class="syncTabsMenuItem" + label="&syncTabsMenu2.label;" + oncommand="BrowserOpenSyncTabs();" + disabled="true"/> +#endif + <menuitem id="historyRestoreLastSession" + label="&historyRestoreLastSession.label;" + command="Browser:RestoreLastSession"/> + <menu id="historyUndoMenu" + class="recentlyClosedTabsMenu" + label="&historyUndoMenu.label;" + disabled="true"> + <menupopup id="historyUndoPopup" + placespopup="true" + onpopupshowing="document.getElementById('history-menu')._placesView.populateUndoSubmenu();"/> + </menu> + <menu id="historyUndoWindowMenu" + class="recentlyClosedWindowsMenu" + label="&historyUndoWindowMenu.label;" + disabled="true"> + <menupopup id="historyUndoWindowPopup" + placespopup="true" + onpopupshowing="document.getElementById('history-menu')._placesView.populateUndoWindowSubmenu();"/> + </menu> + <menuseparator id="startHistorySeparator" + class="hide-if-empty-places-result"/> + </menupopup> + </menu> + + <menu id="bookmarksMenu" + label="&bookmarksMenu.label;" + accesskey="&bookmarksMenu.accesskey;" + ondragenter="PlacesMenuDNDHandler.onDragEnter(event);" + ondragover="PlacesMenuDNDHandler.onDragOver(event);" + ondrop="PlacesMenuDNDHandler.onDrop(event);"> + <menupopup id="bookmarksMenuPopup" + placespopup="true" + context="placesContext" + openInTabs="children" + oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);" + onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);" + onpopupshowing="PlacesCommandHook.updateBookmarkAllTabsCommand(); + if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=BOOKMARKS_MENU');" + tooltip="bhTooltip" popupsinherittooltip="true"> + <menuitem id="bookmarksShowAll" + class="menuitem-iconic" + label="&organizeBookmarks.label;" + command="Browser:ShowAllBookmarks" + key="manBookmarkKb"/> + <menuseparator id="organizeBookmarksSeparator"/> + <menuitem id="menu_bookmarkThisPage" + class="menuitem-iconic" + label="&bookmarkThisPageCmd.label;" + command="Browser:AddBookmarkAs" + key="addBookmarkAsKb"/> + <menuitem id="subscribeToPageMenuitem" + class="menuitem-iconic" + label="&subscribeToPageMenuitem.label;" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);" + observes="singleFeedMenuitemState"/> + <menu id="subscribeToPageMenupopup" + class="menu-iconic" + label="&subscribeToPageMenupopup.label;" + observes="multipleFeedsMenuState"> + <menupopup id="subscribeToPageSubmenuMenupopup" + onpopupshowing="return FeedHandler.buildFeedList(event.target);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </menu> + <menuitem id="menu_bookmarkAllTabs" + label="&addCurPagesCmd.label;" + class="show-only-for-keyboard" + command="Browser:BookmarkAllTabs" + key="bookmarkAllTabsKb"/> + <menuseparator id="bookmarksToolbarSeparator"/> + <menu id="bookmarksToolbarFolderMenu" + class="menu-iconic bookmark-item" + label="&personalbarCmd.label;" + container="true"> + <menupopup id="bookmarksToolbarFolderPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=TOOLBAR');"/> + </menu> + <menuseparator id="bookmarksMenuItemsSeparator"/> + <!-- Bookmarks menu items --> + <menuseparator builder="end" + class="hide-if-empty-places-result"/> + <menuitem id="menu_unsortedBookmarks" + class="menuitem-iconic" + label="&unsortedBookmarksCmd.label;" + oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/> + </menupopup> + </menu> + + <menu id="tools-menu" + label="&toolsMenu.label;" + accesskey="&toolsMenu.accesskey;"> + <menupopup id="menu_ToolsPopup" +#ifdef MOZ_SERVICES_SYNC + onpopupshowing="gSyncUI.updateUI();" +#endif + > + <menuitem id="menu_search" + class="show-only-for-keyboard" + label="&search.label;" + accesskey="&search.accesskey;" + key="key_search" + command="Tools:Search"/> + <menuseparator id="browserToolsSeparator" + class="show-only-for-keyboard"/> + <menuitem id="menu_openDownloads" + label="&downloads.label;" + accesskey="&downloads.accesskey;" + key="key_openDownloads" + command="Tools:Downloads"/> + <menuitem id="menu_openAddons" + label="&addons.label;" + accesskey="&addons.accesskey;" + key="key_openAddons" + command="Tools:Addons"/> + <menuitem id="menu_openPermissions" + label="&permissions.label;" + command="Tools:Permissions" + accesskey="&permissions.accesskey;"/> +#ifdef MOZ_SERVICES_SYNC + <!-- only one of sync-setup or sync-menu will be showing at once --> + <menuitem id="sync-setup" + label="&syncSetup.label;" + accesskey="&syncSetup.accesskey;" + observes="sync-setup-state" + oncommand="gSyncUI.openSetup()"/> + <menuitem id="sync-syncnowitem" + label="&syncSyncNowItem.label;" + accesskey="&syncSyncNowItem.accesskey;" + observes="sync-syncnow-state" + oncommand="gSyncUI.doSync(event);"/> +#endif + <menuseparator id="devToolsSeparator"/> + <menu id="webDeveloperMenu" + label="&webDeveloperMenu.label;" + accesskey="&webDeveloperMenu.accesskey;"> + <menupopup id="menuWebDeveloperPopup"> + <menuitem id="menu_pageSource" + observes="devtoolsMenuBroadcaster_PageSource" + accesskey="&pageSourceCmd.accesskey;"/> + <menuitem id="javascriptConsole" + observes="devtoolsMenuBroadcaster_ErrorConsole" + accesskey="&errorConsoleCmd.accesskey;"/> + </menupopup> + </menu> + <menuitem id="menu_pageInfo" + accesskey="&pageInfoCmd.accesskey;" + label="&pageInfoCmd.label;" +#ifndef XP_WIN + key="key_viewInfo" +#endif + command="View:PageInfo"/> + <menuseparator id="prefSep"/> + <menuitem id="menu_preferences" + label="&preferencesCmd2.label;" + accesskey="&preferencesCmd2.accesskey;" + oncommand="openPreferences();"/> + </menupopup> + </menu> + <menu id="helpMenu" /> + </menubar> diff --git a/browser/base/content/browser-menudragging.js b/browser/base/content/browser-menudragging.js new file mode 100644 index 000000000..29142a662 --- /dev/null +++ b/browser/base/content/browser-menudragging.js @@ -0,0 +1,351 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Based on original code by alice0775 https://github.com/alice0775 + + +"use strict"; +var browserMenuDragging = { + //-- config -- + STAY_OPEN_ONDRAGEXIT: false, + DEBUG: false, + //-- config -- + + menupopup: ['bookmarksMenuPopup', + 'PlacesToolbar', + 'BMB_bookmarksPopup', + 'appmenu_bookmarksPopup', + 'BookmarksMenuToolButtonPopup', + 'UnsortedBookmarksFolderToolButtonPopup', + 'bookmarksMenuPopup-context'], + timer:[], + count:[], + + + init: function() { + window.removeEventListener('load', this, false); + window.addEventListener('unload', this, false); + this.addPrefListener(this.PrefListener); + + window.addEventListener('aftercustomization', this, false); + + this.initPref(); + this.delayedStartup(); + }, + + uninit: function() { + window.removeEventListener('unload', this, false); + this.removePrefListener(this.PrefListener); + + window.removeEventListener('aftercustomization', this, false); + + for (var i = 0; i < this.menupopup.length; i++){ + var menupopup = document.getElementById(this.menupopup[i]); + if (menupopup){ + menupopup.removeEventListener('popupshowing', this, false); + menupopup.removeEventListener('popuphiding', this, false); + } + } + + }, + + initPref: function() { + this.STAY_OPEN_ONDRAGEXIT = + Services.prefs.getBoolPref('browser.menu.dragging.stayOpen', false); + this.DEBUG = + Services.prefs.getBoolPref('browser.menu.dragging.debug', false); + }, + + //delayed startup + delayedStartup: function() { + //wait until construction of bookmarksBarContent is completed. + for (var i = 0; i < this.menupopup.length; i++) { + this.count[i] = 0; + this.timer[i] = setInterval(function(self, i) { + if(++self.count[i] > 50 || document.getElementById(self.menupopup[i])) { + clearInterval(self.timer[i]); + var menupopup = document.getElementById(self.menupopup[i]); + if (menupopup) { + menupopup.addEventListener('popupshowing', self, false); + menupopup.addEventListener('popuphiding', self, false); + } + } + }, 250, this, i); + } + }, + + handleEvent: function(event) { + switch (event.type) { + case 'popupshowing': + this.popupshowing(event); + break; + case 'popuphiding': + this.popuphiding(event); + break; + case 'aftercustomization': + setTimeout(function(self){self.delayedStartup(self);}, 0, this); + break; + case 'load': + this.init(); + break; + case 'unload': + this.uninit(); + break; + } + }, + + popuphiding: function(event) { + var menupopup = event.originalTarget; + menupopup.parentNode.parentNode.openNode = null; + + if (menupopup.parentNode.localName == 'toolbarbutton') { + // Fix for Bug 225434 - dragging bookmark from personal toolbar and releasing + // (on same bookmark or elsewhere) or clicking on bookmark menu then cancelling + // leaves button depressed/sunken when hovered + menupopup.parentNode.parentNode._openedMenuButton = null; + + if (!PlacesControllerDragHelper.getSession()) { + // Clear the dragover attribute if present, if we are dragging into a + // folder in the hierachy of current opened popup we don't clear + // this attribute on clearOverFolder. See Notify for closeTimer. + if (menupopup.parentNode.hasAttribute('dragover')) { + menupopup.parentNode.removeAttribute('dragover'); + } + } + } + }, + + popupshowing: function(event) { + var menupopup = event.originalTarget; + browserMenuDragging.debug("popupshowing ===============\n" + menupopup.parentNode.getAttribute('label')); + + var parentPopup = menupopup.parentNode.parentNode; + + if (!!parentPopup.openNode) { + try { + parentPopup.openNode.hidePopup(); + } catch(e) {} + } + parentPopup.openNode = menupopup; + + menupopup.onDragStart = function (event) { + // Bug 555474 - While bookmark is dragged, the tooltip should not appear + browserMenuDragging.hideTooltip(); + } + + menupopup.onDragOver = function (event) { + // Bug 555474 - While bookmark is dragged, the tooltip should not appear + browserMenuDragging.hideTooltip(); + + var target = event.originalTarget; + while (target) { + if (/menupopup/.test(target.localName)) { + break; + } + target = target.parentNode; + } + + if (this != target) { + return; + } + + event.stopPropagation(); + browserMenuDragging.debug("onDragOver " + "\n" + this.parentNode.getAttribute('label')); + + PlacesControllerDragHelper.currentDropTarget = event.target; + let dt = event.dataTransfer; + + let dropPoint = this._getDropPoint(event); + + if (!dropPoint || !dropPoint.ip || + !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) { + this._indicatorBar.hidden = true; + event.stopPropagation(); + return; + } + + // Mark this popup as being dragged over. + this.setAttribute('dragover', 'true'); + + if (dropPoint.folderElt) { + // We are dragging over a folder. + // _overFolder should take the care of opening it on a timer. + if (this._overFolder.elt && + this._overFolder.elt != dropPoint.folderElt) { + } + if (!this._overFolder.elt) { + this._overFolder.elt = dropPoint.folderElt; + // Create the timer to open this folder. + this._overFolder.openTimer = this._overFolder + .setTimer(this._overFolder.hoverTime); + } + } + else { + // We are not dragging over a folder. + } + + // Autoscroll the popup strip if we drag over the scroll buttons. + let anonid = event.originalTarget.getAttribute('anonid'); + let scrollDir = anonid == 'scrollbutton-up' ? -1 : + anonid == 'scrollbutton-down' ? 1 : 0; + if (scrollDir != 0) { + this._scrollBox.scrollByIndex(scrollDir, false); + } + + // Check if we should hide the drop indicator for this target. + if (dropPoint.folderElt || this._hideDropIndicator(event)) { + this._indicatorBar.hidden = true; + event.preventDefault(); + event.stopPropagation(); + return; + } + + // We should display the drop indicator relative to the arrowscrollbox. + let sbo = this._scrollBox.scrollBoxObject; + let newMarginTop = 0; + if (scrollDir == 0) { + let elt = this.firstChild; + while (elt && event.screenY > elt.boxObject.screenY + + elt.boxObject.height / 2) { + elt = elt.nextSibling; + } + newMarginTop = elt ? elt.boxObject.screenY - sbo.screenY : sbo.height; + } else if (scrollDir == 1) { + newMarginTop = sbo.height; + } + + // Set the new marginTop based on arrowscrollbox. + newMarginTop += sbo.y - this._scrollBox.boxObject.y; + this._indicatorBar.firstChild.style.marginTop = newMarginTop + 'px'; + this._indicatorBar.hidden = false; + + event.preventDefault(); + event.stopPropagation(); + } + + menupopup.onDragExit = function (event) { + var target = event.originalTarget; + while (target) { + if (/menupopup/.test(target.localName)) { + break; + } + target = target.parentNode; + } + + if (this != target) { + return; + } + + event.stopPropagation(); + browserMenuDragging.debug("onDragExit " + browserMenuDragging.STAY_OPEN_ONDRAGEXIT); + + PlacesControllerDragHelper.currentDropTarget = null; + this.removeAttribute('dragover'); + + // If we have not moved to a valid new target clear the drop indicator + // this happens when moving out of the popup. + target = event.relatedTarget; + if (!target) { + this._indicatorBar.hidden = true; + } + + // Close any folder being hovered over + if (this._overFolder.elt) { + this._overFolder.closeTimer = this._overFolder.setTimer(this._overFolder.hoverTime); + } + + // The auto-opened attribute is set when this folder was automatically + // opened after the user dragged over it. If this attribute is set, + // auto-close the folder on drag exit. + // We should also try to close this popup if the drag has started + // from here, the timer will check if we are dragging over a child. + if (this.hasAttribute('autoopened') || + !browserMenuDragging.STAY_OPEN_ONDRAGEXIT && + this.hasAttribute('dragstart')) { + this._overFolder.closeMenuTimer = this._overFolder + .setTimer(this._overFolder.hoverTime); + } + + event.stopPropagation(); + } + + menupopup.addEventListener('dragstart', menupopup.onDragStart, true); + menupopup.addEventListener('dragover', menupopup.onDragOver, true); + menupopup.addEventListener('dragleave', menupopup.onDragExit, true); + }, + + hideTooltip: function() { + ['bhTooltip', 'btTooltip2'].forEach(function(id) { + var tooltip = document.getElementById(id); + if (tooltip) { + tooltip.hidePopup(); + } + }); + }, + + get getVer() { + const Cc = Components.classes; + const Ci = Components.interfaces; + var info = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo); + var ver = parseInt(info.version.substr(0,3) * 10,10) / 10; + return ver; + }, + + debug: function(aMsg) { + if (!browserMenuDragging.DEBUG) { + return; + } + Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService) + .logStringMessage(aMsg); + }, + + setPref: function(aPrefString, aPrefType, aValue) { + var xpPref = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + try{ + switch (aPrefType){ + case 'complex': + return xpPref.setComplexValue(aPrefString, Components.interfaces.nsILocalFile, aValue); break; + case 'str': + return xpPref.setCharPref(aPrefString, aValue); break; + case 'int': + aValue = parseInt(aValue); + return xpPref.setIntPref(aPrefString, aValue); break; + case 'bool': + default: + return xpPref.setBoolPref(aPrefString, aValue); break; + } + } catch(e) {} + return null; + }, + + addPrefListener: function(aObserver) { + try { + var pbi = Components.classes["@mozilla.org/preferences;1"]. + getService(Components.interfaces.nsIPrefBranch2); + pbi.addObserver(aObserver.domain, aObserver, false); + } catch(e) {} + }, + + removePrefListener: function(aObserver) { + try { + var pbi = Components.classes["@mozilla.org/preferences;1"]. + getService(Components.interfaces.nsIPrefBranch2); + pbi.removeObserver(aObserver.domain, aObserver); + } catch(e) {} + }, + + PrefListener:{ + domain : 'browser.menu.dragging.stayOpen', + + observe : function(aSubject, aTopic, aPrefstring) { + if (aTopic == 'nsPref:changed') { + browserMenuDragging.initPref(); + } + } + } +} + +window.addEventListener('load', browserMenuDragging, false);
\ No newline at end of file diff --git a/browser/base/content/browser-menudragging.xul b/browser/base/content/browser-menudragging.xul new file mode 100644 index 000000000..f5cabe5f6 --- /dev/null +++ b/browser/base/content/browser-menudragging.xul @@ -0,0 +1,13 @@ +<?xml version="1.0"?> + + +<overlay id="menuDraggingOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<window id="main-window"> + + <script type="application/x-javascript" src="chrome://browser/content/browser-menudragging.js" /> + +</window> + +</overlay> diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js new file mode 100644 index 000000000..6fec2242f --- /dev/null +++ b/browser/base/content/browser-places.js @@ -0,0 +1,1352 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//////////////////////////////////////////////////////////////////////////////// +//// StarUI + +var StarUI = { + _itemId: -1, + uri: null, + _batching: false, + + _element: function(aID) { + return document.getElementById(aID); + }, + + get showForNewBookmarks() { + return Services.prefs.getBoolPref("browser.bookmarks.editDialog.showForNewBookmarks", false); + }, + + // Edit-bookmark panel + get panel() { + delete this.panel; + var element = this._element("editBookmarkPanel"); + // initially the panel is hidden + // to avoid impacting startup / new window performance + element.hidden = false; + element.addEventListener("popuphidden", this, false); + element.addEventListener("keypress", this, false); + return this.panel = element; + }, + + // Array of command elements to disable when the panel is opened. + get _blockedCommands() { + delete this._blockedCommands; + return this._blockedCommands = + ["cmd_close", "cmd_closeWindow"].map(function(id) this._element(id), this); + }, + + _blockCommands: function() { + this._blockedCommands.forEach(function(elt) { + // make sure not to permanently disable this item (see bug 409155) + if (elt.hasAttribute("wasDisabled")) { + return; + } + if (elt.getAttribute("disabled") == "true") { + elt.setAttribute("wasDisabled", "true"); + } else { + elt.setAttribute("wasDisabled", "false"); + elt.setAttribute("disabled", "true"); + } + }); + }, + + _restoreCommandsState: function() { + this._blockedCommands.forEach(function(elt) { + if (elt.getAttribute("wasDisabled") != "true") { + elt.removeAttribute("disabled"); + } + elt.removeAttribute("wasDisabled"); + }); + }, + + // nsIDOMEventListener + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "popuphidden": + if (aEvent.originalTarget == this.panel) { + if (!this._element("editBookmarkPanelContent").hidden) { + this.quitEditMode(); + } + + this._restoreCommandsState(); + this._itemId = -1; + if (this._batching) { + PlacesUtils.transactionManager.endBatch(false); + this._batching = false; + } + + switch (this._actionOnHide) { + case "cancel": { + PlacesUtils.transactionManager.undoTransaction(); + break; + } + case "remove": { + // Remove all bookmarks for the bookmark's url, this also removes + // the tags for the url. + PlacesUtils.transactionManager.beginBatch(null); + let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval); + for (let i = 0; i < itemIds.length; i++) { + let txn = new PlacesRemoveItemTransaction(itemIds[i]); + PlacesUtils.transactionManager.doTransaction(txn); + } + PlacesUtils.transactionManager.endBatch(false); + break; + } + } + this._actionOnHide = ""; + } + break; + case "keypress": + if (aEvent.defaultPrevented) { + // The event has already been consumed inside of the panel. + break; + } + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_ESCAPE: + if (!this._element("editBookmarkPanelContent").hidden) { + this.cancelButtonOnCommand(); + } + break; + case KeyEvent.DOM_VK_RETURN: + if (aEvent.target.className == "expander-up" || + aEvent.target.className == "expander-down" || + aEvent.target.id == "editBMPanel_newFolderButton") { + //XXX Why is this necessary? The defaultPrevented check should + // be enough. + break; + } + this.panel.hidePopup(); + break; + } + break; + } + }, + + _overlayLoaded: false, + _overlayLoading: false, + showEditBookmarkPopup: + function(aItemId, aAnchorElement, aPosition) { + // Performance: load the overlay the first time the panel is opened + // (see bug 392443). + if (this._overlayLoading) { + return; + } + + if (this._overlayLoaded) { + this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition); + return; + } + + this._overlayLoading = true; + document.loadOverlay( + "chrome://browser/content/places/editBookmarkOverlay.xul", + (function(aSubject, aTopic, aData) { + //XXX We just caused localstore.rdf to be re-applied (bug 640158) + retrieveToolbarIconsizesFromTheme(); + + // Move the header (star, title, button) into the grid, + // so that it aligns nicely with the other items (bug 484022). + let header = this._element("editBookmarkPanelHeader"); + let rows = this._element("editBookmarkPanelGrid").lastChild; + rows.insertBefore(header, rows.firstChild); + header.hidden = false; + + this._overlayLoading = false; + this._overlayLoaded = true; + this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition); + }).bind(this) + ); + }, + + _doShowEditBookmarkPanel: + function(aItemId, aAnchorElement, aPosition) { + if (this.panel.state != "closed") { + return; + } + + this._blockCommands(); // un-done in the popuphiding handler + + // Set panel title: + // if we are batching, i.e. the bookmark has been added now, + // then show Page Bookmarked, else if the bookmark did already exist, + // we are about editing it, then use Edit This Bookmark. + this._element("editBookmarkPanelTitle").value = + this._batching ? + gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") : + gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"); + + // No description; show the Done, Cancel; + this._element("editBookmarkPanelDescription").textContent = ""; + this._element("editBookmarkPanelBottomButtons").hidden = false; + this._element("editBookmarkPanelContent").hidden = false; + + // The remove button is shown only if we're not already batching, i.e. + // if the cancel button/ESC does not remove the bookmark. + this._element("editBookmarkPanelRemoveButton").hidden = this._batching; + + // The label of the remove button differs if the URI is bookmarked + // multiple times. + var bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI); + var forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label"); + var label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length); + this._element("editBookmarkPanelRemoveButton").label = label; + + // unset the unstarred state, if set + this._element("editBookmarkPanelStarIcon").removeAttribute("unstarred"); + + this._itemId = aItemId !== undefined ? aItemId : this._itemId; + this.beginBatch(); + + let onPanelReady = fn => { + let target = this.panel; + if (target.parentNode) { + // By targeting the panel's parent and using a capturing listener, we + // can have our listener called before others waiting for the panel to + // be shown (which probably expect the panel to be fully initialized) + target = target.parentNode; + } + target.addEventListener("popupshown", function(event) { + fn(); + }, {"capture": true, "once": true}); + }; + gEditItemOverlay.initPanel(this._itemId, + { onPanelReady, + hiddenRows: ["description", "location", + "loadInSidebar", "keyword"] }); + + this.panel.openPopup(aAnchorElement, aPosition); + }, + + panelShown: + function(aEvent) { + if (aEvent.target == this.panel) { + if (!this._element("editBookmarkPanelContent").hidden) { + let fieldToFocus = "editBMPanel_" + + gPrefService.getCharPref("browser.bookmarks.editDialog.firstEditField"); + var elt = this._element(fieldToFocus); + elt.focus(); + elt.select(); + } else { + // Note this isn't actually used anymore, we should remove this + // once we decide not to bring back the page bookmarked notification + this.panel.focus(); + } + } + }, + + quitEditMode: function() { + this._element("editBookmarkPanelContent").hidden = true; + this._element("editBookmarkPanelBottomButtons").hidden = true; + gEditItemOverlay.uninitPanel(true); + }, + + cancelButtonOnCommand: function() { + this._actionOnHide = "cancel"; + this.panel.hidePopup(); + }, + + removeBookmarkButtonCommand: function() { + this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); + this._actionOnHide = "remove"; + this.panel.hidePopup(); + }, + + beginBatch: function() { + if (!this._batching) { + PlacesUtils.transactionManager.beginBatch(null); + this._batching = true; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// PlacesCommandHook + +var PlacesCommandHook = { + /** + * Adds a bookmark to the page loaded in the given browser. + * + * @param aBrowser + * a <browser> element. + * @param [optional] aParent + * The folder in which to create a new bookmark if the page loaded in + * aBrowser isn't bookmarked yet, defaults to the unfiled root. + * @param [optional] aShowEditUI + * whether or not to show the edit-bookmark UI for the bookmark item + */ + bookmarkPage: function(aBrowser, aParent, aShowEditUI) { + var uri = aBrowser.currentURI; + var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri); + if (itemId == -1) { + // Copied over from addBookmarkForBrowser: + // Bug 52536: We obtain the URL and title from the nsIWebNavigation + // associated with a <browser/> rather than from a DOMWindow. + // This is because when a full page plugin is loaded, there is + // no DOMWindow (?) but information about the loaded document + // may still be obtained from the webNavigation. + var webNav = aBrowser.webNavigation; + var url = webNav.currentURI; + var title; + var description; + var charset; + try { + let isErrorPage = /^about:(neterror|certerror|blocked)/ + .test(webNav.document.documentURI); + title = isErrorPage ? PlacesUtils.history.getPageTitle(url) + : webNav.document.title; + title = title || url.spec; + description = PlacesUIUtils.getDescriptionFromDocument(webNav.document); + charset = webNav.document.characterSet; + } catch(e) {} + + if (aShowEditUI) { + // If we bookmark the page here (i.e. page was not "starred" already) + // but open right into the "edit" state, start batching here, so + // "Cancel" in that state removes the bookmark. + StarUI.beginBatch(); + } + + var parent = aParent != undefined ? + aParent : + PlacesUtils.unfiledBookmarksFolderId; + var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description }; + var txn = new PlacesCreateBookmarkTransaction(uri, parent, + PlacesUtils.bookmarks.DEFAULT_INDEX, + title, null, [descAnno]); + PlacesUtils.transactionManager.doTransaction(txn); + itemId = txn.item.id; + // Set the character-set + if (charset && !PrivateBrowsingUtils.isWindowPrivate(aBrowser.contentWindow)) { + PlacesUtils.setCharsetForURI(uri, charset); + } + } + + // Revert the contents of the location bar + if (gURLBar) { + gURLBar.handleRevert(); + } + + // If it was not requested to open directly in "edit" mode, we are done. + if (!aShowEditUI) { + return; + } + + // Try to dock the panel to: + // 1. the bookmarks menu button + // 2. the page-proxy-favicon + // 3. the content area + if (BookmarkingUI.anchor) { + StarUI.showEditBookmarkPopup(itemId, BookmarkingUI.anchor, + "bottomcenter topright"); + return; + } + + let pageProxyFavicon = document.getElementById("page-proxy-favicon"); + if (isElementVisible(pageProxyFavicon)) { + StarUI.showEditBookmarkPopup(itemId, pageProxyFavicon, + "bottomcenter topright"); + } else { + StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap"); + } + }, + + /** + * Adds a bookmark to the page loaded in the current tab. + */ + bookmarkCurrentPage: function(aShowEditUI, aParent) { + this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI); + }, + + /** + * Adds a bookmark to the page targeted by a link. + * @param aParent + * The folder in which to create a new bookmark if aURL isn't + * bookmarked. + * @param aURL (string) + * the address of the link target + * @param aTitle + * The link text + */ + bookmarkLink: function(aParent, aURL, aTitle) { + var linkURI = makeURI(aURL); + var itemId = PlacesUtils.getMostRecentBookmarkForURI(linkURI); + if (itemId == -1) { + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "bookmark", + uri: linkURI, + title: aTitle, + hiddenRows: [ "description", + "location", + "loadInSidebar", + "keyword" ] + }, window); + } else { + PlacesUIUtils.showBookmarkDialog({ action: "edit", + type: "bookmark", + itemId: itemId + }, window); + } + }, + + /** + * List of nsIURI objects characterizing the tabs currently open in the + * browser, modulo pinned tabs. The URIs will be in the order in which their + * corresponding tabs appeared and duplicates are discarded. + */ + get uniqueCurrentPages() { + let uniquePages = {}; + let URIs = []; + gBrowser.visibleTabs.forEach(function(tab) { + let spec = tab.linkedBrowser.currentURI.spec; + if (!tab.pinned && !(spec in uniquePages)) { + uniquePages[spec] = null; + URIs.push(tab.linkedBrowser.currentURI); + } + }); + return URIs; + }, + + /** + * Adds a folder with bookmarks to all of the currently open tabs in this + * window. + */ + bookmarkCurrentPages: function() { + let pages = this.uniqueCurrentPages; + if (pages.length > 1) { + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "folder", + URIList: pages, + hiddenRows: [ "description" ] + }, window); + } + }, + + /** + * Updates disabled state for the "Bookmark All Tabs" command. + */ + updateBookmarkAllTabsCommand: function() { + // There's nothing to do in non-browser windows. + if (window.location.href != getBrowserURL()) { + return; + } + + // Disable "Bookmark All Tabs" if there are less than two + // "unique current pages". + goSetCommandEnabled("Browser:BookmarkAllTabs", + this.uniqueCurrentPages.length >= 2); + }, + + /** + * Adds a Live Bookmark to a feed associated with the current page. + * @param url + * The nsIURI of the page the feed was attached to + * @title title + * The title of the feed. Optional. + * @subtitle subtitle + * A short description of the feed. Optional. + */ + addLiveBookmark: function(url, feedTitle, feedSubtitle) { + let toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId, -1); + + let feedURI = makeURI(url); + let title = feedTitle || gBrowser.contentTitle; + let description = feedSubtitle; + if (!description) { + description = PlacesUIUtils.getDescriptionFromDocument(gBrowser.contentDocument); + } + + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "livemark", + feedURI: feedURI, + siteURI: gBrowser.currentURI, + title: title, + description: description, + defaultInsertionPoint: toolbarIP, + hiddenRows: [ "feedLocation", + "siteLocation", + "description" ] + }, window); + }, + + /** + * Opens the Places Organizer. + * @param aLeftPaneRoot + * The query to select in the organizer window - options + * are: History, AllBookmarks, BookmarksMenu, BookmarksToolbar, + * UnfiledBookmarks, Tags and Downloads. + */ + showPlacesOrganizer: function(aLeftPaneRoot) { + var organizer = Services.wm.getMostRecentWindow("Places:Organizer"); + // Due to bug 528706, getMostRecentWindow can return closed windows. + if (!organizer || organizer.closed) { + // No currently open places window, so open one with the specified mode. + openDialog("chrome://browser/content/places/places.xul", + "", "chrome,toolbar=yes,dialog=no,resizable", aLeftPaneRoot); + } else { + organizer.PlacesOrganizer.selectLeftPaneQuery(aLeftPaneRoot); + organizer.focus(); + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// HistoryMenu + +// View for the history menu. +function HistoryMenu(aPopupShowingEvent) { + // Workaround for Bug 610187. The sidebar does not include all the Places + // views definitions, and we don't need them there. + // Defining the prototype inheritance in the prototype itself would cause + // browser.js to halt on "PlacesMenu is not defined" error. + this.__proto__.__proto__ = PlacesMenu.prototype; + XPCOMUtils.defineLazyServiceGetter(this, "_ss", + "@mozilla.org/browser/sessionstore;1", + "nsISessionStore"); + PlacesMenu.call(this, aPopupShowingEvent, + "place:sort=4&maxResults=15"); +} + +HistoryMenu.prototype = { + toggleRestoreLastSession: function() { + let restoreItem = this._rootElt.ownerDocument.getElementById("Browser:RestoreLastSession"); + + if (this._ss.canRestoreLastSession && + !PrivateBrowsingUtils.isWindowPrivate(window)) { + restoreItem.removeAttribute("disabled"); + } else { + restoreItem.setAttribute("disabled", true); + } + }, + + toggleRecentlyClosedTabs: function() { + // enable/disable the Recently Closed Tabs sub menu + var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0]; + + // no restorable tabs, so disable menu + if (this._ss.getClosedTabCount(window) == 0) { + undoMenu.setAttribute("disabled", true); + } else { + undoMenu.removeAttribute("disabled"); + } + }, + + /** + * Re-open a closed tab and put it to the end of the tab strip. + * Used for a middle click. + * @param aEvent + * The event when the user clicks the menu item + */ + _undoCloseMiddleClick: function(aEvent) { + if (aEvent.button != 1) { + return; + } + + undoCloseTab(aEvent.originalTarget.value); + gBrowser.moveTabToEnd(); + }, + + /** + * Populate when the history menu is opened + */ + populateUndoSubmenu: function() { + var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0]; + var undoPopup = undoMenu.firstChild; + + // remove existing menu items + while (undoPopup.hasChildNodes()) { + undoPopup.removeChild(undoPopup.firstChild); + } + + // no restorable tabs, so make sure menu is disabled, and return + if (this._ss.getClosedTabCount(window) == 0) { + undoMenu.setAttribute("disabled", true); + return; + } + + // enable menu + undoMenu.removeAttribute("disabled"); + + // populate menu + var undoItems = JSON.parse(this._ss.getClosedTabData(window)); + for (var i = 0; i < undoItems.length; i++) { + var m = document.createElement("menuitem"); + m.setAttribute("label", undoItems[i].title); + if (undoItems[i].image) { + let iconURL = undoItems[i].image; + // don't initiate a connection just to fetch a favicon (see bug 467828) + if (/^https?:/.test(iconURL)) { + iconURL = "moz-anno:favicon:" + iconURL; + } + m.setAttribute("image", iconURL); + } + m.setAttribute("class", "menuitem-iconic bookmark-item menuitem-with-favicon"); + m.setAttribute("value", i); + m.setAttribute("oncommand", "undoCloseTab(" + i + ");"); + + // Set the targetURI attribute so it will be shown in tooltip and trigger + // onLinkHovered. SessionStore uses one-based indexes, so we need to + // normalize them. + let tabData = undoItems[i].state; + let activeIndex = (tabData.index || tabData.entries.length) - 1; + if (activeIndex >= 0 && tabData.entries[activeIndex]) { + m.setAttribute("targetURI", tabData.entries[activeIndex].url); + } + + m.addEventListener("click", this._undoCloseMiddleClick, false); + if (i == 0) { + m.setAttribute("key", "key_undoCloseTab"); + } + undoPopup.appendChild(m); + } + + // "Restore All Tabs" + var strings = gNavigatorBundle; + undoPopup.appendChild(document.createElement("menuseparator")); + m = undoPopup.appendChild(document.createElement("menuitem")); + m.id = "menu_restoreAllTabs"; + m.setAttribute("label", strings.getString("menuRestoreAllTabs.label")); + m.addEventListener("command", function() { + for (var i = 0; i < undoItems.length; i++) { + undoCloseTab(); + } + }, false); + }, + + toggleRecentlyClosedWindows: function() { + // enable/disable the Recently Closed Windows sub menu + var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0]; + + // no restorable windows, so disable menu + if (this._ss.getClosedWindowCount() == 0) { + undoMenu.setAttribute("disabled", true); + } else { + undoMenu.removeAttribute("disabled"); + } + }, + + /** + * Populate when the history menu is opened + */ + populateUndoWindowSubmenu: function() { + let undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0]; + let undoPopup = undoMenu.firstChild; + let menuLabelString = gNavigatorBundle.getString("menuUndoCloseWindowLabel"); + let menuLabelStringSingleTab = gNavigatorBundle.getString("menuUndoCloseWindowSingleTabLabel"); + + // remove existing menu items + while (undoPopup.hasChildNodes()) { + undoPopup.removeChild(undoPopup.firstChild); + } + + // no restorable windows, so make sure menu is disabled, and return + if (this._ss.getClosedWindowCount() == 0) { + undoMenu.setAttribute("disabled", true); + return; + } + + // enable menu + undoMenu.removeAttribute("disabled"); + + // populate menu + let undoItems = JSON.parse(this._ss.getClosedWindowData()); + for (let i = 0; i < undoItems.length; i++) { + let undoItem = undoItems[i]; + let otherTabsCount = undoItem.tabs.length - 1; + let label = (otherTabsCount == 0) ? menuLabelStringSingleTab + : PluralForm.get(otherTabsCount, menuLabelString); + let menuLabel = label.replace("#1", undoItem.title) + .replace("#2", otherTabsCount); + let m = document.createElement("menuitem"); + m.setAttribute("label", menuLabel); + let selectedTab = undoItem.tabs[undoItem.selected - 1]; + if (selectedTab.image) { + let iconURL = selectedTab.image; + // don't initiate a connection just to fetch a favicon (see bug 467828) + if (/^https?:/.test(iconURL)) { + iconURL = "moz-anno:favicon:" + iconURL; + } + m.setAttribute("image", iconURL); + } + m.setAttribute("class", "menuitem-iconic bookmark-item menuitem-with-favicon"); + m.setAttribute("oncommand", "undoCloseWindow(" + i + ");"); + + // Set the targetURI attribute so it will be shown in tooltip. + // SessionStore uses one-based indexes, so we need to normalize them. + let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1; + if (activeIndex >= 0 && selectedTab.entries[activeIndex]) { + m.setAttribute("targetURI", selectedTab.entries[activeIndex].url); + } + + if (i == 0) { + m.setAttribute("key", "key_undoCloseWindow"); + } + undoPopup.appendChild(m); + } + + // "Open All in Windows" + undoPopup.appendChild(document.createElement("menuseparator")); + let m = undoPopup.appendChild(document.createElement("menuitem")); + m.id = "menu_restoreAllWindows"; + m.setAttribute("label", gNavigatorBundle.getString("menuRestoreAllWindows.label")); + m.setAttribute("oncommand", + "for (var i = 0; i < " + undoItems.length + "; i++) undoCloseWindow();"); + }, + + toggleTabsFromOtherComputers: function() { + // This is a no-op if MOZ_SERVICES_SYNC isn't defined +#ifdef MOZ_SERVICES_SYNC + // Enable/disable the Tabs From Other Computers menu. Some of the menus handled + // by HistoryMenu do not have this menuitem. + let menuitem = this._rootElt.getElementsByClassName("syncTabsMenuItem")[0]; + if (!menuitem) { + return; + } + + // If Sync isn't configured yet, then don't show the menuitem. + if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || + Weave.Svc.Prefs.get("firstSync", "") == "notReady") { + menuitem.setAttribute("hidden", true); + return; + } + + // The tabs engine might never be inited (if services.sync.registerEngines + // is modified), so make sure we avoid undefined errors. + let enabled = Weave.Service.isLoggedIn && + Weave.Service.engineManager.get("tabs") && + Weave.Service.engineManager.get("tabs").enabled; + menuitem.setAttribute("disabled", !enabled); + menuitem.setAttribute("hidden", false); +#endif + }, + + _onPopupShowing: function(aEvent) { + PlacesMenu.prototype._onPopupShowing.apply(this, arguments); + + // Don't handle events for submenus. + if (aEvent.target != aEvent.currentTarget) { + return; + } + + this.toggleRestoreLastSession(); + this.toggleRecentlyClosedTabs(); + this.toggleRecentlyClosedWindows(); + this.toggleTabsFromOtherComputers(); + }, + + _onCommand: function(aEvent) { + let placesNode = aEvent.target._placesNode; + if (placesNode) { + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + PlacesUIUtils.markPageAsTyped(placesNode.uri); + } + openUILink(placesNode.uri, aEvent, { ignoreAlt: true }); + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// BookmarksEventHandler + +/** + * Functions for handling events in the Bookmarks Toolbar and menu. + */ +var BookmarksEventHandler = { + /** + * Handler for click event for an item in the bookmarks toolbar or menu. + * Menus and submenus from the folder buttons bubble up to this handler. + * Left-click is handled in the onCommand function. + * When items are middle-clicked (or clicked with modifier), open in tabs. + * If the click came through a menu, close the menu. + * @param aEvent + * DOMEvent for the click + * @param aView + * The places view which aEvent should be associated with. + */ + onClick: function(aEvent, aView) { + // Only handle middle-click or left-click with modifiers. + var modifKey = aEvent.ctrlKey || aEvent.shiftKey; + if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey)) { + return; + } + + var target = aEvent.originalTarget; + // If this event bubbled up from a menu or menuitem, close the menus. + // Do this before opening tabs, to avoid hiding the open tabs confirm-dialog. + if (target.localName == "menu" || target.localName == "menuitem") { + for (node = target.parentNode; node; node = node.parentNode) { + if (node.localName == "menupopup") { + node.hidePopup(); + } else if (node.localName != "menu" && + node.localName != "splitmenu" && + node.localName != "hbox" && + node.localName != "vbox" ) { + break; + } + } + } + + if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) { + // Don't open the root folder in tabs when the empty area on the toolbar + // is middle-clicked or when a non-bookmark item except for Open in Tabs) + // in a bookmarks menupopup is middle-clicked. + if (target.localName == "menu" || target.localName == "toolbarbutton") { + PlacesUIUtils.openContainerNodeInTabs(target._placesNode, aEvent, aView); + } + } else if (aEvent.button == 1) { + // left-clicks with modifier are already served by onCommand + this.onCommand(aEvent, aView); + } + }, + + /** + * Handler for command event for an item in the bookmarks toolbar. + * Menus and submenus from the folder buttons bubble up to this handler. + * Opens the item. + * @param aEvent + * DOMEvent for the command + * @param aView + * The places view which aEvent should be associated with. + */ + onCommand: function(aEvent, aView) { + var target = aEvent.originalTarget; + if (target._placesNode) { + PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView); + } + }, + + fillInBHTooltip: function(aDocument, aEvent) { + var node; + var cropped = false; + var targetURI; + + if (aDocument.tooltipNode.localName == "treechildren") { + var tree = aDocument.tooltipNode.parentNode; + var tbo = tree.treeBoxObject; + var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY); + if (cell.row == -1) { + return false; + } + node = tree.view.nodeForTreeIndex(cell.row); + cropped = tbo.isCellCropped(cell.row, cell.col); + } else { + // Check whether the tooltipNode is a Places node. + // In such a case use it, otherwise check for targetURI attribute. + var tooltipNode = aDocument.tooltipNode; + if (tooltipNode._placesNode) { + node = tooltipNode._placesNode; + } else { + // This is a static non-Places node. + targetURI = tooltipNode.getAttribute("targetURI"); + } + } + + if (!node && !targetURI) { + return false; + } + + // Show node.label as tooltip's title for non-Places nodes. + var title = node ? node.title : tooltipNode.label; + + // Show URL only for Places URI-nodes or nodes with a targetURI attribute. + var url; + if (targetURI || PlacesUtils.nodeIsURI(node)) { + url = targetURI || node.uri; + } + + // Show tooltip for containers only if their title is cropped. + if (!cropped && !url) { + return false; + } + + var tooltipTitle = aDocument.getElementById("bhtTitleText"); + tooltipTitle.hidden = (!title || (title == url)); + if (!tooltipTitle.hidden) { + tooltipTitle.textContent = title; + } + + var tooltipUrl = aDocument.getElementById("bhtUrlText"); + tooltipUrl.hidden = !url; + if (!tooltipUrl.hidden) { + tooltipUrl.value = url; + } + + // Show tooltip. + return true; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// PlacesMenuDNDHandler + +// Handles special drag and drop functionality for Places menus that are not +// part of a Places view (e.g. the bookmarks menu in the menubar). +var PlacesMenuDNDHandler = { + _springLoadDelay: 350, // milliseconds + _loadTimer: null, + _closerTimer: null, + + /** + * Called when the user enters the <menu> element during a drag. + * @param event + * The DragEnter event that spawned the opening. + */ + onDragEnter: function(event) { + // Opening menus in a Places popup is handled by the view itself. + if (!this._isStaticContainer(event.target)) { + return; + } + + let popup = event.target.lastChild; + if (this._loadTimer || popup.state === "showing" || popup.state === "open") { + return; + } + + this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._loadTimer.initWithCallback(() => { + this._loadTimer = null; + popup.setAttribute("autoopened", "true"); + popup.showPopup(popup); + }, this._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Handles dragleave on the <menu> element. + * @returns true if the element is a container element (menu or + * menu-toolbarbutton), false otherwise. + */ + onDragLeave: function(event) { + // Handle menu-button separate targets. + if (event.relatedTarget === event.currentTarget || + event.relatedTarget.parentNode === event.currentTarget) { + return; + } + + // Closing menus in a Places popup is handled by the view itself. + if (!this._isStaticContainer(event.target)) { + return; + } + + let popup = event.target.lastChild; + + if (this._loadTimer) { + this._loadTimer.cancel(); + this._loadTimer = null; + } + this._closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._closeTimer.initWithCallback(function() { + this._closeTimer = null; + let node = PlacesControllerDragHelper.currentDropTarget; + let inHierarchy = false; + while (node && !inHierarchy) { + inHierarchy = node == event.target; + node = node.parentNode; + } + if (!inHierarchy && popup && popup.hasAttribute("autoopened")) { + popup.removeAttribute("autoopened"); + popup.hidePopup(); + } + }, this._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT); + }, + + /** + * Determines if a XUL element represents a static container. + * @returns true if the element is a container element (menu or + *` menu-toolbarbutton), false otherwise. + */ + _isStaticContainer: function(node) { + let isMenu = node.localName == "menu" || + (node.localName == "toolbarbutton" && + (node.getAttribute("type") == "menu" || + node.getAttribute("type") == "menu-button")); + let isStatic = !("_placesNode" in node) && + node.lastChild && + node.lastChild.hasAttribute("placespopup") && + !node.parentNode.hasAttribute("placespopup"); + return isMenu && isStatic; + }, + + /** + * Called when the user drags over the <menu> element. + * @param event + * The DragOver event. + */ + onDragOver: function(event) { + let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Ci.nsITreeView.DROP_ON); + if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer)) { + event.preventDefault(); + } + + event.stopPropagation(); + }, + + /** + * Called when the user drops on the <menu> element. + * @param event + * The Drop event. + */ + onDrop: function(event) { + // Put the item at the end of bookmark menu. + let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Ci.nsITreeView.DROP_ON); + PlacesControllerDragHelper.onDrop(ip, event.dataTransfer); + event.stopPropagation(); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// PlacesToolbarHelper + +/** + * This object handles the initialization and uninitialization of the bookmarks + * toolbar. + */ +var PlacesToolbarHelper = { + _place: "place:folder=TOOLBAR", + + get _viewElt() { + return document.getElementById("PlacesToolbar"); + }, + + init: function() { + let viewElt = this._viewElt; + if (!viewElt || viewElt._placesView) { + return; + } + + // If the bookmarks toolbar item is hidden because the parent toolbar is + // collapsed or hidden (i.e. in a popup), spare the initialization. Also, + // there is no need to initialize the toolbar if customizing because + // init() will be called when the customization is done. + let toolbar = viewElt.parentNode.parentNode; + if (toolbar.collapsed || + getComputedStyle(toolbar, "").display == "none" || + this._isCustomizing) { + return; + } + + new PlacesToolbar(this._place); + }, + + customizeStart: function() { + let viewElt = this._viewElt; + if (viewElt && viewElt._placesView) { + viewElt._placesView.uninit(); + } + + this._isCustomizing = true; + }, + + customizeDone: function() { + this._isCustomizing = false; + this.init(); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// BookmarkingUI + +/** + * Handles the bookmarks star button in the URL bar, as well as the bookmark + * menu button. + */ + +var BookmarkingUI = { + get button() { + if (!this._button) { + this._button = document.getElementById("bookmarks-menu-button"); + } + return this._button; + }, + + get star() { + if (!this._star) { + this._star = document.getElementById("star-button"); + } + return this._star; + }, + + get anchor() { + if (this.star && isElementVisible(this.star)) { + // Anchor to the icon, so the panel looks more natural. + return this.star; + } + return null; + }, + + STATUS_UPDATING: -1, + STATUS_UNSTARRED: 0, + STATUS_STARRED: 1, + get status() { + if (this._pendingStmt) { + return this.STATUS_UPDATING; + } + return this.star && + this.star.hasAttribute("starred") ? this.STATUS_STARRED : this.STATUS_UNSTARRED; + }, + + get _starredTooltip() + { + delete this._starredTooltip; + return this._starredTooltip = gNavigatorBundle.getString("starButtonOn.tooltip"); + }, + + get _unstarredTooltip() + { + delete this._unstarredTooltip; + return this._unstarredTooltip = gNavigatorBundle.getString("starButtonOff.tooltip"); + }, + + /** + * The popup contents must be updated when the user customizes the UI, or + * changes the personal toolbar collapsed status. In such a case, any needed + * change should be handled in the popupshowing helper, for performance + * reasons. + */ + _popupNeedsUpdate: true, + onToolbarVisibilityChange: function() { + this._popupNeedsUpdate = true; + }, + + onPopupShowing: function(event) { + // Don't handle events for submenus. + if (event.target != event.currentTarget) { + return; + } + + if (!this._popupNeedsUpdate) { + return; + } + + this._popupNeedsUpdate = false; + + let popup = event.target; + let getPlacesAnonymousElement = + aAnonId => document.getAnonymousElementByAttribute(popup.parentNode, + "placesanonid", + aAnonId); + + let viewToolbarMenuitem = getPlacesAnonymousElement("view-toolbar"); + if (viewToolbarMenuitem) { + // Update View bookmarks toolbar checkbox menuitem. + let personalToolbar = document.getElementById("PersonalToolbar"); + viewToolbarMenuitem.setAttribute("checked", !personalToolbar.collapsed); + } + + let toolbarMenuitem = getPlacesAnonymousElement("toolbar-autohide"); + if (toolbarMenuitem) { + // If bookmarks items are visible, hide Bookmarks Toolbar menu and the + // separator after it. + toolbarMenuitem.collapsed = toolbarMenuitem.nextSibling.collapsed = + isElementVisible(document.getElementById("personal-bookmarks")); + } + }, + + /** + * Handles star styling based on page proxy state changes. + */ + onPageProxyStateChanged: function(aState) { + if (!this.star) { + return; + } + + if (aState == "invalid") { + this.star.setAttribute("disabled", "true"); + this.star.removeAttribute("starred"); + } else { + this.star.removeAttribute("disabled"); + } + }, + + _updateToolbarStyle: function() { + if (!this.button) { + return; + } + + let personalToolbar = document.getElementById("PersonalToolbar"); + let onPersonalToolbar = this.button.parentNode == personalToolbar || + this.button.parentNode.parentNode == personalToolbar; + + if (onPersonalToolbar) { + this.button.classList.add("bookmark-item"); + this.button.classList.remove("toolbarbutton-1"); + } else { + this.button.classList.remove("bookmark-item"); + this.button.classList.add("toolbarbutton-1"); + } + }, + + _uninitView: function() { + // When an element with a placesView attached is removed and re-inserted, + // XBL reapplies the binding causing any kind of issues and possible leaks, + // so kill current view and let popupshowing generate a new one. + if (this.button && this.button._placesView) { + this.button._placesView.uninit(); + } + // Also uninit the main menubar placesView, since it would have the same + // issues. + let menubar = document.getElementById("bookmarksMenu"); + if (menubar && menubar._placesView) { + menubar._placesView.uninit(); + } + }, + + customizeStart: function() { + this._uninitView(); + }, + + customizeChange: function() { + this._updateToolbarStyle(); + }, + + customizeDone: function() { + delete this._button; + this.onToolbarVisibilityChange(); + this._updateToolbarStyle(); + }, + + _hasBookmarksObserver: false, + uninit: function() { + this._uninitView(); + + if (this._hasBookmarksObserver) { + PlacesUtils.removeLazyBookmarkObserver(this); + } + + if (this._pendingStmt) { + this._pendingStmt.cancel(); + delete this._pendingStmt; + } + }, + + onLocationChange: function() { + if (this._uri && gBrowser.currentURI.equals(this._uri)) { + return; + } + this.updateStarState(); + }, + + updateStarState: function() { + // Reset tracked values. + this._uri = gBrowser.currentURI; + this._itemIds = []; + + if (this._pendingStmt) { + this._pendingStmt.cancel(); + delete this._pendingStmt; + } + + // We can load about:blank before the actual page, but there is no point in handling that page. + if (isBlankPageURL(this._uri.spec)) { + return; + } + + this._pendingStmt = PlacesUtils.asyncGetBookmarkIds(this._uri, (aItemIds, aURI) => { + // Safety check that the bookmarked URI equals the tracked one. + if (!aURI.equals(this._uri)) { + Components.utils.reportError("BookmarkingUI did not receive current URI"); + return; + } + + // It's possible that onItemAdded gets called before the async statement + // calls back. For such an edge case, retain all unique entries from both + // arrays. + this._itemIds = this._itemIds.filter( + function(id) aItemIds.indexOf(id) == -1 + ).concat(aItemIds); + + this._updateStar(); + + // Start observing bookmarks if needed. + if (!this._hasBookmarksObserver) { + try { + PlacesUtils.addLazyBookmarkObserver(this); + this._hasBookmarksObserver = true; + } catch(ex) { + Components.utils.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex); + } + } + + delete this._pendingStmt; + }, this); + }, + + _updateStar: function() { + if (!this.star) { + return; + } + + if (this._itemIds.length > 0) { + this.star.setAttribute("starred", "true"); + this.star.setAttribute("tooltiptext", this._starredTooltip); + } else { + this.star.removeAttribute("starred"); + this.star.setAttribute("tooltiptext", this._unstarredTooltip); + } + }, + + onCommand: function(aEvent) { + if (aEvent.target != aEvent.currentTarget) { + return; + } + // Ignore clicks on the star if we are updating its state. + if (!this._pendingStmt) { + PlacesCommandHook.bookmarkCurrentPage(this._itemIds.length > 0 || + StarUI.showForNewBookmarks); + } + }, + + // nsINavBookmarkObserver + onItemAdded: function(aItemId, aParentId, aIndex, aItemType, + aURI) { + if (aURI && aURI.equals(this._uri)) { + // If a new bookmark has been added to the tracked uri, register it. + if (this._itemIds.indexOf(aItemId) == -1) { + this._itemIds.push(aItemId); + this._updateStar(); + } + } + }, + + onItemRemoved: function(aItemId) { + let index = this._itemIds.indexOf(aItemId); + // If one of the tracked bookmarks has been removed, unregister it. + if (index != -1) { + this._itemIds.splice(index, 1); + this._updateStar(); + } + }, + + onItemChanged: function(aItemId, aProperty, + aIsAnnotationProperty, aNewValue) { + if (aProperty == "uri") { + let index = this._itemIds.indexOf(aItemId); + // If the changed bookmark was tracked, check if it is now pointing to + // a different uri and unregister it. + if (index != -1 && aNewValue != this._uri.spec) { + this._itemIds.splice(index, 1); + this._updateStar(); + } else if (index == -1 && aNewValue == this._uri.spec) { + // If another bookmark is now pointing to the tracked uri, register it. + this._itemIds.push(aItemId); + this._updateStar(); + } + } + }, + + onBeginUpdateBatch: function() {}, + onEndUpdateBatch: function() {}, + onBeforeItemRemoved: function() {}, + onItemVisited: function() {}, + onItemMoved: function() {}, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver + ]) +}; diff --git a/browser/base/content/browser-plugins.js b/browser/base/content/browser-plugins.js new file mode 100644 index 000000000..9e44981bb --- /dev/null +++ b/browser/base/content/browser-plugins.js @@ -0,0 +1,789 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +const kPrefSessionPersistMinutes = "plugin.sessionPermissionNow.intervalInMinutes"; +const kPrefPersistentDays = "plugin.persistentPermissionAlways.intervalInDays"; + +var gPluginHandler = { + PLUGIN_SCRIPTED_STATE_NONE: 0, + PLUGIN_SCRIPTED_STATE_FIRED: 1, + PLUGIN_SCRIPTED_STATE_DONE: 2, + + getPluginUI: function(plugin, anonid) { + return plugin.ownerDocument. + getAnonymousElementByAttribute(plugin, "anonid", anonid); + }, + + _getPluginInfo: function(pluginElement) { + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + pluginElement.QueryInterface(Ci.nsIObjectLoadingContent); + + let tagMimetype; + let pluginName = gNavigatorBundle.getString("pluginInfo.unknownPlugin"); + let pluginTag = null; + let permissionString = null; + let fallbackType = null; + let blocklistState = null; + + if (pluginElement instanceof HTMLAppletElement) { + tagMimetype = "application/x-java-vm"; + } else { + tagMimetype = pluginElement.actualType; + + if (tagMimetype == "") { + tagMimetype = pluginElement.type; + } + } + + if (gPluginHandler.isKnownPlugin(pluginElement)) { + pluginTag = pluginHost.getPluginTagForType(pluginElement.actualType); + pluginName = gPluginHandler.makeNicePluginName(pluginTag.name); + + permissionString = pluginHost.getPermissionStringForType(pluginElement.actualType); + fallbackType = pluginElement.defaultFallbackType; + blocklistState = pluginHost.getBlocklistStateForType(pluginElement.actualType); + // Make state-softblocked == state-notblocked for our purposes, + // they have the same UI. STATE_OUTDATED should not exist for plugin + // items, but let's alias it anyway, just in case. + if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED || + blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) { + blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + } + } + + return { mimetype: tagMimetype, + pluginName: pluginName, + pluginTag: pluginTag, + permissionString: permissionString, + fallbackType: fallbackType, + blocklistState: blocklistState, + }; + }, + + // Map the plugin's name to a filtered version more suitable for user UI. + makeNicePluginName : function(aName) { + if (aName == "Shockwave Flash") { + return "Adobe Flash"; + } + + // Clean up the plugin name by stripping off any trailing version numbers + // or "plugin". EG, "Foo Bar Plugin 1.23_02" --> "Foo Bar" + // Do this by first stripping the numbers, etc. off the end, and then + // removing "Plugin" (and then trimming to get rid of any whitespace). + // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled) + let newName = aName.replace(/[\s\d\.\-\_\(\)]+$/, "").replace(/\bplug-?in\b/i, "").trim(); + return newName; + }, + + isTooSmall : function(plugin, overlay) { + // Is the <object>'s size too small to hold what we want to show? + let pluginRect = plugin.getBoundingClientRect(); + // XXX bug 446693. The text-shadow on the submitted-report text at + // the bottom causes scrollHeight to be larger than it should be. + // Clamp width/height to properly show CTP overlay on different + // zoom levels when embedded in iframes (rounding bug). (Bug 972237) + let overflows = (overlay.scrollWidth > Math.ceil(pluginRect.width)) || + (overlay.scrollHeight - 5 > Math.ceil(pluginRect.height)); + return overflows; + }, + + addLinkClickCallback: function(linkNode, callbackName /*callbackArgs...*/) { + // XXX just doing (callback)(arg) was giving a same-origin error. bug? + let self = this; + let callbackArgs = Array.prototype.slice.call(arguments).slice(2); + linkNode.addEventListener("click", + function(evt) { + if (!evt.isTrusted) { + return; + } + evt.preventDefault(); + if (callbackArgs.length == 0) { + callbackArgs = [ evt ]; + } + (self[callbackName]).apply(self, callbackArgs); + }, true); + + linkNode.addEventListener("keydown", + function(evt) { + if (!evt.isTrusted) { + return; + } + if (evt.keyCode == evt.DOM_VK_RETURN) { + evt.preventDefault(); + if (callbackArgs.length == 0) { + callbackArgs = [ evt ]; + } + evt.preventDefault(); + (self[callbackName]).apply(self, callbackArgs); + } + }, true); + }, + + // Helper to get the binding handler type from a plugin object + _getBindingType : function(plugin) { + if (!(plugin instanceof Ci.nsIObjectLoadingContent)) { + return null; + } + + switch (plugin.pluginFallbackType) { + case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED: + return "PluginNotFound"; + case Ci.nsIObjectLoadingContent.PLUGIN_DISABLED: + return "PluginDisabled"; + case Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED: + return "PluginBlocklisted"; + case Ci.nsIObjectLoadingContent.PLUGIN_OUTDATED: + return "PluginOutdated"; + case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY: + return "PluginClickToPlay"; + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE: + return "PluginVulnerableUpdatable"; + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE: + return "PluginVulnerableNoUpdate"; + case Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW: + return "PluginPlayPreview"; + default: + // Not all states map to a handler + return null; + } + }, + + handleEvent : function(event) { + let plugin; + let doc; + + let eventType = event.type; + if (eventType === "PluginRemoved") { + doc = event.target; + } else { + plugin = event.target; + doc = plugin.ownerDocument; + + if (!(plugin instanceof Ci.nsIObjectLoadingContent)) { + return; + } + } + + if (eventType == "PluginBindingAttached") { + // The plugin binding fires this event when it is created. + // As an untrusted event, ensure that this object actually has a binding + // and make sure we don't handle it twice + let overlay = this.getPluginUI(plugin, "main"); + if (!overlay || overlay._bindingHandled) { + return; + } + overlay._bindingHandled = true; + + // Lookup the handler for this binding + eventType = this._getBindingType(plugin); + if (!eventType) { + // Not all bindings have handlers + return; + } + } + + let shouldShowNotification = false; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + if (!browser) { + return; + } + + switch (eventType) { + case "PluginCrashed": + this.pluginInstanceCrashed(plugin, event); + break; + + case "PluginNotFound": + /* No action (plugin finder obsolete) */ + break; + + case "PluginBlocklisted": + case "PluginOutdated": + shouldShowNotification = true; + break; + + case "PluginVulnerableUpdatable": + let updateLink = this.getPluginUI(plugin, "checkForUpdatesLink"); + this.addLinkClickCallback(updateLink, "openPluginUpdatePage"); + /* FALLTHRU */ + + case "PluginVulnerableNoUpdate": + case "PluginClickToPlay": + this._handleClickToPlayEvent(plugin); + let overlay = this.getPluginUI(plugin, "main"); + let pluginName = this._getPluginInfo(plugin).pluginName; + let messageString = gNavigatorBundle.getFormattedString("PluginClickToActivate", [pluginName]); + let overlayText = this.getPluginUI(plugin, "clickToPlay"); + overlayText.textContent = messageString; + if (eventType == "PluginVulnerableUpdatable" || + eventType == "PluginVulnerableNoUpdate") { + let vulnerabilityString = gNavigatorBundle.getString(eventType); + let vulnerabilityText = this.getPluginUI(plugin, "vulnerabilityStatus"); + vulnerabilityText.textContent = vulnerabilityString; + } + shouldShowNotification = true; + break; + + case "PluginPlayPreview": + this._handlePlayPreviewEvent(plugin); + break; + + case "PluginDisabled": + let manageLink = this.getPluginUI(plugin, "managePluginsLink"); + this.addLinkClickCallback(manageLink, "managePlugins"); + shouldShowNotification = true; + break; + + case "PluginInstantiated": + //Pale Moon: don't show the indicator when plugins are enabled/allowed + if (gPrefService.getBoolPref("plugins.always_show_indicator")) { + shouldShowNotification = true; + } + break; + case "PluginRemoved": + shouldShowNotification = true; + break; + } + + // Show the in-content UI if it's not too big. The crashed plugin handler already did this. + if (eventType != "PluginCrashed" && eventType != "PluginRemoved") { + let overlay = this.getPluginUI(plugin, "main"); + if (overlay != null) { + if (!this.isTooSmall(plugin, overlay)) { + overlay.style.visibility = "visible"; + } + plugin.addEventListener("overflow", function(event) { + overlay.style.visibility = "hidden"; + }); + plugin.addEventListener("underflow", function(event) { + // this is triggered if only one dimension underflows, + // the other dimension might still overflow + if (!gPluginHandler.isTooSmall(plugin, overlay)) { + overlay.style.visibility = "visible"; + } + }); + } + } + + // Only show the notification after we've done the isTooSmall check, so + // that the notification can decide whether to show the "alert" icon + if (shouldShowNotification) { + this._showClickToPlayNotification(browser); + } + }, + + isKnownPlugin: function(objLoadingContent) { + return (objLoadingContent.getContentTypeForMIMEType(objLoadingContent.actualType) == + Ci.nsIObjectLoadingContent.TYPE_PLUGIN); + }, + + canActivatePlugin: function(objLoadingContent) { + // if this isn't a known plugin, we can't activate it + // (this also guards pluginHost.getPermissionStringForType against + // unexpected input) + if (!gPluginHandler.isKnownPlugin(objLoadingContent)) { + return false; + } + + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType); + let principal = objLoadingContent.ownerDocument.defaultView.top.document.nodePrincipal; + let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString); + + let isFallbackTypeValid = + objLoadingContent.pluginFallbackType >= Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY && + objLoadingContent.pluginFallbackType <= Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE; + + if (objLoadingContent.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW) { + // checking if play preview is subject to CTP rules + let playPreviewInfo = pluginHost.getPlayPreviewInfo(objLoadingContent.actualType); + isFallbackTypeValid = !playPreviewInfo.ignoreCTP; + } + + return !objLoadingContent.activated && + pluginPermission != Ci.nsIPermissionManager.DENY_ACTION && + isFallbackTypeValid; + }, + + hideClickToPlayOverlay: function(aPlugin) { + let overlay = this.getPluginUI(aPlugin, "main"); + if (overlay) { + overlay.style.visibility = "hidden"; + } + }, + + stopPlayPreview: function(aPlugin, aPlayPlugin) { + let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (objLoadingContent.activated) { + return; + } + + if (aPlayPlugin) { + objLoadingContent.playPlugin(); + } else { + objLoadingContent.cancelPlayPreview(); + } + }, + + // Callback for user clicking on a disabled plugin + managePlugins: function(aEvent) { + BrowserOpenAddonsMgr("addons://list/plugin"); + }, + + // Callback for user clicking a "reload page" link + reloadPage: function(browser) { + browser.reload(); + }, + + // Callback for user clicking the help icon + openHelpPage: function() { + openHelpLink("plugin-crashed", false); + }, + + // Event listener for click-to-play plugins. + _handleClickToPlayEvent: function(aPlugin) { + let doc = aPlugin.ownerDocument; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent); + // guard against giving pluginHost.getPermissionStringForType a type + // not associated with any known plugin + if (!gPluginHandler.isKnownPlugin(objLoadingContent)) { + return; + } + let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType); + let principal = doc.defaultView.top.document.nodePrincipal; + let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString); + + let overlay = this.getPluginUI(aPlugin, "main"); + + if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) { + if (overlay) { + overlay.style.visibility = "hidden"; + } + return; + } + + if (overlay) { + overlay.addEventListener("click", gPluginHandler._overlayClickListener, true); + let closeIcon = gPluginHandler.getPluginUI(aPlugin, "closeIcon"); + closeIcon.addEventListener("click", function(aEvent) { + if (aEvent.button == 0 && aEvent.isTrusted) { + gPluginHandler.hideClickToPlayOverlay(aPlugin); + } + }, true); + } + }, + + _overlayClickListener: { + handleEvent: function(aEvent) { + let plugin = document.getBindingParent(aEvent.target); + let contentWindow = plugin.ownerDocument.defaultView.top; + // gBrowser.getBrowserForDocument does not exist in the case where we + // drag-and-dropped a tab from a window containing only that tab. In + // that case, the window gets destroyed. + let browser = gBrowser.getBrowserForDocument ? + gBrowser.getBrowserForDocument(contentWindow.document) : + null; + // If browser is null here, we've been drag-and-dropped from another + // window, and this is the wrong click handler. + if (!browser) { + aEvent.target.removeEventListener("click", gPluginHandler._overlayClickListener, true); + return; + } + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + // Have to check that the target is not the link to update the plugin + if (!(aEvent.originalTarget instanceof HTMLAnchorElement) && + (aEvent.originalTarget.getAttribute('anonid') != 'closeIcon') && + aEvent.button == 0 && aEvent.isTrusted) { + gPluginHandler._showClickToPlayNotification(browser, plugin); + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + } + }, + + _handlePlayPreviewEvent: function(aPlugin) { + let doc = aPlugin.ownerDocument; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let pluginInfo = this._getPluginInfo(aPlugin); + let playPreviewInfo = pluginHost.getPlayPreviewInfo(pluginInfo.mimetype); + + let previewContent = this.getPluginUI(aPlugin, "previewPluginContent"); + let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0]; + if (!iframe) { + // lazy initialization of the iframe + iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe"); + iframe.className = "previewPluginContentFrame"; + previewContent.appendChild(iframe); + + // Force a style flush, so that we ensure our binding is attached. + aPlugin.clientTop; + } + iframe.src = playPreviewInfo.redirectURL; + + // MozPlayPlugin event can be dispatched from the extension chrome + // code to replace the preview content with the native plugin + previewContent.addEventListener("MozPlayPlugin", function playPluginHandler(aEvent) { + if (!aEvent.isTrusted) { + return; + } + + previewContent.removeEventListener("MozPlayPlugin", playPluginHandler, true); + + let playPlugin = !aEvent.detail; + gPluginHandler.stopPlayPreview(aPlugin, playPlugin); + + // cleaning up: removes overlay iframe from the DOM + let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0]; + if (iframe) { + previewContent.removeChild(iframe); + } + }, true); + + if (!playPreviewInfo.ignoreCTP) { + gPluginHandler._showClickToPlayNotification(browser); + } + }, + + reshowClickToPlayNotification: function() { + let browser = gBrowser.selectedBrowser; + let contentWindow = browser.contentWindow; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let doc = contentWindow.document; + let plugins = cwu.plugins; + for (let plugin of plugins) { + let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main"); + if (overlay) { + overlay.removeEventListener("click", gPluginHandler._overlayClickListener, true); + } + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (gPluginHandler.canActivatePlugin(objLoadingContent)) { + gPluginHandler._handleClickToPlayEvent(plugin); + } + } + gPluginHandler._showClickToPlayNotification(browser); + }, + + _clickToPlayNotificationEventCallback: function(event) { + if (event == "showing") { + gPluginHandler._makeCenterActions(this); + } else if (event == "dismissed") { + // Once the popup is dismissed, clicking the icon should show the full + // list again + this.options.primaryPlugin = null; + } + }, + + _makeCenterActions: function(notification) { + let contentWindow = notification.browser.contentWindow; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let principal = contentWindow.document.nodePrincipal; + + let centerActions = []; + let pluginsFound = new Set(); + for (let plugin of cwu.plugins) { + plugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (plugin.getContentTypeForMIMEType(plugin.actualType) != Ci.nsIObjectLoadingContent.TYPE_PLUGIN) { + continue; + } + + let pluginInfo = this._getPluginInfo(plugin); + if (pluginInfo.permissionString === null) { + Components.utils.reportError("No permission string for active plugin."); + continue; + } + if (pluginsFound.has(pluginInfo.permissionString)) { + continue; + } + pluginsFound.add(pluginInfo.permissionString); + + // Add the per-site permissions and details URLs to pluginInfo here + // because they are more expensive to compute and so we avoid it in + // the tighter loop above. + let permissionObj = Services.perms. + getPermissionObject(principal, pluginInfo.permissionString, false); + if (permissionObj) { + pluginInfo.pluginPermissionPrePath = permissionObj.principal.originNoSuffix; + pluginInfo.pluginPermissionType = permissionObj.expireType; + } else { + pluginInfo.pluginPermissionPrePath = principal.originNoSuffix; + pluginInfo.pluginPermissionType = undefined; + } + + let url; + if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) { + url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag); + } + pluginInfo.detailsLink = url; + + centerActions.push(pluginInfo); + } + centerActions.sort(function(a, b) { + return a.pluginName.localeCompare(b.pluginName); + }); + + notification.options.centerActions = centerActions; + }, + + /** + * Called from the plugin doorhanger to set the new permissions for a plugin + * and activate plugins if necessary. + * aNewState should be either "allownow" "allowalways" or "block" + */ + _updatePluginPermission: function(aNotification, aPluginInfo, aNewState) { + let permission; + let expireType; + let expireTime; + + switch (aNewState) { + case "allownow": + permission = Ci.nsIPermissionManager.ALLOW_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_SESSION; + expireTime = Date.now() + Services.prefs.getIntPref(kPrefSessionPersistMinutes) * 60 * 1000; + break; + + case "allowalways": + permission = Ci.nsIPermissionManager.ALLOW_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_TIME; + expireTime = Date.now() + + Services.prefs.getIntPref(kPrefPersistentDays) * 24 * 60 * 60 * 1000; + break; + + case "block": + permission = Ci.nsIPermissionManager.PROMPT_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_NEVER; + expireTime = 0; + break; + + // In case a plugin has already been allowed in another tab, the "continue allowing" button + // shouldn't change any permissions but should run the plugin-enablement code below. + case "continue": + break; + + default: + Cu.reportError(Error("Unexpected plugin state: " + aNewState)); + return; + } + + let browser = aNotification.browser; + let contentWindow = browser.contentWindow; + if (aNewState != "continue") { + let principal = contentWindow.document.nodePrincipal; + Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString, + permission, expireType, expireTime); + + if (aNewState == "block") { + return; + } + } + + // Manually activate the plugins that would have been automatically + // activated. + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let plugins = cwu.plugins; + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + + for (let plugin of plugins) { + plugin.QueryInterface(Ci.nsIObjectLoadingContent); + // canActivatePlugin will return false if this isn't a known plugin type, + // so the pluginHost.getPermissionStringForType call is protected + if (gPluginHandler.canActivatePlugin(plugin) && + aPluginInfo.permissionString == pluginHost.getPermissionStringForType(plugin.actualType)) { + plugin.playPlugin(); + } + } + }, + + _showClickToPlayNotification: function(aBrowser, aPrimaryPlugin) { + let notification = PopupNotifications.getNotification("click-to-play-plugins", aBrowser); + + let contentWindow = aBrowser.contentWindow; + let contentDoc = aBrowser.contentDocument; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + // Pale Moon: cwu.plugins may contain non-plugin <object>s, filter them out + let plugins = cwu.plugins.filter(function(plugin) { + return (plugin.getContentTypeForMIMEType(plugin.actualType) == + Ci.nsIObjectLoadingContent.TYPE_PLUGIN); + }); + if (plugins.length == 0) { + if (notification) { + PopupNotifications.remove(notification); + } + return; + } + + let icon = 'plugins-notification-icon'; + for (let plugin of plugins) { + let fallbackType = plugin.pluginFallbackType; + if (fallbackType == plugin.PLUGIN_VULNERABLE_UPDATABLE || + fallbackType == plugin.PLUGIN_VULNERABLE_NO_UPDATE || + fallbackType == plugin.PLUGIN_BLOCKLISTED) { + icon = 'blocked-plugins-notification-icon'; + break; + } + if (fallbackType == plugin.PLUGIN_CLICK_TO_PLAY) { + let overlay = contentDoc.getAnonymousElementByAttribute(plugin, "anonid", "main"); + if (!overlay || overlay.style.visibility == 'hidden') { + icon = 'alert-plugins-notification-icon'; + } + } + } + + let dismissed = notification ? notification.dismissed : true; + if (aPrimaryPlugin) { + dismissed = false; + } + + let primaryPluginPermission = null; + if (aPrimaryPlugin) { + primaryPluginPermission = this._getPluginInfo(aPrimaryPlugin).permissionString; + } + + let options = { + dismissed: dismissed, + eventCallback: this._clickToPlayNotificationEventCallback, + primaryPlugin: primaryPluginPermission + }; + PopupNotifications.show(aBrowser, "click-to-play-plugins", + "", icon, null, null, options); + }, + + // Crashed-plugin observer. Notified once per plugin crash, before events + // are dispatched to individual plugin instances. + pluginCrashed : function(subject, topic, data) { + let propertyBag = subject; + if (!(propertyBag instanceof Ci.nsIPropertyBag2) || + !(propertyBag instanceof Ci.nsIWritablePropertyBag2)) { + return; + } + }, + + // Crashed-plugin event listener. Called for every instance of a + // plugin in content. + pluginInstanceCrashed: function(plugin, aEvent) { + // Ensure the plugin and event are of the right type. + if (!(aEvent instanceof Ci.nsIDOMDataContainerEvent)) { + return; + } + + let submittedReport = aEvent.getData("submittedCrashReport"); + let doPrompt = true; // XXX followup for .getData("doPrompt"); + let submitReports = true; // XXX followup for .getData("submitReports"); + let pluginName = aEvent.getData("pluginName"); + let pluginDumpID = aEvent.getData("pluginDumpID"); + let browserDumpID = aEvent.getData("browserDumpID"); + + // Remap the plugin name to a more user-presentable form. + pluginName = this.makeNicePluginName(pluginName); + + let messageString = gNavigatorBundle.getFormattedString("crashedpluginsMessage.title", [pluginName]); + + // + // Configure the crashed-plugin placeholder. + // + + // Force a layout flush so the binding is attached. + plugin.clientTop; + let doc = plugin.ownerDocument; + let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + let statusDiv = doc.getAnonymousElementByAttribute(plugin, "class", "submitStatus"); + + let crashText = doc.getAnonymousElementByAttribute(plugin, "class", "msgCrashedText"); + crashText.textContent = messageString; + + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + + let link = doc.getAnonymousElementByAttribute(plugin, "class", "reloadLink"); + this.addLinkClickCallback(link, "reloadPage", browser); + + let notificationBox = gBrowser.getNotificationBox(browser); + + let isShowing = true; + + // Is the <object>'s size too small to hold what we want to show? + if (this.isTooSmall(plugin, overlay)) { + // First try hiding the crash report submission UI. + statusDiv.removeAttribute("status"); + + if (this.isTooSmall(plugin, overlay)) { + // Hide the overlay's contents. Use visibility style, so that it doesn't + // collapse down to 0x0. + overlay.style.visibility = "hidden"; + isShowing = false; + } + } + + if (isShowing) { + // If a previous plugin on the page was too small and resulted in adding a + // notification bar, then remove it because this plugin instance it big + // enough to serve as in-content notification. + hideNotificationBar(); + doc.mozNoPluginCrashedNotification = true; + } else { + // If another plugin on the page was large enough to show our UI, we don't + // want to show a notification bar. + if (!doc.mozNoPluginCrashedNotification) + showNotificationBar(pluginDumpID, browserDumpID); + } + + function hideNotificationBar() { + let notification = notificationBox.getNotificationWithValue("plugin-crashed"); + if (notification) { + notificationBox.removeNotification(notification, true); + } + } + + function showNotificationBar(pluginDumpID, browserDumpID) { + // If there's already an existing notification bar, don't do anything. + let notification = notificationBox.getNotificationWithValue("plugin-crashed"); + if (notification) { + return; + } + + // Configure the notification bar + let priority = notificationBox.PRIORITY_WARNING_MEDIUM; + let iconURL = "chrome://mozapps/skin/plugins/notifyPluginCrashed.png"; + let reloadLabel = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.label"); + let reloadKey = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.accesskey"); + let submitLabel = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.label"); + let submitKey = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.accesskey"); + + let buttons = [{ + label: reloadLabel, + accessKey: reloadKey, + popup: null, + callback: function() { browser.reload(); }, + }]; + + notification = notificationBox.appendNotification(messageString, "plugin-crashed", + iconURL, priority, buttons); + + // Add the "learn more" link. + let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let link = notification.ownerDocument.createElementNS(XULNS, "label"); + link.className = "text-link"; + link.setAttribute("value", gNavigatorBundle.getString("crashedpluginsMessage.learnMore")); + let crashurl = formatURL("app.support.baseURL", true); + crashurl += "plugin-crashed-notificationbar"; + link.href = crashurl; + + let description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText"); + description.appendChild(link); + + // Remove the notfication when the page is reloaded. + doc.defaultView.top.addEventListener("unload", function() { + notificationBox.removeNotification(notification); + }, false); + } + + } +}; diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc new file mode 100644 index 000000000..905b7d8d5 --- /dev/null +++ b/browser/base/content/browser-sets.inc @@ -0,0 +1,325 @@ +# -*- 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/. + +#ifdef XP_UNIX +#define XP_GNOME 1 +#endif + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/> + <stringbundle id="bundle_shell" src="chrome://browser/locale/shellservice.properties"/> + <stringbundle id="bundle_preferences" src="chrome://browser/locale/preferences/preferences.properties"/> + </stringbundleset> + + <commandset id="mainCommandSet"> + <command id="cmd_newNavigator" oncommand="OpenBrowserWindow()"/> + <command id="cmd_handleBackspace" oncommand="BrowserHandleBackspace();" /> + <command id="cmd_handleShiftBackspace" oncommand="BrowserHandleShiftBackspace();" /> + + <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab();"/> + <command id="Browser:OpenFile" oncommand="BrowserOpenFileWindow();"/> + <command id="Browser:SavePage" oncommand="saveDocument(window.content.document);"/> + + <command id="Browser:SendLink" + oncommand="MailIntegration.sendLinkForWindow(window.content);"/> + + <command id="cmd_pageSetup" oncommand="PrintUtils.showPageSetup();"/> + <command id="cmd_print" oncommand="PrintUtils.print();"/> + <command id="cmd_printPreview" oncommand="PrintUtils.printPreview(PrintPreviewListener);"/> + <command id="cmd_close" oncommand="BrowserCloseTabOrWindow()"/> + <command id="cmd_closeWindow" oncommand="BrowserTryToCloseWindow()"/> + <command id="cmd_toggleMute" oncommand="gBrowser.selectedTab.toggleMuteAudio()"/> + <command id="cmd_ToggleTabsOnTop" oncommand="TabsOnTop.toggle()"/> + <command id="cmd_CustomizeToolbars" oncommand="BrowserCustomizeToolbar()"/> + <command id="cmd_restartApplication" oncommand="restart(false);"/> + <command id="cmd_quitApplication" oncommand="goQuitApplication()"/> + + + <commandset id="editMenuCommands"/> + + <command id="View:PageSource" oncommand="BrowserViewSourceOfDocument(content.document);" observes="isImage"/> + <command id="View:PageInfo" oncommand="BrowserPageInfo();"/> + <command id="View:FullScreen" oncommand="BrowserFullScreen();"/> + <command id="cmd_find" + oncommand="gFindBar.onFindCommand();" + observes="isImage"/> + <command id="cmd_findAgain" + oncommand="gFindBar.onFindAgainCommand(false);" + observes="isImage"/> + <command id="cmd_findPrevious" + oncommand="gFindBar.onFindAgainCommand(true);" + observes="isImage"/> + <!-- work-around bug 392512 --> + <command id="Browser:AddBookmarkAs" + oncommand="PlacesCommandHook.bookmarkCurrentPage(true, PlacesUtils.bookmarksMenuFolderId);"/> + <!-- The command disabled state must be manually updated through + PlacesCommandHook.updateBookmarkAllTabsCommand() --> + <command id="Browser:BookmarkAllTabs" + oncommand="PlacesCommandHook.bookmarkCurrentPages();"/> + <command id="Browser:Home" oncommand="BrowserHome();"/> + <command id="Browser:Back" oncommand="BrowserBack();" disabled="true"/> + <command id="Browser:BackOrBackDuplicate" oncommand="BrowserBack(event);" disabled="true"> + <observes element="Browser:Back" attribute="disabled"/> + </command> + <command id="Browser:Forward" oncommand="BrowserForward();" disabled="true"/> + <command id="Browser:ForwardOrForwardDuplicate" oncommand="BrowserForward(event);" disabled="true"> + <observes element="Browser:Forward" attribute="disabled"/> + </command> + <command id="Browser:Stop" oncommand="BrowserStop();" disabled="true"/> + <command id="Browser:Reload" oncommand="if (event.shiftKey) BrowserReloadSkipCache(); else BrowserReload()" disabled="true"/> + <command id="Browser:ReloadOrDuplicate" oncommand="BrowserReloadOrDuplicate(event)" disabled="true"> + <observes element="Browser:Reload" attribute="disabled"/> + </command> + <command id="Browser:ReloadSkipCache" oncommand="BrowserReloadSkipCache()" disabled="true"> + <observes element="Browser:Reload" attribute="disabled"/> + </command> + <command id="Browser:NextTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(1, true);"/> + <command id="Browser:PrevTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(-1, true);"/> + <command id="Browser:ShowAllTabs" oncommand="allTabs.open();"/> + <command id="cmd_fullZoomReduce" oncommand="FullZoom.reduce()"/> + <command id="cmd_fullZoomEnlarge" oncommand="FullZoom.enlarge()"/> + <command id="cmd_fullZoomReset" oncommand="FullZoom.reset()"/> + <command id="cmd_fullZoomToggle" oncommand="ZoomManager.toggleZoom();"/> + <command id="cmd_gestureRotateLeft" oncommand="gGestureSupport.rotate(event.sourceEvent)"/> + <command id="cmd_gestureRotateRight" oncommand="gGestureSupport.rotate(event.sourceEvent)"/> + <command id="cmd_gestureRotateEnd" oncommand="gGestureSupport.rotateEnd()"/> + <command id="Browser:OpenLocation" oncommand="openLocation();"/> + <command id="Browser:RestoreLastSession" oncommand="restoreLastSession();" disabled="true"/> + + <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/> + <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/> + <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/> + <command id="Tools:Permissions" oncommand="BrowserOpenPermissionsMgr();"/> + <command id="Tools:ErrorConsole" oncommand="toJavaScriptConsole()" disabled="true" hidden="true"/> + <command id="Tools:Sanitize" + oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/> + <command id="Tools:PrivateBrowsing" + oncommand="OpenBrowserWindow({private: true});"/> + <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/> + <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/> + <command id="Browser:ToggleAddonBar" oncommand="toggleAddonBar();"/> + </commandset> + + <commandset id="placesCommands"> + <command id="Browser:ShowAllBookmarks" + oncommand="PlacesCommandHook.showPlacesOrganizer('AllBookmarks');"/> + <command id="Browser:ShowAllHistory" + oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/> + </commandset> + + <broadcasterset id="mainBroadcasterSet"> + <broadcaster id="viewBookmarksSidebar" autoCheck="false" sidebartitle="&bookmarksButton.label;" + type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul" + oncommand="toggleSidebar('viewBookmarksSidebar');"/> + + <!-- for both places and non-places, the sidebar lives at + chrome://browser/content/history/history-panel.xul so there are no + problems when switching between versions --> + <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;" + type="checkbox" group="sidebar" + sidebarurl="chrome://browser/content/history/history-panel.xul" + oncommand="toggleSidebar('viewHistorySidebar');"/> + + <broadcaster id="viewWebPanelsSidebar" autoCheck="false" + type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul" + oncommand="toggleSidebar('viewWebPanelsSidebar');"/> + + <!-- popup blocking menu items --> + <broadcaster id="blockedPopupAllowSite" + accesskey="&allowPopups.accesskey;" + oncommand="gPopupBlockerObserver.toggleAllowPopupsForSite(event);"/> + <broadcaster id="blockedPopupEditSettings" +#ifdef XP_WIN + label="&editPopupSettings.label;" +#else + label="&editPopupSettingsUnix.label;" +#endif + accesskey="&editPopupSettings.accesskey;" + oncommand="gPopupBlockerObserver.editPopupSettings();"/> + <broadcaster id="blockedPopupDontShowMessage" + accesskey="&dontShowMessage.accesskey;" + type="checkbox" + oncommand="gPopupBlockerObserver.dontShowMessage();"/> + <broadcaster id="blockedPopupsSeparator"/> + <broadcaster id="isImage"/> + <broadcaster id="isFrameImage"/> + <broadcaster id="singleFeedMenuitemState" disabled="true"/> + <broadcaster id="multipleFeedsMenuState" hidden="true"/> +#ifdef MOZ_SERVICES_SYNC + <broadcaster id="sync-setup-state"/> + <broadcaster id="sync-syncnow-state"/> +#endif + <broadcaster id="workOfflineMenuitemState"/> + + <broadcaster id="devtoolsMenuBroadcaster_PageSource" + label="&pageSourceCmd.label;" + key="key_viewSource" + command="View:PageSource"/> + <broadcaster id="devtoolsMenuBroadcaster_ErrorConsole" + label="&errorConsoleCmd.label;" + command="Tools:ErrorConsole"/> + </broadcasterset> + + <keyset id="mainKeyset"> + <key id="key_newNavigator" + key="&newNavigatorCmd.key;" + command="cmd_newNavigator" + modifiers="accel"/> + <key id="key_newNavigatorTab" key="&tabCmd.commandkey;" modifiers="accel" command="cmd_newNavigatorTab"/> + <key id="focusURLBar" key="&openCmd.commandkey;" command="Browser:OpenLocation" + modifiers="accel"/> + <key id="focusURLBar2" key="&urlbar.accesskey;" command="Browser:OpenLocation" + modifiers="alt"/> + +# +# Search Command Key Logic works like this: +# +# Unix: Ctrl+K (cross platform binding) +# Ctrl+J (in case of emacs Ctrl-K conflict) +# Mac: Cmd+K (cross platform binding) +# Cmd+Opt+F (platform convention) +# Win: Ctrl+K (cross platform binding) +# Ctrl+E (IE compat) +# +# We support Ctrl+K on all platforms now and advertise it in the menu since it is +# our standard - it is a "safe" choice since it is near no harmful keys like "W" as +# "E" is. People mourning the loss of Ctrl+K for emacs compat can switch their GTK +# system setting to use emacs emulation, and we should respect it. Focus-Search-Box +# is a fundamental keybinding and we are maintaining a XP binding so that it is easy +# for people to switch to Linux. +# + <key id="key_search" key="&searchFocus.commandkey;" command="Tools:Search" modifiers="accel"/> +#ifdef XP_WIN + <key id="key_search2" key="&searchFocus.commandkey2;" command="Tools:Search" modifiers="accel"/> +#endif +#ifdef XP_GNOME + <key id="key_search2" key="&searchFocusUnix.commandkey;" command="Tools:Search" modifiers="accel"/> + <key id="key_openDownloads" key="&downloadsUnix.commandkey;" command="Tools:Downloads" modifiers="accel,shift"/> +#else + <key id="key_openDownloads" key="&downloads.commandkey;" command="Tools:Downloads" modifiers="accel"/> +#endif + <key id="key_openAddons" key="&addons.commandkey;" command="Tools:Addons" modifiers="accel,shift"/> + <key id="openFileKb" key="&openFileCmd.commandkey;" command="Browser:OpenFile" modifiers="accel"/> + <key id="key_savePage" key="&savePageCmd.commandkey;" command="Browser:SavePage" modifiers="accel"/> + <key id="printKb" key="&printCmd.commandkey;" command="cmd_print" modifiers="accel"/> + <key id="key_close" key="&closeCmd.key;" command="cmd_close" modifiers="accel"/> + <key id="key_closeWindow" key="&closeCmd.key;" command="cmd_closeWindow" modifiers="accel,shift"/> + <key id="key_toggleMute" key="&toggleMuteCmd.key;" command="cmd_toggleMute" modifiers="control"/> + <key id="key_undo" + key="&undoCmd.key;" + modifiers="accel"/> +#ifdef XP_UNIX + <key id="key_redo" key="&undoCmd.key;" modifiers="accel,shift"/> +#else + <key id="key_redo" key="&redoCmd.key;" modifiers="accel"/> +#endif + <key id="key_cut" + key="&cutCmd.key;" + modifiers="accel"/> + <key id="key_copy" + key="©Cmd.key;" + modifiers="accel"/> + <key id="key_paste" + key="&pasteCmd.key;" + modifiers="accel"/> + <key id="key_delete" keycode="VK_DELETE" command="cmd_delete"/> + <key id="key_selectAll" key="&selectAllCmd.key;" modifiers="accel"/> + + <key keycode="VK_BACK" command="cmd_handleBackspace"/> + <key keycode="VK_BACK" command="cmd_handleShiftBackspace" modifiers="shift"/> + <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="alt"/> + <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="alt"/> +#ifdef XP_UNIX + <key id="goBackKb2" key="&goBackCmd.commandKey;" command="Browser:Back" modifiers="accel"/> + <key id="goForwardKb2" key="&goForwardCmd.commandKey;" command="Browser:Forward" modifiers="accel"/> +#endif + <key id="goHome" keycode="VK_HOME" command="Browser:Home" modifiers="alt"/> + <key keycode="VK_F5" command="Browser:Reload"/> + <key id="showAllHistoryKb" key="&showAllHistoryCmd.commandkey;" command="Browser:ShowAllHistory" modifiers="accel,shift"/> + <key keycode="VK_F5" command="Browser:ReloadSkipCache" modifiers="accel"/> + <key id="key_fullScreen" keycode="VK_F11" command="View:FullScreen"/> + <key key="&reloadCmd.commandkey;" command="Browser:Reload" modifiers="accel" id="key_reload"/> + <key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/> + <key id="key_viewSource" key="&pageSourceCmd.commandkey;" command="View:PageSource" modifiers="accel"/> +#ifndef XP_WIN + <key id="key_viewInfo" key="&pageInfoCmd.commandkey;" command="View:PageInfo" modifiers="accel"/> +#endif + <key id="key_find" key="&findOnCmd.commandkey;" command="cmd_find" modifiers="accel"/> + <key id="key_findAgain" key="&findAgainCmd.commandkey;" command="cmd_findAgain" modifiers="accel"/> + <key id="key_findPrevious" key="&findAgainCmd.commandkey;" command="cmd_findPrevious" modifiers="accel,shift"/> + <key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/> + <key keycode="&findAgainCmd.commandkey2;" command="cmd_findPrevious" modifiers="shift"/> + + <key id="addBookmarkAsKb" key="&bookmarkThisPageCmd.commandkey;" command="Browser:AddBookmarkAs" modifiers="accel"/> +# Accel+Shift+A-F are reserved on GTK +#ifndef MOZ_WIDGET_GTK + <key id="bookmarkAllTabsKb" key="&bookmarkThisPageCmd.commandkey;" oncommand="PlacesCommandHook.bookmarkCurrentPages();" modifiers="accel,shift"/> + <key id="manBookmarkKb" key="&bookmarksCmd.commandkey;" command="Browser:ShowAllBookmarks" modifiers="accel,shift"/> +#else + <key id="manBookmarkKb" key="&bookmarksGtkCmd.commandkey;" command="Browser:ShowAllBookmarks" modifiers="accel,shift"/> +#endif + <key id="viewBookmarksSidebarKb" key="&bookmarksCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/> +#ifdef XP_WIN +# Cmd+I is conventially mapped to Info on MacOS X, thus it should not be +# overridden for other purposes there. + <key id="viewBookmarksSidebarWinKb" key="&bookmarksWinCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/> +#endif + +# Navigation cancel keys: Esc performs a cancel on loading (i.e.: stop button equivalent) +# Shift-Esc (and similar Shift-modified stop on Mac) performs a "superstop": this halts all +# networking requests, XHR, animated gifs, etc. + <key id="key_stop" keycode="VK_ESCAPE" command="Browser:Stop"/> + <key id="key_stop_all" keycode="VK_ESCAPE" modifiers="shift" oncommand="BrowserStop();"/> + + <key id="key_gotoHistory" + key="&historySidebarCmd.commandKey;" + modifiers="accel" + command="viewHistorySidebar"/> + + <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/> + <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/> + <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/> + <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/> + <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/> + <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/> + <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/> + + <key id="key_showAllTabs" command="Browser:ShowAllTabs" keycode="VK_TAB" modifiers="control,shift"/> + + <key id="key_switchTextDirection" key="&bidiSwitchTextDirectionItem.commandkey;" command="cmd_switchTextDirection" modifiers="accel,shift" /> + + <key id="key_privatebrowsing" command="Tools:PrivateBrowsing" key="&privateBrowsingCmd.commandkey;" modifiers="accel,shift"/> + <key id="key_sanitize" command="Tools:Sanitize" keycode="VK_DELETE" modifiers="accel,shift"/> +#ifdef XP_UNIX + <key id="key_quitApplication" key="&quitApplicationCmdUnix.key;" command="cmd_quitApplication" modifiers="accel"/> +#endif + +#ifdef FULL_BROWSER_WINDOW + <key id="key_undoCloseTab" command="History:UndoCloseTab" key="&tabCmd.commandkey;" modifiers="accel,shift"/> +#endif + <key id="key_undoCloseWindow" command="History:UndoCloseWindow" key="&newNavigatorCmd.key;" modifiers="accel,shift"/> + +#ifdef XP_GNOME +#define NUM_SELECT_TAB_MODIFIER alt +#else +#define NUM_SELECT_TAB_MODIFIER accel +#endif + +#expand <key id="key_selectTab1" oncommand="gBrowser.selectTabAtIndex(0, event);" key="1" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab2" oncommand="gBrowser.selectTabAtIndex(1, event);" key="2" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab3" oncommand="gBrowser.selectTabAtIndex(2, event);" key="3" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab4" oncommand="gBrowser.selectTabAtIndex(3, event);" key="4" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab5" oncommand="gBrowser.selectTabAtIndex(4, event);" key="5" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab6" oncommand="gBrowser.selectTabAtIndex(5, event);" key="6" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab7" oncommand="gBrowser.selectTabAtIndex(6, event);" key="7" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab8" oncommand="gBrowser.selectTabAtIndex(7, event);" key="8" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectLastTab" oncommand="gBrowser.selectTabAtIndex(-1, event);" key="9" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> + + <key id="key_toggleAddonBar" command="Browser:ToggleAddonBar" key="&toggleAddonBarCmd.key;" modifiers="accel"/> + + </keyset> + +# Used by baseMenuOverlay + <keyset id="baseMenuKeyset" /> diff --git a/browser/base/content/browser-syncui.js b/browser/base/content/browser-syncui.js new file mode 100644 index 000000000..884c2587a --- /dev/null +++ b/browser/base/content/browser-syncui.js @@ -0,0 +1,491 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// gSyncUI handles updating the tools menu +var gSyncUI = { + _obs: ["weave:service:sync:start", + "weave:service:sync:delayed", + "weave:service:quota:remaining", + "weave:service:setup-complete", + "weave:service:login:start", + "weave:service:login:finish", + "weave:service:logout:finish", + "weave:service:start-over", + "weave:ui:login:error", + "weave:ui:sync:error", + "weave:ui:sync:finish", + "weave:ui:clear-error", + ], + + _unloaded: false, + + init: function() { + // Proceed to set up the UI if Sync has already started up. + // Otherwise we'll do it when Sync is firing up. + let xps = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + if (xps.ready) { + this.initUI(); + return; + } + + Services.obs.addObserver(this, "weave:service:ready", true); + + // Remove the observer if the window is closed before the observer + // was triggered. + window.addEventListener("unload", function onUnload() { + gSyncUI._unloaded = true; + window.removeEventListener("unload", onUnload, false); + Services.obs.removeObserver(gSyncUI, "weave:service:ready"); + + if (Weave.Status.ready) { + gSyncUI._obs.forEach(function(topic) { + Services.obs.removeObserver(gSyncUI, topic); + }); + } + }, false); + }, + + initUI: function() { + // If this is a browser window? + if (gBrowser) { + this._obs.push("weave:notification:added"); + } + + this._obs.forEach(function(topic) { + Services.obs.addObserver(this, topic, true); + }, this); + + if (gBrowser && Weave.Notifications.notifications.length) { + this.initNotifications(); + } + this.updateUI(); + }, + + initNotifications: function() { + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let notificationbox = document.createElementNS(XULNS, "notificationbox"); + notificationbox.id = "sync-notifications"; + notificationbox.setAttribute("flex", "1"); + + let bottombox = document.getElementById("browser-bottombox"); + bottombox.insertBefore(notificationbox, bottombox.firstChild); + + // Force a style flush to ensure that our binding is attached. + notificationbox.clientTop; + + // notificationbox will listen to observers from now on. + Services.obs.removeObserver(this, "weave:notification:added"); + }, + + _wasDelayed: false, + + _needsSetup: function() { + let firstSync = Services.prefs.getCharPref("services.sync.firstSync", ""); + return Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || + firstSync == "notReady"; + }, + + updateUI: function() { + let needsSetup = this._needsSetup(); + document.getElementById("sync-setup-state").hidden = !needsSetup; + document.getElementById("sync-syncnow-state").hidden = needsSetup; + + if (!gBrowser) { + return; + } + + let button = document.getElementById("sync-button"); + if (!button) { + return; + } + + button.removeAttribute("status"); + + this._updateLastSyncTime(); + + if (needsSetup) { + button.removeAttribute("tooltiptext"); + button.setAttribute("label", this._stringBundle.GetStringFromName("setupsync.label")); + } else { + button.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label")); + } + }, + + + // Functions called by observers + onActivityStart: function() { + if (!gBrowser) { + return; + } + + let button = document.getElementById("sync-button"); + if (!button) { + return; + } + + button.setAttribute("status", "active"); + button.setAttribute("label", this._stringBundle.GetStringFromName("syncing2.label")); + }, + + onSyncDelay: function() { + // basically, we want to just inform users that stuff is going to take a while + let title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + let description = this._stringBundle.GetStringFromName("error.sync.no_node_found"); + let buttons = [new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"), + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"), + function() { gSyncUI.openServerStatus(); return true; } + )]; + let notification = new Weave.Notification( + title, description, null, Weave.Notifications.PRIORITY_INFO, buttons); + Weave.Notifications.replaceTitle(notification); + this._wasDelayed = true; + }, + + onLoginFinish: function() { + // Clear out any login failure notifications + let title = this._stringBundle.GetStringFromName("error.login.title"); + this.clearError(title); + }, + + onSetupComplete: function() { + this.onLoginFinish(); + }, + + onLoginError: function() { + // if login fails, any other notifications are essentially moot + Weave.Notifications.removeAll(); + + // if we haven't set up the client, don't show errors + if (this._needsSetup()) { + this.updateUI(); + return; + } + + let title = this._stringBundle.GetStringFromName("error.login.title"); + + let description; + if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) { + // Convert to days + let lastSync = + Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; + description = + this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1); + } else { + let reason = Weave.Utils.getErrorString(Weave.Status.login); + description = + this._stringBundle.formatStringFromName("error.sync.description", [reason], 1); + } + + let buttons = []; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.login.prefs.label"), + this._stringBundle.GetStringFromName("error.login.prefs.accesskey"), + function() { + gSyncUI.openPrefs(); + return true; + } + )); + + let notification = new Weave.Notification(title, description, null, + Weave.Notifications.PRIORITY_WARNING, buttons); + Weave.Notifications.replaceTitle(notification); + this.updateUI(); + }, + + onLogout: function() { + this.updateUI(); + }, + + onStartOver: function() { + this.clearError(); + }, + + onQuotaNotice: function onQuotaNotice(subject, data) { + let title = this._stringBundle.GetStringFromName("warning.sync.quota.label"); + let description = this._stringBundle.GetStringFromName("warning.sync.quota.description"); + let buttons = []; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.label"), + this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.accesskey"), + function() { + gSyncUI.openQuotaDialog(); + return true; + } + )); + + let notification = new Weave.Notification( + title, description, null, Weave.Notifications.PRIORITY_WARNING, buttons); + Weave.Notifications.replaceTitle(notification); + }, + + openServerStatus: function() { + let statusURL = Services.prefs.getCharPref("services.sync.statusURL"); + window.openUILinkIn(statusURL, "tab"); + }, + + // Commands + doSync: function() { + setTimeout(function() Weave.Service.errorHandler.syncAndReportErrors(), 0); + }, + + handleToolbarButton: function() { + if (this._needsSetup()) { + this.openSetup(); + } else { + this.doSync(); + } + }, + + //XXXzpao should be part of syncCommon.js - which we might want to make a module... + // To be fixed in a followup (bug 583366) + + /** + * Invoke the Sync setup wizard. + * + * @param wizardType + * Indicates type of wizard to launch: + * null -- regular set up wizard + * "pair" -- pair a device first + * "reset" -- reset sync + */ + + openSetup: function(wizardType) { + let win = Services.wm.getMostRecentWindow("Weave:AccountSetup"); + if (win) { + win.focus(); + } else { + window.openDialog("chrome://weave/content/setup.xul", + "weaveSetup", "centerscreen,chrome,resizable=no", + wizardType); + } + }, + + openAddDevice: function() { + if (!Weave.Utils.ensureMPUnlocked()) { + return; + } + + let win = Services.wm.getMostRecentWindow("Sync:AddDevice"); + if (win) { + win.focus(); + } else { + window.openDialog("chrome://weave/content/addDevice.xul", + "syncAddDevice", "centerscreen,chrome,resizable=no"); + } + }, + + openQuotaDialog: function() { + let win = Services.wm.getMostRecentWindow("Sync:ViewQuota"); + if (win) { + win.focus(); + } else { + Services.ww.activeWindow.openDialog( + "chrome://weave/content/quota.xul", "", + "centerscreen,chrome,dialog,modal"); + } + }, + + openPrefs: function() { + openPreferences("paneSync"); + }, + + + // Helpers + _updateLastSyncTime: function() { + if (!gBrowser) { + return; + } + + let syncButton = document.getElementById("sync-button"); + if (!syncButton) { + return; + } + + let lastSync = Services.prefs.getCharPref("services.sync.lastSync", ""); + if (!lastSync || this._needsSetup()) { + syncButton.removeAttribute("tooltiptext"); + return; + } + + // Show the day-of-week and time (HH:MM) of last sync + let lastSyncDate = new Date(lastSync).toLocaleFormat("%a %H:%M"); + let lastSyncLabel = + this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDate], 1); + + syncButton.setAttribute("tooltiptext", lastSyncLabel); + }, + + clearError: function(errorString) { + Weave.Notifications.removeAll(errorString); + this.updateUI(); + }, + + onSyncFinish: function() { + let title = this._stringBundle.GetStringFromName("error.sync.title"); + + // Clear out sync failures on a successful sync + this.clearError(title); + + if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) { + title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + this.clearError(title); + this._wasDelayed = false; + } + }, + + onSyncError: function() { + let title = this._stringBundle.GetStringFromName("error.sync.title"); + + if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) { + this.onLoginError(); + return; + } + + let description; + if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) { + // Convert to days + let lastSync = + Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; + description = + this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1); + } else { + let error = Weave.Utils.getErrorString(Weave.Status.sync); + description = + this._stringBundle.formatStringFromName("error.sync.description", [error], 1); + } + let priority = Weave.Notifications.PRIORITY_WARNING; + let buttons = []; + + // Check if the client is outdated in some way + let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE; + for (let [engine, reason] in Iterator(Weave.Status.engines)) { + outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE; + } + + if (outdated) { + description = this._stringBundle.GetStringFromName( + "error.sync.needUpdate.description"); + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.needUpdate.label"), + this._stringBundle.GetStringFromName("error.sync.needUpdate.accesskey"), + function() { + window.openUILinkIn(Services.prefs.getCharPref("services.sync.outdated.url"), "tab"); + return true; + } + )); + } else if (Weave.Status.sync == Weave.OVER_QUOTA) { + description = this._stringBundle.GetStringFromName( + "error.sync.quota.description"); + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName( + "error.sync.viewQuotaButton.label"), + this._stringBundle.GetStringFromName( + "error.sync.viewQuotaButton.accesskey"), + function() { + gSyncUI.openQuotaDialog(); + return true; + } + )); + } else if (Weave.Status.enforceBackoff) { + priority = Weave.Notifications.PRIORITY_INFO; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"), + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"), + function() { + gSyncUI.openServerStatus(); + return true; + } + )); + } else { + priority = Weave.Notifications.PRIORITY_INFO; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"), + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"), + function() { + gSyncUI.doSync(); + return true; + } + )); + } + + let notification = + new Weave.Notification(title, description, null, priority, buttons); + Weave.Notifications.replaceTitle(notification); + + if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) { + title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + Weave.Notifications.removeAll(title); + this._wasDelayed = false; + } + + this.updateUI(); + }, + + observe: function(subject, topic, data) { + if (this._unloaded) { + Cu.reportError("SyncUI observer called after unload: " + topic); + return; + } + + switch (topic) { + case "weave:service:sync:start": + this.onActivityStart(); + break; + case "weave:ui:sync:finish": + this.onSyncFinish(); + break; + case "weave:ui:sync:error": + this.onSyncError(); + break; + case "weave:service:sync:delayed": + this.onSyncDelay(); + break; + case "weave:service:quota:remaining": + this.onQuotaNotice(); + break; + case "weave:service:setup-complete": + this.onSetupComplete(); + break; + case "weave:service:login:start": + this.onActivityStart(); + break; + case "weave:service:login:finish": + this.onLoginFinish(); + break; + case "weave:ui:login:error": + this.onLoginError(); + break; + case "weave:service:logout:finish": + this.onLogout(); + break; + case "weave:service:start-over": + this.onStartOver(); + break; + case "weave:service:ready": + this.initUI(); + break; + case "weave:notification:added": + this.initNotifications(); + break; + case "weave:ui:clear-error": + this.clearError(); + break; + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]) +}; + +XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() { + return Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle("chrome://weave/locale/sync.properties"); +}); + diff --git a/browser/base/content/browser-tabPreviews.js b/browser/base/content/browser-tabPreviews.js new file mode 100644 index 000000000..b2d1b0c69 --- /dev/null +++ b/browser/base/content/browser-tabPreviews.js @@ -0,0 +1,1139 @@ +/* +#ifdef 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/. +#endif + */ + +/** + * Tab previews utility, produces thumbnails + */ +var tabPreviews = { + aspectRatio: 0.5625, // 16:9 + + get width() { + delete this.width; + return this.width = Math.ceil(screen.availWidth / 5.75); + }, + + get height() { + delete this.height; + return this.height = Math.round(this.width * this.aspectRatio); + }, + + init: function() { + if (this._selectedTab) { + return; + } + this._selectedTab = gBrowser.selectedTab; + + gBrowser.tabContainer.addEventListener("TabSelect", this, false); + gBrowser.tabContainer.addEventListener("SSTabRestored", this, false); + }, + + get: function(aTab) { + let uri = aTab.linkedBrowser.currentURI.spec; + + if (aTab.__thumbnail_lastURI && + aTab.__thumbnail_lastURI != uri) { + aTab.__thumbnail = null; + aTab.__thumbnail_lastURI = null; + } + + if (aTab.__thumbnail) { + return aTab.__thumbnail; + } + + if (aTab.getAttribute("pending") == "true") { + let img = new Image; + img.src = PageThumbs.getThumbnailURL(uri); + return img; + } + + return this.capture(aTab, !aTab.hasAttribute("busy")); + }, + + capture: function(aTab, aStore) { + var thumbnail = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + thumbnail.mozOpaque = true; + thumbnail.height = this.height; + thumbnail.width = this.width; + + var ctx = thumbnail.getContext("2d"); + var win = aTab.linkedBrowser.contentWindow; + var snippetWidth = win.innerWidth * .6; + var scale = this.width / snippetWidth; + ctx.scale(scale, scale); + ctx.drawWindow(win, win.scrollX, win.scrollY, + snippetWidth, snippetWidth * this.aspectRatio, "rgb(255,255,255)"); + + if (aStore && + aTab.linkedBrowser /* bug 795608: the tab may got removed while drawing the thumbnail */) { + aTab.__thumbnail = thumbnail; + aTab.__thumbnail_lastURI = aTab.linkedBrowser.currentURI.spec; + } + + return thumbnail; + }, + + handleEvent: function(event) { + switch (event.type) { + case "TabSelect": + if (this._selectedTab && + this._selectedTab.parentNode && + !this._pendingUpdate) { + // Generate a thumbnail for the tab that was selected. + // The timeout keeps the UI snappy and prevents us from generating thumbnails + // for tabs that will be closed. During that timeout, don't generate other + // thumbnails in case multiple TabSelect events occur fast in succession. + this._pendingUpdate = true; + setTimeout(function(self, aTab) { + self._pendingUpdate = false; + if (aTab.parentNode && + !aTab.hasAttribute("busy") && + !aTab.hasAttribute("pending")) { + self.capture(aTab, true); + } + }, 2000, this, this._selectedTab); + } + this._selectedTab = event.target; + break; + case "SSTabRestored": + this.capture(event.target, true); + break; + } + } +}; + +var tabPreviewPanelHelper = { + opening: function(host) { + host.panel.hidden = false; + + var handler = this._generateHandler(host); + host.panel.addEventListener("popupshown", handler, false); + host.panel.addEventListener("popuphiding", handler, false); + + host._prevFocus = document.commandDispatcher.focusedElement; + }, + _generateHandler: function(host) { + var self = this; + return function(event) { + if (event.target == host.panel) { + host.panel.removeEventListener(event.type, arguments.callee, false); + self["_" + event.type](host); + } + }; + }, + _popupshown: function(host) { + if ("setupGUI" in host) { + host.setupGUI(); + } + }, + _popuphiding: function(host) { + if ("suspendGUI" in host) { + host.suspendGUI(); + } + + if (host._prevFocus) { + Cc["@mozilla.org/focus-manager;1"] + .getService(Ci.nsIFocusManager) + .setFocus(host._prevFocus, Ci.nsIFocusManager.FLAG_NOSCROLL); + host._prevFocus = null; + } else { + gBrowser.selectedBrowser.focus(); + } + + if (host.tabToSelect) { + gBrowser.selectedTab = host.tabToSelect; + host.tabToSelect = null; + } + } +}; + +/** + * Ctrl-Tab panel + */ +var ctrlTab = { + get panel() { + delete this.panel; + return this.panel = document.getElementById("ctrlTab-panel"); + }, + get showAllButton() { + delete this.showAllButton; + return this.showAllButton = document.getElementById("ctrlTab-showAll"); + }, + get previews() { + delete this.previews; + return this.previews = this.panel.getElementsByClassName("ctrlTab-preview"); + }, + get recentlyUsedLimit() { + delete this.recentlyUsedLimit; + return this.recentlyUsedLimit = gPrefService.getIntPref("browser.ctrlTab.recentlyUsedLimit"); + }, + get keys() { + var keys = {}; + ["close", "find", "selectAll"].forEach(function(key) { + keys[key] = document.getElementById("key_" + key) + .getAttribute("key") + .toLocaleLowerCase().charCodeAt(0); + }); + delete this.keys; + return this.keys = keys; + }, + _selectedIndex: 0, + get selected() { + return this._selectedIndex < 0 ? document.activeElement : this.previews.item(this._selectedIndex); + }, + get isOpen() { + return this.panel.state == "open" || this.panel.state == "showing" || this._timer; + }, + get tabCount() { + return this.tabList.length; + }, + get tabPreviewCount() { + return Math.min(this.previews.length - 1, this.tabCount); + }, + get canvasWidth() { + return Math.min(tabPreviews.width, Math.ceil(screen.availWidth * .85 / this.tabPreviewCount)); + }, + get canvasHeight() { + return Math.round(this.canvasWidth * tabPreviews.aspectRatio); + }, + + get tabList() { + if (this._tabList) + return this._tabList; + + // Using gBrowser.tabs instead of gBrowser.visibleTabs, as the latter + // exlcudes closing tabs, breaking the following loop in case the the + // selected tab is closing. + let list = Array.filter(gBrowser.tabs, function(tab) !tab.hidden); + + // Rotate the list until the selected tab is first + while (!list[0].selected) { + list.push(list.shift()); + } + + list = list.filter(function(tab) !tab.closing); + + if (this.recentlyUsedLimit != 0) { + let recentlyUsedTabs = []; + for (let tab of this._recentlyUsedTabs) { + if (!tab.hidden && !tab.closing) { + recentlyUsedTabs.push(tab); + if (this.recentlyUsedLimit > 0 && recentlyUsedTabs.length >= this.recentlyUsedLimit) { + break; + } + } + } + for (let i = recentlyUsedTabs.length - 1; i >= 0; i--) { + list.splice(list.indexOf(recentlyUsedTabs[i]), 1); + list.unshift(recentlyUsedTabs[i]); + } + } + + let hidePinnedTabs = gPrefService.getBoolPref("browser.ctrlTab.hidePinnedTabs"); + if (hidePinnedTabs) { + regularTabsList = list.filter(function(tab) !tab.pinned); + // Don't hide pinned tabs if we only have 1 regular tab + if (regularTabsList.length > 1) { + list = regularTabsList; + } + } + + return this._tabList = list; + }, + + init: function() { + if (!this._recentlyUsedTabs) { + tabPreviews.init(); + + this._recentlyUsedTabs = [gBrowser.selectedTab]; + this._init(true); + } + }, + + uninit: function() { + this._recentlyUsedTabs = null; + this._init(false); + }, + + prefName: "browser.ctrlTab.previews", + readPref: function() { + var enable = + gPrefService.getBoolPref(this.prefName) && + (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") || + !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders")); + + if (enable) { + this.init(); + } else { + this.uninit(); + } + }, + observe: function(aSubject, aTopic, aPrefName) { + this.readPref(); + }, + + updatePreviews: function() { + for (let i = 0; i < this.previews.length; i++) { + this.updatePreview(this.previews[i], this.tabList[i]); + } + + var showAllLabel = gNavigatorBundle.getString("ctrlTab.showAll.label"); + this.showAllButton.label = + PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount); + }, + + updatePreview: function(aPreview, aTab) { + if (aPreview == this.showAllButton) { + return; + } + + aPreview._tab = aTab; + + if (aPreview.firstChild) { + aPreview.removeChild(aPreview.firstChild); + } + if (aTab) { + let canvasWidth = this.canvasWidth; + let canvasHeight = this.canvasHeight; + aPreview.appendChild(tabPreviews.get(aTab)); + aPreview.setAttribute("label", aTab.label); + aPreview.setAttribute("tooltiptext", aTab.label); + aPreview.setAttribute("crop", aTab.crop); + aPreview.setAttribute("canvaswidth", canvasWidth); + aPreview.setAttribute("canvasstyle", + "max-width:" + canvasWidth + "px;" + + "min-width:" + canvasWidth + "px;" + + "max-height:" + canvasHeight + "px;" + + "min-height:" + canvasHeight + "px;"); + if (aTab.image) + aPreview.setAttribute("image", aTab.image); + else + aPreview.removeAttribute("image"); + aPreview.hidden = false; + } else { + aPreview.hidden = true; + aPreview.removeAttribute("label"); + aPreview.removeAttribute("tooltiptext"); + aPreview.removeAttribute("image"); + } + }, + + advanceFocus: function(aForward) { + let selectedIndex = Array.indexOf(this.previews, this.selected); + do { + selectedIndex += aForward ? 1 : -1; + if (selectedIndex < 0) { + selectedIndex = this.previews.length - 1; + } else if (selectedIndex >= this.previews.length) { + selectedIndex = 0; + } + } while (this.previews[selectedIndex].hidden); + + if (this._selectedIndex == -1) { + // Focus is already in the panel. + this.previews[selectedIndex].focus(); + } else { + this._selectedIndex = selectedIndex; + } + + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + this._openPanel(); + } + }, + + _mouseOverFocus: function(aPreview) { + if (this._trackMouseOver) + aPreview.focus(); + }, + + pick: function(aPreview) { + if (!this.tabCount) { + return; + } + + var select = (aPreview || this.selected); + + if (select == this.showAllButton) { + this.showAllTabs(); + } else { + this.close(select._tab); + } + }, + + showAllTabs: function(aPreview) { + this.close(); + document.getElementById("Browser:ShowAllTabs").doCommand(); + }, + + remove: function(aPreview) { + if (aPreview._tab) { + gBrowser.removeTab(aPreview._tab); + } + }, + + attachTab: function(aTab, aPos) { + if (aPos == 0) { + this._recentlyUsedTabs.unshift(aTab); + } else if (aPos) { + this._recentlyUsedTabs.splice(aPos, 0, aTab); + } else { + this._recentlyUsedTabs.push(aTab); + } + }, + detachTab: function(aTab) { + var i = this._recentlyUsedTabs.indexOf(aTab); + if (i >= 0) { + this._recentlyUsedTabs.splice(i, 1); + } + }, + + open: function() { + if (this.isOpen) { + return; + } + + allTabs.close(); + + document.addEventListener("keyup", this, true); + + this.updatePreviews(); + this._selectedIndex = 1; + + // Add a slight delay before showing the UI, so that a quick + // "ctrl-tab" keypress just flips back to the MRU tab. + this._timer = setTimeout(function(self) { + self._timer = null; + self._openPanel(); + }, 200, this); + }, + + _openPanel: function() { + tabPreviewPanelHelper.opening(this); + + this.panel.width = Math.min(screen.availWidth * .99, + this.canvasWidth * 1.25 * this.tabPreviewCount); + var estimateHeight = this.canvasHeight * 1.25 + 75; + this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2, + screen.availTop + (screen.availHeight - estimateHeight) / 2, + false); + }, + + close: function(aTabToSelect) { + if (!this.isOpen) { + return; + } + + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + this.suspendGUI(); + if (aTabToSelect) { + gBrowser.selectedTab = aTabToSelect; + } + return; + } + + this.tabToSelect = aTabToSelect; + this.panel.hidePopup(); + }, + + setupGUI: function() { + this.selected.focus(); + this._selectedIndex = -1; + + // Track mouse movement after a brief delay so that the item that happens + // to be under the mouse pointer initially won't be selected unintentionally. + this._trackMouseOver = false; + setTimeout(function(self) { + if (self.isOpen) { + self._trackMouseOver = true; + } + }, 0, this); + }, + + suspendGUI: function() { + document.removeEventListener("keyup", this, true); + + Array.forEach(this.previews, function(preview) { + this.updatePreview(preview, null); + }, this); + + this._tabList = null; + }, + + onKeyPress: function(event) { + var isOpen = this.isOpen; + + if (isOpen) { + event.preventDefault(); + event.stopPropagation(); + } + + switch (event.keyCode) { + case event.DOM_VK_TAB: + if (event.ctrlKey && !event.altKey && !event.metaKey) { + if (isOpen) { + this.advanceFocus(!event.shiftKey); + } else if (!event.shiftKey) { + event.preventDefault(); + event.stopPropagation(); + let tabs = gBrowser.visibleTabs; + if (tabs.length > 2) { + this.open(); + } else if (tabs.length == 2) { + let index = tabs[0].selected ? 1 : 0; + gBrowser.selectedTab = tabs[index]; + } + } + } + break; + default: + if (isOpen && event.ctrlKey) { + if (event.keyCode == event.DOM_VK_DELETE) { + this.remove(this.selected); + break; + } + switch (event.charCode) { + case this.keys.close: + this.remove(this.selected); + break; + case this.keys.find: + case this.keys.selectAll: + this.showAllTabs(); + break; + } + } + } + }, + + removeClosingTabFromUI: function(aTab) { + if (this.tabCount == 2) { + this.close(); + return; + } + + this._tabList = null; + this.updatePreviews(); + + if (this.selected.hidden) { + this.advanceFocus(false); + } + if (this.selected == this.showAllButton) { + this.advanceFocus(false); + } + + // If the current tab is removed, another tab can steal our focus. + if (aTab.selected && this.panel.state == "open") { + setTimeout(function(selected) { + selected.focus(); + }, 0, this.selected); + } + }, + + handleEvent: function(event) { + switch (event.type) { + case "TabAttrModified": + // tab attribute modified (e.g. label, crop, busy, image, selected) + for (let i = this.previews.length - 1; i >= 0; i--) { + if (this.previews[i]._tab && this.previews[i]._tab == event.target) { + this.updatePreview(this.previews[i], event.target); + break; + } + } + break; + case "TabSelect": + this.detachTab(event.target); + this.attachTab(event.target, 0); + break; + case "TabOpen": + this.attachTab(event.target, 1); + break; + case "TabClose": + this.detachTab(event.target); + if (this.isOpen) { + this.removeClosingTabFromUI(event.target); + } + break; + case "keypress": + this.onKeyPress(event); + break; + case "keyup": + if (event.keyCode == event.DOM_VK_CONTROL) { + this.pick(); + } + break; + } + }, + + _init: function(enable) { + var toggleEventListener = enable ? "addEventListener" : "removeEventListener"; + + var tabContainer = gBrowser.tabContainer; + tabContainer[toggleEventListener]("TabOpen", this, false); + tabContainer[toggleEventListener]("TabAttrModified", this, false); + tabContainer[toggleEventListener]("TabSelect", this, false); + tabContainer[toggleEventListener]("TabClose", this, false); + + document[toggleEventListener]("keypress", this, false); + gBrowser.mTabBox.handleCtrlTab = !enable; + + // If we're not running, hide the "Show All Tabs" menu item, + // as Shift+Ctrl+Tab will be handled by the tab bar. + document.getElementById("menu_showAllTabs").hidden = !enable; + + // Also disable the <key> to ensure Shift+Ctrl+Tab never triggers + // Show All Tabs. + var key_showAllTabs = document.getElementById("key_showAllTabs"); + if (enable) { + key_showAllTabs.removeAttribute("disabled"); + } else { + key_showAllTabs.setAttribute("disabled", "true"); + } + } +}; + + +/** + * All Tabs panel + */ +var allTabs = { + get panel() { + delete this.panel; + return this.panel = document.getElementById("allTabs-panel"); + }, + get filterField() { + delete this.filterField; + return this.filterField = document.getElementById("allTabs-filter"); + }, + get container() { + delete this.container; + return this.container = document.getElementById("allTabs-container"); + }, + get tabCloseButton() { + delete this.tabCloseButton; + return this.tabCloseButton = document.getElementById("allTabs-tab-close-button"); + }, + get toolbarButton() { + return document.getElementById("alltabs-button"); + }, + get previews() { + return this.container.getElementsByClassName("allTabs-preview"); + }, + get isOpen() { + return this.panel.state == "open" || this.panel.state == "showing"; + }, + + init: function() { + if (this._initiated) { + return; + } + this._initiated = true; + + tabPreviews.init(); + + Array.forEach(gBrowser.tabs, function(tab) { + this._addPreview(tab); + }, this); + + gBrowser.tabContainer.addEventListener("TabOpen", this, false); + gBrowser.tabContainer.addEventListener("TabAttrModified", this, false); + gBrowser.tabContainer.addEventListener("TabMove", this, false); + gBrowser.tabContainer.addEventListener("TabClose", this, false); + }, + + uninit: function() { + if (!this._initiated) { + return; + } + + gBrowser.tabContainer.removeEventListener("TabOpen", this, false); + gBrowser.tabContainer.removeEventListener("TabAttrModified", this, false); + gBrowser.tabContainer.removeEventListener("TabMove", this, false); + gBrowser.tabContainer.removeEventListener("TabClose", this, false); + + while (this.container.hasChildNodes()) { + this.container.removeChild(this.container.firstChild); + } + + this._initiated = false; + }, + + prefName: "browser.allTabs.previews", + readPref: function() { + var allTabsButton = this.toolbarButton; + if (!allTabsButton) { + return; + } + + if (gPrefService.getBoolPref(this.prefName)) { + allTabsButton.removeAttribute("type"); + allTabsButton.setAttribute("command", "Browser:ShowAllTabs"); + } else { + allTabsButton.setAttribute("type", "menu"); + allTabsButton.removeAttribute("command"); + allTabsButton.removeAttribute("oncommand"); + } + }, + observe: function(aSubject, aTopic, aPrefName) { + this.readPref(); + }, + + pick: function(aPreview) { + if (!aPreview) { + aPreview = this._firstVisiblePreview; + } + if (aPreview) { + this.tabToSelect = aPreview._tab; + } + + this.close(); + }, + + closeTab: function(event) { + this.filterField.focus(); + gBrowser.removeTab(event.currentTarget._targetPreview._tab); + }, + + filter: function() { + if (this._currentFilter == this.filterField.value) { + return; + } + + this._currentFilter = this.filterField.value; + + let hidePinnedTabs = gPrefService.getBoolPref("browser.allTabs.hidePinnedTabs"); + if (hidePinnedTabs) { + let regularTabsList = Array.filter(this.previews, function(preview) !preview._tab.pinned); + // Show pinned tabs if we don't have any regular tabs + if (regularTabsList.length == 0) { + hidePinnedTabs = false; + } + } + + var filter = this._currentFilter.split(/\s+/g); + this._visible = 0; + Array.forEach(this.previews, function(preview) { + var tab = preview._tab; + var matches = 0; + if (filter.length && !tab.hidden) { + let tabstring = tab.linkedBrowser.currentURI.spec; + try { + tabstring = decodeURI(tabstring); + } catch(e) {} + tabstring = tab.label + " " + tab.label.toLocaleLowerCase() + " " + tabstring; + for (let i = 0; i < filter.length; i++) { + matches += tabstring.includes(filter[i]); + } + } + if (matches < filter.length || tab.hidden || (hidePinnedTabs && tab.pinned)) { + preview.hidden = true; + } else { + this._visible++; + this._updatePreview(preview); + preview.hidden = false; + } + }, this); + + this._reflow(); + }, + + open: function() { + var allTabsButton = this.toolbarButton; + if (allTabsButton && + allTabsButton.getAttribute("type") == "menu") { + // Without setTimeout, the menupopup won't stay open when invoking + // "View > Show All Tabs" and the menu bar auto-hides. + setTimeout(function() { + allTabsButton.open = true; + }, 0); + return; + } + + this.init(); + + if (this.isOpen) { + return; + } + + this._maxPanelHeight = Math.max(gBrowser.clientHeight, screen.availHeight / 2); + this._maxPanelWidth = Math.max(gBrowser.clientWidth, screen.availWidth / 2); + + this.filter(); + + tabPreviewPanelHelper.opening(this); + + this.panel.popupBoxObject.setConsumeRollupEvent(PopupBoxObject.ROLLUP_NO_CONSUME); + this.panel.openPopup(gBrowser, "overlap", 0, 0, false, true); + }, + + close: function() { + this.panel.hidePopup(); + }, + + setupGUI: function() { + this.filterField.focus(); + this.filterField.placeholder = this.filterField.tooltipText; + + this.panel.addEventListener("keypress", this, false); + this.panel.addEventListener("keypress", this, true); + this._browserCommandSet.addEventListener("command", this, false); + + // When the panel is open, a second click on the all tabs button should + // close the panel but not re-open it. + document.getElementById("Browser:ShowAllTabs").setAttribute("disabled", "true"); + }, + + suspendGUI: function() { + this.filterField.placeholder = ""; + this.filterField.value = ""; + this._currentFilter = null; + + this._updateTabCloseButton(); + + this.panel.removeEventListener("keypress", this, false); + this.panel.removeEventListener("keypress", this, true); + this._browserCommandSet.removeEventListener("command", this, false); + + setTimeout(function() { + document.getElementById("Browser:ShowAllTabs").removeAttribute("disabled"); + }, 300); + }, + + handleEvent: function(event) { + if (event.type.startsWith("Tab")) { + var tab = event.target; + if (event.type != "TabOpen") { + var preview = this._getPreview(tab); + } + } + switch (event.type) { + case "TabAttrModified": + // tab attribute modified (e.g. label, crop, busy, image) + if (!preview.hidden) { + this._updatePreview(preview); + } + break; + case "TabOpen": + if (this.isOpen) { + this.close(); + } + this._addPreview(tab); + break; + case "TabMove": + let siblingPreview = tab.nextSibling && + this._getPreview(tab.nextSibling); + if (siblingPreview) { + siblingPreview.parentNode.insertBefore(preview, siblingPreview); + } else { + this.container.lastChild.appendChild(preview); + } + if (this.isOpen && !preview.hidden) { + this._reflow(); + preview.focus(); + } + break; + case "TabClose": + this._removePreview(preview); + break; + case "keypress": + this._onKeyPress(event); + break; + case "command": + if (event.target.id != "Browser:ShowAllTabs") { + // Close the panel when there's a browser command executing in the background. + this.close(); + } + break; + } + }, + + _visible: 0, + _currentFilter: null, + get _stack() { + delete this._stack; + return this._stack = document.getElementById("allTabs-stack"); + }, + get _browserCommandSet() { + delete this._browserCommandSet; + return this._browserCommandSet = document.getElementById("mainCommandSet"); + }, + get _previewLabelHeight() { + delete this._previewLabelHeight; + return this._previewLabelHeight = parseInt(getComputedStyle(this.previews[0], "").lineHeight); + }, + + get _visiblePreviews() + Array.filter(this.previews, function(preview) !preview.hidden), + + get _firstVisiblePreview() { + if (this._visible == 0) + return null; + var previews = this.previews; + for (let i = 0; i < previews.length; i++) { + if (!previews[i].hidden) + return previews[i]; + } + return null; + }, + + _reflow: function() { + this._updateTabCloseButton(); + + const CONTAINER_MAX_WIDTH = this._maxPanelWidth * .95; + const CONTAINER_MAX_HEIGHT = this._maxPanelHeight - 35; + // the size of the whole preview relative to the thumbnail + const REL_PREVIEW_THUMBNAIL = 1.2; + const REL_PREVIEW_HEIGHT_WIDTH = tabPreviews.height / tabPreviews.width; + const PREVIEW_MAX_WIDTH = tabPreviews.width * REL_PREVIEW_THUMBNAIL; + + var rows, previewHeight, previewWidth, outerHeight; + this._columns = Math.floor(CONTAINER_MAX_WIDTH / PREVIEW_MAX_WIDTH); + do { + rows = Math.ceil(this._visible / this._columns); + previewWidth = Math.min(PREVIEW_MAX_WIDTH, + Math.round(CONTAINER_MAX_WIDTH / this._columns)); + previewHeight = Math.round(previewWidth * REL_PREVIEW_HEIGHT_WIDTH); + outerHeight = previewHeight + this._previewLabelHeight; + } while (rows * outerHeight > CONTAINER_MAX_HEIGHT && ++this._columns); + + var outerWidth = previewWidth; + { + let innerWidth = Math.ceil(previewWidth / REL_PREVIEW_THUMBNAIL); + let innerHeight = Math.ceil(previewHeight / REL_PREVIEW_THUMBNAIL); + var canvasStyle = "max-width:" + innerWidth + "px;" + + "min-width:" + innerWidth + "px;" + + "max-height:" + innerHeight + "px;" + + "min-height:" + innerHeight + "px;"; + } + + var previews = Array.slice(this.previews); + + while (this.container.hasChildNodes()) { + this.container.removeChild(this.container.firstChild); + } + for (let i = rows || 1; i > 0; i--) { + this.container.appendChild(document.createElement("hbox")); + } + + var row = this.container.firstChild; + var colCount = 0; + previews.forEach(function(preview) { + if (!preview.hidden && + ++colCount > this._columns) { + row = row.nextSibling; + colCount = 1; + } + preview.setAttribute("minwidth", outerWidth); + preview.setAttribute("height", outerHeight); + preview.setAttribute("canvasstyle", canvasStyle); + preview.removeAttribute("closebuttonhover"); + row.appendChild(preview); + }, this); + + this._stack.width = this._maxPanelWidth; + this.container.width = Math.ceil(outerWidth * Math.min(this._columns, this._visible)); + this.container.left = Math.round((this._maxPanelWidth - this.container.width) / 2); + this.container.maxWidth = this._maxPanelWidth - this.container.left; + this.container.maxHeight = rows * outerHeight; + }, + + _addPreview: function(aTab) { + var preview = document.createElement("button"); + preview.className = "allTabs-preview"; + preview._tab = aTab; + this.container.lastChild.appendChild(preview); + }, + + _removePreview: function(aPreview) { + var updateUI = (this.isOpen && !aPreview.hidden); + aPreview._tab = null; + aPreview.parentNode.removeChild(aPreview); + if (updateUI) { + this._visible--; + this._reflow(); + this.filterField.focus(); + } + }, + + _getPreview: function(aTab) { + var previews = this.previews; + for (let i = 0; i < previews.length; i++) { + if (previews[i]._tab == aTab) { + return previews[i]; + } + } + return null; + }, + + _updateTabCloseButton: function(event) { + if (event && event.target == this.tabCloseButton) { + return; + } + + if (this.tabCloseButton._targetPreview) { + if (event && event.target == this.tabCloseButton._targetPreview) { + return; + } + + this.tabCloseButton._targetPreview.removeAttribute("closebuttonhover"); + } + + if (event && + event.target.parentNode.parentNode == this.container && + (event.target._tab.previousSibling || event.target._tab.nextSibling)) { + let canvas = event.target.firstChild.getBoundingClientRect(); + let container = this.container.getBoundingClientRect(); + let tabCloseButton = this.tabCloseButton.getBoundingClientRect(); + let alignLeft = getComputedStyle(this.panel, "").direction == "rtl"; + this.tabCloseButton.left = canvas.left - + container.left + + parseInt(this.container.left) + + (alignLeft ? 0 : + canvas.width - tabCloseButton.width); + this.tabCloseButton.top = canvas.top - container.top; + this.tabCloseButton._targetPreview = event.target; + this.tabCloseButton.style.visibility = "visible"; + event.target.setAttribute("closebuttonhover", "true"); + } else { + this.tabCloseButton.style.visibility = "hidden"; + this.tabCloseButton.left = this.tabCloseButton.top = 0; + this.tabCloseButton._targetPreview = null; + } + }, + + _updatePreview: function(aPreview) { + aPreview.setAttribute("label", aPreview._tab.label); + aPreview.setAttribute("tooltiptext", aPreview._tab.label); + aPreview.setAttribute("crop", aPreview._tab.crop); + if (aPreview._tab.image) { + aPreview.setAttribute("image", aPreview._tab.image); + } else { + aPreview.removeAttribute("image"); + } + + aPreview.removeAttribute("soundplaying"); + aPreview.removeAttribute("muted"); + if (aPreview._tab.hasAttribute("muted")) { + aPreview.setAttribute("muted", "true"); + } else if (aPreview._tab.hasAttribute("soundplaying")) { + aPreview.setAttribute("soundplaying", "true"); + } + + var thumbnail = tabPreviews.get(aPreview._tab); + if (aPreview.firstChild) { + if (aPreview.firstChild == thumbnail) { + return; + } + aPreview.removeChild(aPreview.firstChild); + } + aPreview.appendChild(thumbnail); + }, + + _onKeyPress: function(event) { + if (event.eventPhase == event.CAPTURING_PHASE) { + this._onCapturingKeyPress(event); + return; + } + + if (event.keyCode == event.DOM_VK_ESCAPE) { + this.close(); + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (event.target == this.filterField) { + switch (event.keyCode) { + case event.DOM_VK_UP: + if (this._visible) { + let previews = this._visiblePreviews; + let columns = Math.min(previews.length, this._columns); + previews[Math.floor(previews.length / columns) * columns - 1].focus(); + event.preventDefault(); + event.stopPropagation(); + } + break; + case event.DOM_VK_DOWN: + if (this._visible) { + this._firstVisiblePreview.focus(); + event.preventDefault(); + event.stopPropagation(); + } + break; + } + } + }, + + _onCapturingKeyPress: function(event) { + switch (event.keyCode) { + case event.DOM_VK_UP: + case event.DOM_VK_DOWN: + if (event.target != this.filterField) { + this._advanceFocusVertically(event); + } + break; + case event.DOM_VK_RETURN: + if (event.target == this.filterField) { + this.filter(); + this.pick(); + event.preventDefault(); + event.stopPropagation(); + } + break; + } + }, + + _advanceFocusVertically: function(event) { + var preview = document.activeElement; + if (!preview || preview.parentNode.parentNode != this.container) { + return; + } + + event.stopPropagation(); + + var up = (event.keyCode == event.DOM_VK_UP); + var previews = this._visiblePreviews; + + if (up && preview == previews[0]) { + this.filterField.focus(); + return; + } + + var i = previews.indexOf(preview); + var columns = Math.min(previews.length, this._columns); + var column = i % columns; + var row = Math.floor(i / columns); + + function newIndex() row * columns + column; + function outOfBounds() newIndex() >= previews.length; + + if (up) { + row--; + if (row < 0) { + let rows = Math.ceil(previews.length / columns); + row = rows - 1; + column--; + if (outOfBounds()) { + row--; + } + } + } else { + row++; + if (outOfBounds()) { + if (column == columns - 1) { + this.filterField.focus(); + return; + } + row = 0; + column++; + } + } + previews[newIndex()].focus(); + } +}; diff --git a/browser/base/content/browser-tabPreviews.xml b/browser/base/content/browser-tabPreviews.xml new file mode 100644 index 000000000..c2bfa63c7 --- /dev/null +++ b/browser/base/content/browser-tabPreviews.xml @@ -0,0 +1,74 @@ +<?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/. --> + +<bindings id="tabPreviews" + 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="ctrlTab-preview" extends="chrome://global/content/bindings/button.xml#button-base"> + <content pack="center"> + <xul:stack> + <xul:vbox class="ctrlTab-preview-inner" align="center" pack="center" + xbl:inherits="width=canvaswidth"> + <xul:hbox class="tabPreview-canvas" xbl:inherits="style=canvasstyle"> + <children/> + </xul:hbox> + <xul:label xbl:inherits="value=label,crop" class="plain"/> + </xul:vbox> + <xul:hbox class="ctrlTab-favicon-container" xbl:inherits="hidden=noicon"> + <xul:image class="ctrlTab-favicon" xbl:inherits="src=image"/> + </xul:hbox> + </xul:stack> + </content> + <handlers> + <handler event="mouseover" action="ctrlTab._mouseOverFocus(this);"/> + <handler event="command" action="ctrlTab.pick(this);"/> + <handler event="click" button="1" action="ctrlTab.remove(this);"/> + </handlers> + </binding> + + <binding id="allTabs-preview" extends="chrome://global/content/bindings/button.xml#button-base"> + <content pack="center" align="center"> + <xul:stack> + <xul:vbox class="allTabs-preview-inner" align="center" pack="center"> + <xul:hbox class="tabPreview-canvas" xbl:inherits="style=canvasstyle"> + <children/> + </xul:hbox> + <xul:hbox align="center"> + <xul:image xbl:inherits="soundplaying,muted" class="allTabs-endimage"/> + <xul:label flex="1" xbl:inherits="value=label,crop" class="allTabs-preview-label plain"/> + </xul:hbox> + </xul:vbox> + <xul:hbox class="allTabs-favicon-container"> + <xul:image class="allTabs-favicon" xbl:inherits="src=image"/> + </xul:hbox> + </xul:stack> + </content> + <handlers> + <handler event="command" action="allTabs.pick(this);"/> + <handler event="click" button="1" action="gBrowser.removeTab(this._tab);"/> + + <handler event="dragstart"><![CDATA[ + event.dataTransfer.mozSetDataAt("application/x-moz-node", this._tab, 0); + ]]></handler> + + <handler event="dragover"><![CDATA[ + let tab = event.dataTransfer.mozGetDataAt("application/x-moz-node", 0); + if (tab && tab.parentNode == gBrowser.tabContainer) + event.preventDefault(); + ]]></handler> + + <handler event="drop"><![CDATA[ + let tab = event.dataTransfer.mozGetDataAt("application/x-moz-node", 0); + if (tab && tab.parentNode == gBrowser.tabContainer) { + let newIndex = Array.indexOf(gBrowser.tabs, this._tab); + gBrowser.moveTabTo(tab, newIndex); + } + ]]></handler> + </handlers> + </binding> +</bindings> diff --git a/browser/base/content/browser-thumbnails.js b/browser/base/content/browser-thumbnails.js new file mode 100644 index 000000000..84fc349ff --- /dev/null +++ b/browser/base/content/browser-thumbnails.js @@ -0,0 +1,211 @@ +#ifdef 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/. */ +#endif + +/** + * Keeps thumbnails of open web pages up-to-date. + */ +var gBrowserThumbnails = { + /** + * Pref that controls whether we can store SSL content on disk + */ + PREF_DISK_CACHE_SSL: "browser.cache.disk_cache_ssl", + + _captureDelayMS: 1000, + + /** + * Used to keep track of disk_cache_ssl preference + */ + _sslDiskCacheEnabled: null, + + /** + * Map of capture() timeouts assigned to their browsers. + */ + _timeouts: null, + + /** + * List of tab events we want to listen for. + */ + _tabEvents: ["TabClose", "TabSelect"], + + init: function() { + try { + if (Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled")) + return; + } catch(e) {} + + PageThumbs.addExpirationFilter(this); + gBrowser.addTabsProgressListener(this); + Services.prefs.addObserver(this.PREF_DISK_CACHE_SSL, this, false); + + this._sslDiskCacheEnabled = + Services.prefs.getBoolPref(this.PREF_DISK_CACHE_SSL); + + this._tabEvents.forEach(function(aEvent) { + gBrowser.tabContainer.addEventListener(aEvent, this, false); + }, this); + + this._timeouts = new WeakMap(); + }, + + uninit: function() { + PageThumbs.removeExpirationFilter(this); + gBrowser.removeTabsProgressListener(this); + Services.prefs.removeObserver(this.PREF_DISK_CACHE_SSL, this); + + this._tabEvents.forEach(function(aEvent) { + gBrowser.tabContainer.removeEventListener(aEvent, this, false); + }, this); + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "scroll": + let browser = aEvent.currentTarget; + if (this._timeouts.has(browser)) { + this._delayedCapture(browser); + } + break; + case "TabSelect": + this._delayedCapture(aEvent.target.linkedBrowser); + break; + case "TabClose": { + this._clearTimeout(aEvent.target.linkedBrowser); + break; + } + } + }, + + observe: function() { + this._sslDiskCacheEnabled = + Services.prefs.getBoolPref(this.PREF_DISK_CACHE_SSL); + }, + + filterForThumbnailExpiration: + function(aCallback) { + // Tycho: aCallback([browser.currentURI.spec for (browser of gBrowser.browsers)]); + let result = []; + for (let browser of gBrowser.browsers) { + result.push(browser.currentURI.spec); + } + aCallback(result); + }, + + /** + * State change progress listener for all tabs. + */ + onStateChange: function(aBrowser, aWebProgress, + aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + this._delayedCapture(aBrowser); + } + }, + + _capture: function(aBrowser) { + if (this._shouldCapture(aBrowser)) { + PageThumbs.captureAndStore(aBrowser); + } + }, + + _delayedCapture: function(aBrowser) { + if (this._timeouts.has(aBrowser)) { + clearTimeout(this._timeouts.get(aBrowser)); + } else { + aBrowser.addEventListener("scroll", this, true); + } + + let timeout = setTimeout(function() { + this._clearTimeout(aBrowser); + this._capture(aBrowser); + }.bind(this), this._captureDelayMS); + + this._timeouts.set(aBrowser, timeout); + }, + + _shouldCapture: function(aBrowser) { + // Capture only if it's the currently selected tab. + if (aBrowser != gBrowser.selectedBrowser) { + return false; + } + + // Don't capture in per-window private browsing mode. + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + return false; + } + + let doc = aBrowser.contentDocument; + + // FIXME Bug 720575 - Don't capture thumbnails for SVG or XML documents as + // that currently regresses Talos SVG tests. + if (doc instanceof XMLDocument) { + return false; + } + + // There's no point in taking screenshot of loading pages. + if (aBrowser.docShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) { + return false; + } + + // Don't take screenshots of about: pages. + if (aBrowser.currentURI.schemeIs("about")) { + return false; + } + + let channel = aBrowser.docShell.currentDocumentChannel; + + // No valid document channel. We shouldn't take a screenshot. + if (!channel) { + return false; + } + + // Don't take screenshots of internally redirecting about: pages. + // This includes error pages. + let uri = channel.originalURI; + if (uri.schemeIs("about")) { + return false; + } + + let httpChannel; + try { + httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + } catch(e) { + // Not an HTTP channel. + } + + if (httpChannel) { + // Continue only if we have a 2xx status code. + try { + if (Math.floor(httpChannel.responseStatus / 100) != 2) { + return false; + } + } catch(e) { + // Can't get response information from the httpChannel + // because mResponseHead is not available. + return false; + } + + // Cache-Control: no-store. + if (httpChannel.isNoStoreResponse()) { + return false; + } + + // Don't capture HTTPS pages unless the user explicitly enabled it. + if (uri.schemeIs("https") && !this._sslDiskCacheEnabled) { + return false; + } + } + + return true; + }, + + _clearTimeout: function(aBrowser) { + if (this._timeouts.has(aBrowser)) { + aBrowser.removeEventListener("scroll", this, false); + clearTimeout(this._timeouts.get(aBrowser)); + this._timeouts.delete(aBrowser); + } + } +}; diff --git a/browser/base/content/browser-title.css b/browser/base/content/browser-title.css new file mode 100644 index 000000000..5f7e77564 --- /dev/null +++ b/browser/base/content/browser-title.css @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#main-window::after { + content: attr(title); + line-height: 50px; + max-height: 50px; + overflow: -moz-hidden-unscrollable; + pointer-events: none; + position: fixed; + word-wrap: break-word; + -moz-hyphens: auto; + color: CaptionText; + font-weight: bold; + text-align: left; +} + +#main-window:-moz-window-inactive::after { + color: InactiveCaptionText; +} + +/* Win10 doesn't respond to inactive caption, so dim it instead */ +@media (-moz-os-version: windows-win10) { + #main-window:-moz-window-inactive::after { + opacity: 0.5; + } +} + +/* Hide in fullscreen/TiT mode */ +#main-window[inFullscreen="true"]::after, +#main-window[sizemode="maximized"][tabsintitlebar="true"]::after, +#main-window:not([chromemargin])::after { + opacity: 0 !important; +} + + +#main-window::after { + padding: 0 132px; /* AppMenu button/wincontrols width offset */ + left: 0; + right: 0; +} + +#main-window[privatebrowsingmode=temporary]::after { + padding: 0px 132px 0px 152px; /* AppMenu button width offset for PB mode */ +} + +#main-window[sizemode="normal"]::after { + left: -12px; + right: -12px; +} + +/* Windows Classic theme */ + +@media all and (-moz-windows-classic) { + + #main-window::after { + top: -13px; + text-shadow: none !important; + background-position: 2px 18px; + } + +} + + +/* Windows Aero (Vista, non-glass 7/8) */ + +@media all and (-moz-windows-theme: aero) { + + #main-window::after { + top: -11px; + font-weight: normal; + text-shadow: none; + background-position: 2px 17px; + } + + #main-window[sizemode="maximized"]::after { + top: -7px; + } + +} + + +/* Windows Aero Glass */ + +@media (-moz-windows-glass) { + + #main-window::after { + top: -13px; + color: black; + text-shadow: rgba(255,255,255,.6) 7px -1px 12px, + rgba(255,255,255,.6) 6px -1px 13px, + rgba(255,255,255,.9) 5px -1px 14px, + rgba(255,255,255,.6) -7px -1px 12px, + rgba(255,255,255,.6) -6px -1px 13px, + rgba(255,255,255,.9) -5px -1px 14px; + z-index: -99999; + background-position: 2px 18px; + font-weight: bold; + } + + #main-window[sizemode="maximized"]::after { + top: -7px; + } + + #main-window:-moz-window-inactive::after { + opacity: .9; + color: black; + text-shadow: rgba(255,255,255,.7) 7px -1px 12px, + rgba(255,255,255,.5) 6px -1px 13px, + rgba(255,255,255,.5) 5px -1px 14px, + rgba(255,255,255,.7) -7px -1px 12px, + rgba(255,255,255,.5) -6px -1px 13px, + rgba(255,255,255,.5) -5px -1px 14px; + } + +} + + +/* Generic other themes */ + +@media all and (-moz-windows-theme: generic) { + + #main-window::after { + font-family: trebuchet MS; + font-size: 13px; + text-shadow: 1px 1px rgba(0, 0, 0, .2); + top: -9px; + background-position: 2px 16px; + } + + #main-window:-moz-window-inactive::after { + text-shadow: none; + } + +} + + +/* Compositor style for Win 8/10 */ + +@media all and (-moz-windows-compositor) { + @media not all and (-moz-windows-glass) { + + #main-window::after { + background-position: 4px 17px; + top: -13px; + } + + @media (-moz-os-version: windows-win8) { + #main-window::after { + font-size: 15px; + text-align: center; + } + + #main-window[darkwindowframe="true"]:not(:-moz-window-inactive):not(:-moz-lwtheme)::after { + /* Dark window frame/accent color on Win 8 */ + color: white; + } + } + + @media (-moz-os-version: windows-win10) { + #main-window::after { + text-align: left; + } + + @media (-moz-windows-accent-color-applies: 0) { + #main-window:not(:-moz-window-inactive):not(:-moz-lwtheme)::after { + /* Default Windows 10 styling is white - apply black text styling */ + color: black; + } + } + + @media (-moz-windows-accent-color-applies) { + #main-window:not(:-moz-window-inactive):not(:-moz-lwtheme)::after { + /* Accent color is applied - use the associated text styling */ + color: -moz-win-accentcolortext; + } + } + } + + #main-window[sizemode="maximized"]::after { + top: -5px; + } + } + +} + +/* Lightweight Themes */ + +#main-window:-moz-lwtheme::after { + color: inherit; + text-shadow: inherit; +} + + +/* Hide for small windows */ + +@media not all and (min-width: 320px) { + + #main-window::after { + opacity: 0 !important; + } + +}
\ No newline at end of file diff --git a/browser/base/content/browser-uacompat.js b/browser/base/content/browser-uacompat.js new file mode 100644 index 000000000..933aa55d1 --- /dev/null +++ b/browser/base/content/browser-uacompat.js @@ -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/. */ + +var UserAgentCompatibility = { + PREF_UA_COMPAT: "general.useragent.compatMode", + PREF_UA_COMPAT_GECKO: "general.useragent.compatMode.gecko", + PREF_UA_COMPAT_FIREFOX: "general.useragent.compatMode.firefox", + + init: function() { + this.checkPreferences(); + Services.prefs.addObserver(this.PREF_UA_COMPAT, this, false); + Services.prefs.addObserver(this.PREF_UA_COMPAT_GECKO, this, false); + Services.prefs.addObserver(this.PREF_UA_COMPAT_FIREFOX, this, false); + }, + + uninit: function() { + Services.prefs.removeObserver(this.PREF_UA_COMPAT, this, false); + Services.prefs.removeObserver(this.PREF_UA_COMPAT_GECKO, this, false); + Services.prefs.removeObserver(this.PREF_UA_COMPAT_FIREFOX, this, false); + }, + + observe: function() { + this.checkPreferences(); + }, + + checkPreferences: function() { + var compatMode = Services.prefs.getIntPref(this.PREF_UA_COMPAT); + + switch(compatMode) { + case 0: + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_GECKO, false); + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_FIREFOX, false); + break; + case 1: + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_GECKO, true); + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_FIREFOX, false); + break; + case 2: + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_GECKO, true); + Services.prefs.setBoolPref(this.PREF_UA_COMPAT_FIREFOX, true); + break; + } + } +}; diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css new file mode 100644 index 000000000..7fe0a65c5 --- /dev/null +++ b/browser/base/content/browser.css @@ -0,0 +1,753 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"); + +searchbar { + -moz-binding: url("chrome://browser/content/search/search.xml#searchbar"); +} + +browser[remote="true"] { + -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser"); +} + +tabbrowser { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser"); +} + +.tabbrowser-tabs { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabs"); +} + +#tabbrowser-tabs:not([overflow="true"]) ~ #alltabs-button, +#tabbrowser-tabs:not([overflow="true"]) + #new-tab-button, +#tabbrowser-tabs[overflow="true"] > .tabbrowser-arrowscrollbox > .tabs-newtab-button, +#TabsToolbar[currentset]:not([currentset*="tabbrowser-tabs,new-tab-button"]) > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .tabs-newtab-button, +#TabsToolbar[customizing="true"] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .tabs-newtab-button { + visibility: collapse; +} + +#alltabs-button { /* Pale Moon: Always show this button! (less jumpy UI) */ + visibility: visible !important; +} + +#tabbrowser-tabs:not([overflow="true"])[using-closing-tabs-spacer] ~ #alltabs-button { + visibility: hidden; /* temporary space to keep a tab's close button under the cursor */ +} + +.tabbrowser-tab { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tab"); +} + +.tabbrowser-tab:not([pinned]) { + -moz-box-flex: 100; + max-width: 250px; + min-width: 100px; + width: 0; + transition: min-width 175ms ease-out, + max-width 200ms ease-out, + opacity 80ms ease-out 20ms /* hide the tab for the first 20ms of the max-width transition */; +} + +.tabbrowser-tab:not([pinned]):not([fadein]) { + max-width: 0.1px; + min-width: 0.1px; + opacity: 0 !important; + transition: min-width 175ms ease-out, + max-width 200ms ease-out, + opacity 80ms ease-out 180ms /* hide the tab for the last 20ms of the max-width transition */; +} + +.tab-throbber:not([fadein]):not([pinned]), +.tab-label:not([fadein]):not([pinned]), +.tab-icon-image:not([fadein]):not([pinned]), +.tab-close-button:not([fadein]):not([pinned]) { + display: none; +} + +.tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] { + position: fixed !important; + display: block; /* position:fixed already does this (bug 579776), but let's be explicit */ +} + +.tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected] { + position: relative; + z-index: 2; + pointer-events: none; /* avoid blocking dragover events on scroll buttons */ +} + +.tabbrowser-tabs[movingtab] > .tabbrowser-tab[fadein]:not([selected]) { + transition: transform 200ms ease-out; +} + +#alltabs-popup { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-alltabs-popup"); +} + +toolbar[printpreview="true"] { + -moz-binding: url("chrome://global/content/printPreviewBindings.xml#printpreviewtoolbar"); +} + +#toolbar-menubar { + -moz-box-ordinal-group: 5; +} + +#navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar) { + -moz-box-ordinal-group: 50; +} + +#TabsToolbar { + -moz-box-ordinal-group: 100; +} + +#TabsToolbar[tabsontop="true"] { + -moz-box-ordinal-group: 10; +} + +%ifdef MOZ_CAN_DRAW_IN_TITLEBAR +#main-window[inFullscreen] > #titlebar, +#main-window[inFullscreen] .titlebar-placeholder, +#main-window:not([tabsintitlebar]) .titlebar-placeholder { + display: none; +} + +#titlebar { + -moz-binding: url("chrome://global/content/bindings/general.xml#windowdragbox"); + -moz-window-dragging: drag; +} + +%ifdef XP_WIN +#main-window[tabsontop="true"] #TabsToolbar, +#main-window[tabsontop="true"] #toolbar-menubar, +#main-window[tabsontop="true"] #navigator-toolbox > toolbar:-moz-lwtheme { + -moz-window-dragging: drag; +} +%endif + +#titlebar-spacer { + pointer-events: none; +} + +#main-window[tabsintitlebar] #appmenu-button-container, +#main-window[tabsintitlebar] #titlebar-buttonbox { + position: relative; +} +%endif + +#main-window[inDOMFullscreen] #sidebar-box, +#main-window[inDOMFullscreen] #sidebar-splitter { + visibility: collapse; +} + +.bookmarks-toolbar-customize, +#wrapper-personal-bookmarks > #personal-bookmarks > #PlacesToolbar > hbox > #PlacesToolbarItems { + display: none; +} + +#wrapper-personal-bookmarks[place="toolbar"] > #personal-bookmarks > #PlacesToolbar > .bookmarks-toolbar-customize { + display: -moz-box; +} + +#main-window[disablechrome] #navigator-toolbox[tabsontop="true"] > toolbar:not(#toolbar-menubar):not(#TabsToolbar) { + visibility: collapse; +} + +#wrapper-urlbar-container #urlbar-container > #urlbar > toolbarbutton, +#urlbar-container:not([combined]) > #urlbar > toolbarbutton, +#urlbar-container[combined] + #reload-button + #stop-button, +#urlbar-container[combined] + #reload-button, +toolbar:not([mode="icons"]) > #urlbar-container > #urlbar > toolbarbutton, +toolbar[mode="icons"] > #urlbar-container > #urlbar > #urlbar-reload-button:not([displaystop]) + #urlbar-stop-button, +toolbar[mode="icons"] > #urlbar-container > #urlbar > #urlbar-reload-button[displaystop], +toolbar[mode="icons"] > #reload-button:not([displaystop]) + #stop-button, +toolbar[mode="icons"] > #reload-button[displaystop] { + visibility: collapse; +} + +#feed-button > .toolbarbutton-menu-dropmarker { + display: none; +} + +#feed-menu > .feed-menuitem:-moz-locale-dir(rtl) { + direction: rtl; +} + +#main-window:-moz-lwtheme { + background-repeat: no-repeat; + background-position: top right; +} + +#browser-bottombox[lwthemefooter="true"] { + background-repeat: no-repeat; + background-position: bottom left; +} + +splitmenu { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#splitmenu"); +} + +.splitmenu-menuitem { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem"); + list-style-image: inherit; + -moz-image-region: inherit; +} + +.splitmenu-menuitem[iconic="true"] { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic"); +} + +.splitmenu-menu > .menu-text, +:-moz-any(.splitmenu-menu, .splitmenu-menuitem) > .menu-accel-container, +#appmenu-editmenu > .menu-text, +#appmenu-editmenu > .menu-accel-container { + display: none; +} + +.menuitem-tooltip { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#menuitem-tooltip"); +} + +.menuitem-iconic-tooltip, +.menuitem-tooltip[type="checkbox"], +.menuitem-tooltip[type="radio"] { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#menuitem-iconic-tooltip"); +} + +%ifdef MENUBAR_CAN_AUTOHIDE +%ifndef MOZ_CAN_DRAW_IN_TITLEBAR +#appmenu-toolbar-button > .toolbarbutton-text { + display: -moz-box; +} + +window[shellshowingmenubar="true"] #appmenu-toolbar-button { + display: none; +} +%endif + +#appmenu_offlineModeRecovery:not([checked=true]) { + display: none; +} +%endif + +/* Hide menu elements intended for keyboard access support */ +#main-menubar[openedwithkey=false] .show-only-for-keyboard { + display: none; +} + +/* ::::: location bar ::::: */ +#urlbar { + -moz-binding: url(chrome://browser/content/urlbarBindings.xml#urlbar); +} + +.ac-url-text:-moz-locale-dir(rtl), +.ac-title:-moz-locale-dir(rtl) > description { + direction: ltr !important; +} + +/* For results that are actions, their description text is shown instead of + the URL - this needs to follow the locale's direction, unlike URLs. */ +panel:not([noactions]) > richlistbox > richlistitem[type~="action"]:-moz-locale-dir(rtl) > .ac-url-box { + direction: rtl; +} + +panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-url > .ac-action-text, +panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-action-icon { + visibility: collapse; +} + +panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-url > .ac-url-text { + visibility: visible; +} + +#urlbar:not([actiontype]) > #urlbar-display-box { + display: none; +} + +#wrapper-urlbar-container > #urlbar-container > #urlbar { + -moz-user-input: disabled; + cursor: -moz-grab; +} + +#PopupAutoComplete { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup"); +} + +#PopupAutoCompleteRichResult { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup"); +} + +#DateTimePickerPanel[active="true"] { + -moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup"); +} + +/* Pale Moon: Address bar: Feeds */ +#ub-feed-button > .button-box > .box-inherit > .button-text, +#ub-feed-button > .button-box > .button-menu-dropmarker { + display: none; +} + +#ub-feed-menu > .feed-menuitem:-moz-locale-dir(rtl) { + direction: rtl; +} + + +#urlbar-container[combined] > #urlbar > #urlbar-icons > #go-button, +#urlbar[pageproxystate="invalid"] > #urlbar-icons > .urlbar-icon:not(#go-button), +#urlbar[pageproxystate="valid"] > #urlbar-icons > #go-button, +#urlbar[pageproxystate="invalid"][focused="true"] > #urlbar-go-button ~ toolbarbutton, +#urlbar[pageproxystate="valid"] > #urlbar-go-button, +#urlbar:not([focused="true"]) > #urlbar-go-button { + visibility: collapse; +} + +#urlbar[pageproxystate="invalid"] > #identity-box > #identity-icon-labels { + visibility: collapse; +} + +#urlbar[pageproxystate="invalid"] > #identity-box { + pointer-events: none; +} + +#identity-icon-labels { + max-width: 18em; +} + +#identity-icon-country-label { + direction: ltr; +} + +#identity-box.verifiedIdentity > #identity-icon-labels > #identity-icon-label { + margin-inline-end: 0.25em !important; +} + +#wrapper-search-container > #search-container > #searchbar > .searchbar-textbox > .autocomplete-textbox-container > .textbox-input-box > html|*.textbox-input { + visibility: hidden; +} + +/* Private Autocomplete */ +textbox[type="private-autocomplete"] { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete"); +} + +panel[type="private-autocomplete"] { + -moz-binding: url("chrome://browser/content/autocomplete.xml#autocomplete-result-popup"); +} + +panel[type="private-autocomplete-richlistbox"] { + -moz-binding: url("chrome://browser/content/autocomplete.xml#private-autocomplete-rich-result-popup"); +} + +.private-autocomplete-tree { + -moz-binding: url("chrome://browser/content/autocomplete.xml#private-autocomplete-tree"); + -moz-user-focus: ignore; +} + +.private-autocomplete-treebody { + -moz-binding: url("chrome://browser/content/autocomplete.xml#private-autocomplete-treebody"); +} + +.private-autocomplete-richlistbox { + -moz-binding: url("chrome://browser/content/autocomplete.xml#private-autocomplete-richlistbox"); + -moz-user-focus: ignore; +} + +.private-autocomplete-richlistbox > scrollbox { + overflow-x: hidden !important; +} + +.private-autocomplete-richlistitem { + -moz-binding: url("chrome://browser/content/autocomplete.xml#private-autocomplete-richlistitem"); + -moz-box-orient: vertical; + overflow: -moz-hidden-unscrollable; +} + +.private-autocomplete-treerows { + -moz-binding: url("chrome://browser/content/autocomplete.xml#private-autocomplete-treerows"); +} + +.private-autocomplete-history-dropmarker { + display: none; +} + +.private-autocomplete-history-dropmarker[enablehistory="true"] { + display: -moz-box; + -moz-binding: url("chrome://browser/content/autocomplete.xml#private-history-dropmarker"); +} + +.ac-ellipsis-after { + visibility: hidden; +} + +.ac-url-text[type~="action"], +.ac-action-text:not([type~="action"]) { + visibility: collapse; +} + + +/* ::::: Unified Back-/Forward Button ::::: */ +#back-button > .toolbarbutton-menu-dropmarker, +#forward-button > .toolbarbutton-menu-dropmarker { + display: none; +} +.unified-nav-current { + font-weight: bold; +} + +toolbarbutton.bookmark-item { + max-width: 13em; +} + +%ifdef MENUBAR_CAN_AUTOHIDE +#toolbar-menubar:not([autohide="true"]) ~ toolbar > #bookmarks-menu-button, +#toolbar-menubar:not([autohide="true"]) > #bookmarks-menu-button, +#toolbar-menubar:not([autohide="true"]) ~ toolbar > #history-menu-button, +#toolbar-menubar:not([autohide="true"]) > #history-menu-button { + display: none; +} +%endif + +#editBMPanel_tagsSelector { + /* override default listbox width from xul.css */ + width: auto; +} + +menupopup[emptyplacesresult="true"] > .hide-if-empty-places-result { + display: none; +} + +menuitem.spell-suggestion { + font-weight: bold; +} + +#sidebar-header > .tabs-closebutton { + -moz-user-focus: normal; +} + +/* apply Fitts' law to the notification bar's close button */ +window[sizemode="maximized"] #content .notification-inner { + border-right: 0px !important; +} + +/* Hide extension toolbars that neglected to set the proper class */ +window[chromehidden~="location"][chromehidden~="toolbar"] toolbar:not(.chromeclass-menubar), +window[chromehidden~="toolbar"] toolbar:not(.toolbar-primary):not(#nav-bar):not(#TabsToolbar):not(#print-preview-toolbar):not(.chromeclass-menubar) { + display: none; +} + +#navigator-toolbox , +#status-bar , +#mainPopupSet { + min-width: 1px; +} + +%ifdef MOZ_SERVICES_SYNC +/* Sync notification UI */ +#sync-notifications { + -moz-binding: url("chrome://weave/content/notification.xml#notificationbox"); + overflow-y: visible !important; +} + +#sync-notifications notification { + -moz-binding: url("chrome://weave/content/notification.xml#notification"); +} +%endif + +/* History Swipe Animation */ + +#historySwipeAnimationContainer { + overflow: hidden; +} + +#historySwipeAnimationPreviousPage, +#historySwipeAnimationCurrentPage, +#historySwipeAnimationNextPage { + background: none top left no-repeat white; +} + +#historySwipeAnimationPreviousPage { + background-image: -moz-element(#historySwipeAnimationPreviousPageSnapshot); +} + +#historySwipeAnimationCurrentPage { + background-image: -moz-element(#historySwipeAnimationCurrentPageSnapshot); +} + +#historySwipeAnimationNextPage { + background-image: -moz-element(#historySwipeAnimationNextPageSnapshot); +} + +/* Identity UI */ +#identity-popup-content-box.unknownIdentity > #identity-popup-connectedToLabel , +#identity-popup-content-box.unknownIdentity > #identity-popup-runByLabel , +#identity-popup-content-box.unknownIdentity > #identity-popup-content-host , +#identity-popup-content-box.unknownIdentity > #identity-popup-content-owner , +#identity-popup-content-box.verifiedIdentity > #identity-popup-connectedToLabel2 , +#identity-popup-content-box.verifiedDomain > #identity-popup-connectedToLabel2 { + display: none; +} + +/* Full Screen UI */ + +#fullscr-toggler { + height: 1px; + background: black; +} + +#full-screen-warning-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2147483647 !important; + pointer-events: none; +} + +#full-screen-warning-container[fade-warning-out] { + transition-property: opacity !important; + transition-duration: 500ms !important; + opacity: 0.0; +} + +#full-screen-warning-message { + /* We must specify a max-width, otherwise word-wrap:break-word doesn't + work in descendant <description> and <label> elements. Bug 630864. */ + max-width: 800px; +} + +#full-screen-domain-text { + word-wrap: break-word; + /* We must specify a min-width, otherwise word-wrap:break-word doesn't work. Bug 630864. */ + min-width: 1px; +} + +#nav-bar[mode="text"] > #window-controls > toolbarbutton > .toolbarbutton-icon { + display: -moz-box; +} + +#nav-bar[mode="text"] > #window-controls > toolbarbutton > .toolbarbutton-text { + display: none; +} + +/* ::::: Keyboard UI Panel ::::: */ +.KUI-panel-closebutton { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-image"); +} + +:-moz-any(.ctrlTab-preview, .allTabs-preview) > html|img, +:-moz-any(.ctrlTab-preview, .allTabs-preview) > html|canvas { + min-width: inherit; + max-width: inherit; + min-height: inherit; + max-height: inherit; +} + +.ctrlTab-favicon-container, +.allTabs-favicon-container { + -moz-box-align: start; + -moz-box-pack: start; +} + +.ctrlTab-favicon, +.allTabs-favicon { + width: 16px; + height: 16px; +} + +/* ::::: Ctrl-Tab Panel ::::: */ +.ctrlTab-preview { + -moz-binding: url("chrome://browser/content/browser-tabPreviews.xml#ctrlTab-preview"); +} + +/* ::::: All Tabs Panel ::::: */ +.allTabs-preview { + -moz-binding: url("chrome://browser/content/browser-tabPreviews.xml#allTabs-preview"); +} + +#allTabs-tab-close-button { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-image"); + margin: 0; +} + + +/* notification anchors should only be visible when their associated + notifications are */ +.notification-anchor-icon { + -moz-user-focus: normal; +} + +.notification-anchor-icon:not([showing]) { + display: none; +} + +/* This was added with the identity toolkit, does it have any other purpose? */ +#notification-popup .text-link.custom-link { + -moz-binding: url("chrome://global/content/bindings/text.xml#text-label"); + text-decoration: none; +} + +#invalid-form-popup > description { + max-width: 280px; +} + +.popup-anchor { + /* should occupy space but not be visible */ + opacity: 0; + pointer-events: none; + -moz-stack-sizing: ignore; +} + +#addon-progress-notification { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#addon-progress-notification"); +} + +#click-to-play-plugins-notification { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification"); +} + +.plugin-popupnotification-centeritem { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#plugin-popupnotification-center-item"); +} + +/* override hidden="true" for the status bar compatibility shim + in case it was persisted for the real status bar */ +#status-bar { + display: -moz-box; +} + +/* Remove the resizer from the statusbar compatibility shim */ +#status-bar[hideresizer] > .statusbar-resizerpanel { + display: none; +} + +browser[tabmodalPromptShowing] { + -moz-user-focus: none !important; +} + +/* Status panel */ + +statuspanel { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#statuspanel"); + position: fixed; + margin-top: -3em; + left: 0; + max-width: calc(100% - 5px); + pointer-events: none; +} + +statuspanel:-moz-locale-dir(ltr)[mirror], +statuspanel:-moz-locale-dir(rtl):not([mirror]) { + left: auto; + right: 0; +} + +statuspanel[sizelimit] { + max-width: 50%; +} + +statuspanel[type=status] { + min-width: 23em; +} + +@media all and (max-width: 800px) { + statuspanel[type=status] { + min-width: 33%; + } +} + +statuspanel[type=overLink] { + transition: opacity 120ms ease-out; + direction: ltr; +} + +statuspanel[inactive] { + transition: none; + opacity: 0; +} + +statuspanel[inactive][previoustype=overLink] { + transition: opacity 200ms ease-out; +} + +.statuspanel-inner { + height: 3em; + width: 100%; + -moz-box-align: end; +} + +/* highlighter */ +%include highlighter.css + +/* gcli */ + +html|*#gcli-tooltip-frame, +html|*#gcli-output-frame, +#gcli-output, +#gcli-tooltip { + overflow-x: hidden; +} + +.gclitoolbar-input-node, +.gclitoolbar-complete-node { + direction: ltr; +} + +#developer-toolbar-toolbox-button[error-count] > .toolbarbutton-icon { + display: none; +} + +#developer-toolbar-toolbox-button[error-count]:before { + content: attr(error-count); + display: -moz-box; + -moz-box-pack: center; +} + +/* Responsive Mode */ + +.browserContainer[responsivemode] { + overflow: auto; +} + +.devtools-responsiveui-toolbar:-moz-locale-dir(rtl) { + -moz-box-pack: end; +} + +.browserStack[responsivemode] { + transition-duration: 200ms; + transition-timing-function: linear; +} + +.browserStack[responsivemode] { + transition-property: min-width, max-width, min-height, max-height; +} + +.browserStack[responsivemode][notransition] { + transition: none; +} + +.toolbarbutton-badge[badge]:not([badge=""])::after { + content: attr(badge); +} + +toolbarbutton[type="badged"] { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#toolbarbutton-badged"); +} + +/* Strict icon size for PMkit 'ui/button' */ +toolbarbutton[pmkit-button="true"] > .toolbarbutton-badge-stack > .toolbarbutton-icon { + width: 16px; + height: 16px; +} + +/* Remove white bar at the bottom of the screen when watching HTML5 video in fullscreen */ +#main-window[inFullscreen] #global-notificationbox, +#main-window[inFullscreen] #high-priority-global-notificationbox { + visibility: collapse; +} + +#navigator-toolbox[fullscreenShouldAnimate] { + transition: 0.7s margin-top ease-out; + transition-delay: 0.8s; +} diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js new file mode 100644 index 000000000..55a1de8a8 --- /dev/null +++ b/browser/base/content/browser.js @@ -0,0 +1,7383 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/RecentWindow.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", + "resource:///modules/private/CharsetMenu.jsm"); + +const nsIWebNavigation = Ci.nsIWebNavigation; +const gToolbarInfoSeparators = ["|", "-"]; + +var gLastBrowserCharset = null; +var gPrevCharset = null; +var gProxyFavIcon = null; +var gLastValidURLStr = ""; +var gInPrintPreviewMode = false; +var gContextMenu = null; // nsContextMenu instance + +var gEditUIVisible = true; +[ + ["gBrowser", "content"], + ["gNavToolbox", "navigator-toolbox"], + ["gURLBar", "urlbar"], + ["gNavigatorBundle", "bundle_browser"] +].forEach(function(elementGlobal) { + var [name, id] = elementGlobal; + window.__defineGetter__(name, function() { + var element = document.getElementById(id); + if (!element) + return null; + delete window[name]; + return window[name] = element; + }); + window.__defineSetter__(name, function(val) { + delete window[name]; + return window[name] = val; + }); +}); + +// Smart getter for the findbar. If you don't wish to force the creation of +// the findbar, check gFindBarInitialized first. +var gFindBarInitialized = false; +XPCOMUtils.defineLazyGetter(window, "gFindBar", function() { + let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let findbar = document.createElementNS(XULNS, "findbar"); + findbar.id = "FindToolbar"; + + let browserBottomBox = document.getElementById("browser-bottombox"); + browserBottomBox.insertBefore(findbar, browserBottomBox.firstChild); + + // Force a style flush to ensure that our binding is attached. + findbar.clientTop; + findbar.browser = gBrowser.mCurrentBrowser; + window.gFindBarInitialized = true; + return findbar; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gPrefService", function() { + return Services.prefs; +}); + +this.__defineGetter__("AddonManager", function() { + let tmp = {}; + Cu.import("resource://gre/modules/AddonManager.jsm", tmp); + return this.AddonManager = tmp.AddonManager; +}); +this.__defineSetter__("AddonManager", function(val) { + delete this.AddonManager; + return this.AddonManager = val; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils", "resource:///modules/AboutHomeUtils.jsm"); + +#ifdef MOZ_SERVICES_SYNC +XPCOMUtils.defineLazyModuleGetter(this, "Weave", "resource://services-sync/main.js"); +#endif + +XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function() { + let tmp = {}; + Cu.import("resource:///modules/private/PopupNotifications.jsm", tmp); + try { + return new tmp.PopupNotifications(gBrowser, + document.getElementById("notification-popup"), + document.getElementById("notification-popup-box")); + } catch(ex) { + Cu.reportError(ex); + return null; + } +}); + +XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", "resource://gre/modules/PageThumbs.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "gBrowserNewTabPreloader", "resource:///modules/BrowserNewTabPreloader.jsm", + "BrowserNewTabPreloader"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"); + +var gInitialPages = [ + "about:blank", + "about:newtab", + "about:home", + "about:privatebrowsing", + "about:sessionrestore", + "about:logopage" +]; + +#include browser-addons.js +#include browser-feeds.js +#include browser-fullScreen.js +#include browser-fullZoom.js +#include browser-places.js +#include browser-plugins.js +#include browser-tabPreviews.js +#include browser-thumbnails.js +#include browser-uacompat.js + +#include browser-gestureSupport.js + +#ifdef MOZ_SERVICES_SYNC +#include browser-syncui.js +#endif + +XPCOMUtils.defineLazyGetter(this, "Win7Features", function() { +#ifdef XP_WIN + const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; + if (WINTASKBAR_CONTRACTID in Cc && + Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) { + let AeroPeek = Cu.import("resource:///modules/WindowsPreviewPerTab.jsm", {}).AeroPeek; + return { + onOpenWindow: function() { + AeroPeek.onOpenWindow(window); + }, + onCloseWindow: function() { + AeroPeek.onCloseWindow(window); + } + }; + } +#endif + return null; +}); + +XPCOMUtils.defineLazyGetter(this, "PageMenu", function() { + let tmp = {}; + Cu.import("resource:///modules/private/PageMenu.jsm", tmp); + return new tmp.PageMenu(); +}); + +/** +* We can avoid adding multiple load event listeners and save some time by adding +* one listener that calls all real handlers. +*/ +function pageShowEventHandlers(persisted) { + charsetLoadListener(); + XULBrowserWindow.asyncUpdateUI(); + + // The PluginClickToPlay events are not fired when navigating using the + // BF cache. |persisted| is true when the page is loaded from the + // BF cache, so this code reshows the notification if necessary. + if (persisted) { + gPluginHandler.reshowClickToPlayNotification(); + } +} + +function UpdateBackForwardCommands(aWebNavigation) { + var backBroadcaster = document.getElementById("Browser:Back"); + var forwardBroadcaster = document.getElementById("Browser:Forward"); + + // Avoid setting attributes on broadcasters if the value hasn't changed! + // Remember, guys, setting attributes on elements is expensive! They + // get inherited into anonymous content, broadcast to other widgets, etc.! + // Don't do it if the value hasn't changed! - dwh + + var backDisabled = backBroadcaster.hasAttribute("disabled"); + var forwardDisabled = forwardBroadcaster.hasAttribute("disabled"); + if (backDisabled == aWebNavigation.canGoBack) { + if (backDisabled) { + backBroadcaster.removeAttribute("disabled"); + } else { + backBroadcaster.setAttribute("disabled", true); + } + } + + if (forwardDisabled == aWebNavigation.canGoForward) { + if (forwardDisabled) { + forwardBroadcaster.removeAttribute("disabled"); + } else { + forwardBroadcaster.setAttribute("disabled", true); + } + } +} + +/** + * Click-and-Hold implementation for the Back and Forward buttons + * XXXmano: should this live in toolbarbutton.xml? + */ +function SetClickAndHoldHandlers() { + var timer; + + function openMenu(aButton) { + cancelHold(aButton); + aButton.firstChild.hidden = false; + aButton.open = true; + } + + function mousedownHandler(aEvent) { + if (aEvent.button != 0 || + aEvent.currentTarget.open || + aEvent.currentTarget.disabled) + return; + + // Prevent the menupopup from opening immediately + aEvent.currentTarget.firstChild.hidden = true; + + aEvent.currentTarget.addEventListener("mouseout", mouseoutHandler, false); + aEvent.currentTarget.addEventListener("mouseup", mouseupHandler, false); + timer = setTimeout(openMenu, 500, aEvent.currentTarget); + } + + function mouseoutHandler(aEvent) { + let buttonRect = aEvent.currentTarget.getBoundingClientRect(); + if (aEvent.clientX >= buttonRect.left && + aEvent.clientX <= buttonRect.right && + aEvent.clientY >= buttonRect.bottom) { + openMenu(aEvent.currentTarget); + } else { + cancelHold(aEvent.currentTarget); + } + } + + function mouseupHandler(aEvent) { + cancelHold(aEvent.currentTarget); + } + + function cancelHold(aButton) { + clearTimeout(timer); + aButton.removeEventListener("mouseout", mouseoutHandler, false); + aButton.removeEventListener("mouseup", mouseupHandler, false); + } + + function clickHandler(aEvent) { + if (aEvent.button == 0 && + aEvent.target == aEvent.currentTarget && + !aEvent.currentTarget.open && + !aEvent.currentTarget.disabled) { + let cmdEvent = document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent("command", true, true, window, 0, + aEvent.ctrlKey, aEvent.altKey, aEvent.shiftKey, + aEvent.metaKey, null); + aEvent.currentTarget.dispatchEvent(cmdEvent); + } + } + + function _addClickAndHoldListenersOnElement(aElm) { + aElm.addEventListener("mousedown", mousedownHandler, true); + aElm.addEventListener("click", clickHandler, true); + } + + // Bug 414797: Clone unified-back-forward-button's context menu into both the + // back and the forward buttons. + var unifiedButton = document.getElementById("unified-back-forward-button"); + if (unifiedButton && !unifiedButton._clickHandlersAttached) { + unifiedButton._clickHandlersAttached = true; + + let popup = document.getElementById("backForwardMenu").cloneNode(true); + popup.removeAttribute("id"); + // Prevent the context attribute on unified-back-forward-button from being + // inherited. + popup.setAttribute("context", ""); + + let backButton = document.getElementById("back-button"); + backButton.setAttribute("type", "menu"); + backButton.appendChild(popup); + _addClickAndHoldListenersOnElement(backButton); + + let forwardButton = document.getElementById("forward-button"); + popup = popup.cloneNode(true); + forwardButton.setAttribute("type", "menu"); + forwardButton.appendChild(popup); + _addClickAndHoldListenersOnElement(forwardButton); + } +} + +const gSessionHistoryObserver = { + observe: function(subject, topic, data) { + if (topic != "browser:purge-session-history") { + return; + } + + var backCommand = document.getElementById("Browser:Back"); + backCommand.setAttribute("disabled", "true"); + var fwdCommand = document.getElementById("Browser:Forward"); + fwdCommand.setAttribute("disabled", "true"); + + // Hide session restore button on about:home + window.messageManager.broadcastAsyncMessage("Browser:HideSessionRestoreButton"); + + if (gURLBar) { + // Clear undo history of the URL bar + gURLBar.editor.transactionManager.clear() + } + } +}; + +var gFindBarSettings = { + messageName: "Findbar:Keypress", + prefName: "accessibility.typeaheadfind", + findAsYouType: null, + + init: function() { + window.messageManager.addMessageListener(this.messageName, this); + + gPrefService.addObserver(this.prefName, this, false); + this.writeFindAsYouType(); + }, + + uninit: function() { + window.messageManager.removeMessageListener(this.messageName, this); + + try { + gPrefService.removeObserver(this.prefName, this); + } catch(ex) { + Cu.reportError(ex); + } + }, + + observe: function(aSubject, aTopic, aData) { + if (aTopic != "nsPref:changed") { + return; + } + + this.writeFindAsYouType(); + }, + + writeFindAsYouType: function() { + this.findAsYouType = gPrefService.getBoolPref(this.prefName); + }, + + receiveMessage: function(aMessage) { + switch (aMessage.name) { + case this.messageName: + // If the find bar for chrome window's context is not yet alive, + // only initialize it if there's a possibility FindAsYouType + // will be used. + // There's no point in doing it for most random keypresses. + if (!gFindBarInitialized && aMessage.data.shouldFastFind) { + let shouldFastFind = this.findAsYouType; + if (!shouldFastFind) { + // Please keep in sync with toolkit/content/widgets/findbar.xml + const FAYT_LINKS_KEY = "'"; + const FAYT_TEXT_KEY = "/"; + let charCode = aMessage.data.fakeEvent.charCode; + let key = charCode ? String.fromCharCode(charCode) : null; + shouldFastFind = key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY; + } + if (shouldFastFind) { + // Make sure we return the result. + return gFindBar.receiveMessage(aMessage); + } + } + break; + } + } +}; + +var gURLBarSettings = { + prefSuggest: "browser.urlbar.suggest.", + // For searching in the source code: + // browser.urlbar.suggest.bookmark + // browser.urlbar.suggest.history + // browser.urlbar.suggest.openpage + prefSuggests: [ + "bookmark", + "history", + "openpage" + ], + prefKeyword: "keyword.enabled", + + observe: function(aSubject, aTopic, aData) { + if (aTopic != "nsPref:changed") { + return; + } + + this.writePlaceholder(); + }, + + writePlaceholder: function() { + if (!gURLBar) { + return; + } + + let attribute = "placeholder"; + let prefs = this.prefSuggests.map(pref => { + return this.prefSuggest + pref; + }); + prefs.push(this.prefKeyword); + let placeholderDefault = prefs.some(pref => { + return gPrefService.getBoolPref(pref); + }); + + if (placeholderDefault) { + gURLBar.setAttribute(attribute, gNavigatorBundle.getString("urlbar.placeholder")); + } else { + gURLBar.setAttribute(attribute, gNavigatorBundle.getString("urlbar.placeholderURLOnly")); + } + } +}; + +/** + * Given a starting docshell and a URI to look up, find the docshell the URI + * is loaded in. + * @param aDocument + * A document to find instead of using just a URI - this is more specific. + * @param aDocShell + * The doc shell to start at + * @param aSoughtURI + * The URI that we're looking for + * @returns The doc shell that the sought URI is loaded in. Can be in + * subframes. + */ +function findChildShell(aDocument, aDocShell, aSoughtURI) { + aDocShell.QueryInterface(Components.interfaces.nsIWebNavigation); + aDocShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor); + var doc = aDocShell.getInterface(Components.interfaces.nsIDOMDocument); + if ((aDocument && doc == aDocument) || + (aSoughtURI && aSoughtURI.spec == aDocShell.currentURI.spec)) { + return aDocShell; + } + + var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem); + for (var i = 0; i < node.childCount; ++i) { + var docShell = node.getChildAt(i); + docShell = findChildShell(aDocument, docShell, aSoughtURI); + if (docShell) { + return docShell; + } + } + return null; +} + +var gPopupBlockerObserver = { + _reportButton: null, + + onReportButtonClick: function(aEvent) { + if (aEvent.button != 0 || aEvent.target != this._reportButton) { + return; + } + + document.getElementById("blockedPopupOptions") + .openPopup(this._reportButton, "after_end", 0, 2, false, false, aEvent); + }, + + handleEvent: function(aEvent) { + if (aEvent.originalTarget != gBrowser.selectedBrowser) { + return; + } + + if (!this._reportButton && gURLBar) { + this._reportButton = document.getElementById("page-report-button"); + } + + if (!gBrowser.selectedBrowser.blockedPopups || + !gBrowser.selectedBrowser.blockedPopups.length) { + // Hide the icon in the location bar (if the location bar exists) + if (gURLBar) { + this._reportButton.hidden = true; + } + return; + } + + if (gURLBar) { + this._reportButton.hidden = false; + } + + // Only show the notification again if we've not already shown it. Since + // notifications are per-browser, we don't need to worry about re-adding + // it. + if (!gBrowser.selectedBrowser.blockedPopups.reported) { + if (gPrefService.getBoolPref("privacy.popups.showBrowserMessage")) { + var brandBundle = document.getElementById("bundle_brand"); + var brandShortName = brandBundle.getString("brandShortName"); + var popupCount = gBrowser.selectedBrowser.blockedPopups.length; + var popupButtonText = gNavigatorBundle.getString("popupWarningButton"); + var popupButtonAccesskey = gNavigatorBundle.getString("popupWarningButton.accesskey"); + var messageBase = gNavigatorBundle.getString("popupWarning.message"); + var message = PluralForm.get(popupCount, messageBase) + .replace("#1", brandShortName) + .replace("#2", popupCount); + + var notificationBox = gBrowser.getNotificationBox(); + var notification = notificationBox.getNotificationWithValue("popup-blocked"); + if (notification) { + notification.label = message; + } else { + var buttons = [{ + label: popupButtonText, + accessKey: popupButtonAccesskey, + popup: "blockedPopupOptions", + callback: null + }]; + + const priority = notificationBox.PRIORITY_WARNING_MEDIUM; + notificationBox.appendNotification(message, "popup-blocked", + "chrome://browser/skin/Info.png", + priority, buttons); + } + } + + // Record the fact that we've reported this blocked popup, so we don't + // show it again. + gBrowser.selectedBrowser.blockedPopups.reported = true; + } + }, + + toggleAllowPopupsForSite: function(aEvent) { + var pm = Services.perms; + var shouldBlock = aEvent.target.getAttribute("block") == "true"; + var perm = shouldBlock ? pm.DENY_ACTION : pm.ALLOW_ACTION; + pm.add(gBrowser.currentURI, "popup", perm); + + gBrowser.getNotificationBox().removeCurrentNotification(); + }, + + fillPopupList: function(aEvent) { + // XXXben - rather than using |currentURI| here, which breaks down on multi-framed sites + // we should really walk the blockedPopups and create a list of "allow for <host>" + // menuitems for the common subset of hosts present in the report, this will + // make us frame-safe. + // + // XXXjst - Note that when this is fixed to work with multi-framed sites, + // also back out the fix for bug 343772 where + // nsGlobalWindow::CheckOpenAllow() was changed to also + // check if the top window's location is whitelisted. + let browser = gBrowser.selectedBrowser; + var uri = browser.currentURI; + var blockedPopupAllowSite = document.getElementById("blockedPopupAllowSite"); + try { + blockedPopupAllowSite.removeAttribute("hidden"); + + var pm = Services.perms; + if (pm.testPermission(uri, "popup") == pm.ALLOW_ACTION) { + // Offer an item to block popups for this site, if a whitelist entry exists + // already for it. + let blockString = gNavigatorBundle.getFormattedString("popupBlock", [uri.host || uri.spec]); + blockedPopupAllowSite.setAttribute("label", blockString); + blockedPopupAllowSite.setAttribute("block", "true"); + } else { + // Offer an item to allow popups for this site + let allowString = gNavigatorBundle.getFormattedString("popupAllow", [uri.host || uri.spec]); + blockedPopupAllowSite.setAttribute("label", allowString); + blockedPopupAllowSite.removeAttribute("block"); + } + } catch(e) { + blockedPopupAllowSite.setAttribute("hidden", "true"); + } + + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + blockedPopupAllowSite.setAttribute("disabled", "true"); + } else { + blockedPopupAllowSite.removeAttribute("disabled"); + } + + let blockedPopupDontShowMessage = document.getElementById("blockedPopupDontShowMessage"); + let showMessage = gPrefService.getBoolPref("privacy.popups.showBrowserMessage"); + blockedPopupDontShowMessage.setAttribute("checked", !showMessage); + if (aEvent.target.anchorNode.id == "page-report-button") { + aEvent.target.anchorNode.setAttribute("open", "true"); + blockedPopupDontShowMessage.setAttribute("label", + gNavigatorBundle.getString("popupWarningDontShowFromLocationbar")); + } else { + blockedPopupDontShowMessage.setAttribute("label", + gNavigatorBundle.getString("popupWarningDontShowFromMessage")); + } + + let blockedPopupsSeparator = document.getElementById("blockedPopupsSeparator"); + blockedPopupsSeparator.setAttribute("hidden", true); + + gBrowser.selectedBrowser.retrieveListOfBlockedPopups().then(blockedPopups => { + let foundUsablePopupURI = false; + if (blockedPopups) { + for (let i = 0; i < blockedPopups.length; i++) { + let blockedPopup = blockedPopups[i]; + + // popupWindowURI will be null if the file picker popup is blocked. + // xxxdz this should make the option say "Show file picker" and do it (Bug 590306) + if (!blockedPopup.popupWindowURIspec) { + continue; + } + + var popupURIspec = blockedPopup.popupWindowURIspec; + + // Sometimes the popup URI that we get back from the blockedPopup + // isn't useful (for instance, netscape.com's popup URI ends up + // being "http://www.netscape.com", which isn't really the URI of + // the popup they're trying to show). This isn't going to be + // useful to the user, so we won't create a menu item for it. + if (popupURIspec == "" || + popupURIspec == "about:blank" || + popupURIspec == "<self>" || + popupURIspec == uri.spec) { + continue; + } + + // Because of the short-circuit above, we may end up in a situation + // in which we don't have any usable popup addresses to show in + // the menu, and therefore we shouldn't show the separator. However, + // since we got past the short-circuit, we must've found at least + // one usable popup URI and thus we'll turn on the separator later. + foundUsablePopupURI = true; + + var menuitem = document.createElement("menuitem"); + var label = gNavigatorBundle.getFormattedString("popupShowPopupPrefix", [popupURIspec]); + menuitem.setAttribute("label", label); + menuitem.setAttribute("oncommand", "gPopupBlockerObserver.showBlockedPopup(event);"); + menuitem.setAttribute("popupReportIndex", i); + menuitem.popupReportBrowser = browser; + aEvent.target.appendChild(menuitem); + } + } + + // Show the separator if we added any + // showable popup addresses to the menu. + if (foundUsablePopupURI) { + blockedPopupsSeparator.removeAttribute("hidden"); + } + }, null); + }, + + onPopupHiding: function(aEvent) { + if (aEvent.target.anchorNode.id == "page-report-button") { + aEvent.target.anchorNode.removeAttribute("open"); + } + + let item = aEvent.target.lastChild; + while (item && item.getAttribute("observes") != "blockedPopupsSeparator") { + let next = item.previousSibling; + item.parentNode.removeChild(item); + item = next; + } + }, + + showBlockedPopup: function(aEvent) { + var target = aEvent.target; + var popupReportIndex = target.getAttribute("popupReportIndex"); + let browser = target.popupReportBrowser; + browser.unblockPopup(popupReportIndex); + }, + + editPopupSettings: function() { + var host = ""; + try { + host = gBrowser.currentURI.host; + } catch(e) {} + + var bundlePreferences = document.getElementById("bundle_preferences"); + var params = { blockVisible : false, + sessionVisible : false, + allowVisible : true, + prefilledHost : host, + permissionType : "popup", + windowTitle : bundlePreferences.getString("popuppermissionstitle"), + introText : bundlePreferences.getString("popuppermissionstext") }; + var existingWindow = Services.wm.getMostRecentWindow("Browser:Permissions"); + if (existingWindow) { + existingWindow.initWithParams(params); + existingWindow.focus(); + } else { + window.openDialog("chrome://browser/content/preferences/permissions.xul", + "_blank", "resizable,dialog=no,centerscreen", params); + } + }, + + dontShowMessage: function() { + var showMessage = gPrefService.getBoolPref("privacy.popups.showBrowserMessage"); + gPrefService.setBoolPref("privacy.popups.showBrowserMessage", !showMessage); + gBrowser.getNotificationBox().removeCurrentNotification(); + } +}; + +const gXSSObserver = { + + observe: function(aSubject, aTopic, aData) { + // Don't do anything if the notification is disabled. + if (!gPrefService.getBoolPref("security.xssfilter.displayWarning")) { + return; + } + + // Parse incoming XSS array + aSubject.QueryInterface(Ci.nsIArray); + var policy = aSubject.queryElementAt(0, Ci.nsISupportsString).data; + var content = aSubject.queryElementAt(1, Ci.nsISupportsString).data; + var domain = aSubject.queryElementAt(2, Ci.nsISupportsString).data; + var url = aSubject.queryElementAt(3, Ci.nsISupportsCString).data; + var blockMode = aSubject.queryElementAt(4, Ci.nsISupportsPRBool).data; + + // If it is a block mode event, do not display the infobar + if (blockMode) { + return; + } + + var nb = gBrowser.getNotificationBox(); + const priority = nb.PRIORITY_WARNING_MEDIUM; + + var buttons = [{ + label: 'View Unsafe Content', + accessKey: 'V', + popup: null, + callback: function() { + alert(content); + } + }]; + + if (domain !== "") { + buttons.push({ + label: 'Add Domain Exception', + accessKey: 'A', + popup: null, + callback: function() { + let whitelist = gPrefService.getCharPref("security.xssfilter.whitelist"); + if (whitelist != "") { + whitelist = whitelist + "," + domain; + } else { + whitelist = domain; + } + // Write the updated whitelist. Since this is observed by the XSS filter, + // it will automatically sync to the back-end and update immediately. + gPrefService.setCharPref("security.xssfilter.whitelist", whitelist); + // After setting this, we automatically reload the page. + BrowserReloadSkipCache(); + } + }); + } + + nb.appendNotification("The XSS Filter has detected a potential XSS attack. Type: " + + policy, 'popup-blocked', 'chrome://browser/skin/Info.png', + priority, buttons); + } +}; + +var gBrowserInit = { + delayedStartupFinished: false, + + onLoad: function() { + var mustLoadSidebar = false; + + Cc["@mozilla.org/eventlistenerservice;1"] + .getService(Ci.nsIEventListenerService) + .addSystemEventListener(gBrowser, "click", contentAreaClick, true); + + gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false); + + // Note that the XBL binding is untrusted + gBrowser.addEventListener("PluginBindingAttached", gPluginHandler, true, true); + gBrowser.addEventListener("PluginCrashed", gPluginHandler, true); + gBrowser.addEventListener("PluginOutdated", gPluginHandler, true); + gBrowser.addEventListener("PluginInstantiated", gPluginHandler, true); + gBrowser.addEventListener("PluginRemoved", gPluginHandler, true); + + Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false); + + window.addEventListener("AppCommand", HandleAppCommandEvent, true); + + // These routines add message listeners. They must run before + // loading the frame script to ensure that we don't miss any + // message sent between when the frame script is loaded and when + // the listener is registered. +#ifdef MOZ_DEVTOOLS + DevToolsTheme.init(); +#endif + gFindBarSettings.init(); + + messageManager.loadFrameScript("chrome://browser/content/content.js", true); + messageManager.loadFrameScript("chrome://browser/content/content-sessionStore.js", true); + + // initialize observers and listeners + // and give C++ access to gBrowser + XULBrowserWindow.init(); + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow) + .XULBrowserWindow = window.XULBrowserWindow; + window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess(); + + // set default character set if provided + // window.arguments[1]: character set (string) + if ("arguments" in window && window.arguments.length > 1 && window.arguments[1]) { + if (window.arguments[1].startsWith("charset=")) { + var arrayArgComponents = window.arguments[1].split("="); + if (arrayArgComponents) { + //we should "inherit" the charset menu setting in a new window + //TFE FIXME: this is now a wrappednative and can't be set this way. + //getMarkupDocumentViewer().defaultCharacterSet = arrayArgComponents[1]; + } + } + } + + // Manually hook up session and global history for the first browser + // so that we don't have to load global history before bringing up a + // window. + // Wire up session and global history before any possible + // progress notifications for back/forward button updating + gBrowser.webNavigation.sessionHistory = Cc["@mozilla.org/browser/shistory;1"] + .createInstance(Ci.nsISHistory); + Services.obs.addObserver(gBrowser.browsers[0], "browser:purge-session-history", false); + + // remove the disablehistory attribute so the browser cleans up, as + // though it had done this work itself + gBrowser.browsers[0].removeAttribute("disablehistory"); + + // enable global history + try { + gBrowser.docShell.useGlobalHistory = true; + } catch(ex) { + Cu.reportError("Places database may be locked: " + ex); + } + + // hook up UI through progress listener + gBrowser.addProgressListener(window.XULBrowserWindow); + gBrowser.addTabsProgressListener(window.TabsProgressListener); + + // setup our common DOMLinkAdded listener + gBrowser.addEventListener("DOMLinkAdded", DOMLinkHandler, false); + + // setup our MozApplicationManifest listener + gBrowser.addEventListener("MozApplicationManifest", + OfflineApps, false); + + // setup simple gestures support + gGestureSupport.init(true); + + // setup history swipe animation + gHistorySwipeAnimation.init(); + + if (window.opener && !window.opener.closed) { + let openerSidebarBox = window.opener.document.getElementById("sidebar-box"); + // If the opener had a sidebar, open the same sidebar in our window. + // The opener can be the hidden window too, if we're coming from the state + // where no windows are open, and the hidden window has no sidebar box. + if (openerSidebarBox && !openerSidebarBox.hidden) { + let sidebarCmd = openerSidebarBox.getAttribute("sidebarcommand"); + let sidebarCmdElem = document.getElementById(sidebarCmd); + + // dynamically generated sidebars will fail this check. + if (sidebarCmdElem) { + let sidebarBox = document.getElementById("sidebar-box"); + let sidebarTitle = document.getElementById("sidebar-title"); + + sidebarTitle.setAttribute( + "value", window.opener.document.getElementById("sidebar-title").getAttribute("value")); + sidebarBox.setAttribute("width", openerSidebarBox.boxObject.width); + + sidebarBox.setAttribute("sidebarcommand", sidebarCmd); + // Note: we're setting 'src' on sidebarBox, which is a <vbox>, not on + // the <browser id="sidebar">. This lets us delay the actual load until + // delayedStartup(). + sidebarBox.setAttribute( + "src", window.opener.document.getElementById("sidebar").getAttribute("src")); + mustLoadSidebar = true; + + sidebarBox.hidden = false; + document.getElementById("sidebar-splitter").hidden = false; + sidebarCmdElem.setAttribute("checked", "true"); + } + } + } else { + let box = document.getElementById("sidebar-box"); + if (box.hasAttribute("sidebarcommand")) { + let commandID = box.getAttribute("sidebarcommand"); + if (commandID) { + let command = document.getElementById(commandID); + if (command) { + mustLoadSidebar = true; + box.hidden = false; + document.getElementById("sidebar-splitter").hidden = false; + command.setAttribute("checked", "true"); + } else { + // Remove the |sidebarcommand| attribute, because the element it + // refers to no longer exists, so we should assume this sidebar + // panel has been uninstalled. (249883) + box.removeAttribute("sidebarcommand"); + } + } + } + } + + // Certain kinds of automigration rely on this notification to complete their + // tasks BEFORE the browser window is shown. + Services.obs.notifyObservers(null, "browser-window-before-show", ""); + + // Set a sane starting width/height for all resolutions on new profiles. + if (!document.documentElement.hasAttribute("width")) { + let defaultWidth; + let defaultHeight; + + // Very small: maximize the window + // Portrait : use about full width and 3/4 height, to view entire pages + // at once (without being obnoxiously tall) + // Widescreen: use about half width, to suggest side-by-side page view + // Otherwise : use 3/4 height and width + if (screen.availHeight <= 600) { + document.documentElement.setAttribute("sizemode", "maximized"); + defaultWidth = 610; + defaultHeight = 450; + } else { + if (screen.availWidth <= screen.availHeight) { + defaultWidth = screen.availWidth * .9; + defaultHeight = screen.availHeight * .75; + } else if (screen.availWidth >= 2048) { + defaultWidth = (screen.availWidth / 2) - 20; + defaultHeight = screen.availHeight - 10; + } else { + defaultWidth = screen.availWidth * .75; + defaultHeight = screen.availHeight * .75; + } + +#ifdef MOZ_WIDGET_GTK2 + // On X, we're not currently able to account for the size of the window + // border. Use 28px as a guess (titlebar + bottom window border) + defaultHeight -= 28; +#endif + } + document.documentElement.setAttribute("width", defaultWidth); + document.documentElement.setAttribute("height", defaultHeight); + } + + if (!gShowPageResizers) { + document.getElementById("status-bar").setAttribute("hideresizer", "true"); + } + + if (!window.toolbar.visible) { + // adjust browser UI for popups + if (gURLBar) { + gURLBar.setAttribute("readonly", "true"); + gURLBar.setAttribute("enablehistory", "false"); + } + goSetCommandEnabled("cmd_newNavigatorTab", false); + } + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif + + // Misc. inits. + CombinedStopReload.init(); + allTabs.readPref(); + TabsOnTop.init(); + AudioIndicator.init(); + gPrivateBrowsingUI.init(); + TabsInTitlebar.init(); + retrieveToolbarIconsizesFromTheme(); + ToolbarIconColor.init(); + UserAgentCompatibility.init(); + +#ifdef XP_WIN + if (window.matchMedia("(-moz-os-version: windows-win8)").matches && + window.matchMedia("(-moz-windows-default-theme)").matches) { + let windows8WindowFrameColor = Cu.import("resource:///modules/Windows8WindowFrameColor.jsm", {}).Windows8WindowFrameColor; + + var windowFrameColor; + windowFrameColor = windows8WindowFrameColor.get_win8(); + + // Formula from Microsoft's UWP guideline. + let backgroundLuminance = (windowFrameColor[0] * 2 + + windowFrameColor[1] * 5 + + windowFrameColor[2]) / 8; + if (backgroundLuminance <= 128) { + document.documentElement.setAttribute("darkwindowframe", "true"); + } + } +#endif + + // Wait until chrome is painted before executing code not critical to making the window visible + this._boundDelayedStartup = this._delayedStartup.bind(this, mustLoadSidebar); + window.addEventListener("MozAfterPaint", this._boundDelayedStartup); + + this._loadHandled = true; + }, + + _cancelDelayedStartup: function() { + window.removeEventListener("MozAfterPaint", this._boundDelayedStartup); + this._boundDelayedStartup = null; + }, + + _delayedStartup: function(mustLoadSidebar) { + let tmp = {}; + + this._cancelDelayedStartup(); + + let uriToLoad = this._getUriToLoad(); + var isLoadingBlank = isBlankPageURL(uriToLoad); + + // This pageshow listener needs to be registered before we may call + // swapBrowsersAndCloseOther() to receive pageshow events fired by that. + gBrowser.addEventListener("pageshow", function(event) { + // Filter out events that are not about the document load we are interested in + if (content && event.target == content.document) { + setTimeout(pageShowEventHandlers, 0, event.persisted); + } + }, true); + + if (uriToLoad && uriToLoad != "about:blank") { + if (uriToLoad instanceof Ci.nsISupportsArray) { + let count = uriToLoad.Count(); + let specs = []; + for (let i = 0; i < count; i++) { + let urisstring = uriToLoad.GetElementAt(i).QueryInterface(Ci.nsISupportsString); + specs.push(urisstring.data); + } + + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(specs, false, true); + } catch(e) {} + } else if (uriToLoad instanceof XULElement) { + // swap the given tab with the default about:blank tab and then close + // the original tab in the other window. + + // Stop the about:blank load + gBrowser.stop(); + // make sure it has a docshell + gBrowser.docShell; + + gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, uriToLoad); + } else if (window.arguments.length >= 3) { + // window.arguments[2]: referrer (nsIURI | string) + // [3]: postData (nsIInputStream) + // [4]: allowThirdPartyFixup (bool) + // [5]: referrerPolicy (int) + // [6]: originPrincipal (nsIPrincipal) + // [7]: triggeringPrincipal (nsIPrincipal) + + let referrerURI = window.arguments[2]; + if (typeof(referrerURI) == "string") { + try { + referrerURI = makeURI(referrerURI); + } catch(e) { + referrerURI = null; + } + } + let referrerPolicy = (window.arguments[5] != undefined ? + window.arguments[5] : + Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT); + loadURI(uriToLoad, referrerURI, window.arguments[3] || null, + window.arguments[4] || false, referrerPolicy, + // pass the origin principal (if any) and force its use to create + // an initial about:blank viewer if present: + window.arguments[6], !!window.arguments[6], window.arguments[7]); + window.focus(); + } else { + // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3. + // Such callers expect that window.arguments[0] is handled as a single URI. + loadOneOrMoreURIs(uriToLoad); + } + } + + Services.obs.addObserver(gSessionHistoryObserver, "browser:purge-session-history", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-started", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-origin-blocked", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-failed", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false); + Services.obs.addObserver(gXSSObserver, "xss-on-violate-policy", false); + + gPrefService.addObserver(gURLBarSettings.prefSuggest, gURLBarSettings, false); + gPrefService.addObserver(gURLBarSettings.prefKeyword, gURLBarSettings, false); + + gURLBarSettings.writePlaceholder(); + + BrowserOffline.init(); + OfflineApps.init(); + IndexedDBPromptHelper.init(); + AddonManager.addAddonListener(AddonsMgrListener); + + // Ensure login manager is up and running. + Services.logins; + + if (mustLoadSidebar) { + let sidebar = document.getElementById("sidebar"); + let sidebarBox = document.getElementById("sidebar-box"); + sidebar.setAttribute("src", sidebarBox.getAttribute("src")); + } + + UpdateUrlbarSearchSplitterState(); + + if (!isLoadingBlank || !focusAndSelectUrlBar()) { + gBrowser.selectedBrowser.focus(); + } + + gNavToolbox.customizeDone = BrowserToolboxCustomizeDone; + gNavToolbox.customizeChange = BrowserToolboxCustomizeChange; + + // Set up Sanitize Item + this._initializeSanitizer(); + + // Enable/Disable auto-hide tabbar + gBrowser.tabContainer.updateVisibility(); + + gPrefService.addObserver(gHomeButton.prefDomain, gHomeButton, false); + + var homeButton = document.getElementById("home-button"); + gHomeButton.updateTooltip(homeButton); + gHomeButton.updatePersonalToolbarStyle(homeButton); + + // BiDi UI + gBidiUI = isBidiEnabled(); + if (gBidiUI) { + document.getElementById("documentDirection-separator").hidden = false; + document.getElementById("documentDirection-swap").hidden = false; + document.getElementById("textfieldDirection-separator").hidden = false; + document.getElementById("textfieldDirection-swap").hidden = false; + } + + // Setup click-and-hold gestures access to the session history + // menus if global click-and-hold isn't turned on + if (!Services.prefs.getBoolPref("ui.click_hold_context_menus", false)) { + SetClickAndHoldHandlers(); + } + + // Initialize the full zoom setting. + // We do this before the session restore service gets initialized so we can + // apply full zoom settings to tabs restored by the session restore service. + FullZoom.init(); + + // NetworkPrioritizer + let NP = {}; + Cu.import("resource:///modules/NetworkPrioritizer.jsm", NP); + NP.trackBrowserWindow(window); + + // initialize the session-restore service (in case it's not already running) + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + let ssPromise = ss.init(window); + + PlacesToolbarHelper.init(); + + ctrlTab.readPref(); + gPrefService.addObserver(ctrlTab.prefName, ctrlTab, false); + gPrefService.addObserver(allTabs.prefName, allTabs, false); + + // Initialize the download manager some time after the app starts so that + // auto-resume downloads begin (such as after crashing or quitting with + // active downloads) and speeds up the first-load of the download manager UI. + // If the user manually opens the download manager before the timeout, the + // downloads will start right away, and getting the service again won't hurt. + setTimeout(function() { + try { + Cu.import("resource:///modules/DownloadsCommon.jsm", {}) + .DownloadsCommon.initializeAllDataLinks(); + Cu.import("resource:///modules/DownloadsTaskbar.jsm", {}) + .DownloadsTaskbar.registerIndicator(window); + } catch(ex) { + Cu.reportError(ex); + } + }, 10000); + + // Load the Login Manager data from disk off the main thread, some time + // after startup. If the data is required before the timeout, for example + // because a restored page contains a password field, it will be loaded on + // the main thread, and this initialization request will be ignored. + setTimeout(function() { + try { + Services.logins; + } catch(ex) { + Cu.reportError(ex); + } + }, 3000); + + // The object handling the downloads indicator is also initialized here in the + // delayed startup function, but the actual indicator element is not loaded + // unless there are downloads to be displayed. + DownloadsButton.initializeIndicator(); + + updateEditUIVisibility(); + let placesContext = document.getElementById("placesContext"); + placesContext.addEventListener("popupshowing", updateEditUIVisibility, false); + placesContext.addEventListener("popuphiding", updateEditUIVisibility, false); + + gBrowser.mPanelContainer.addEventListener("InstallBrowserTheme", LightWeightThemeWebInstaller, false, true); + gBrowser.mPanelContainer.addEventListener("PreviewBrowserTheme", LightWeightThemeWebInstaller, false, true); + gBrowser.mPanelContainer.addEventListener("ResetBrowserThemePreview", LightWeightThemeWebInstaller, false, true); + + // AeroPeek + if (Win7Features) { + Win7Features.onOpenWindow(); + } + + // called when we go into full screen, even if initiated by a web page script + window.addEventListener("fullscreen", onFullScreen, true); + + // Called when we enter DOM full-screen mode. Note we can already be in browser + // full-screen mode when we enter DOM full-screen mode. + window.addEventListener("MozDOMFullscreen:NewOrigin", onMozEnteredDomFullscreen, true); + + if (window.fullScreen) { + onFullScreen(); + } + if (document.mozFullScreen) { + onMozEnteredDomFullscreen(); + } + +#ifdef MOZ_SERVICES_SYNC + // initialize the sync UI + gSyncUI.init(); +#endif + + gBrowserThumbnails.init(); + + setUrlAndSearchBarWidthForConditionalForwardButton(); + window.addEventListener("resize", function resizeHandler(event) { + if (event.target == window) { + setUrlAndSearchBarWidthForConditionalForwardButton(); + } + }); + + // Enable Error Console? + let consoleEnabled = gPrefService.getBoolPref("devtools.errorconsole.enabled"); + if (consoleEnabled) { + let cmd = document.getElementById("Tools:ErrorConsole"); + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + } + +#ifdef MENUBAR_CAN_AUTOHIDE + // If the user (or the locale) hasn't enabled the top-level "Character + // Encoding" menu via the "browser.menu.showCharacterEncoding" preference, + // hide it. + if ("true" != gPrefService.getComplexValue("browser.menu.showCharacterEncoding", + Ci.nsIPrefLocalizedString).data) { + document.getElementById("appmenu_charsetMenu").hidden = true; + } +#endif + + let appMenuButton = document.getElementById("appmenu-button"); + let appMenuPopup = document.getElementById("appmenu-popup"); + if (appMenuButton && appMenuPopup) { + let appMenuOpening = null; + appMenuButton.addEventListener("mousedown", function(event) { + if (event.button == 0) { + appMenuOpening = new Date(); + } + }, false); + appMenuPopup.addEventListener("popupshown", function(event) { + if (event.target != appMenuPopup || !appMenuOpening) { + return; + } + let duration = new Date() - appMenuOpening; + appMenuOpening = null; + }, false); + } + + window.addEventListener("mousemove", MousePosTracker, false); + window.addEventListener("dragover", MousePosTracker, false); + + // End startup crash tracking after a delay to catch crashes while restoring + // tabs and to postpone saving the pref to disk. + try { + const startupCrashEndDelay = 30 * 1000; + setTimeout(Services.startup.trackStartupCrashEnd, startupCrashEndDelay); + } catch(ex) { + Cu.reportError("Could not end startup crash tracking: " + ex); + } + + ssPromise.then(() => { + // Bail out if the window has been closed in the meantime. + if (window.closed) { + return; + } + if ("TabView" in window) { + TabView.init(); + } + // XXX: do we still need this?... + setTimeout(function() { BrowserChromeTest.markAsReady(); }, 0); + }); + + this.delayedStartupFinished = true; + + Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); + }, + + // Returns the URI(s) to load at startup. + _getUriToLoad: function() { + // window.arguments[0]: URI to load (string), or an nsISupportsArray of + // nsISupportsStrings to load, or a xul:tab of + // a tabbrowser, which will be replaced by this + // window (for this case, all other arguments are + // ignored). + if (!window.arguments || !window.arguments[0]) { + return null; + } + + let uri = window.arguments[0]; + let sessionStartup = Cc["@mozilla.org/browser/sessionstartup;1"] + .getService(Ci.nsISessionStartup); + let defaultArgs = Cc["@mozilla.org/browser/clh;1"] + .getService(Ci.nsIBrowserHandler) + .defaultArgs; + + // If the given URI matches defaultArgs (the default homepage) we want + // to block its load if we're going to restore a session anyway. + if (uri == defaultArgs && sessionStartup.willOverrideHomepage) { + return null; + } + + return uri; + }, + + onUnload: function() { + // In certain scenarios it's possible for unload to be fired before onload, + // (e.g. if the window is being closed after browser.js loads but before the + // load completes). In that case, there's nothing to do here. + if (!this._loadHandled) + return; + + // First clean up services initialized in gBrowserInit.onLoad (or those whose + // uninit methods don't depend on the services having been initialized). + + allTabs.uninit(); + + CombinedStopReload.uninit(); + + gGestureSupport.init(false); + + gHistorySwipeAnimation.uninit(); + + FullScreen.cleanup(); + + Services.obs.removeObserver(gPluginHandler.pluginCrashed, "plugin-crashed"); + + try { + gBrowser.removeProgressListener(window.XULBrowserWindow); + gBrowser.removeTabsProgressListener(window.TabsProgressListener); + } catch(ex) {} + + BookmarkingUI.uninit(); + + TabsOnTop.uninit(); + + AudioIndicator.uninit(); + + TabsInTitlebar.uninit(); + + ToolbarIconColor.uninit(); + +#ifdef MOZ_DEVTOOLS + DevToolsTheme.uninit(); +#endif + gFindBarSettings.uninit(); + + UserAgentCompatibility.uninit(); + + var enumerator = Services.wm.getEnumerator(null); + enumerator.getNext(); + if (!enumerator.hasMoreElements()) { + document.persist("sidebar-box", "sidebarcommand"); + document.persist("sidebar-box", "width"); + document.persist("sidebar-box", "src"); + document.persist("sidebar-title", "value"); + } + + // Now either cancel delayedStartup, or clean up the services initialized from + // it. + if (this._boundDelayedStartup) { + this._cancelDelayedStartup(); + } else { + if (Win7Features) { + Win7Features.onCloseWindow(); + } + + gPrefService.removeObserver(ctrlTab.prefName, ctrlTab); + gPrefService.removeObserver(allTabs.prefName, allTabs); + ctrlTab.uninit(); + if ("TabView" in window) { + TabView.uninit(); + } + gBrowserThumbnails.uninit(); + FullZoom.destroy(); + + Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-started"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-origin-blocked"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete"); + Services.obs.removeObserver(gXSSObserver, "xss-on-violate-policy"); + + try { + gPrefService.removeObserver(gURLBarSettings.prefSuggest, gURLBarSettings); + gPrefService.removeObserver(gURLBarSettings.prefKeyword, gURLBarSettings); + } catch(ex) { + Cu.reportError(ex); + } + + try { + gPrefService.removeObserver(gHomeButton.prefDomain, gHomeButton); + } catch(ex) { + Cu.reportError(ex); + } + + BrowserOffline.uninit(); + OfflineApps.uninit(); + IndexedDBPromptHelper.uninit(); + AddonManager.removeAddonListener(AddonsMgrListener); + } + + // Final window teardown, do this last. + window.XULBrowserWindow.destroy(); + window.XULBrowserWindow = null; + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow) + .XULBrowserWindow = null; + window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = null; + }, + + _initializeSanitizer: function() { + const kDidSanitizeDomain = "privacy.sanitize.didShutdownSanitize"; + if (gPrefService.prefHasUserValue(kDidSanitizeDomain)) { + gPrefService.clearUserPref(kDidSanitizeDomain); + // We need to persist this preference change, since we want to + // check it at next app start even if the browser exits abruptly + gPrefService.savePrefFile(null); + } + + /** + * Migrate Firefox 3.0 privacy.item prefs under one of these conditions: + * + * a) User has customized any privacy.item prefs + * b) privacy.sanitize.sanitizeOnShutdown is set + */ + if (!gPrefService.getBoolPref("privacy.sanitize.migrateFx3Prefs")) { + let itemBranch = gPrefService.getBranch("privacy.item."); + let itemArray = itemBranch.getChildList(""); + + // See if any privacy.item prefs are set + let doMigrate = itemArray.some(function(name) itemBranch.prefHasUserValue(name)); + // Or if sanitizeOnShutdown is set + if (!doMigrate) { + doMigrate = gPrefService.getBoolPref("privacy.sanitize.sanitizeOnShutdown"); + } + + if (doMigrate) { + let cpdBranch = gPrefService.getBranch("privacy.cpd."); + let clearOnShutdownBranch = gPrefService.getBranch("privacy.clearOnShutdown."); + for (let name of itemArray) { + try { + // don't migrate password or offlineApps clearing in the CRH dialog since + // there's no UI for those anymore. They default to false. bug 497656 + if (name != "passwords" && name != "offlineApps") + cpdBranch.setBoolPref(name, itemBranch.getBoolPref(name)); + clearOnShutdownBranch.setBoolPref(name, itemBranch.getBoolPref(name)); + } catch(e) { + Cu.reportError("Exception thrown during privacy pref migration: " + e); + } + } + } + + gPrefService.setBoolPref("privacy.sanitize.migrateFx3Prefs", true); + } + }, +} + + +/* Legacy global init functions */ +var BrowserStartup = gBrowserInit.onLoad.bind(gBrowserInit); +var BrowserShutdown = gBrowserInit.onUnload.bind(gBrowserInit); + +function HandleAppCommandEvent(evt) { + switch (evt.command) { + case "Back": + BrowserBack(); + break; + case "Forward": + BrowserForward(); + break; + case "Reload": + BrowserReloadSkipCache(); + break; + case "Stop": + if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") { + BrowserStop(); + } + break; + case "Search": + BrowserSearch.webSearch(); + break; + case "Bookmarks": + toggleSidebar('viewBookmarksSidebar'); + break; + case "Home": + BrowserHome(); + break; + case "New": + BrowserOpenTab(); + break; + case "Close": + BrowserCloseTabOrWindow(); + break; + case "Find": + gFindBar.onFindCommand(); + break; + case "Help": + openHelpLink('pale-moon-help'); + break; + case "Open": + BrowserOpenFileWindow(); + break; + case "Print": + PrintUtils.print(); + break; + case "Save": + saveDocument(window.content.document); + break; + case "SendMail": + MailIntegration.sendLinkForWindow(window.content); + break; + default: + return; + } + evt.stopPropagation(); + evt.preventDefault(); +} + +function gotoHistoryIndex(aEvent) { + let index = aEvent.target.getAttribute("index"); + if (!index) { + return false; + } + + let where = whereToOpenLink(aEvent); + + if (where == "current") { + // Normal click. Go there in the current tab and update session history. + + try { + gBrowser.gotoIndex(index); + } catch(ex) { + return false; + } + return true; + } + // Modified click. Go there in a new tab/window. + + duplicateTabIn(gBrowser.selectedTab, where, index - gBrowser.sessionHistory.index); + return true; +} + +function BrowserForward(aEvent) { + let where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goForward(); + } catch(ex) {} + } else { + duplicateTabIn(gBrowser.selectedTab, where, 1); + } +} + +function BrowserBack(aEvent) { + let where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goBack(); + } catch(ex) {} + } else { + duplicateTabIn(gBrowser.selectedTab, where, -1); + } +} + +function BrowserHandleBackspace() +{ + switch (gPrefService.getIntPref("browser.backspace_action")) { + case 0: + BrowserBack(); + break; + case 1: + goDoCommand("cmd_scrollPageUp"); + break; + } +} + +function BrowserHandleShiftBackspace() +{ + switch (gPrefService.getIntPref("browser.backspace_action")) { + case 0: + BrowserForward(); + break; + case 1: + goDoCommand("cmd_scrollPageDown"); + break; + } +} + +function BrowserStop() { + const stopFlags = nsIWebNavigation.STOP_ALL; + gBrowser.webNavigation.stop(stopFlags); +} + +function BrowserReloadOrDuplicate(aEvent) { + var backgroundTabModifier = aEvent.button == 1 || + aEvent.ctrlKey; + if (aEvent.shiftKey && !backgroundTabModifier) { + BrowserReloadSkipCache(); + return; + } + + let where = whereToOpenLink(aEvent, false, true); + if (where == "current") { + BrowserReload(); + } else { + duplicateTabIn(gBrowser.selectedTab, where); + } +} + +function BrowserReload() { + const reloadFlags = nsIWebNavigation.LOAD_FLAGS_NONE; + BrowserReloadWithFlags(reloadFlags); +} + +function BrowserReloadSkipCache() { + // Bypass proxy and cache. + const reloadFlags = nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + BrowserReloadWithFlags(reloadFlags); +} + +var BrowserHome = BrowserGoHome; +function BrowserGoHome(aEvent) { + if (aEvent && "button" in aEvent && aEvent.button == 2) { + // right-click: do nothing + return; + } + + var homePage = gHomeButton.getHomePage(); + var where = whereToOpenLink(aEvent, false, true); + var urls; + + // Home page should open in a new tab when current tab is an app tab + if (where == "current" && gBrowser && gBrowser.selectedTab.pinned) { + where = "tab"; + } + + // openUILinkIn in utilityOverlay.js doesn't handle loading multiple pages + switch (where) { + case "current": + loadOneOrMoreURIs(homePage); + break; + case "tabshifted": + case "tab": + urls = homePage.split("|"); + var loadInBackground = Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground", false); + gBrowser.loadTabs(urls, loadInBackground); + break; + case "window": + OpenBrowserWindow(); + break; + } +} + +function loadOneOrMoreURIs(aURIString) { + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(aURIString.split("|"), false, true); + } catch(e) {} +} + +function focusAndSelectUrlBar() { + if (gURLBar) { + if (window.fullScreen) { + FullScreen.showNavToolbox(); + } + + gURLBar.select(); + if (document.activeElement == gURLBar.inputField) { + return true; + } + } + return false; +} + +function openLocation() { + if (focusAndSelectUrlBar()) { + return; + } + + openDialog("chrome://browser/content/openLocation.xul", "_blank", + "chrome,modal,titlebar", window); +} + +function openLocationCallback() { + // make sure the DOM is ready + setTimeout(function() { this.openLocation(); }, 0); +} + +function BrowserOpenTab() { + openUILinkIn(BROWSER_NEW_TAB_URL, "tab"); +} + +/* Called from the openLocation dialog. This allows that dialog to instruct + its opener to open a new window and then step completely out of the way. + Anything less byzantine is causing horrible crashes, rather believably, + though oddly only on Linux. */ +function delayedOpenWindow(chrome, flags, href, postData) { + // The other way to use setTimeout, + // setTimeout(openDialog, 10, chrome, "_blank", flags, url), + // doesn't work here. The extra "magic" extra argument setTimeout adds to + // the callback function would confuse gBrowserInit.onLoad() by making + // window.arguments[1] be an integer instead of null. + setTimeout(function() { openDialog(chrome, "_blank", flags, href, null, null, postData); }, 10); +} + +/* Required because the tab needs time to set up its content viewers and get the load of + the URI kicked off before becoming the active content area. */ +function delayedOpenTab(aUrl, aReferrer, aCharset, aPostData, aAllowThirdPartyFixup) { + gBrowser.loadOneTab(aUrl, { referrerURI: aReferrer, + charset: aCharset, + postData: aPostData, + inBackground: false, + allowThirdPartyFixup: aAllowThirdPartyFixup }); +} + +var gLastOpenDirectory = { + _lastDir: null, + get path() { + if (!this._lastDir || !this._lastDir.exists()) { + try { + this._lastDir = gPrefService.getComplexValue("browser.open.lastDir", + Ci.nsILocalFile); + if (!this._lastDir.exists()) + this._lastDir = null; + } catch(e) {} + } + return this._lastDir; + }, + set path(val) { + try { + if (!val || !val.isDirectory()) { + return; + } + } catch(e) { + return; + } + + this._lastDir = val.clone(); + + // Don't save the last open directory pref inside the Private Browsing mode + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + gPrefService.setComplexValue("browser.open.lastDir", Ci.nsILocalFile, this._lastDir); + } + }, + reset: function() { + this._lastDir = null; + } +}; + +function BrowserOpenFileWindow() { + // Get filepicker component. + try { + const nsIFilePicker = Ci.nsIFilePicker; + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK) { + try { + if (fp.file) { + gLastOpenDirectory.path = + fp.file.parent.QueryInterface(Ci.nsILocalFile); + } + } catch(ex) {} + openUILinkIn(fp.fileURL.spec, "current"); + } + }; + + fp.init(window, gNavigatorBundle.getString("openFile"), + nsIFilePicker.modeOpen); + fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText | + nsIFilePicker.filterImages | nsIFilePicker.filterXML | + nsIFilePicker.filterHTML); + fp.displayDirectory = gLastOpenDirectory.path; + fp.open(fpCallback); + } catch(ex) {} +} + +function BrowserCloseTabOrWindow() { + // If the current tab is the last one, this will close the window. + gBrowser.removeCurrentTab({animate: true}); +} + +function BrowserTryToCloseWindow() +{ + if (WindowIsClosing()) { + // WindowIsClosing does all the necessary checks + window.close(); + } +} + +function loadURI(uri, referrer, postData, allowThirdPartyFixup, referrerPolicy, + originPrincipal, forceAboutBlankViewerInCurrent, + triggeringPrincipal) { + if (postData === undefined) { + postData = null; + } + + var flags = nsIWebNavigation.LOAD_FLAGS_NONE; + if (allowThirdPartyFixup) { + flags |= nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + flags |= nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + + try { + gBrowser.loadURIWithFlags(uri, { flags: flags, + referrerURI: referrer, + referrerPolicy: referrerPolicy, + postData: postData, + originPrincipal: originPrincipal, + triggeringPrincipal: triggeringPrincipal, + forceAboutBlankViewerInCurrent: forceAboutBlankViewerInCurrent }); + } catch(e) {} +} + +/** + * Given a urlbar value, discerns between URIs, keywords and aliases. + * + * @param url + * The urlbar value. + * @param callback (optional, deprecated) + * The callback function invoked when done. This parameter is + * deprecated, please use the Promise that is returned. + * + * @return Promise<{ postData, url, mayInheritPrincipal }> + */ +function getShortcutOrURIAndPostData(url, callback = null) { + if (callback) { + Deprecated.warning("Please use the Promise returned by " + + "getShortcutOrURIAndPostData() instead of passing a " + + "callback", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1100294"); + } + + return Task.spawn(function* () { + let mayInheritPrincipal = false; + let postData = null; + let shortcutURL = null; + let keyword = url; + let param = ""; + + let offset = url.indexOf(" "); + if (offset > 0) { + keyword = url.substr(0, offset); + param = url.substr(offset + 1); + } + + let engine = Services.search.getEngineByAlias(keyword); + if (engine) { + let submission = engine.getSubmission(param, null, "keyword"); + postData = submission.postData; + return { postData: submission.postData, url: submission.uri.spec, + mayInheritPrincipal }; + } + + let entry = yield PlacesUtils.keywords.fetch(keyword); + if (entry) { + shortcutURL = entry.url.href; + postData = entry.postData; + } + + if (!shortcutURL) { + return { postData, url, mayInheritPrincipal }; + } + + let escapedPostData = ""; + if (postData) { + escapedPostData = unescape(postData); + } + + if (/%s/i.test(shortcutURL) || /%s/i.test(escapedPostData)) { + let charset = ""; + const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/; + let matches = shortcutURL.match(re); + + if (matches) { + [, shortcutURL, charset] = matches; + } else { + let uri; + try { + // makeURI() throws if URI is invalid. + uri = makeURI(shortcutURL); + } catch(ex) {} + + if (uri) { + // Try to get the saved character-set. + // Will return an empty string if character-set is not found. + charset = yield PlacesUtils.getCharsetForURI(uri); + } + } + + // encodeURIComponent produces UTF-8, and cannot be used for other charsets. + // escape() works in those cases, but it doesn't uri-encode +, @, and /. + // Therefore we need to manually replace these ASCII characters by their + // encodeURIComponent result, to match the behavior of nsEscape() with + // url_XPAlphas + let encodedParam = ""; + if (charset && charset != "UTF-8") { + encodedParam = escape(convertFromUnicode(charset, param)). + replace(/[+@\/]+/g, encodeURIComponent); + } else { + // Default charset is UTF-8 + encodedParam = encodeURIComponent(param); + } + + shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param); + + if (/%s/i.test(escapedPostData)) { + // POST keyword + postData = getPostDataStream(escapedPostData, param, encodedParam, + "application/x-www-form-urlencoded"); + } + + // This URL came from a bookmark, so it's safe to let it inherit the current + // document's principal. + mayInheritPrincipal = true; + + return { postData, url: shortcutURL, mayInheritPrincipal }; + } + + if (param) { + // This keyword doesn't take a parameter, but one was provided. Just return + // the original URL. + postData = null; + + return { postData, url, mayInheritPrincipal }; + } + + // This URL came from a bookmark, so it's safe to let it inherit the current + // document's principal. + mayInheritPrincipal = true; + + return { postData, url: shortcutURL, mayInheritPrincipal }; + }).then(data => { + if (callback) { + callback(data); + } + + return data; + }); +} + +function getPostDataStream(aStringData, aKeyword, aEncKeyword, aType) { + var dataStream = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + aStringData = aStringData.replace(/%s/g, aEncKeyword).replace(/%S/g, aKeyword); + dataStream.data = aStringData; + + var mimeStream = Cc["@mozilla.org/network/mime-input-stream;1"]. + createInstance(Ci.nsIMIMEInputStream); + mimeStream.addHeader("Content-Type", aType); + mimeStream.addContentLength = true; + mimeStream.setData(dataStream); + return mimeStream.QueryInterface(Ci.nsIInputStream); +} + +function getLoadContext() { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); +} + +function readFromClipboard() +{ + var url; + + try { + // Create transferable that will transfer the text. + var trans = Components.classes["@mozilla.org/widget/transferable;1"] + .createInstance(Components.interfaces.nsITransferable); + trans.init(getLoadContext()); + + trans.addDataFlavor("text/unicode"); + + // If available, use selection clipboard, otherwise global one + if (Services.clipboard.supportsSelectionClipboard()) { + Services.clipboard.getData(trans, Services.clipboard.kSelectionClipboard); + } else { + Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); + } + + var data = {}; + var dataLen = {}; + trans.getTransferData("text/unicode", data, dataLen); + + if (data) { + data = data.value.QueryInterface(Components.interfaces.nsISupportsString); + url = data.data.substring(0, dataLen.value / 2); + } + } catch(ex) {} + + return url; +} + +function BrowserViewSourceOfDocument(aArgsOrDocument) +{ + let args; + + if (aArgsOrDocument instanceof Document) { + let doc = aArgsOrDocument; + + let requestor = doc.defaultView + .QueryInterface(Ci.nsIInterfaceRequestor); + let browser = requestor.getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + let outerWindowID = requestor.getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + let URL = browser.currentURI.spec; + args = { browser, outerWindowID, URL }; + } else { + args = aArgsOrDocument; + } + + let viewInternal = () => { + top.gViewSourceUtils.viewSource(args); + } + + // Check if external view source is enabled. If so, try it. If it fails, + // fallback to internal view source. + if (Services.prefs.getBoolPref("view_source.editor.external")) { + top.gViewSourceUtils + .openInExternalEditor(args, null, null, null, result => { + if (!result) { + viewInternal(); + } + }); + } else { + // Display using internal view source + viewInternal(); + } +} + +// doc - document to use for source, or null for this window's document +// initialTab - name of the initial tab to display, or null for the first tab +// imageElement - image to load in the Media Tab of the Page Info window; can be null/omitted +function BrowserPageInfo(doc, initialTab, imageElement) { + var args = {doc: doc, initialTab: initialTab, imageElement: imageElement}; + var windows = Services.wm.getEnumerator("Browser:page-info"); + + var documentURL = doc ? doc.location : window.content.document.location; + + // Check for windows matching the url + while (windows.hasMoreElements()) { + var currentWindow = windows.getNext(); + if (currentWindow.document.documentElement.getAttribute("relatedUrl") == documentURL) { + currentWindow.focus(); + currentWindow.resetPageInfo(args); + return currentWindow; + } + } + + // We didn't find a matching window, so open a new one. + return openDialog("chrome://browser/content/pageinfo/pageInfo.xul", "", + "chrome,toolbar,dialog=no,resizable", args); +} + +function URLBarSetURI(aURI) { + var value = gBrowser.userTypedValue; + var valid = false; + + if (value == null) { + let uri = aURI || gBrowser.currentURI; + // Strip off "wyciwyg://" and passwords for the location bar + try { + uri = Services.uriFixup.createExposableURI(uri); + } catch(e) {} + + // Replace initial page URIs with an empty string + // only if there's no opener (bug 370555). + if (gInitialPages.indexOf(uri.spec) != -1) { + value = content.opener ? uri.spec : ""; + } else { + value = losslessDecodeURI(uri); + } + + valid = !isBlankPageURL(uri.spec); + } + + let isDifferentValidValue = valid && value != gURLBar.value; + gURLBar.value = value; + gURLBar.valueIsTyped = !valid; + if (isDifferentValidValue) { + gURLBar.selectionStart = gURLBar.selectionEnd = 0; + } + + SetPageProxyState(valid ? "valid" : "invalid"); +} + +function losslessDecodeURI(aURI) { + let scheme = aURI.scheme; + let decodeASCIIOnly = !(/(https|http|file|ftp)/i.test(scheme)); + + var value = aURI.spec; + + // Try to decode as UTF-8 if there's no encoding sequence that we would break. + if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) { + if (decodeASCIIOnly) { + // This only decodes ASCII characters (hex) 20-7e, except 25 (%). + // This avoids both cases stipulated below (%-related issues, and \r, \n + // and \t, which would be %0d, %0a and %09, respectively) as well as any + // non-US-ascii characters. + value = value.replace(/%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, decodeURI); + } else { + try { + value = decodeURI(value) + // 1. decodeURI decodes %25 to %, which creates unintended + // encoding sequences. Re-encode it, unless it's part of + // a sequence that survived decodeURI, i.e. one for: + // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#' + // (RFC 3987 section 3.2) + // 2. Re-encode whitespace so that it doesn't get eaten away + // by the location bar (bug 410726). + .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|[\r\n\t]/ig, + encodeURIComponent); + } catch(e) {} + } + } + + // Encode invisible characters (C0/C1 control characters, U+007F [DEL], + // U+00A0 [no-break space], line and paragraph separator, braille space, + // object replacement character) (bug 452979, bug 909264) + value = value.replace(/[\u0000-\u001f\u007f-\u00a0\u2028\u2029\u2800\ufffc]/g, + encodeURIComponent); + + // Encode default ignorable characters (bug 546013) + // except ZWNJ (U+200C) and ZWJ (U+200D) (bug 582186). + // This includes all bidirectional formatting characters. + // (RFC 3987 sections 3.2 and 4.1 paragraph 6) + value = value.replace(/[\u00ad\u034f\u115f-\u1160\u17b4-\u17b5\u180b-\u180d\u200b\u200e-\u200f\u202a-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufff8]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]/g, + encodeURIComponent); + return value; +} + +function UpdateUrlbarSearchSplitterState() { + var splitter = document.getElementById("urlbar-search-splitter"); + var urlbar = document.getElementById("urlbar-container"); + var searchbar = document.getElementById("search-container"); + var stop = document.getElementById("stop-button"); + + var ibefore = null; + if (urlbar && searchbar) { + if (urlbar.nextSibling == searchbar || + urlbar.getAttribute("combined") && + stop && stop.nextSibling == searchbar) { + ibefore = searchbar; + } else if (searchbar.nextSibling == urlbar) { + ibefore = urlbar; + } + } + + if (ibefore) { + if (!splitter) { + splitter = document.createElement("splitter"); + splitter.id = "urlbar-search-splitter"; + splitter.setAttribute("resizebefore", "flex"); + splitter.setAttribute("resizeafter", "flex"); + splitter.setAttribute("skipintoolbarset", "true"); + splitter.className = "chromeclass-toolbar-additional"; + } + urlbar.parentNode.insertBefore(splitter, ibefore); + } else if (splitter) { + splitter.parentNode.removeChild(splitter); + } +} + +function setUrlAndSearchBarWidthForConditionalForwardButton() { + // Workaround for bug 694084: Showing/hiding the conditional forward button resizes + // the search bar when the url/search bar splitter hasn't been used. + var urlbarContainer = document.getElementById("urlbar-container"); + var searchbarContainer = document.getElementById("search-container"); + if (!urlbarContainer || + !searchbarContainer || + urlbarContainer.hasAttribute("width") || + searchbarContainer.hasAttribute("width") || + urlbarContainer.parentNode != searchbarContainer.parentNode) { + return; + } + urlbarContainer.style.width = searchbarContainer.style.width = ""; + var urlbarWidth = urlbarContainer.clientWidth; + var searchbarWidth = searchbarContainer.clientWidth; + urlbarContainer.style.width = urlbarWidth + "px"; + searchbarContainer.style.width = searchbarWidth + "px"; +} + +function UpdatePageProxyState() +{ + if (gURLBar && gURLBar.value != gLastValidURLStr) { + SetPageProxyState("invalid"); + } +} + +function SetPageProxyState(aState) +{ + BookmarkingUI.onPageProxyStateChanged(aState); + + if (!gURLBar) { + return; + } + + if (!gProxyFavIcon) { + gProxyFavIcon = document.getElementById("page-proxy-favicon"); + } + + gURLBar.setAttribute("pageproxystate", aState); + gProxyFavIcon.setAttribute("pageproxystate", aState); + + // the page proxy state is set to valid via OnLocationChange, which + // gets called when we switch tabs. + if (aState == "valid") { + gLastValidURLStr = gURLBar.value; + gURLBar.addEventListener("input", UpdatePageProxyState, false); + PageProxySetIcon(gBrowser.getIcon()); + } else if (aState == "invalid") { + gURLBar.removeEventListener("input", UpdatePageProxyState, false); + PageProxyClearIcon(); + } +} + +function PageProxySetIcon (aURL) { + if (!gProxyFavIcon) { + return; + } + + if (gBrowser.selectedBrowser.contentDocument instanceof ImageDocument) { + // PageProxyClearIcon(); + gProxyFavIcon.setAttribute("src", "chrome://browser/skin/imagedocument.png"); + return; + } + + if (!aURL) { + PageProxyClearIcon(); + } else if (gProxyFavIcon.getAttribute("src") != aURL) { + gProxyFavIcon.setAttribute("src", aURL); + } +} + +function PageProxyClearIcon () { + gProxyFavIcon.removeAttribute("src"); +} + +function PageProxyClickHandler(aEvent) { + if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste")) + middleMousePaste(aEvent); +} + +/** + * Handle load of some pages (about:*) so that we can make modifications + * to the DOM for unprivileged pages. + */ +function BrowserOnAboutPageLoad(doc) { + + /* === about:home === */ + + if (doc.documentURI.toLowerCase() == "about:home") { + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + let wrapper = {}; + Cu.import("resource:///modules/sessionstore/SessionStore.jsm", wrapper); + let ss = wrapper.SessionStore; + ss.promiseInitialized.then(function() { + if (ss.canRestoreLastSession) { + doc.getElementById("launcher").setAttribute("session", "true"); + } + }).then(null, function onError(x) { + Cu.reportError("Error in SessionStore init while processing 'about:home': " + x); + }); + } + + // Inject search engine and snippets URL. + let docElt = doc.documentElement; + if (AboutHomeUtils.showKnowYourRights) { + docElt.setAttribute("showKnowYourRights", "true"); + // Set pref to indicate we've shown the notification. + let currentVersion = Services.prefs.getIntPref("browser.rights.version"); + Services.prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true); + } + + function updateSearchEngine() { + let engine = AboutHomeUtils.defaultSearchEngine; + docElt.setAttribute("searchEngineName", engine.name); + docElt.setAttribute("searchEnginePostData", engine.postDataString || ""); + docElt.setAttribute("searchEngineURL", engine.searchURL); + } + Services.search.init(updateSearchEngine); + + // Listen for the event that's triggered when the user changes search engine. + // At this point we simply reload about:home to reflect the change. + Services.obs.addObserver(updateSearchEngine, "browser-search-engine-modified", false); + + // Remove the observer when the page is reloaded or closed. + doc.defaultView.addEventListener("pagehide", function removeObserver() { + doc.defaultView.removeEventListener("pagehide", removeObserver); + Services.obs.removeObserver(updateSearchEngine, "browser-search-engine-modified"); + }, false); + } + + /* === about:newtab === */ + + if (doc.documentURI.toLowerCase() == "about:newtab") { + + let docElt = doc.documentElement; + + function updateSearchEngine() { + let engine = AboutHomeUtils.defaultSearchEngine; + docElt.setAttribute("searchEngineName", engine.name); + docElt.setAttribute("searchEnginePostData", engine.postDataString || ""); + docElt.setAttribute("searchEngineURL", engine.searchURL); + } + Services.search.init(updateSearchEngine); + + // Listen for the event that's triggered when the user changes search engine. + // At this point we simply reload about:newtab to reflect the change. + Services.obs.addObserver(updateSearchEngine, "browser-search-engine-modified", false); + + // Remove the observer when the page is reloaded or closed. + doc.defaultView.addEventListener("pagehide", function removeObserver() { + doc.defaultView.removeEventListener("pagehide", removeObserver); + Services.obs.removeObserver(updateSearchEngine, "browser-search-engine-modified"); + }, false); + } + +} + +/** + * Handle command events bubbling up from error page content + */ +var BrowserOnClick = { + handleEvent: function(aEvent) { + if (!aEvent.isTrusted || // Don't trust synthetic events + aEvent.button == 2 || aEvent.target.localName != "button") { + return; + } + + let originalTarget = aEvent.originalTarget; + let ownerDoc = originalTarget.ownerDocument; + + // If the event came from an ssl error page, it is probably either the "Add + // Exception…" or "Get me out of here!" button + if (ownerDoc.documentURI.startsWith("about:certerror")) { + this.onAboutCertError(originalTarget, ownerDoc); + } else if (ownerDoc.documentURI.startsWith("about:neterror")) { + this.onAboutNetError(originalTarget, ownerDoc); + } else if (ownerDoc.documentURI.toLowerCase() == "about:home") { + this.onAboutHome(originalTarget, ownerDoc); + } + }, + + onAboutCertError: function(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + let isTopFrame = (aOwnerDoc.defaultView.parent === aOwnerDoc.defaultView); + + switch (elmId) { + case "exceptionDialogButton": + let params = { exceptionAdded : false }; + + try { + switch (Services.prefs.getIntPref("browser.ssl_override_behavior")) { + case 2 : // Pre-fetch & pre-populate + params.prefetchCert = true; + case 1 : // Pre-populate + params.location = aOwnerDoc.location.href; + } + } catch(e) { + Components.utils.reportError("Couldn't get ssl_override pref: " + e); + } + + window.openDialog('chrome://pippki/content/exceptionDialog.xul', + '','chrome,centerscreen,modal', params); + + // If the user added the exception cert, attempt to reload the page + if (params.exceptionAdded) { + aOwnerDoc.location.reload(); + } + break; + + case "getMeOutOfHereButton": + getMeOutOfHere(); + break; + + case "technicalContent": + break; + + case "expertContent": + break; + + } + }, + + onAboutNetError: function(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + if (elmId != "errorTryAgain" || !/e=netOffline/.test(aOwnerDoc.documentURI)) { + return; + } + Services.io.offline = false; + }, + + onAboutHome: function(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + + switch (elmId) { + case "restorePreviousSession": + let ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore); + if (ss.canRestoreLastSession) { + ss.restoreLastSession(); + } + aOwnerDoc.getElementById("launcher").removeAttribute("session"); + break; + + case "downloads": + BrowserDownloadsUI(); + break; + + case "bookmarks": + PlacesCommandHook.showPlacesOrganizer("AllBookmarks"); + break; + + case "history": + PlacesCommandHook.showPlacesOrganizer("History"); + break; + + case "addons": + BrowserOpenAddonsMgr(); + break; + + case "sync": + openPreferences("paneSync"); + break; + + case "settings": + openPreferences(); + break; + } + }, +}; + +/** + * Re-direct the browser to a known-safe page. This function is + * used when, for example, the user browses to a known malware page + * and is presented with about:blocked. The "Get me out of here!" + * button should take the user to the default start page so that even + * when their own homepage is infected, we can get them somewhere safe. + */ +function getMeOutOfHere() { + try { + let toBlank = Services.prefs.getBoolPref("browser.escape_to_blank"); + if (toBlank) { + content.location = "about:logopage"; + return; + } + } catch(e) { + Components.utils.reportError("Couldn't get escape pref: " + e); + } + // Get the start page from the *default* pref branch, not the user's + var prefs = Services.prefs.getDefaultBranch(null); + var url = BROWSER_NEW_TAB_URL; + try { + url = prefs.getComplexValue("browser.startup.homepage", + Ci.nsIPrefLocalizedString).data; + // If url is a pipe-delimited set of pages, just take the first one. + if (url.includes("|")) + url = url.split("|")[0]; + } catch(e) { + Components.utils.reportError("Couldn't get homepage pref: " + e); + } + content.location = url; +} + +function BrowserFullScreen() { + window.fullScreen = !window.fullScreen; +} + +function onFullScreen() { + FullScreen.toggle(); +} + +function onMozEnteredDomFullscreen(event) { + FullScreen.enterDomFullscreen(event); +} + +function getWebNavigation() { + return gBrowser.webNavigation; +} + +function BrowserReloadWithFlags(reloadFlags) { + + // Reset DOS mitigation for auth prompts when user initiates a reload. + let browser = gBrowser.selectedBrowser; + delete browser.authPromptCounter; + + // First, we'll try to use the session history object to reload so + // that framesets are handled properly. If we're in a special + // window (such as view-source) that has no session history, fall + // back on using the web navigation's reload method. + + var webNav = gBrowser.webNavigation; + try { + var sh = webNav.sessionHistory; + if (sh) + webNav = sh.QueryInterface(nsIWebNavigation); + } catch(e) {} + + try { + webNav.reload(reloadFlags); + } catch(e) {} +} + +var PrintPreviewListener = { + _printPreviewTab: null, + _tabBeforePrintPreview: null, + + getPrintPreviewBrowser: function() { + if (!this._printPreviewTab) { + this._tabBeforePrintPreview = gBrowser.selectedTab; + this._printPreviewTab = gBrowser.loadOneTab("about:blank", + { inBackground: false }); + gBrowser.selectedTab = this._printPreviewTab; + } + return gBrowser.getBrowserForTab(this._printPreviewTab); + }, + getSourceBrowser: function() { + return this._tabBeforePrintPreview ? + this._tabBeforePrintPreview.linkedBrowser : gBrowser.selectedBrowser; + }, + getNavToolbox: function() { + return gNavToolbox; + }, + onEnter: function() { + // We might have accidentally switched tabs since the user invoked print + // preview + if (gBrowser.selectedTab != this._printPreviewTab) { + gBrowser.selectedTab = this._printPreviewTab; + } + gInPrintPreviewMode = true; + this._toggleAffectedChrome(); + }, + onExit: function() { + gBrowser.selectedTab = this._tabBeforePrintPreview; + this._tabBeforePrintPreview = null; + gInPrintPreviewMode = false; + this._toggleAffectedChrome(); + gBrowser.removeTab(this._printPreviewTab); + this._printPreviewTab = null; + }, + _toggleAffectedChrome: function() { + gNavToolbox.collapsed = gInPrintPreviewMode; + + if (gInPrintPreviewMode) { + this._hideChrome(); + } else { + this._showChrome(); + } + + if (this._chromeState.sidebarOpen) { + toggleSidebar(this._sidebarCommand); + } + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif + }, + + _hideChrome: function() { + this._chromeState = {}; + + var sidebar = document.getElementById("sidebar-box"); + this._chromeState.sidebarOpen = !sidebar.hidden; + this._sidebarCommand = sidebar.getAttribute("sidebarcommand"); + + var notificationBox = gBrowser.getNotificationBox(); + this._chromeState.notificationsOpen = !notificationBox.notificationsHidden; + notificationBox.notificationsHidden = true; + + document.getElementById("sidebar").setAttribute("src", "about:blank"); + var addonBar = document.getElementById("addon-bar"); + this._chromeState.addonBarOpen = !addonBar.collapsed; + addonBar.collapsed = true; + gBrowser.updateWindowResizers(); + + this._chromeState.findOpen = gFindBarInitialized && !gFindBar.hidden; + if (gFindBarInitialized) { + gFindBar.close(); + } + + var globalNotificationBox = document.getElementById("global-notificationbox"); + this._chromeState.globalNotificationsOpen = !globalNotificationBox.notificationsHidden; + globalNotificationBox.notificationsHidden = true; + +#ifdef MOZ_SERVICES_SYNC + this._chromeState.syncNotificationsOpen = false; + var syncNotifications = document.getElementById("sync-notifications"); + if (syncNotifications) { + this._chromeState.syncNotificationsOpen = !syncNotifications.notificationsHidden; + syncNotifications.notificationsHidden = true; + } +#endif + }, + + _showChrome: function() { + if (this._chromeState.notificationsOpen) { + gBrowser.getNotificationBox().notificationsHidden = false; + } + + if (this._chromeState.addonBarOpen) { + document.getElementById("addon-bar").collapsed = false; + gBrowser.updateWindowResizers(); + } + + if (this._chromeState.findOpen) { + gFindBar.open(); + } + + if (this._chromeState.globalNotificationsOpen) { + document.getElementById("global-notificationbox").notificationsHidden = false; + } + +#ifdef MOZ_SERVICES_SYNC + if (this._chromeState.syncNotificationsOpen) { + document.getElementById("sync-notifications").notificationsHidden = false; + } +#endif + } +} + +function getMarkupDocumentViewer() { + return gBrowser.markupDocumentViewer; +} + +// This function is obsolete. Newer code should use <tooltip page="true"/> instead. +function FillInHTMLTooltip(tipElement) { + document.getElementById("aHTMLTooltip").fillInPageTooltip(tipElement); +} + +var browserDragAndDrop = { + canDropLink: function(aEvent) { + return Services.droppedLinkHandler.canDropLink(aEvent, true) + }, + + dragOver: function(aEvent) { + if (this.canDropLink(aEvent)) { + aEvent.preventDefault(); + } + }, + + dropLinks: function(aEvent, aDisallowInherit) { + return Services.droppedLinkHandler.dropLinks(aEvent, aDisallowInherit); + } +}; + +var homeButtonObserver = { + onDrop: function(aEvent) { + // disallow setting home pages that inherit the principal + let links = browserDragAndDrop.dropLinks(aEvent, true); + if (links.length) { + setTimeout(openHomeDialog, 0, links.map(link => link.url).join("|")); + } + }, + + onDragOver: function(aEvent) { + browserDragAndDrop.dragOver(aEvent); + aEvent.dropEffect = "link"; + }, + + onDragExit: function(aEvent) { + } +} + +function openHomeDialog(aURL) { + var promptTitle = gNavigatorBundle.getString("droponhometitle"); + var promptMsg; + if (aURL.includes("|")) { + promptMsg = gNavigatorBundle.getString("droponhomemsgMultiple"); + } else { + promptMsg = gNavigatorBundle.getString("droponhomemsg"); + } + + var pressedVal = Services.prompt.confirmEx(window, promptTitle, promptMsg, Services.prompt.STD_YES_NO_BUTTONS, + null, null, null, null, {value: 0}); + + if (pressedVal == 0) { + try { + var homepageStr = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); + homepageStr.data = aURL; + gPrefService.setComplexValue("browser.startup.homepage", + Components.interfaces.nsISupportsString, homepageStr); + } catch(ex) { + dump("Failed to set the home page.\n"+ex+"\n"); + } + } +} + +var bookmarksButtonObserver = { + onDrop: function(aEvent) { + let name = {}; + let url = browserDragAndDrop.drop(aEvent, name); + try { + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "bookmark", + uri: makeURI(url), + title: name, + hiddenRows: [ "description", + "location", + "loadInSidebar", + "keyword" ] + }, window); + } catch(ex) {} + }, + + onDragOver: function(aEvent) { + browserDragAndDrop.dragOver(aEvent); + aEvent.dropEffect = "link"; + }, + + onDragExit: function(aEvent) { + } +} + +var newTabButtonObserver = { + onDragOver: function(aEvent) { + browserDragAndDrop.dragOver(aEvent); + }, + + onDragExit: function(aEvent) {}, + + onDrop: function(aEvent) { + let links = browserDragAndDrop.dropLinks(aEvent); + Task.spawn(function*() { + for (let link of links) { + let data = yield getShortcutOrURIAndPostData(link.url); + if (data.url) { + // allow third-party services to fixup this URL + openNewTabWith(data.url, null, data.postData, aEvent, true); + } + } + }); + } +} + +var newWindowButtonObserver = { + onDragOver: function(aEvent) { + browserDragAndDrop.dragOver(aEvent); + }, + + onDragExit: function(aEvent) {}, + + onDrop: function(aEvent) { + let links = browserDragAndDrop.dropLinks(aEvent); + Task.spawn(function*() { + for (let link of links) { + let data = yield getShortcutOrURIAndPostData(link.url); + if (data.url) { + // allow third-party services to fixup this URL + openNewWindowWith(data.url, null, data.postData, true); + } + } + }); + } +} + +const DOMLinkHandler = { + handleEvent: function(event) { + switch (event.type) { + case "DOMLinkAdded": + this.onLinkAdded(event); + break; + } + }, + + getLinkIconURI: function(aLink) { + let targetDoc = aLink.ownerDocument; + var uri = makeURI(aLink.href, targetDoc.characterSet); + + // Verify that the load of this icon is legal. + // Some error or special pages can load their favicon. + // To be on the safe side, only allow chrome:// favicons. + var isAllowedPage = [ + /^about:neterror\?/, + /^about:blocked\?/, + /^about:certerror\?/, + /^about:home$/, + ].some(function(re) re.test(targetDoc.documentURI)); + + if (!isAllowedPage || !uri.schemeIs("chrome")) { + var ssm = Services.scriptSecurityManager; + try { + ssm.checkLoadURIWithPrincipal(targetDoc.nodePrincipal, uri, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + } catch(e) { + return null; + } + } + + try { + var contentPolicy = Cc["@mozilla.org/layout/content-policy;1"]. + getService(Ci.nsIContentPolicy); + } catch(e) { + // Refuse to load if we can't do a security check. + return null; + } + + // Security says okay, now ask content policy + if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_IMAGE, + uri, targetDoc.documentURIObject, + aLink, aLink.type, null) + != Ci.nsIContentPolicy.ACCEPT) { + return null; + } + + try { + uri.userPass = ""; + } catch(e) { + // some URIs are immutable + } + + return uri; + }, + + onLinkAdded: function(event) { + var link = event.originalTarget; + var rel = link.rel && link.rel.toLowerCase(); + if (!link || !link.ownerDocument || !rel || !link.href) { + return; + } + + var feedAdded = false; + var iconAdded = false; + var searchAdded = false; + var rels = {}; + for (let relString of rel.split(/\s+/)) { + rels[relString] = true; + } + + for (let relVal in rels) { + switch (relVal) { + case "feed": + case "alternate": + if (!feedAdded) { + if (!rels.feed && rels.alternate && rels.stylesheet) { + break; + } + + if (isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) { + FeedHandler.addFeed(link, link.ownerDocument); + feedAdded = true; + } + } + break; + case "icon": + if (!iconAdded) { + if (!gPrefService.getBoolPref("browser.chrome.site_icons")) { + break; + } + + var uri = this.getLinkIconURI(link); + if (!uri) { + break; + } + + if (gBrowser.isFailedIcon(uri)) { + break; + } + + var browserIndex = gBrowser.getBrowserIndexForDocument(link.ownerDocument); + if (browserIndex == -1) { + // no browser? no favicon. + break; + } + + let tab = gBrowser.tabs[browserIndex]; + gBrowser.setIcon(tab, uri.spec, link.ownerDocument.nodePrincipal); + iconAdded = true; + } + break; + case "search": + if (!searchAdded) { + var type = link.type && link.type.toLowerCase(); + type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); + + if (type == "application/opensearchdescription+xml" && + link.title && + /^(?:https?|ftp):/i.test(link.href) && + !PrivateBrowsingUtils.isWindowPrivate(window)) { + var engine = { title: link.title, href: link.href }; + Services.search.init(function() { + BrowserSearch.addEngine(engine, link.ownerDocument); + }); + searchAdded = true; + } + } + break; + } + } + } +} + +const BrowserSearch = { + addEngine: function(engine, targetDoc) { + if (!this.searchBar) { + return; + } + + var browser = gBrowser.getBrowserForDocument(targetDoc); + + // ignore search engines from subframes (see bug 479408) + if (!browser) { + return; + } + + // Check to see whether we've already added an engine with this title + if (browser.engines) { + if (browser.engines.some(function(e) e.title == engine.title)) { + return; + } + } + + // Append the URI and an appropriate title to the browser data. + // Use documentURIObject in the check for shouldLoadFavIcon so that we + // do the right thing with about:-style error pages. Bug 453442 + var iconURL = null; + if (gBrowser.shouldLoadFavIcon(targetDoc.documentURIObject)) { + iconURL = targetDoc.documentURIObject.prePath + "/favicon.ico"; + } + + var hidden = false; + // If this engine (identified by title) is already in the list, add it + // to the list of hidden engines rather than to the main list. + // XXX This will need to be changed when engines are identified by URL; + // see bug 335102. + if (Services.search.getEngineByName(engine.title)) { + hidden = true; + } + + var engines = (hidden ? browser.hiddenEngines : browser.engines) || []; + + engines.push({ uri: engine.href, + title: engine.title, + icon: iconURL }); + + if (hidden) { + browser.hiddenEngines = engines; + } else { + browser.engines = engines; + } + }, + + /** + * Gives focus to the search bar, if it is present on the toolbar, or loads + * the default engine's search form otherwise. For Mac, opens a new window + * or focuses an existing window, if necessary. + */ + webSearch: function() { + var searchBar = this.searchBar; + if (searchBar && window.fullScreen) { + FullScreen.showNavToolbox(); + } + if (searchBar) { + searchBar.select(); + } + if (!searchBar || document.activeElement != searchBar.textbox.inputField) { + openUILinkIn(Services.search.defaultEngine.searchForm, "current"); + } + }, + + /** + * Loads a search results page, given a set of search terms. Uses the current + * engine if the search bar is visible, or the default engine otherwise. + * + * @param searchText + * The search terms to use for the search. + * + * @param useNewTab + * Boolean indicating whether or not the search should load in a new + * tab. + * + * @param purpose [optional] + * A string meant to indicate the context of the search request. This + * allows the search service to provide a different nsISearchSubmission + * depending on e.g. where the search is triggered in the UI. + * + * @return string Name of the search engine used to perform a search or null + * if a search was not performed. + */ + loadSearch: function(searchText, useNewTab, purpose) { + var engine; + + // If the search bar is visible, use the current engine, otherwise, fall + // back to the default engine. + if (isElementVisible(this.searchBar)) { + engine = Services.search.currentEngine; + } else { + engine = Services.search.defaultEngine; + } + + var submission = engine.getSubmission(searchText, null, purpose); // HTML response + + // getSubmission can return null if the engine doesn't have a URL + // with a text/html response type. This is unlikely (since + // SearchService._addEngineToStore() should fail for such an engine), + // but let's be on the safe side. + if (!submission) { + return null; + } + + let inBackground = Services.prefs.getBoolPref("browser.search.context.loadInBackground"); + openLinkIn(submission.uri.spec, + useNewTab ? "tab" : "current", + { postData: submission.postData, + inBackground: inBackground, + relatedToCurrent: true }); + + return engine.name; + }, + + /** + * Perform a search initiated from the context menu. + * + * This should only be called from the context menu. See + * BrowserSearch.loadSearch for the preferred API. + */ + loadSearchFromContext: function(terms) { + let engine = BrowserSearch.loadSearch(terms, true, "contextmenu"); + }, + + /** + * Returns the search bar element if it is present in the toolbar, null otherwise. + */ + get searchBar() { + return document.getElementById("searchbar"); + }, + + loadAddEngines: function() { + var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow"); + var where = newWindowPref == 3 ? "tab" : "window"; + var searchEnginesURL = formatURL("browser.search.searchEnginesURL", true); + openUILinkIn(searchEnginesURL, where); + }, +}; + +XPCOMUtils.defineConstant(this, "BrowserSearch", BrowserSearch); + +function FillHistoryMenu(aParent) { + // Lazily add the hover listeners on first showing and never remove them + if (!aParent.hasStatusListener) { + // Show history item's uri in the status bar when hovering, and clear on exit + aParent.addEventListener("DOMMenuItemActive", function(aEvent) { + // Only the current page should have the checked attribute, so skip it + if (!aEvent.target.hasAttribute("checked")) + XULBrowserWindow.setOverLink(aEvent.target.getAttribute("uri")); + }, false); + aParent.addEventListener("DOMMenuItemInactive", function() { + XULBrowserWindow.setOverLink(""); + }, false); + + aParent.hasStatusListener = true; + } + + // Remove old entries if any + var children = aParent.childNodes; + for (var i = children.length - 1; i >= 0; --i) { + if (children[i].hasAttribute("index")) { + aParent.removeChild(children[i]); + } + } + + var webNav = gBrowser.webNavigation; + var sessionHistory = webNav.sessionHistory; + + var count = sessionHistory.count; + if (count <= 1) { + // don't display the popup for a single item + return false; + } + + const MAX_HISTORY_MENU_ITEMS = 15; + var index = sessionHistory.index; + var half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2); + var start = Math.max(index - half_length, 0); + var end = Math.min(start == 0 ? MAX_HISTORY_MENU_ITEMS : index + half_length + 1, count); + if (end == count) { + start = Math.max(count - MAX_HISTORY_MENU_ITEMS, 0); + } + + var tooltipBack = gNavigatorBundle.getString("tabHistory.goBack"); + var tooltipCurrent = gNavigatorBundle.getString("tabHistory.current"); + var tooltipForward = gNavigatorBundle.getString("tabHistory.goForward"); + + for (var j = end - 1; j >= start; j--) { + let item = document.createElement("menuitem"); + let entry = sessionHistory.getEntryAtIndex(j, false); + let uri = entry.URI.spec; + + item.setAttribute("uri", uri); + item.setAttribute("label", entry.title || uri); + item.setAttribute("index", j); + + if (j != index) { + PlacesUtils.favicons.getFaviconURLForPage(entry.URI, function(aURI) { + if (aURI) { + let iconURL = PlacesUtils.favicons.getFaviconLinkForIcon(aURI).spec; + item.style.listStyleImage = "url(" + iconURL + ")"; + } + }); + } + + if (j < index) { + item.className = "unified-nav-back menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipBack); + } else if (j == index) { + item.setAttribute("type", "radio"); + item.setAttribute("checked", "true"); + item.className = "unified-nav-current"; + item.setAttribute("tooltiptext", tooltipCurrent); + } else { + item.className = "unified-nav-forward menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipForward); + } + + aParent.appendChild(item); + } + return true; +} + +function addToUrlbarHistory(aUrlToAdd) { + if (!PrivateBrowsingUtils.isWindowPrivate(window) && + aUrlToAdd && + !aUrlToAdd.includes(" ") && + !/[\x00-\x1F]/.test(aUrlToAdd)) { + PlacesUIUtils.markPageAsTyped(aUrlToAdd); + } +} + +function toJavaScriptConsole() { + toOpenWindowByType("global:console", "chrome://global/content/console.xul"); +} + +function BrowserDownloadsUI() { + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + openUILinkIn("about:downloads", "tab"); + } else { + PlacesCommandHook.showPlacesOrganizer("Downloads"); + } +} + +function toOpenWindowByType(inType, uri, features) { + var topWindow = Services.wm.getMostRecentWindow(inType); + + if (topWindow) { + topWindow.focus(); + } else if (features) { + window.open(uri, "_blank", features); + } else { + window.open(uri, "_blank", "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar"); + } +} + +function OpenBrowserWindow(options) +{ + function newDocumentShown(doc, topic, data) { + if (topic == "document-shown" && + doc != document && + doc.defaultView == win) { + Services.obs.removeObserver(newDocumentShown, "document-shown"); + } + }; + Services.obs.addObserver(newDocumentShown, "document-shown", false); + + var charsetArg = new String(); + var handler = Components.classes["@mozilla.org/browser/clh;1"] + .getService(Components.interfaces.nsIBrowserHandler); + var defaultArgs = handler.defaultArgs; + var wintype = document.documentElement.getAttribute('windowtype'); + + var extraFeatures = ""; + if (options && options.private) { + extraFeatures = ",private"; + if (!PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Force the new window to load about:privatebrowsing instead of the default home page + defaultArgs = "about:privatebrowsing"; + } + } else { + extraFeatures = ",non-private"; + } + + // if and only if the current window is a browser window and it has a document with a character + // set, then extract the current charset menu setting from the current document and use it to + // initialize the new browser window... + var win; + if (window && (wintype == "navigator:browser") && window.content && window.content.document) { + var DocCharset = window.content.document.characterSet; + charsetArg = "charset="+DocCharset; + + //we should "inherit" the charset menu setting in a new window + win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs, charsetArg); + } else { + // forget about the charset information. + win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs); + } + + return win; +} + +var gCustomizeSheet = false; +function BrowserCustomizeToolbar() { + // Disable the toolbar context menu items + var menubar = document.getElementById("main-menubar"); + for (let childNode of menubar.childNodes) { + childNode.setAttribute("disabled", true); + } + + var cmd = document.getElementById("cmd_CustomizeToolbars"); + cmd.setAttribute("disabled", "true"); + + var splitter = document.getElementById("urlbar-search-splitter"); + if (splitter) { + splitter.parentNode.removeChild(splitter); + } + + CombinedStopReload.uninit(); + + PlacesToolbarHelper.customizeStart(); + BookmarkingUI.customizeStart(); + DownloadsButton.customizeStart(); + + TabsInTitlebar.allowedBy("customizing-toolbars", false); + + var customizeURL = "chrome://global/content/customizeToolbar.xul"; + gCustomizeSheet = Services.prefs.getBoolPref("toolbar.customization.usesheet", false); + + if (gCustomizeSheet) { + let sheetFrame = document.createElement("iframe"); + let panel = document.getElementById("customizeToolbarSheetPopup"); + sheetFrame.id = "customizeToolbarSheetIFrame"; + sheetFrame.toolbox = gNavToolbox; + sheetFrame.panel = panel; + sheetFrame.setAttribute("style", panel.getAttribute("sheetstyle")); + panel.appendChild(sheetFrame); + + // Open the panel, but make it invisible until the iframe has loaded so + // that the user doesn't see a white flash. + panel.style.visibility = "hidden"; + gNavToolbox.addEventListener("beforecustomization", function onBeforeCustomization() { + gNavToolbox.removeEventListener("beforecustomization", onBeforeCustomization, false); + panel.style.removeProperty("visibility"); + }, false); + + sheetFrame.setAttribute("src", customizeURL); + + panel.openPopup(gNavToolbox, "after_start", 0, 0); + } else { + window.openDialog(customizeURL, + "CustomizeToolbar", + "chrome,titlebar,toolbar,location,resizable,dependent", + gNavToolbox); + } +} + +function BrowserToolboxCustomizeDone(aToolboxChanged) { + if (gCustomizeSheet) { + document.getElementById("customizeToolbarSheetPopup").hidePopup(); + let iframe = document.getElementById("customizeToolbarSheetIFrame"); + iframe.parentNode.removeChild(iframe); + } + + // Update global UI elements that may have been added or removed + if (aToolboxChanged) { + gURLBar = document.getElementById("urlbar"); + + gProxyFavIcon = document.getElementById("page-proxy-favicon"); + gHomeButton.updateTooltip(); + gIdentityHandler._cacheElements(); + window.XULBrowserWindow.init(); + + updateEditUIVisibility(); + + // Hacky: update the PopupNotifications' object's reference to the iconBox, + // if it already exists, since it may have changed if the URL bar was + // added/removed. + if (!window.__lookupGetter__("PopupNotifications")) { + PopupNotifications.iconBox = document.getElementById("notification-popup-box"); + } + } + + PlacesToolbarHelper.customizeDone(); + BookmarkingUI.customizeDone(); + DownloadsButton.customizeDone(); + + // The url bar splitter state is dependent on whether stop/reload + // and the location bar are combined, so we need this ordering + CombinedStopReload.init(); + UpdateUrlbarSearchSplitterState(); + setUrlAndSearchBarWidthForConditionalForwardButton(); + + // Update the urlbar + if (gURLBar) { + gURLBarSettings.writePlaceholder(); + URLBarSetURI(); + XULBrowserWindow.asyncUpdateUI(); + BookmarkingUI.updateStarState(); + } + + TabsInTitlebar.allowedBy("customizing-toolbars", true); + + // Re-enable parts of the UI we disabled during the dialog + var menubar = document.getElementById("main-menubar"); + for (let childNode of menubar.childNodes) { + childNode.setAttribute("disabled", false); + } + var cmd = document.getElementById("cmd_CustomizeToolbars"); + cmd.removeAttribute("disabled"); + + // make sure to re-enable click-and-hold + if (!Services.prefs.getBoolPref("ui.click_hold_context_menus", false)) { + SetClickAndHoldHandlers(); + } + + gBrowser.selectedBrowser.focus(); +} + +function BrowserToolboxCustomizeChange(aType) { + switch (aType) { + case "iconsize": + case "mode": + retrieveToolbarIconsizesFromTheme(); + break; + default: + gHomeButton.updatePersonalToolbarStyle(); + BookmarkingUI.customizeChange(); + allTabs.readPref(); + } +} + +/** + * Allows themes to override the "iconsize" attribute on toolbars. + */ +function retrieveToolbarIconsizesFromTheme() { + function retrieveToolbarIconsize(aToolbar) { + if (aToolbar.localName != "toolbar") { + return; + } + + // The theme indicates that it wants to override the "iconsize" attribute + // by specifying a special value for the "counter-reset" property on the + // toolbar. A custom property cannot be used because getComputedStyle can + // only return the values of standard CSS properties. + let counterReset = getComputedStyle(aToolbar).counterReset; + if (counterReset == "smallicons 0") { + aToolbar.setAttribute("iconsize", "small"); + } else if (counterReset == "largeicons 0") { + aToolbar.setAttribute("iconsize", "large"); + } + } + + Array.forEach(gNavToolbox.childNodes, retrieveToolbarIconsize); + gNavToolbox.externalToolbars.forEach(retrieveToolbarIconsize); +} + +/** + * Update the global flag that tracks whether or not any edit UI (the Edit menu, + * edit-related items in the context menu, and edit-related toolbar buttons + * is visible, then update the edit commands' enabled state accordingly. We use + * this flag to skip updating the edit commands on focus or selection changes + * when no UI is visible to improve performance (including pageload performance, + * since focus changes when you load a new page). + * + * If UI is visible, we use goUpdateGlobalEditMenuItems to set the commands' + * enabled state so the UI will reflect it appropriately. + * + * If the UI isn't visible, we enable all edit commands so keyboard shortcuts + * still work and just lazily disable them as needed when the user presses a + * shortcut. + * + * This doesn't work on Mac, since Mac menus flash when users press their + * keyboard shortcuts, so edit UI is essentially always visible on the Mac, + * and we need to always update the edit commands. Thus on Mac this function + * is a no op. + */ +function updateEditUIVisibility() { + let editMenuPopupState = document.getElementById("menu_EditPopup").state; + let contextMenuPopupState = document.getElementById("contentAreaContextMenu").state; + let placesContextMenuPopupState = document.getElementById("placesContext").state; +#ifdef MENUBAR_CAN_AUTOHIDE + let appMenuPopupState = document.getElementById("appmenu-popup").state; +#endif + + // The UI is visible if the Edit menu is opening or open, if the context menu + // is open, or if the toolbar has been customized to include the Cut, Copy, + // or Paste toolbar buttons. + gEditUIVisible = editMenuPopupState == "showing" || + editMenuPopupState == "open" || + contextMenuPopupState == "showing" || + contextMenuPopupState == "open" || + placesContextMenuPopupState == "showing" || + placesContextMenuPopupState == "open" || +#ifdef MENUBAR_CAN_AUTOHIDE + appMenuPopupState == "showing" || + appMenuPopupState == "open" || +#endif + document.getElementById("cut-button") || + document.getElementById("copy-button") || + document.getElementById("paste-button") ? true : false; + + if (gEditUIVisible) { + // If UI is visible, update the edit commands' enabled state to reflect + // whether or not they are actually enabled for the current focus/selection. + goUpdateGlobalEditMenuItems(); + } else { + // Otherwise, enable all commands, so that keyboard shortcuts still work, + // then lazily determine their actual enabled state when the user presses + // a keyboard shortcut. + goSetCommandEnabled("cmd_undo", true); + goSetCommandEnabled("cmd_redo", true); + goSetCommandEnabled("cmd_cut", true); + goSetCommandEnabled("cmd_copy", true); + goSetCommandEnabled("cmd_paste", true); + goSetCommandEnabled("cmd_selectAll", true); + goSetCommandEnabled("cmd_delete", true); + goSetCommandEnabled("cmd_switchTextDirection", true); + } +} + +/** + * Makes the Character Encoding menu enabled or disabled as appropriate. + * To be called when the View menu or the app menu is opened. + */ +function updateCharacterEncodingMenuState() { + let charsetMenu = document.getElementById("charsetMenu"); + let appCharsetMenu = document.getElementById("appmenu_charsetMenu"); + let appDevCharsetMenu = document.getElementById("appmenu_developer_charsetMenu"); + // gBrowser is null on Mac when the menubar shows in the context of + // non-browser windows. The above elements may be null depending on + // what parts of the menubar are present. E.g. no app menu on Mac. + if (gBrowser && + gBrowser.docShell && + gBrowser.docShell.mayEnableCharacterEncodingMenu) { + if (charsetMenu) { + charsetMenu.removeAttribute("disabled"); + } + if (appCharsetMenu) { + appCharsetMenu.removeAttribute("disabled"); + } + if (appDevCharsetMenu) { + appDevCharsetMenu.removeAttribute("disabled"); + } + } else { + if (charsetMenu) { + charsetMenu.setAttribute("disabled", "true"); + } + if (appCharsetMenu) { + appCharsetMenu.setAttribute("disabled", "true"); + } + if (appDevCharsetMenu) { + appDevCharsetMenu.setAttribute("disabled", "true"); + } + } +} + +var XULBrowserWindow = { + // Stored Status, Link and Loading values + status: "", + defaultStatus: "", + overLink: "", + startTime: 0, + statusText: "", + isBusy: false, + // Don't hide navigation controls and toolbars for "special" pages. SBaD, M! + // If necessary, can add pages that need this treatment like so: + // inContentWhitelist: ["about:addons", "about:downloads", "about:permissions", "about:sync-progress"], + inContentWhitelist: [], + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsIWebProgressListener2) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsIXULBrowserWindow) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_NOINTERFACE; + }, + + get stopCommand () { + delete this.stopCommand; + return this.stopCommand = document.getElementById("Browser:Stop"); + }, + get reloadCommand () { + delete this.reloadCommand; + return this.reloadCommand = document.getElementById("Browser:Reload"); + }, + get statusTextField () { + delete this.statusTextField; + return this.statusTextField = document.getElementById("statusbar-display"); + }, + get isImage () { + delete this.isImage; + return this.isImage = document.getElementById("isImage"); + }, + + init: function() { + this.throbberElement = document.getElementById("navigator-throbber"); + + // Initialize the security button's state and tooltip text. Remember to reset + // _hostChanged, otherwise onSecurityChange will short circuit. + var securityUI = gBrowser.securityUI; + this._hostChanged = true; + this.onSecurityChange(null, null, securityUI.state); + }, + + destroy: function() { + // XXXjag to avoid leaks :-/, see bug 60729 + delete this.throbberElement; + delete this.stopCommand; + delete this.reloadCommand; + delete this.statusTextField; + delete this.statusText; + }, + + setJSStatus: function() { + // unsupported + }, + + setDefaultStatus: function(status) { + this.defaultStatus = status; + this.updateStatusField(); + }, + + setOverLink: function(url, anchorElt) { + // Encode bidirectional formatting characters. + // (RFC 3987 sections 3.2 and 4.1 paragraph 6) + url = url.replace(/[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g, + encodeURIComponent); + + if (gURLBar && gURLBar._mayTrimURLs /* corresponds to browser.urlbar.trimURLs */) { + url = trimURL(url); + } + + this.overLink = url; + LinkTargetDisplay.update(); + }, + + updateStatusField: function() { + var text, type, types = ["overLink"]; + if (this._busyUI) { + types.push("status"); + } + types.push("defaultStatus"); + for (type of types) { + text = this[type]; + if (text) { + break; + } + } + + // check the current value so we don't trigger an attribute change + // and cause needless (slow!) UI updates + if (this.statusText != text) { + let field = this.statusTextField; + field.setAttribute("previoustype", field.getAttribute("type")); + field.setAttribute("type", type); + field.label = text; + field.setAttribute("crop", type == "overLink" ? "center" : "end"); + this.statusText = text; + } + }, + + // Called before links are navigated to to allow us to retarget them if needed. + onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + let target = this._onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab); + return target; + }, + + _onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + // Don't modify non-default targets or targets that aren't in top-level app + // tab docshells (isAppTab will be false for app tab subframes). + if (originalTarget != "" || !isAppTab) { + return originalTarget; + } + + // External links from within app tabs should always open in new tabs + // instead of replacing the app tab's page (Bug 575561) + let linkHost; + let docHost; + try { + linkHost = linkURI.host; + docHost = linkNode.ownerDocument.documentURIObject.host; + } catch(e) { + // nsIURI.host can throw for non-nsStandardURL nsIURIs. + // If we fail to get either host, just return originalTarget. + return originalTarget; + } + + if (docHost == linkHost) { + return originalTarget; + } + + // Special case: ignore "www" prefix if it is part of host string + let [longHost, shortHost] = linkHost.length > docHost.length ? + [linkHost, docHost] : + [docHost, linkHost]; + if (longHost == "www." + shortHost) { + return originalTarget; + } + + return "_blank"; + }, + + onLinkIconAvailable: function(aIconURL) { + if (gProxyFavIcon && gBrowser.userTypedValue === null) { + PageProxySetIcon(aIconURL); // update the favicon in the URL bar + } + }, + + onProgressChange: function(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + // Do nothing. + }, + + onProgressChange64: function(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + return this.onProgressChange(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress); + }, + + // This function fires only for the currently selected tab. + onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { + const nsIWebProgressListener = Ci.nsIWebProgressListener; + const nsIChannel = Ci.nsIChannel; + + if (aStateFlags & nsIWebProgressListener.STATE_START && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + + if (aRequest && aWebProgress.isTopLevel) { + // clear out feed data + gBrowser.selectedBrowser.feeds = null; + + // clear out search-engine data + gBrowser.selectedBrowser.engines = null; + } + + this.isBusy = true; + + if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { + this._busyUI = true; + + // Turn the throbber on. + if (this.throbberElement) { + this.throbberElement.setAttribute("busy", "true"); + } + + // XXX: This needs to be based on window activity... + this.stopCommand.removeAttribute("disabled"); + CombinedStopReload.switchToStop(); + } + } else if (aStateFlags & nsIWebProgressListener.STATE_STOP) { + // This (thanks to the filter) is a network stop or the last + // request stop outside of loading the document, stop throbbers + // and progress bars and such + if (aRequest) { + let msg = ""; + let location; + // Get the URI either from a channel or a pseudo-object + if (aRequest instanceof nsIChannel || "URI" in aRequest) { + location = aRequest.URI; + + // For keyword URIs clear the user typed value since they will be changed into real URIs + if (location.scheme == "keyword" && aWebProgress.isTopLevel) { + gBrowser.userTypedValue = null; + } + + if (location.spec != "about:blank") { + switch (aStatus) { + case Components.results.NS_ERROR_NET_TIMEOUT: + msg = gNavigatorBundle.getString("nv_timeout"); + break; + } + } + } + + this.status = ""; + this.setDefaultStatus(msg); + + // Disable menu entries for images, enable otherwise + if (content.document && BrowserUtils.mimeTypeIsTextBased(content.document.contentType)) { + this.isImage.removeAttribute('disabled'); + } else { + this.isImage.setAttribute('disabled', 'true'); + } + } + + this.isBusy = false; + + if (this._busyUI) { + this._busyUI = false; + + // Turn the throbber off. + if (this.throbberElement) { + this.throbberElement.removeAttribute("busy"); + } + + this.stopCommand.setAttribute("disabled", "true"); + CombinedStopReload.switchToReload(aRequest instanceof Ci.nsIRequest); + } + } + }, + + onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) { + var location = aLocationURI ? aLocationURI.spec : ""; + this._hostChanged = true; + + // If displayed, hide the form validation popup. + FormValidationHandler.hidePopup(); + + let pageTooltip = document.getElementById("aHTMLTooltip"); + let tooltipNode = pageTooltip.triggerNode; + if (tooltipNode) { + // Optimise for the common case + if (aWebProgress.isTopLevel) { + pageTooltip.hidePopup(); + } else { + for (let tooltipWindow = tooltipNode.ownerDocument.defaultView; + tooltipWindow != tooltipWindow.parent; + tooltipWindow = tooltipWindow.parent) { + if (tooltipWindow == aWebProgress.DOMWindow) { + pageTooltip.hidePopup(); + break; + } + } + } + } + + // Disable menu entries for images, enable otherwise + if (content.document && BrowserUtils.mimeTypeIsTextBased(content.document.contentType)) { + this.isImage.removeAttribute('disabled'); + } else { + this.isImage.setAttribute('disabled', 'true'); + } + + this.hideOverLinkImmediately = true; + this.setOverLink("", null); + this.hideOverLinkImmediately = false; + + // We should probably not do this if the value has changed since the user + // searched. + // Update urlbar only if a new page was loaded on the primary content area. + // Do not update urlbar if there was a subframe navigation + + var browser = gBrowser.selectedBrowser; + if (aWebProgress.isTopLevel) { + if ((location == "about:blank" && !content.opener) || location == "") { + // Second condition is for new tabs, otherwise + // reload function is enabled until tab is refreshed. + this.reloadCommand.setAttribute("disabled", "true"); + } else { + this.reloadCommand.removeAttribute("disabled"); + } + + if (gURLBar) { + URLBarSetURI(aLocationURI); + + // Update starring UI + BookmarkingUI.updateStarState(); + } + + // Show or hide browser chrome based on the whitelist + if (this.hideChromeForLocation(location)) { + document.documentElement.setAttribute("disablechrome", "true"); + } else { + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + if (ss.getTabValue(gBrowser.selectedTab, "appOrigin")) { + document.documentElement.setAttribute("disablechrome", "true"); + } else { + document.documentElement.removeAttribute("disablechrome"); + } + } + + // Utility functions for disabling find + var shouldDisableFind = function(aDocument) { + let docElt = aDocument.documentElement; + return docElt && docElt.getAttribute("disablefastfind") == "true"; + } + + var disableFindCommands = function(aDisable) { + let findCommands = [document.getElementById("cmd_find"), + document.getElementById("cmd_findAgain"), + document.getElementById("cmd_findPrevious")]; + for (let elt of findCommands) { + if (aDisable) { + elt.setAttribute("disabled", "true"); + } else { + elt.removeAttribute("disabled"); + } + } + if (gFindBarInitialized) { + if (!gFindBar.hidden && aDisable) { + gFindBar.hidden = true; + this._findbarTemporarilyHidden = true; + } else if (this._findbarTemporarilyHidden && !aDisable) { + gFindBar.hidden = false; + this._findbarTemporarilyHidden = false; + } + } + }.bind(this); + + var onContentRSChange = function onContentRSChange(e) { + if (e.target.readyState != "interactive" && e.target.readyState != "complete") { + return; + } + + e.target.removeEventListener("readystatechange", onContentRSChange); + disableFindCommands(shouldDisableFind(e.target)); + } + + // Disable find commands in documents that ask for them to be disabled. + if (aLocationURI && + (aLocationURI.schemeIs("about") || aLocationURI.schemeIs("chrome"))) { + // Don't need to re-enable/disable find commands for same-document location changes + // (e.g. the replaceStates in about:addons) + if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { + if (content.document.readyState == "interactive" || content.document.readyState == "complete") { + disableFindCommands(shouldDisableFind(content.document)); + } else { + content.document.addEventListener("readystatechange", onContentRSChange); + } + } + } else { + disableFindCommands(false); + } + + if (gFindBarInitialized) { + if (gFindBar.findMode != gFindBar.FIND_NORMAL) { + // Close the Find toolbar if we're in old-style TAF mode + gFindBar.close(); + } + + // XXX + // See: https://github.com/MoonchildProductions/Pale-Moon/issues/364 + // An actual preference: findbar.highlightAll + /* + if (!(gPrefService.getBoolPref("accessibility.typeaheadfind.highlightallremember") || + gPrefService.getBoolPref("accessibility.typeaheadfind.highlightallbydefault"))) { + // fix bug 253793 - turn off highlight when page changes + gFindBar.getElement("highlight").checked = false; + } + */ + } + } + UpdateBackForwardCommands(gBrowser.webNavigation); + + gGestureSupport.restoreRotationState(); + + // See bug 358202, when tabs are switched during a drag operation, + // timers don't fire on windows (bug 203573) + if (aRequest) { + setTimeout(function() { XULBrowserWindow.asyncUpdateUI(); }, 0); + } else { + this.asyncUpdateUI(); + } + }, + + asyncUpdateUI: function() { + FeedHandler.updateFeeds(); + }, + + hideChromeForLocation: function(aLocation) { + aLocation = aLocation.toLowerCase(); + return this.inContentWhitelist.some(function(aSpec) { + return aSpec == aLocation; + }); + }, + + onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) { + this.status = aMessage; + this.updateStatusField(); + }, + + // Properties used to cache security state used to update the UI + _state: null, + _hostChanged: false, // onLocationChange will flip this bit + + onSecurityChange: function(aWebProgress, aRequest, aState) { + // Don't need to do anything if the data we use to update the UI hasn't + // changed + if (this._state == aState && + !this._hostChanged) { +#ifdef DEBUG + try { + var contentHost = gBrowser.contentWindow.location.host; + if (this._host !== undefined && this._host != contentHost) { + Components.utils.reportError( + "ASSERTION: browser.js host is inconsistent. Content window has " + + "<" + contentHost + "> but cached host is <" + this._host + ">.\n" + ); + } + } catch(ex) {} +#endif + return; + } + this._state = aState; + +#ifdef DEBUG + try { + this._host = gBrowser.contentWindow.location.host; + } catch(ex) { + this._host = null; + } +#endif + + this._hostChanged = false; + + // aState is defined as a bitmask that may be extended in the future. + // We filter out any unknown bits before testing for known values. + const wpl = Components.interfaces.nsIWebProgressListener; + const wpl_security_bits = wpl.STATE_IS_SECURE | + wpl.STATE_IS_BROKEN | + wpl.STATE_IS_INSECURE; + var level; + + switch (this._state & wpl_security_bits) { + case wpl.STATE_IS_SECURE: + level = "high"; + break; + case wpl.STATE_IS_BROKEN: + level = "broken"; + break; + } + + if (level) { + // We don't style the Location Bar based on the the 'level' attribute + // anymore, but still set it for third-party themes. + if (gURLBar) { + gURLBar.setAttribute("level", level); + } + } else { + if (gURLBar) { + gURLBar.removeAttribute("level"); + } + } + + // Don't pass in the actual location object, since it can cause us to + // hold on to the window object too long. Just pass in the fields we + // care about. (bug 424829) + var location = gBrowser.contentWindow.location; + var locationObj = {}; + try { + // about:blank can be used by webpages so pretend it is http + locationObj.protocol = location == "about:blank" ? "http:" : location.protocol; + locationObj.host = location.host; + locationObj.hostname = location.hostname; + locationObj.port = location.port; + } catch(ex) { + // Can sometimes throw if the URL being visited has no host/hostname, + // e.g. about:blank. The _state for these pages means we won't need these + // properties anyways, though. + } + gIdentityHandler.checkIdentity(this._state, locationObj); + }, + + // simulate all change notifications after switching tabs + onUpdateCurrentBrowser: function(aStateFlags, aStatus, aMessage, aTotalProgress) { + if (FullZoom.updateBackgroundTabs) { + FullZoom.onLocationChange(gBrowser.currentURI, true); + } + var nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + var loadingDone = aStateFlags & nsIWebProgressListener.STATE_STOP; + // use a pseudo-object instead of a (potentially nonexistent) channel for getting + // a correct error message - and make sure that the UI is always either in + // loading (STATE_START) or done (STATE_STOP) mode + this.onStateChange( + gBrowser.webProgress, + { URI: gBrowser.currentURI }, + loadingDone ? nsIWebProgressListener.STATE_STOP : nsIWebProgressListener.STATE_START, + aStatus + ); + // status message and progress value are undefined if we're done with loading + if (loadingDone) { + return; + } + this.onStatusChange(gBrowser.webProgress, null, 0, aMessage); + } +}; + +var LinkTargetDisplay = { + get DELAY_SHOW() { + delete this.DELAY_SHOW; + return this.DELAY_SHOW = Services.prefs.getIntPref("browser.overlink-delay"); + }, + + DELAY_HIDE: 250, + _timer: 0, + + get _isVisible () { + return XULBrowserWindow.statusTextField.label != ""; + }, + + update: function() { + clearTimeout(this._timer); + window.removeEventListener("mousemove", this, true); + + if (!XULBrowserWindow.overLink) { + if (XULBrowserWindow.hideOverLinkImmediately) { + this._hide(); + } else { + this._timer = setTimeout(this._hide.bind(this), this.DELAY_HIDE); + } + return; + } + + if (this._isVisible) { + XULBrowserWindow.updateStatusField(); + } else { + // Let the display appear when the mouse doesn't move within the delay + this._showDelayed(); + window.addEventListener("mousemove", this, true); + } + }, + + handleEvent: function(event) { + switch (event.type) { + case "mousemove": + // Restart the delay since the mouse was moved + clearTimeout(this._timer); + this._showDelayed(); + break; + } + }, + + _showDelayed: function() { + this._timer = setTimeout(function(self) { + XULBrowserWindow.updateStatusField(); + window.removeEventListener("mousemove", self, true); + }, this.DELAY_SHOW, this); + }, + + _hide: function() { + clearTimeout(this._timer); + + XULBrowserWindow.updateStatusField(); + } +}; + +var CombinedStopReload = { + init: function() { + if (this._initialized) { + return; + } + + var urlbar = document.getElementById("urlbar-container"); + var reload = document.getElementById("reload-button"); + var stop = document.getElementById("stop-button"); + + if (urlbar) { + if (urlbar.parentNode.getAttribute("mode") != "icons" || + !reload || urlbar.nextSibling != reload || + !stop || reload.nextSibling != stop) { + urlbar.removeAttribute("combined"); + } else { + urlbar.setAttribute("combined", "true"); + reload = document.getElementById("urlbar-reload-button"); + stop = document.getElementById("urlbar-stop-button"); + } + } + if (!stop || !reload || reload.nextSibling != stop) { + return; + } + + this._initialized = true; + if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") { + reload.setAttribute("displaystop", "true"); + } + stop.addEventListener("click", this, false); + this.reload = reload; + this.stop = stop; + }, + + uninit: function() { + if (!this._initialized) { + return; + } + + this._cancelTransition(); + this._initialized = false; + this.stop.removeEventListener("click", this, false); + this.reload = null; + this.stop = null; + }, + + handleEvent: function(event) { + // the only event we listen to is "click" on the stop button + if (event.button == 0 && + !this.stop.disabled) { + this._stopClicked = true; + } + }, + + switchToStop: function() { + if (!this._initialized) { + return; + } + + this._cancelTransition(); + this.reload.setAttribute("displaystop", "true"); + }, + + switchToReload: function(aDelay) { + if (!this._initialized) { + return; + } + + this.reload.removeAttribute("displaystop"); + + if (!aDelay || this._stopClicked) { + this._stopClicked = false; + this._cancelTransition(); + this.reload.disabled = XULBrowserWindow.reloadCommand + .getAttribute("disabled") == "true"; + return; + } + + if (this._timer) { + return; + } + + // Temporarily disable the reload button to prevent the user from + // accidentally reloading the page when intending to click the stop button + this.reload.disabled = true; + this._timer = setTimeout(function(self) { + self._timer = 0; + self.reload.disabled = XULBrowserWindow.reloadCommand + .getAttribute("disabled") == "true"; + }, 650, this); + }, + + _cancelTransition: function() { + if (this._timer) { + clearTimeout(this._timer); + this._timer = 0; + } + } +}; + +var TabsProgressListener = { + onStateChange: function(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + + // Attach a listener to watch for "click" events bubbling up from error + // pages and other similar page. This lets us fix bugs like 401575 which + // require error page UI to do privileged things, without letting error + // pages have any privilege themselves. + // We can't look for this during onLocationChange since at that point the + // document URI is not yet the about:-uri of the error page. + + let doc = aWebProgress.DOMWindow.document; + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + Components.isSuccessCode(aStatus) && + doc.documentURI.startsWith("about:") && + !doc.documentURI.toLowerCase().startsWith("about:blank") && + !doc.documentElement.hasAttribute("hasBrowserHandlers")) { + // STATE_STOP may be received twice for documents, thus store an + // attribute to ensure handling it just once. + doc.documentElement.setAttribute("hasBrowserHandlers", "true"); + aBrowser.addEventListener("click", BrowserOnClick, true); + aBrowser.addEventListener("pagehide", function onPageHide(event) { + if (event.target.defaultView.frameElement) { + return; + } + aBrowser.removeEventListener("click", BrowserOnClick, true); + aBrowser.removeEventListener("pagehide", onPageHide, true); + if (event.target.documentElement) { + event.target.documentElement.removeAttribute("hasBrowserHandlers"); + } + }, true); + + // We also want to make changes to page UI for unprivileged about pages. + BrowserOnAboutPageLoad(doc); + } + }, + + onLocationChange: function(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) { + // Filter out location changes caused by anchor navigation + // or history.push/pop/replaceState. + if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) { + return; + } + + // Only need to call locationChange if the PopupNotifications object + // for this window has already been initialized (i.e. its getter no + // longer exists) + if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get) { + PopupNotifications.locationChange(aBrowser); + } + + gBrowser.getNotificationBox(aBrowser).removeTransientNotifications(); + + // Filter out location changes in sub documents. + if (aWebProgress.isTopLevel) { + // Initialize the click-to-play state. + aBrowser._clickToPlayPluginsActivated = new Map(); + aBrowser._clickToPlayAllPluginsActivated = false; + aBrowser._pluginScriptedState = gPluginHandler.PLUGIN_SCRIPTED_STATE_NONE; + + FullZoom.onLocationChange(aLocationURI, false, aBrowser); + } + }, + + onRefreshAttempted: function(aBrowser, aWebProgress, aURI, aDelay, aSameURI) { + if (gPrefService.getBoolPref("accessibility.blockautorefresh")) { + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let refreshButtonText = + gNavigatorBundle.getString("refreshBlocked.goButton"); + let refreshButtonAccesskey = + gNavigatorBundle.getString("refreshBlocked.goButton.accesskey"); + let message = + gNavigatorBundle.getFormattedString(aSameURI ? "refreshBlocked.refreshLabel" + : "refreshBlocked.redirectLabel", + [brandShortName]); + let docShell = aWebProgress.DOMWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + let notificationBox = gBrowser.getNotificationBox(aBrowser); + let notification = notificationBox.getNotificationWithValue("refresh-blocked"); + if (notification) { + notification.label = message; + notification.refreshURI = aURI; + notification.delay = aDelay; + notification.docShell = docShell; + } else { + let buttons = [{ + label: refreshButtonText, + accessKey: refreshButtonAccesskey, + callback: function(aNotification, aButton) { + var refreshURI = aNotification.docShell + .QueryInterface(Ci.nsIRefreshURI); + refreshURI.forceRefreshURI(aNotification.refreshURI, + aNotification.delay, true); + } + }]; + notification = + notificationBox.appendNotification(message, "refresh-blocked", + "chrome://browser/skin/Info.png", + notificationBox.PRIORITY_INFO_MEDIUM, + buttons); + notification.refreshURI = aURI; + notification.delay = aDelay; + notification.docShell = docShell; + } + return false; + } + return true; + } +} + +function nsBrowserAccess() { +} + +nsBrowserAccess.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]), + + openURI: function(aURI, aOpener, aWhere, aContext) { + var newWindow = null; + var isExternal = !!(aContext & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + + if (aOpener && isExternal) { + Cu.reportError("nsBrowserAccess.openURI did not expect an opener to be " + + "passed if the context is OPEN_EXTERNAL."); + throw Cr.NS_ERROR_FAILURE; + } + + if (isExternal && aURI && aURI.schemeIs("chrome")) { + dump("use -chrome command-line option to load external chrome urls\n"); + return null; + } + + if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) { + if (isExternal && + gPrefService.prefHasUserValue("browser.link.open_newwindow.override.external")) { + aWhere = gPrefService.getIntPref("browser.link.open_newwindow.override.external"); + } else { + aWhere = gPrefService.getIntPref("browser.link.open_newwindow"); + } + } + + let referrer = aOpener ? makeURI(aOpener.location.href) : null; + let triggeringPrincipal = null; + let referrerPolicy = Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT; + if (aOpener && aOpener.document) { + referrerPolicy = aOpener.document.referrerPolicy; + triggeringPrincipal = aOpener.document.nodePrincipal; + } + + switch (aWhere) { + case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW : + // FIXME: Bug 408379. So how come this doesn't send the + // referrer like the other loads do? + var url = aURI ? aURI.spec : "about:blank"; + // Pass all params to openDialog to ensure that "url" isn't passed through + // loadOneOrMoreURIs, which splits based on "|" + newWindow = openDialog(getBrowserURL(), "_blank", "all,dialog=no", url, null, null, null); + break; + case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB : + let win, needToFocusWin; + + // try the current window. if we're in a popup, fall back on the most recent browser window + if (window.toolbar.visible) { + win = window; + } else { + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aOpener || window); + win = RecentWindow.getMostRecentBrowserWindow({private: isPrivate}); + needToFocusWin = true; + } + + if (!win) { + // we couldn't find a suitable window, a new one needs to be opened. + return null; + } + + if (isExternal && (!aURI || aURI.spec == "about:blank")) { + win.BrowserOpenTab(); // this also focuses the location bar + win.focus(); + newWindow = win.content; + break; + } + + let loadInBackground = gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground"); + let openerWindow = (aContext & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener; + + let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", { + triggeringPrincipal: triggeringPrincipal, + referrerURI: referrer, + referrerPolicy: referrerPolicy, + fromExternal: isExternal, + inBackground: loadInBackground, + opener: openerWindow }); + let browser = win.gBrowser.getBrowserForTab(tab); + + if (gPrefService.getBoolPref("browser.tabs.noWindowActivationOnExternal")) { + isExternal = false; // this is a hack, but it works + } + + newWindow = browser.contentWindow; + if (needToFocusWin || (!loadInBackground && isExternal)) { + newWindow.focus(); + } + break; + default : // OPEN_CURRENTWINDOW or an illegal value + newWindow = content; + if (aURI) { + let loadflags = isExternal ? + Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL : + Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + gBrowser.loadURIWithFlags(aURI.spec, { flags: loadflags, + triggeringPrincipal: triggeringPrincipal, + referrerURI: referrer, + referrerPolicy: referrerPolicy }); + } + if (!gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground")) { + window.focus(); + } + } + return newWindow; + }, + + isTabContentWindow: function(aWindow) { + return gBrowser.browsers.some(function(browser) browser.contentWindow == aWindow); + } +} + +function onViewToolbarsPopupShowing(aEvent, aInsertPoint) { + var popup = aEvent.target; + if (popup != aEvent.currentTarget) { + return; + } + + // Empty the menu + for (var i = popup.childNodes.length-1; i >= 0; --i) { + var deadItem = popup.childNodes[i]; + if (deadItem.hasAttribute("toolbarId")) { + popup.removeChild(deadItem); + } + } + + var firstMenuItem = aInsertPoint || popup.firstChild; + + let toolbarNodes = Array.slice(gNavToolbox.childNodes); + toolbarNodes.push(document.getElementById("addon-bar")); + + for (let toolbar of toolbarNodes) { +#ifdef MOZ_WIDGET_GTK + if (toolbar.id == "toolbar-menubar" && + document.documentElement.getAttribute("shellshowingmenubar") == "true") { + continue; + } +#endif + let toolbarName = toolbar.getAttribute("toolbarname"); + if (toolbarName) { + let menuItem = document.createElement("menuitem"); + let hidingAttribute = toolbar.getAttribute("type") == "menubar" ? + "autohide" : + "collapsed"; + menuItem.setAttribute("id", "toggle_" + toolbar.id); + menuItem.setAttribute("toolbarId", toolbar.id); + menuItem.setAttribute("type", "checkbox"); + menuItem.setAttribute("label", toolbarName); + menuItem.setAttribute("checked", toolbar.getAttribute(hidingAttribute) != "true"); + if (popup.id != "appmenu_customizeMenu") { + menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey")); + } + if (popup.id != "toolbar-context-menu") { + menuItem.setAttribute("key", toolbar.getAttribute("key")); + } + + popup.insertBefore(menuItem, firstMenuItem); + + menuItem.addEventListener("command", onViewToolbarCommand, false); + } + } +} + +function onViewToolbarCommand(aEvent) { + var toolbarId = aEvent.originalTarget.getAttribute("toolbarId"); + var toolbar = document.getElementById(toolbarId); + var isVisible = aEvent.originalTarget.getAttribute("checked") == "true"; + setToolbarVisibility(toolbar, isVisible); +} + +function setToolbarVisibility(toolbar, isVisible) { + var hidingAttribute = toolbar.getAttribute("type") == "menubar" ? + "autohide" : + "collapsed"; + + toolbar.setAttribute(hidingAttribute, !isVisible); + document.persist(toolbar.id, hidingAttribute); + + // Customizable toolbars - persist the hiding attribute. + if (toolbar.hasAttribute("customindex")) { + var toolbox = toolbar.parentNode; + var name = toolbar.getAttribute("toolbarname"); + if (toolbox.toolbarset) { + try { + // Checking all attributes starting with "toolbar". + Array.prototype.slice.call(toolbox.toolbarset.attributes, 0) + .find(x => { + if (x.name.startsWith("toolbar")) { + var toolbarInfo = x.value; + var infoSplit = toolbarInfo.split(gToolbarInfoSeparators[0]); + if (infoSplit[0] == name) { + infoSplit[1] = [ + infoSplit[1].split(gToolbarInfoSeparators[1], 1), !isVisible + ].join(gToolbarInfoSeparators[1]); + toolbox.toolbarset.setAttribute( + x.name, infoSplit.join(gToolbarInfoSeparators[0])); + document.persist(toolbox.toolbarset.id, x.name); + } + } + }); + } catch(e) { + Components.utils.reportError( + "Customizable toolbars - persist the hiding attribute: " + e); + } + } + } + + PlacesToolbarHelper.init(); + BookmarkingUI.onToolbarVisibilityChange(); + gBrowser.updateWindowResizers(); + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif + + if (isVisible) { + ToolbarIconColor.inferFromText(); + } +} + +var AudioIndicator = { + init: function() { + Services.prefs.addObserver(this._prefName, this, false); + this.syncUI(); + }, + + uninit: function() { + Services.prefs.removeObserver(this._prefName, this); + }, + + toggle: function() { + this.enabled = !Services.prefs.getBoolPref(this._prefName); + }, + + syncUI: function() { + document.getElementById("context_toggleMuteTab").setAttribute("hidden", this.enabled); + document.getElementById("key_toggleMute").setAttribute("disabled", this.enabled); + }, + + get enabled () { + return !Services.prefs.getBoolPref(this._prefName); + }, + + set enabled (val) { + Services.prefs.setBoolPref(this._prefName, !!val); + return val; + }, + + observe: function(subject, topic, data) { + if (topic == "nsPref:changed") { + this.syncUI(); + } + }, + + _prefName: "browser.tabs.showAudioPlayingIcon" +} + +var TabsOnTop = { + init: function() { + Services.prefs.addObserver(this._prefName, this, false); + }, + + uninit: function() { + Services.prefs.removeObserver(this._prefName, this); + }, + + toggle: function() { + this.enabled = !Services.prefs.getBoolPref(this._prefName); + }, + + syncUI: function() { + let userEnabled = Services.prefs.getBoolPref(this._prefName); + let enabled = userEnabled && gBrowser.tabContainer.visible; + + document.getElementById("cmd_ToggleTabsOnTop") + .setAttribute("checked", userEnabled); + + document.documentElement.setAttribute("tabsontop", enabled); + document.getElementById("navigator-toolbox").setAttribute("tabsontop", enabled); + document.getElementById("TabsToolbar").setAttribute("tabsontop", enabled); + document.getElementById("nav-bar").setAttribute("tabsontop", enabled); + gBrowser.tabContainer.setAttribute("tabsontop", enabled); + TabsInTitlebar.allowedBy("tabs-on-top", enabled); + }, + + get enabled () { + return gNavToolbox.getAttribute("tabsontop") == "true"; + }, + + set enabled (val) { + Services.prefs.setBoolPref(this._prefName, !!val); + return val; + }, + + observe: function(subject, topic, data) { + if (topic == "nsPref:changed") { + this.syncUI(); + } + }, + + _prefName: "browser.tabs.onTop" +} + +var TabsInTitlebar = { + init: function() { +#ifdef MOZ_CAN_DRAW_IN_TITLEBAR + this._readPref(); + Services.prefs.addObserver(this._prefName, this, false); + + // Don't trust the initial value of the sizemode attribute; wait for + // the resize event (handled in tabbrowser.xml). + this.allowedBy("sizemode", false); + + this._initialized = true; +#endif + }, + + allowedBy: function(condition, allow) { +#ifdef MOZ_CAN_DRAW_IN_TITLEBAR + if (allow) { + if (condition in this._disallowed) { + delete this._disallowed[condition]; + this._update(); + } + } else { + if (!(condition in this._disallowed)) { + this._disallowed[condition] = null; + this._update(); + } + } +#endif + }, + + get enabled() { + return document.documentElement.getAttribute("tabsintitlebar") == "true"; + }, + +#ifdef MOZ_CAN_DRAW_IN_TITLEBAR + observe: function(subject, topic, data) { + if (topic == "nsPref:changed") + this._readPref(); + }, + + _initialized: false, + _disallowed: {}, + _prefName: "browser.tabs.drawInTitlebar", + + _readPref: function() { + this.allowedBy("pref", Services.prefs.getBoolPref(this._prefName)); + }, + + _update: function() { + function $(id) document.getElementById(id); + function rect(ele) ele.getBoundingClientRect(); + + if (!this._initialized || window.fullScreen) { + return; + } + + let allowed = true; + for (let something in this._disallowed) { + allowed = false; + break; + } + + if (allowed == this.enabled) { + return; + } + + let titlebar = $("titlebar"); + + if (allowed) { + let tabsToolbar = $("TabsToolbar"); + +#ifdef MENUBAR_CAN_AUTOHIDE + let appmenuButtonBox = $("appmenu-button-container"); + this._sizePlaceholder("appmenu-button", rect(appmenuButtonBox).width); +#endif + let captionButtonsBox = $("titlebar-buttonbox"); + this._sizePlaceholder("caption-buttons", rect(captionButtonsBox).width); + + let tabsToolbarRect = rect(tabsToolbar); + let titlebarTop = rect($("titlebar-content")).top; + titlebar.style.marginBottom = - Math.min(tabsToolbarRect.top - titlebarTop, + tabsToolbarRect.height) + "px"; + + document.documentElement.setAttribute("tabsintitlebar", "true"); + + if (!this._draghandle) { + let tmp = {}; + Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp); + this._draghandle = new tmp.WindowDraggingElement(tabsToolbar); + this._draghandle.mouseDownCheck = function() { + return !this._dragBindingAlive && TabsInTitlebar.enabled; + }; + } + } else { + document.documentElement.removeAttribute("tabsintitlebar"); + + titlebar.style.marginBottom = ""; + } + + ToolbarIconColor.inferFromText(); + }, + + _sizePlaceholder: function(type, width) { + Array.forEach(document.querySelectorAll(".titlebar-placeholder[type='"+ type +"']"), + function(node) { node.width = width; }); + }, +#endif + + uninit: function() { +#ifdef MOZ_CAN_DRAW_IN_TITLEBAR + this._initialized = false; + Services.prefs.removeObserver(this._prefName, this); +#endif + } +}; + +#ifdef MENUBAR_CAN_AUTOHIDE +function updateAppButtonDisplay() { + var displayAppButton = + !gInPrintPreviewMode && + window.menubar.visible && + document.getElementById("toolbar-menubar").getAttribute("autohide") == "true"; + +#ifdef MOZ_CAN_DRAW_IN_TITLEBAR + document.getElementById("titlebar").hidden = !displayAppButton; + + if (displayAppButton) { + document.documentElement.setAttribute("chromemargin", "0,2,2,2"); + } else { + document.documentElement.removeAttribute("chromemargin"); + } + + TabsInTitlebar.allowedBy("drawing-in-titlebar", displayAppButton); +#else + document.getElementById("appmenu-toolbar-button").hidden = + !displayAppButton; +#endif +} +#endif + +#ifdef MOZ_CAN_DRAW_IN_TITLEBAR +function onTitlebarMaxClick() { + if (window.windowState == window.STATE_MAXIMIZED) { + window.restore(); + } else { + window.maximize(); + } +} +#endif + +function displaySecurityInfo() { + BrowserPageInfo(null, "securityTab"); +} + +/** + * Opens or closes the sidebar identified by commandID. + * + * @param commandID a string identifying the sidebar to toggle; see the + * note below. (Optional if a sidebar is already open.) + * @param forceOpen boolean indicating whether the sidebar should be + * opened regardless of its current state (optional). + * @note + * We expect to find a xul:broadcaster element with the specified ID. + * The following attributes on that element may be used and/or modified: + * - id (required) the string to match commandID. The convention + * is to use this naming scheme: 'view<sidebar-name>Sidebar'. + * - sidebarurl (required) specifies the URL to load in this sidebar. + * - sidebartitle or label (in that order) specify the title to + * display on the sidebar. + * - checked indicates whether the sidebar is currently displayed. + * Note that toggleSidebar updates this attribute when + * it changes the sidebar's visibility. + * - group this attribute must be set to "sidebar". + */ +function toggleSidebar(commandID, forceOpen) { + + var sidebarBox = document.getElementById("sidebar-box"); + if (!commandID) { + commandID = sidebarBox.getAttribute("sidebarcommand"); + } + + var sidebarBroadcaster = document.getElementById(commandID); + var sidebar = document.getElementById("sidebar"); // xul:browser + var sidebarTitle = document.getElementById("sidebar-title"); + var sidebarSplitter = document.getElementById("sidebar-splitter"); + + if (sidebarBroadcaster.getAttribute("checked") == "true") { + if (!forceOpen) { + // Replace the document currently displayed in the sidebar with about:blank + // so that we can free memory by unloading the page. We need to explicitly + // create a new content viewer because the old one doesn't get destroyed + // until about:blank has loaded (which does not happen as long as the + // element is hidden). + sidebar.setAttribute("src", "about:blank"); + sidebar.docShell.createAboutBlankContentViewer(null); + + sidebarBroadcaster.removeAttribute("checked"); + sidebarBox.setAttribute("sidebarcommand", ""); + sidebarTitle.value = ""; + sidebarBox.hidden = true; + sidebarSplitter.hidden = true; + gBrowser.selectedBrowser.focus(); + } else { + fireSidebarFocusedEvent(); + } + return; + } + + // now we need to show the specified sidebar + + // ..but first update the 'checked' state of all sidebar broadcasters + var broadcasters = document.getElementsByAttribute("group", "sidebar"); + for (let broadcaster of broadcasters) { + // skip elements that observe sidebar broadcasters and random + // other elements + if (broadcaster.localName != "broadcaster") { + continue; + } + + if (broadcaster != sidebarBroadcaster) { + broadcaster.removeAttribute("checked"); + } else { + sidebarBroadcaster.setAttribute("checked", "true"); + } + } + + sidebarBox.hidden = false; + sidebarSplitter.hidden = false; + + var url = sidebarBroadcaster.getAttribute("sidebarurl"); + var title = sidebarBroadcaster.getAttribute("sidebartitle"); + if (!title) { + title = sidebarBroadcaster.getAttribute("label"); + } + sidebar.setAttribute("src", url); // kick off async load + sidebarBox.setAttribute("sidebarcommand", sidebarBroadcaster.id); + sidebarTitle.value = title; + + // We set this attribute here in addition to setting it on the <browser> + // element itself, because the code in gBrowserInit.onUnload persists this + // attribute, not the "src" of the <browser id="sidebar">. The reason it + // does that is that we want to delay sidebar load a bit when a browser + // window opens. See delayedStartup(). + sidebarBox.setAttribute("src", url); + + if (sidebar.contentDocument.location.href != url) { + sidebar.addEventListener("load", sidebarOnLoad, true); + } else { + // older code handled this case, so we do it too + fireSidebarFocusedEvent(); + } +} + +function sidebarOnLoad(event) { + var sidebar = document.getElementById("sidebar"); + sidebar.removeEventListener("load", sidebarOnLoad, true); + // We're handling the 'load' event before it bubbles up to the usual + // (non-capturing) event handlers. Let it bubble up before firing the + // SidebarFocused event. + setTimeout(fireSidebarFocusedEvent, 0); +} + +/** + * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar + * a chance to adjust focus as needed. An additional event is needed, because + * we don't want to focus the sidebar when it's opened on startup or in a new + * window, only when the user opens the sidebar. + */ +function fireSidebarFocusedEvent() { + var sidebar = document.getElementById("sidebar"); + var event = document.createEvent("Events"); + event.initEvent("SidebarFocused", true, false); + sidebar.contentWindow.dispatchEvent(event); +} + +var gHomeButton = { + prefDomain: "browser.startup.homepage", + observe: function(aSubject, aTopic, aPrefName) { + if (aTopic != "nsPref:changed" || aPrefName != this.prefDomain) { + return; + } + + this.updateTooltip(); + }, + + updateTooltip: function(homeButton) { + if (!homeButton) { + homeButton = document.getElementById("home-button"); + } + if (homeButton) { + var homePage = this.getHomePage(); + homePage = homePage.replace(/\|/g,', '); + if (homePage.toLowerCase() == "about:home") { + homeButton.setAttribute("tooltiptext", homeButton.getAttribute("aboutHomeOverrideTooltip")); + } else { + homeButton.setAttribute("tooltiptext", homePage); + } + } + }, + + getHomePage: function() { + var url; + try { + url = gPrefService.getComplexValue(this.prefDomain, + Components.interfaces.nsIPrefLocalizedString).data; + } catch(e) {} + + // use this if we can't find the pref + if (!url) { + var configBundle = Services.strings + .createBundle("chrome://branding/locale/browserconfig.properties"); + url = configBundle.GetStringFromName(this.prefDomain); + } + + return url; + }, + + updatePersonalToolbarStyle: function(homeButton) { + if (!homeButton) { + homeButton = document.getElementById("home-button"); + } + if (homeButton) { + homeButton.className = homeButton.parentNode.id == "PersonalToolbar" || + homeButton.parentNode.parentNode.id == "PersonalToolbar" ? + homeButton.className.replace("toolbarbutton-1", "bookmark-item") : + homeButton.className.replace("bookmark-item", "toolbarbutton-1"); + } + } +}; + +/** + * Gets the selected text in the active browser. Leading and trailing + * whitespace is removed, and consecutive whitespace is replaced by a single + * space. A maximum of 150 characters will be returned, regardless of the value + * of aCharLen. + * + * @param aCharLen + * The maximum number of characters to return. + */ +function getBrowserSelection(aCharLen) { + // selections of more than 150 characters aren't useful + const kMaxSelectionLen = 150; + const charLen = Math.min(aCharLen || kMaxSelectionLen, kMaxSelectionLen); + let commandDispatcher = document.commandDispatcher; + + var focusedWindow = commandDispatcher.focusedWindow; + var selection = focusedWindow.getSelection().toString(); + // try getting a selected text in text input. + if (!selection) { + let element = commandDispatcher.focusedElement; + var isOnTextInput = function isOnTextInput(elem) { + // we avoid to return a value if a selection is in password field. + // ref. bug 565717 + return elem instanceof HTMLTextAreaElement || + (elem instanceof HTMLInputElement && elem.mozIsTextField(true)); + }; + + if (isOnTextInput(element)) { + selection = element.QueryInterface(Ci.nsIDOMNSEditableElement) + .editor.selection.toString(); + } + } + + if (selection) { + if (selection.length > charLen) { + // only use the first charLen important chars. see bug 221361 + var pattern = new RegExp("^(?:\\s*.){0," + charLen + "}"); + pattern.test(selection); + selection = RegExp.lastMatch; + } + + selection = selection.trim().replace(/\s+/g, " "); + + if (selection.length > charLen) { + selection = selection.substr(0, charLen); + } + } + return selection; +} + +var gWebPanelURI; +function openWebPanel(aTitle, aURI) { + // Ensure that the web panels sidebar is open. + toggleSidebar('viewWebPanelsSidebar', true); + + // Set the title of the panel. + document.getElementById("sidebar-title").value = aTitle; + + // Tell the Web Panels sidebar to load the bookmark. + var sidebar = document.getElementById("sidebar"); + if (sidebar.docShell && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser')) { + sidebar.contentWindow.loadWebPanel(aURI); + if (gWebPanelURI) { + gWebPanelURI = ""; + sidebar.removeEventListener("load", asyncOpenWebPanel, true); + } + } else { + // The panel is still being constructed. Attach an onload handler. + if (!gWebPanelURI) + sidebar.addEventListener("load", asyncOpenWebPanel, true); + gWebPanelURI = aURI; + } +} + +function asyncOpenWebPanel(event) { + var sidebar = document.getElementById("sidebar"); + if (gWebPanelURI && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser')) { + sidebar.contentWindow.loadWebPanel(gWebPanelURI); + } + gWebPanelURI = ""; + sidebar.removeEventListener("load", asyncOpenWebPanel, true); +} + +/* + * - [ Dependencies ] --------------------------------------------------------- + * utilityOverlay.js: + * - gatherTextUnder + */ + +/** + * Extracts linkNode and href for the current click target. + * + * @param event + * The click event. + * @return [href, linkNode]. + * + * @note linkNode will be null if the click wasn't on an anchor + * element (or XLink). + */ +function hrefAndLinkNodeForClickEvent(event) { + function isHTMLLink(aNode) { + // Be consistent with what nsContextMenu.js does. + return ((aNode instanceof HTMLAnchorElement && aNode.href) || + (aNode instanceof HTMLAreaElement && aNode.href) || + aNode instanceof HTMLLinkElement); + } + + let node = event.target; + while (node && !isHTMLLink(node)) { + node = node.parentNode; + } + + if (node) { + return [node.href, node]; + } + + // If there is no linkNode, try simple XLink. + let href, baseURI; + node = event.target; + while (node && !href) { + if (node.nodeType == Node.ELEMENT_NODE && + (node.localName == "a" || + node.namespaceURI == "http://www.w3.org/1998/Math/MathML")) { + href = node.getAttributeNS("http://www.w3.org/1999/xlink", "href"); + if (href) { + baseURI = node.baseURI; + break; + } + } + node = node.parentNode; + } + + // In case of XLink, we don't return the node we got href from since + // callers expect <a>-like elements. + return [href ? makeURLAbsolute(baseURI, href) : null, null]; +} + +/** + * Called whenever the user clicks in the content area. + * + * @param event + * The click event. + * @param isPanelClick + * Whether the event comes from a web panel. + * @note default event is prevented if the click is handled. + */ +function contentAreaClick(event, isPanelClick) { + if (!event.isTrusted || event.defaultPrevented || event.button == 2) { + return; + } + + let [href, linkNode] = hrefAndLinkNodeForClickEvent(event); + if (!href) { + // Not a link, handle middle mouse navigation. + if (event.button == 1 && + gPrefService.getBoolPref("middlemouse.contentLoadURL") && + !gPrefService.getBoolPref("general.autoScroll")) { + middleMousePaste(event); + event.preventDefault(); + } + return; + } + + // This code only applies if we have a linkNode (i.e. clicks on real anchor + // elements, as opposed to XLink). + if (linkNode && + event.button == 0 && + !event.ctrlKey && + !event.shiftKey && + !event.altKey && + !event.metaKey) { + // A Web panel's links should target the main content area. Do this + // if no modifier keys are down and if there's no target or the target + // equals _main (the IE convention) or _content (the Mozilla convention). + let target = linkNode.target; + let mainTarget = !target || target == "_content" || target == "_main"; + if (isPanelClick && mainTarget) { + // javascript and data links should be executed in the current browser. + if (linkNode.getAttribute("onclick") || + href.startsWith("javascript:") || + href.startsWith("data:")) { + return; + } + + try { + urlSecurityCheck(href, linkNode.ownerDocument.nodePrincipal); + } catch(ex) { + // Prevent loading unsecure destinations. + event.preventDefault(); + return; + } + + loadURI(href, null, null, false); + event.preventDefault(); + return; + } + + if (linkNode.getAttribute("rel") == "sidebar") { + // This is the Opera convention for a special link that, when clicked, + // allows to add a sidebar panel. The link's title attribute contains + // the title that should be used for the sidebar panel. + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "bookmark", + uri: makeURI(href), + title: linkNode.getAttribute("title"), + loadBookmarkInSidebar: true, + hiddenRows: ["description", + "location", + "keyword"] }, + window); + event.preventDefault(); + return; + } + } + + handleLinkClick(event, href, linkNode); + + // Mark the page as a user followed link. This is done so that history can + // distinguish automatic embed visits from user activated ones. For example + // pages loaded in frames are embed visits and lost with the session, while + // visits across frames should be preserved. + try { + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + PlacesUIUtils.markPageAsFollowedLink(href); + } + } catch(ex) { + // Skip invalid URIs. + } +} + +/** + * Handles clicks on links. + * + * @return true if the click event was handled, false otherwise. + */ +function handleLinkClick(event, href, linkNode) { + if (event.button == 2) { + // right click + return false; + } + + var where = whereToOpenLink(event); + if (where == "current") { + return false; + } + + var doc = event.target.ownerDocument; + + if (where == "save") { + saveURL(href, linkNode ? gatherTextUnder(linkNode) : "", null, true, + true, doc.documentURIObject, doc); + event.preventDefault(); + return true; + } + + urlSecurityCheck(href, doc.nodePrincipal); + openLinkIn(href, where, { referrerURI: doc.documentURIObject, + charset: doc.characterSet, + referrerPolicy: doc.referrerPolicy, + originPrincipal: doc.nodePrincipal, + triggeringPrincipal: doc.nodePrincipal }); + event.preventDefault(); + return true; +} + +function middleMousePaste(event) { + let clipboard = readFromClipboard(); + if (!clipboard) { + return; + } + + // Strip embedded newlines and surrounding whitespace, to match the URL + // bar's behavior (stripsurroundingwhitespace) + clipboard = clipboard.replace(/\s*\n\s*/g, ""); + + // if it's not the current tab, we don't need to do anything because the + // browser doesn't exist. + let where = whereToOpenLink(event, true, false); + let lastLocationChange; + if (where == "current") { + lastLocationChange = gBrowser.selectedBrowser.lastLocationChange; + } + + getShortcutOrURIAndPostData(clipboard).then(data => { + try { + makeURI(data.url); + } catch(ex) { + // Not a valid URI. + return; + } + + try { + addToUrlbarHistory(data.url); + } catch(ex) { + // Things may go wrong when adding url to session history, + // but don't let that interfere with the loading of the url. + Cu.reportError(ex); + } + + if (where != "current" || + lastLocationChange == gBrowser.selectedBrowser.lastLocationChange) { + openUILink(data.url, event, + { ignoreButton: true, + disallowInheritPrincipal: !data.mayInheritPrincipal, + initiatingDoc: event ? event.target.ownerDocument : null }); + } + }); + + event.stopPropagation(); +} + +// handleDroppedLink has the following 2 overloads: +// handleDroppedLink(event, url, name) +// handleDroppedLink(event, links) +function handleDroppedLink(event, urlOrLinks, name) { + let links; + if (Array.isArray(urlOrLinks)) { + links = urlOrLinks; + } else { + links = [{ url: urlOrLinks, name, type: "" }]; + } + + let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange; + + let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + if (event.shiftKey) { + inBackground = !inBackground; + } + + Task.spawn(function*() { + let urls = []; + let postDatas = []; + for (let link of links) { + let data = yield getShortcutOrURIAndPostData(link.url); + urls.push(data.url); + postDatas.push(data.postData); + } + if (lastLocationChange == gBrowser.selectedBrowser.lastLocationChange) { + gBrowser.loadTabs(urls, { inBackground, + replace: true, + allowThirdPartyFixup: false, + postDatas }); + } + }); + + // Keep the event from being handled by the dragDrop listeners + // built-in to Goanna if they happen to be above us. + event.preventDefault(); +}; + +function MultiplexHandler(event) { + try { + var node = event.target; + var name = node.getAttribute('name'); + + if (name == 'detectorGroup') { + BrowserCharsetReload(); + SelectDetector(event, false); + } else if (name == 'charsetGroup') { + var charset = node.getAttribute('id'); + charset = charset.substring(charset.indexOf('charset.') + 'charset.'.length); + BrowserSetForcedCharacterSet(charset); + } else if (name == 'charsetCustomize') { + //do nothing - please remove this else statement, once the charset prefs moves to the pref window + } else { + BrowserSetForcedCharacterSet(node.getAttribute('id')); + } + } catch(ex) { + // XXX: Do we really want an alert here with just the error? + alert(ex); + } +} + +function SelectDetector(event, doReload) { + var uri = event.target.getAttribute("id"); + var prefvalue = uri.substring(uri.indexOf('chardet.') + 'chardet.'.length); + if ("off" == prefvalue) { + // "off" is special value to turn off the detectors + prefvalue = ""; + } + + try { + var str = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + + str.data = prefvalue; + gPrefService.setComplexValue("intl.charset.detector", Ci.nsISupportsString, str); + if (doReload) { + window.content.location.reload(); + } + } catch(ex) { + // XXX: Do we really want to use "dump" here? + dump("Failed to set the intl.charset.detector preference.\n"); + } +} + +function BrowserSetForcedCharacterSet(aCharset) { + gBrowser.docShell.charset = aCharset; + // Save the forced character-set + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + PlacesUtils.setCharsetForURI(getWebNavigation().currentURI, aCharset); + } + BrowserCharsetReload(); +} + +function BrowserCharsetReload() { + BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); +} + +function charsetMenuGetElement(parent, id) { + return parent.getElementsByAttribute("id", id)[0]; +} + +function UpdateCurrentCharset(target) { + // extract the charset from DOM + var wnd = document.commandDispatcher.focusedWindow; + if ((window == wnd) || (wnd == null)) { + wnd = window.content; + } + + // Uncheck previous item + if (gPrevCharset) { + var pref_item = charsetMenuGetElement(target, "charset." + gPrevCharset); + if (pref_item) { + pref_item.setAttribute('checked', 'false'); + } + } + + var menuitem = charsetMenuGetElement(target, "charset." + wnd.document.characterSet); + if (menuitem) { + menuitem.setAttribute('checked', 'true'); + } +} + +function UpdateCharsetDetector(target) { + var prefvalue; + + try { + prefvalue = gPrefService.getComplexValue("intl.charset.detector", Ci.nsIPrefLocalizedString).data; + } catch(ex) {} + + if (!prefvalue) { + prefvalue = "off"; + } + + var menuitem = charsetMenuGetElement(target, "chardet." + prefvalue); + if (menuitem) { + menuitem.setAttribute("checked", "true"); + } +} + +function UpdateMenus(event) { + UpdateCurrentCharset(event.target); + UpdateCharsetDetector(event.target); +} + +function charsetLoadListener() { + var charset = window.content.document.characterSet; + + if (charset.length > 0 && (charset != gLastBrowserCharset)) { + gPrevCharset = gLastBrowserCharset; + gLastBrowserCharset = charset; + } +} + +var gPageStyleMenu = { + + _getAllStyleSheets: function(frameset) { + var styleSheetsArray = Array.slice(frameset.document.styleSheets); + for (let i = 0; i < frameset.frames.length; i++) { + let frameSheets = this._getAllStyleSheets(frameset.frames[i]); + styleSheetsArray = styleSheetsArray.concat(frameSheets); + } + return styleSheetsArray; + }, + + fillPopup: function(menuPopup) { + var noStyle = menuPopup.firstChild; + var persistentOnly = noStyle.nextSibling; + var sep = persistentOnly.nextSibling; + while (sep.nextSibling) { + menuPopup.removeChild(sep.nextSibling); + } + + var styleSheets = this._getAllStyleSheets(window.content); + var currentStyleSheets = {}; + var styleDisabled = getMarkupDocumentViewer().authorStyleDisabled; + var haveAltSheets = false; + var altStyleSelected = false; + + for (let currentStyleSheet of styleSheets) { + if (!currentStyleSheet.title) { + continue; + } + + // Skip any stylesheets whose media attribute doesn't match. + if (currentStyleSheet.media.length > 0) { + let mediaQueryList = currentStyleSheet.media.mediaText; + if (!window.content.matchMedia(mediaQueryList).matches) + continue; + } + + if (!currentStyleSheet.disabled) { + altStyleSelected = true; + } + + haveAltSheets = true; + + let lastWithSameTitle = null; + if (currentStyleSheet.title in currentStyleSheets) { + lastWithSameTitle = currentStyleSheets[currentStyleSheet.title]; + } + + if (!lastWithSameTitle) { + let menuItem = document.createElement("menuitem"); + menuItem.setAttribute("type", "radio"); + menuItem.setAttribute("label", currentStyleSheet.title); + menuItem.setAttribute("data", currentStyleSheet.title); + menuItem.setAttribute("checked", !currentStyleSheet.disabled && !styleDisabled); + menuItem.setAttribute("oncommand", "gPageStyleMenu.switchStyleSheet(this.getAttribute('data'));"); + menuPopup.appendChild(menuItem); + currentStyleSheets[currentStyleSheet.title] = menuItem; + } else if (currentStyleSheet.disabled) { + lastWithSameTitle.removeAttribute("checked"); + } + } + + noStyle.setAttribute("checked", styleDisabled); + persistentOnly.setAttribute("checked", !altStyleSelected && !styleDisabled); + persistentOnly.hidden = (window.content.document.preferredStyleSheetSet) ? haveAltSheets : false; + sep.hidden = (noStyle.hidden && persistentOnly.hidden) || !haveAltSheets; + }, + + _stylesheetInFrame: function(frame, title) { + return Array.some(frame.document.styleSheets, + function(stylesheet) { + return stylesheet.title == title; + }); + }, + + _stylesheetSwitchFrame: function(frame, title) { + var docStyleSheets = frame.document.styleSheets; + + for (let i = 0; i < docStyleSheets.length; ++i) { + let docStyleSheet = docStyleSheets[i]; + + if (docStyleSheet.title) { + docStyleSheet.disabled = (docStyleSheet.title != title); + } else if (docStyleSheet.disabled) { + docStyleSheet.disabled = false; + } + } + }, + + _stylesheetSwitchAll: function(frameset, title) { + if (!title || this._stylesheetInFrame(frameset, title)) { + this._stylesheetSwitchFrame(frameset, title); + } + + for (let i = 0; i < frameset.frames.length; i++) { + this._stylesheetSwitchAll(frameset.frames[i], title); + } + }, + + switchStyleSheet: function(title, contentWindow) { + getMarkupDocumentViewer().authorStyleDisabled = false; + this._stylesheetSwitchAll(contentWindow || content, title); + }, + + disableStyle: function() { + getMarkupDocumentViewer().authorStyleDisabled = true; + }, +}; + +/* Legacy global page-style functions */ +var getAllStyleSheets = gPageStyleMenu._getAllStyleSheets.bind(gPageStyleMenu); +var stylesheetFillPopup = gPageStyleMenu.fillPopup.bind(gPageStyleMenu); +function stylesheetSwitchAll(contentWindow, title) { + gPageStyleMenu.switchStyleSheet(title, contentWindow); +} +function setStyleDisabled(disabled) { + if (disabled) { + gPageStyleMenu.disableStyle(); + } +} + +var BrowserOffline = { + _inited: false, + + ///////////////////////////////////////////////////////////////////////////// + // BrowserOffline Public Methods + init: function() { + if (!this._uiElement) { + this._uiElement = document.getElementById("workOfflineMenuitemState"); + } + + Services.obs.addObserver(this, "network:offline-status-changed", false); + + this._updateOfflineUI(Services.io.offline); + + this._inited = true; + }, + + uninit: function() { + if (this._inited) { + Services.obs.removeObserver(this, "network:offline-status-changed"); + } + }, + + toggleOfflineStatus: function() { + var ioService = Services.io; + + // Stop automatic management of the offline status + try { + ioService.manageOfflineStatus = false; + } catch(ex) {} + + if (!ioService.offline && !this._canGoOffline()) { + this._updateOfflineUI(false); + return; + } + + ioService.offline = !ioService.offline; + }, + + ///////////////////////////////////////////////////////////////////////////// + // nsIObserver + observe: function(aSubject, aTopic, aState) { + if (aTopic != "network:offline-status-changed") { + return; + } + + this._updateOfflineUI(aState == "offline"); + }, + + ///////////////////////////////////////////////////////////////////////////// + // BrowserOffline Implementation Methods + _canGoOffline: function() { + try { + var cancelGoOffline = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelGoOffline, "offline-requested", null); + + // Something aborted the quit process. + if (cancelGoOffline.data) { + return false; + } + } catch(ex) {} + + return true; + }, + + _uiElement: null, + _updateOfflineUI: function(aOffline) { + var offlineLocked = gPrefService.prefIsLocked("network.online"); + if (offlineLocked) { + this._uiElement.setAttribute("disabled", "true"); + } + + this._uiElement.setAttribute("checked", aOffline); + } +}; + +var OfflineApps = { + ///////////////////////////////////////////////////////////////////////////// + // OfflineApps Public Methods + init: function() { + Services.obs.addObserver(this, "offline-cache-update-completed", false); + }, + + uninit: function() { + Services.obs.removeObserver(this, "offline-cache-update-completed"); + }, + + handleEvent: function(event) { + if (event.type == "MozApplicationManifest") { + this.offlineAppRequested(event.originalTarget.defaultView); + } + }, + + ///////////////////////////////////////////////////////////////////////////// + // OfflineApps Implementation Methods + + // XXX: _getBrowserWindowForContentWindow and _getBrowserForContentWindow + // were taken from browser/components/feeds/src/WebContentConverter. + _getBrowserWindowForContentWindow: function(aContentWindow) { + return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .wrappedJSObject; + }, + + _getBrowserForContentWindow: function(aBrowserWindow, aContentWindow) { + // This depends on pseudo APIs of browser.js and tabbrowser.xml + aContentWindow = aContentWindow.top; + var browsers = aBrowserWindow.gBrowser.browsers; + for (let browser of browsers) { + if (browser.contentWindow == aContentWindow) { + return browser; + } + } + // handle other browser/iframe elements that may need popupnotifications + let browser = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + if (browser.getAttribute("popupnotificationanchor")) { + return browser; + } + return null; + }, + + _getManifestURI: function(aWindow) { + if (!aWindow.document.documentElement) { + return null; + } + + var attr = aWindow.document.documentElement.getAttribute("manifest"); + if (!attr) { + return null; + } + + try { + var contentURI = makeURI(aWindow.location.href, null, null); + return makeURI(attr, aWindow.document.characterSet, contentURI); + } catch(e) { + return null; + } + }, + + // A cache update isn't tied to a specific window. Try to find + // the best browser in which to warn the user about space usage + _getBrowserForCacheUpdate: function(aCacheUpdate) { + // Prefer the current browser + var uri = this._getManifestURI(content); + if (uri && uri.equals(aCacheUpdate.manifestURI)) { + return gBrowser.selectedBrowser; + } + + var browsers = gBrowser.browsers; + for (let browser of browsers) { + uri = this._getManifestURI(browser.contentWindow); + if (uri && uri.equals(aCacheUpdate.manifestURI)) { + return browser; + } + } + + // is this from a non-tab browser/iframe? + browsers = document.querySelectorAll("iframe[popupnotificationanchor] | browser[popupnotificationanchor]"); + for (let browser of browsers) { + uri = this._getManifestURI(browser.contentWindow); + if (uri && uri.equals(aCacheUpdate.manifestURI)) { + return browser; + } + } + + return null; + }, + + _warnUsage: function(aBrowser, aURI) { + if (!aBrowser) { + return; + } + + let mainAction = { + label: gNavigatorBundle.getString("offlineApps.manageUsage"), + accessKey: gNavigatorBundle.getString("offlineApps.manageUsageAccessKey"), + callback: OfflineApps.manage + }; + + let warnQuota = gPrefService.getIntPref("offline-apps.quota.warn"); + let message = gNavigatorBundle.getFormattedString("offlineApps.usage", + [ aURI.host, + warnQuota / 1024 ]); + + let anchorID = "indexedDB-notification-icon"; + PopupNotifications.show(aBrowser, "offline-app-usage", message, + anchorID, mainAction); + + // Now that we've warned once, prevent the warning from showing up + // again. + Services.perms.add(aURI, "offline-app", + Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN); + }, + + // XXX: duplicated in preferences/advanced.js + _getOfflineAppUsage: function(host, groups) { + var cacheService = Cc["@mozilla.org/network/application-cache-service;1"] + .getService(Ci.nsIApplicationCacheService); + if (!groups) { + try { + groups = cacheService.getGroups(); + } catch(ex) { + // Cache disabled. + return 0; + } + } + + var usage = 0; + for (let group of groups) { + var uri = Services.io.newURI(group, null, null); + if (uri.asciiHost == host) { + var cache = cacheService.getActiveCache(group); + usage += cache.usage; + } + } + + return usage; + }, + + _checkUsage: function(aURI) { + // if the user has already allowed excessive usage, don't bother checking + if (Services.perms.testExactPermission(aURI, "offline-app") != + Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN) { + var usage = this._getOfflineAppUsage(aURI.asciiHost); + var warnQuota = gPrefService.getIntPref("offline-apps.quota.warn"); + if (usage >= warnQuota * 1024) { + return true; + } + } + + return false; + }, + + offlineAppRequested: function(aContentWindow) { + if (!gPrefService.getBoolPref("browser.offline-apps.notify")) { + return; + } + + let browserWindow = this._getBrowserWindowForContentWindow(aContentWindow); + let browser = this._getBrowserForContentWindow(browserWindow, aContentWindow); + + let currentURI = aContentWindow.document.documentURIObject; + + // don't bother showing UI if the user has already made a decision + if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION) { + return; + } + + try { + if (gPrefService.getBoolPref("offline-apps.allow_by_default")) { + // all pages can use offline capabilities, no need to ask the user + return; + } + } catch(e) { + // this pref isn't set by default, ignore failures + } + + let host = currentURI.asciiHost; + let notificationID = "offline-app-requested-" + host; + let notification = PopupNotifications.getNotification(notificationID, browser); + + if (notification) { + notification.options.documents.push(aContentWindow.document); + } else { + let mainAction = { + label: gNavigatorBundle.getString("offlineApps.allow"), + accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"), + callback: function() { + for (let document of notification.options.documents) { + OfflineApps.allowSite(document); + } + } + }; + let secondaryActions = [{ + label: gNavigatorBundle.getString("offlineApps.never"), + accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"), + callback: function() { + for (let document of notification.options.documents) { + OfflineApps.disallowSite(document); + } + } + }]; + let message = gNavigatorBundle.getFormattedString("offlineApps.available", + [ host ]); + let anchorID = "indexedDB-notification-icon"; + let options= { documents : [ aContentWindow.document ] }; + notification = PopupNotifications.show(browser, notificationID, message, + anchorID, mainAction, + secondaryActions, options); + } + }, + + allowSite: function(aDocument) { + Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.ALLOW_ACTION); + + // When a site is enabled while loading, manifest resources will + // start fetching immediately. This one time we need to do it + // ourselves. + this._startFetching(aDocument); + }, + + disallowSite: function(aDocument) { + Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.DENY_ACTION); + }, + + manage: function() { + openAdvancedPreferences("networkTab"); + }, + + _startFetching: function(aDocument) { + if (!aDocument.documentElement) { + return; + } + + var manifest = aDocument.documentElement.getAttribute("manifest"); + if (!manifest) { + return; + } + + var manifestURI = makeURI(manifest, aDocument.characterSet, + aDocument.documentURIObject); + + var updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"] + .getService(Ci.nsIOfflineCacheUpdateService); + updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject, window); + }, + + ///////////////////////////////////////////////////////////////////////////// + // nsIObserver + observe: function(aSubject, aTopic, aState) + { + if (aTopic == "offline-cache-update-completed") { + var cacheUpdate = aSubject.QueryInterface(Ci.nsIOfflineCacheUpdate); + + var uri = cacheUpdate.manifestURI; + if (OfflineApps._checkUsage(uri)) { + var browser = this._getBrowserForCacheUpdate(cacheUpdate); + if (browser) { + OfflineApps._warnUsage(browser, cacheUpdate.manifestURI); + } + } + } + } +}; + +var IndexedDBPromptHelper = { + _permissionsPrompt: "indexedDB-permissions-prompt", + _permissionsResponse: "indexedDB-permissions-response", + + _quotaPrompt: "indexedDB-quota-prompt", + _quotaResponse: "indexedDB-quota-response", + _quotaCancel: "indexedDB-quota-cancel", + + _notificationIcon: "indexedDB-notification-icon", + + init: + function IndexedDBPromptHelper_init() { + Services.obs.addObserver(this, this._permissionsPrompt, false); + Services.obs.addObserver(this, this._quotaPrompt, false); + Services.obs.addObserver(this, this._quotaCancel, false); + }, + + uninit: + function IndexedDBPromptHelper_uninit() { + Services.obs.removeObserver(this, this._permissionsPrompt); + Services.obs.removeObserver(this, this._quotaPrompt); + Services.obs.removeObserver(this, this._quotaCancel); + }, + + observe: + function IndexedDBPromptHelper_observe(subject, topic, data) { + if (topic != this._permissionsPrompt && + topic != this._quotaPrompt && + topic != this._quotaCancel) { + throw new Error("Unexpected topic!"); + } + + var requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor); + + var contentWindow = requestor.getInterface(Ci.nsIDOMWindow); + var contentDocument = contentWindow.document; + var browserWindow = OfflineApps._getBrowserWindowForContentWindow(contentWindow); + + if (browserWindow != window) { + // Must belong to some other window. + return; + } + + var browser = OfflineApps._getBrowserForContentWindow(browserWindow, contentWindow); + + var host = contentDocument.documentURIObject.asciiHost; + + var message; + var responseTopic; + if (topic == this._permissionsPrompt) { + message = gNavigatorBundle.getFormattedString("offlineApps.available", [ host ]); + responseTopic = this._permissionsResponse; + } else if (topic == this._quotaPrompt) { + message = gNavigatorBundle.getFormattedString("indexedDB.usage", [ host, data ]); + responseTopic = this._quotaResponse; + } else if (topic == this._quotaCancel) { + responseTopic = this._quotaResponse; + } + + const hiddenTimeoutDuration = 30000; // 30 seconds + const firstTimeoutDuration = 300000; // 5 minutes + + var timeoutId; + + var observer = requestor.getInterface(Ci.nsIObserver); + + var mainAction = { + label: gNavigatorBundle.getString("offlineApps.allow"), + accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"), + callback: function() { + clearTimeout(timeoutId); + observer.observe(null, responseTopic, + Ci.nsIPermissionManager.ALLOW_ACTION); + } + }; + + var secondaryActions = [ + { + label: gNavigatorBundle.getString("offlineApps.never"), + accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"), + callback: function() { + clearTimeout(timeoutId); + observer.observe(null, responseTopic, + Ci.nsIPermissionManager.DENY_ACTION); + } + } + ]; + + // This will be set to the result of PopupNotifications.show() below, or to + // the result of PopupNotifications.getNotification() if this is a + // quotaCancel notification. + var notification; + + function timeoutNotification() { + // Remove the notification. + if (notification) { + notification.remove(); + } + + // Clear all of our timeout stuff. We may be called directly, not just + // when the timeout actually elapses. + clearTimeout(timeoutId); + + // And tell the page that the popup timed out. + observer.observe(null, responseTopic, + Ci.nsIPermissionManager.UNKNOWN_ACTION); + } + + var options = { + eventCallback: function(state) { + // Don't do anything if the timeout has not been set yet. + if (!timeoutId) { + return; + } + + // If the popup is being dismissed start the short timeout. + if (state == "dismissed") { + clearTimeout(timeoutId); + timeoutId = setTimeout(timeoutNotification, hiddenTimeoutDuration); + return; + } + + // If the popup is being re-shown then clear the timeout allowing + // unlimited waiting. + if (state == "shown") { + clearTimeout(timeoutId); + } + } + }; + + if (topic == this._quotaCancel) { + notification = PopupNotifications.getNotification(this._quotaPrompt, browser); + timeoutNotification(); + return; + } + + notification = PopupNotifications.show(browser, topic, message, + this._notificationIcon, mainAction, + secondaryActions, options); + + // Set the timeoutId after the popup has been created, and use the long + // timeout value. If the user doesn't notice the popup after this amount of + // time then it is most likely not visible and we want to alert the page. + timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration); + } +}; + +function WindowIsClosing() { + let event = document.createEvent("Events"); + event.initEvent("WindowIsClosing", true, true); + if (!window.dispatchEvent(event)) { + return false; + } + + if (!closeWindow(false, warnAboutClosingWindow)) { + return false; + } + + for (let browser of gBrowser.browsers) { + let ds = browser.docShell; + if (ds.contentViewer && !ds.contentViewer.permitUnload()) { + return false; + } + } + + return true; +} + +/** + * Checks if this is the last full *browser* window around. If it is, this will + * be communicated like quitting. Otherwise, we warn about closing multiple tabs. + * @returns true if closing can proceed, false if it got cancelled. + */ +function warnAboutClosingWindow() { + // Popups aren't considered full browser windows. + let isPBWindow = PrivateBrowsingUtils.isWindowPrivate(window); + if (!isPBWindow && !toolbar.visible) { + return gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); + } + + // Figure out if there's at least one other browser window around. + let e = Services.wm.getEnumerator("navigator:browser"); + let otherPBWindowExists = false; + let nonPopupPresent = false; + while (e.hasMoreElements()) { + let win = e.getNext(); + if (win != window) { + if (isPBWindow && PrivateBrowsingUtils.isWindowPrivate(win)) { + otherPBWindowExists = true; + } + if (win.toolbar.visible) { + nonPopupPresent = true; + } + // If the current window is not in private browsing mode we don't need to + // look for other pb windows, we can leave the loop when finding the + // first non-popup window. If however the current window is in private + // browsing mode then we need at least one other pb and one non-popup + // window to break out early. + if ((!isPBWindow || otherPBWindowExists) && nonPopupPresent) { + break; + } + } + } + + if (isPBWindow && !otherPBWindowExists) { + let exitingCanceled = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + exitingCanceled.data = false; + Services.obs.notifyObservers(exitingCanceled, "last-pb-context-exiting", null); + if (exitingCanceled.data) { + return false; + } + } + + if (nonPopupPresent) { + return isPBWindow || gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); + } + + let os = Services.obs; + + let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + os.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null); + if (closingCanceled.data) { + return false; + } + + os.notifyObservers(null, "browser-lastwindow-close-granted", null); + + return true; +} + +var MailIntegration = { + sendLinkForWindow: function(aWindow) { + this.sendMessage(aWindow.location.href, + aWindow.document.title); + }, + + sendMessage: function(aBody, aSubject) { + // generate a mailto url based on the url and the url's title + var mailtoUrl = "mailto:"; + if (aBody) { + mailtoUrl += "?body=" + encodeURIComponent(aBody); + mailtoUrl += "&subject=" + encodeURIComponent(aSubject); + } + + var uri = makeURI(mailtoUrl); + + // now pass this uri to the operating system + this._launchExternalUrl(uri); + }, + + // a generic method which can be used to pass arbitrary urls to the operating + // system. + // aURL --> a nsIURI which represents the url to launch + _launchExternalUrl: function(aURL) { + var extProtocolSvc = + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + if (extProtocolSvc) { + extProtocolSvc.loadUrl(aURL); + } + } +}; + +function BrowserOpenAddonsMgr(aView) { + if (aView) { + let emWindow; + let browserWindow; + + var receivePong = function (aSubject, aTopic, aData) { + let browserWin = aSubject.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + if (!emWindow || browserWin == window /* favor the current window */) { + emWindow = aSubject; + browserWindow = browserWin; + } + } + Services.obs.addObserver(receivePong, "EM-pong", false); + Services.obs.notifyObservers(null, "EM-ping", ""); + Services.obs.removeObserver(receivePong, "EM-pong"); + + if (emWindow) { + emWindow.loadView(aView); + browserWindow.gBrowser.selectedTab = + browserWindow.gBrowser._getTabForContentWindow(emWindow); + emWindow.focus(); + return; + } + } + + var newLoad = !switchToTabHavingURI("about:addons", true); + + if (aView) { + // This must be a new load, else the ping/pong would have + // found the window above. + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.removeObserver(observer, aTopic); + aSubject.loadView(aView); + }, "EM-loaded", false); + } +} + +function BrowserOpenPermissionsMgr() { + switchToTabHavingURI("about:permissions", true); +} + +function AddKeywordForSearchField() { + var node = document.popupNode; + + var charset = node.ownerDocument.characterSet; + + var docURI = makeURI(node.ownerDocument.URL, + charset); + + var formURI = makeURI(node.form.getAttribute("action"), + charset, + docURI); + + var spec = formURI.spec; + + var isURLEncoded = (node.form.method.toUpperCase() == "POST" && + (node.form.enctype == "application/x-www-form-urlencoded" || + node.form.enctype == "")); + + var title = gNavigatorBundle.getFormattedString("addKeywordTitleAutoFill", + [node.ownerDocument.title]); + var description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument); + + var formData = []; + + function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) { + if (aIsFormUrlEncoded) { + return escape(aName + "=" + aValue); + } else { + return escape(aName) + "=" + escape(aValue); + } + } + + for (let el of node.form.elements) { + if (!el.type) { + // happens with fieldsets + continue; + } + + if (el == node) { + formData.push((isURLEncoded) ? + escapeNameValuePair(el.name, "%s", true) : + // Don't escape "%s", just append + escapeNameValuePair(el.name, "", false) + "%s"); + continue; + } + + let type = el.type.toLowerCase(); + + if (((el instanceof HTMLInputElement && el.mozIsTextField(true)) || + type == "hidden" || type == "textarea") || + ((type == "checkbox" || type == "radio") && el.checked)) { + formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded)); + } else if (el instanceof HTMLSelectElement && el.selectedIndex >= 0) { + for (var j=0; j < el.options.length; j++) { + if (el.options[j].selected) { + formData.push(escapeNameValuePair(el.name, el.options[j].value, + isURLEncoded)); + } + } + } + } + + var postData; + + if (isURLEncoded) { + postData = formData.join("&"); + } else { + spec += "?" + formData.join("&"); + } + + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "bookmark", + uri: makeURI(spec), + title: title, + description: description, + keyword: "", + postData: postData, + charSet: charset, + hiddenRows: [ "location", + "description", + "tags", + "loadInSidebar" ] + }, window); +} + +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 (var run = 0; run < aWindow.frames.length; run++) { + SwitchDocumentDirection(aWindow.frames[run]); + } +} + +function convertFromUnicode(charset, str) +{ + try { + var unicodeConverter = Components + .classes["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); + unicodeConverter.charset = charset; + str = unicodeConverter.ConvertFromUnicode(str); + return str + unicodeConverter.Finish(); + } catch(ex) { + return null; + } +} + +/** + * Re-open a closed tab. + * @param aIndex + * The index of the tab (via nsSessionStore.getClosedTabData) + * @returns a reference to the reopened tab. + */ +function undoCloseTab(aIndex) { + // wallpaper patch to prevent an unnecessary blank tab (bug 343895) + var blankTabToRemove = null; + if (gBrowser.tabs.length == 1 && + !gPrefService.getBoolPref("browser.tabs.autoHide") && + isTabEmpty(gBrowser.selectedTab)) { + blankTabToRemove = gBrowser.selectedTab; + } + + var tab = null; + var ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore); + if (ss.getClosedTabCount(window) > (aIndex || 0)) { + tab = ss.undoCloseTab(window, aIndex || 0); + + if (blankTabToRemove) { + gBrowser.removeTab(blankTabToRemove); + } + } + + return tab; +} + +/** + * Re-open a closed window. + * @param aIndex + * The index of the window (via nsSessionStore.getClosedWindowData) + * @returns a reference to the reopened window. + */ +function undoCloseWindow(aIndex) { + let ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore); + let window = null; + if (ss.getClosedWindowCount() > (aIndex || 0)) { + window = ss.undoCloseWindow(aIndex || 0); + } + + return window; +} + +/* + * Determines if a tab is "empty", usually used in the context of determining + * if it's ok to close the tab. + */ +function isTabEmpty(aTab) { + if (aTab.hasAttribute("busy")) { + return false; + } + + let browser = aTab.linkedBrowser; + if (!isBlankPageURL(browser.currentURI.spec)) { + return false; + } + + if (browser.contentWindow.opener) { + return false; + } + + if (browser.sessionHistory && browser.sessionHistory.count >= 2) { + return false; + } + + return true; +} + +#ifdef MOZ_SERVICES_SYNC +function BrowserOpenSyncTabs() { + switchToTabHavingURI("about:sync-tabs", true); +} +#endif + +/** + * Format a URL + * eg: + * echo formatURL("https://addons.mozilla.org/%LOCALE%/%APP%/%VERSION%/"); + * > https://addons.mozilla.org/en-US/firefox/3.0a1/ + * + * Currently supported built-ins are LOCALE, APP, and any value from nsIXULAppInfo, uppercased. + */ +function formatURL(aFormat, aIsPref) { + var formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter); + return aIsPref ? formatter.formatURLPref(aFormat) : formatter.formatURL(aFormat); +} + +/** + * Utility object to handle manipulations of the identity indicators in the UI + */ +var gIdentityHandler = { + // Mode strings used to control CSS display + IDENTITY_MODE_IDENTIFIED : "verifiedIdentity", // High-quality identity information + IDENTITY_MODE_DOMAIN_VERIFIED : "verifiedDomain", // Minimal SSL CA-signed domain verification + IDENTITY_MODE_UNKNOWN : "unknownIdentity", // No trusted identity information + IDENTITY_MODE_MIXED_CONTENT : "unknownIdentity mixedContent", // SSL with unauthenticated content + IDENTITY_MODE_MIXED_ACTIVE_CONTENT : "unknownIdentity mixedContent mixedActiveContent", // SSL with unauthenticated content + IDENTITY_MODE_CHROMEUI : "chromeUI", // Part of the product's UI + + // Cache the most recent SSLStatus and Location seen in checkIdentity + _lastStatus : null, + _lastLocation : null, + _mode : "unknownIdentity", + + // smart getters + get _encryptionLabel () { + delete this._encryptionLabel; + this._encryptionLabel = {}; + this._encryptionLabel[this.IDENTITY_MODE_DOMAIN_VERIFIED] = + gNavigatorBundle.getString("identity.encrypted"); + this._encryptionLabel[this.IDENTITY_MODE_IDENTIFIED] = + gNavigatorBundle.getString("identity.encrypted"); + this._encryptionLabel[this.IDENTITY_MODE_UNKNOWN] = + gNavigatorBundle.getString("identity.unencrypted"); + this._encryptionLabel[this.IDENTITY_MODE_MIXED_CONTENT] = + gNavigatorBundle.getString("identity.mixed_content"); + this._encryptionLabel[this.IDENTITY_MODE_MIXED_ACTIVE_CONTENT] = + gNavigatorBundle.getString("identity.mixed_content"); + return this._encryptionLabel; + }, + get _identityPopup () { + delete this._identityPopup; + return this._identityPopup = document.getElementById("identity-popup"); + }, + get _identityBox () { + delete this._identityBox; + return this._identityBox = document.getElementById("identity-box"); + }, + get _identityPopupContentBox () { + delete this._identityPopupContentBox; + return this._identityPopupContentBox = + document.getElementById("identity-popup-content-box"); + }, + get _identityPopupContentHost () { + delete this._identityPopupContentHost; + return this._identityPopupContentHost = + document.getElementById("identity-popup-content-host"); + }, + get _identityPopupContentOwner () { + delete this._identityPopupContentOwner; + return this._identityPopupContentOwner = + document.getElementById("identity-popup-content-owner"); + }, + get _identityPopupContentSupp () { + delete this._identityPopupContentSupp; + return this._identityPopupContentSupp = + document.getElementById("identity-popup-content-supplemental"); + }, + get _identityPopupContentVerif () { + delete this._identityPopupContentVerif; + return this._identityPopupContentVerif = + document.getElementById("identity-popup-content-verifier"); + }, + get _identityPopupEncLabel () { + delete this._identityPopupEncLabel; + return this._identityPopupEncLabel = + document.getElementById("identity-popup-encryption-label"); + }, + get _identityIconLabel () { + delete this._identityIconLabel; + return this._identityIconLabel = document.getElementById("identity-icon-label"); + }, + get _overrideService () { + delete this._overrideService; + return this._overrideService = Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService); + }, + get _identityIconCountryLabel () { + delete this._identityIconCountryLabel; + return this._identityIconCountryLabel = document.getElementById("identity-icon-country-label"); + }, + get _identityIcon () { + delete this._identityIcon; + return this._identityIcon = document.getElementById("page-proxy-favicon"); + }, + + /** + * Rebuild cache of the elements that may or may not exist depending + * on whether there's a location bar. + */ + _cacheElements : function() { + delete this._identityBox; + delete this._identityIconLabel; + delete this._identityIconCountryLabel; + delete this._identityIcon; + this._identityBox = document.getElementById("identity-box"); + this._identityIconLabel = document.getElementById("identity-icon-label"); + this._identityIconCountryLabel = document.getElementById("identity-icon-country-label"); + this._identityIcon = document.getElementById("page-proxy-favicon"); + }, + + /** + * Handler for mouseclicks on the "More Information" button in the + * "identity-popup" panel. + */ + handleMoreInfoClick : function(event) { + displaySecurityInfo(); + event.stopPropagation(); + }, + + /** + * Helper to parse out the important parts of _lastStatus (of the SSL cert in + * particular) for use in constructing identity UI strings + */ + getIdentityData : function() { + var result = {}; + var status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus); + var cert = status.serverCert; + + // Human readable name of Subject + result.subjectOrg = cert.organization; + + // SubjectName fields, broken up for individual access + if (cert.subjectName) { + result.subjectNameFields = {}; + cert.subjectName.split(",").forEach(function(v) { + var field = v.split("="); + this[field[0]] = field[1]; + }, result.subjectNameFields); + + // Call out city, state, and country specifically + result.city = result.subjectNameFields.L; + result.state = result.subjectNameFields.ST; + result.country = result.subjectNameFields.C; + } + + // Human readable name of Certificate Authority + result.caOrg = cert.issuerOrganization || cert.issuerCommonName; + result.cert = cert; + + return result; + }, + + /** + * Determine the identity of the page being displayed by examining its SSL cert + * (if available) and, if necessary, update the UI to reflect this. Intended to + * be called by onSecurityChange + * + * @param PRUint32 state + * @param JS Object location that mirrors an nsLocation (i.e. has .host and + * .hostname and .port) + */ + checkIdentity : function(state, location) { + var currentStatus = gBrowser.securityUI + .QueryInterface(Components.interfaces.nsISSLStatusProvider) + .SSLStatus; + this._lastStatus = currentStatus; + this._lastLocation = location; + + let nsIWebProgressListener = Ci.nsIWebProgressListener; + if (location.protocol == "chrome:" || location.protocol == "about:") { + this.setMode(this.IDENTITY_MODE_CHROMEUI); + } else if (state & nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) { + this.setMode(this.IDENTITY_MODE_IDENTIFIED); + } else if (state & nsIWebProgressListener.STATE_IS_SECURE) { + this.setMode(this.IDENTITY_MODE_DOMAIN_VERIFIED); + } else if (state & nsIWebProgressListener.STATE_IS_BROKEN) { + if ((state & nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) && + gPrefService.getBoolPref("security.mixed_content.block_active_content")) { + this.setMode(this.IDENTITY_MODE_MIXED_ACTIVE_CONTENT); + } else { + this.setMode(this.IDENTITY_MODE_MIXED_CONTENT); + } + } else { + this.setMode(this.IDENTITY_MODE_UNKNOWN); + } + + // Ensure the doorhanger is shown when mixed active content is blocked. + if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) { + this.showMixedContentDoorhanger(); + } + }, + + /** + * Display the Mixed Content Blocker doohanger, providing an option + * to the user to override mixed content blocking + */ + showMixedContentDoorhanger : function() { + // If we've already got an active notification, bail out to avoid showing it repeatedly. + if (PopupNotifications.getNotification("mixed-content-blocked", gBrowser.selectedBrowser)) { + return; + } + + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let messageString = gNavigatorBundle.getFormattedString("mixedContentBlocked.message", [brandShortName]); + let action = { + label: gNavigatorBundle.getString("mixedContentBlocked.keepBlockingButton.label"), + accessKey: gNavigatorBundle.getString("mixedContentBlocked.keepBlockingButton.accesskey"), + callback: function() { /* NOP */ } + }; + let secondaryActions = [ + { + label: gNavigatorBundle.getString("mixedContentBlocked.unblock.label"), + accessKey: gNavigatorBundle.getString("mixedContentBlocked.unblock.accesskey"), + callback: function() { + // Reload the page with the content unblocked + BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT); + } + } + ]; + let options = { + dismissed: true, + learnMoreURL: Services.urlFormatter.formatURLPref("browser.mixedcontent.warning.infoURL") + }; + PopupNotifications.show(gBrowser.selectedBrowser, "mixed-content-blocked", + messageString, "mixed-content-blocked-notification-icon", + action, secondaryActions, options); + }, + + /** + * Return the eTLD+1 version of the current hostname + */ + getEffectiveHost : function() { + try { + let baseDomain = Services.eTLD.getBaseDomainFromHost(this._lastLocation.hostname); + return this._IDNService.convertToDisplayIDN(baseDomain, {}); + } catch(e) { + // If something goes wrong (e.g. hostname is an IP address) just fall back + // to the full domain. + return this._lastLocation.hostname; + } + }, + + /** + * Update the UI to reflect the specified mode, which should be one of the + * IDENTITY_MODE_* constants. + */ + setMode : function(newMode) { + if (!this._identityBox) { + // No identity box means the identity box is not visible, in which + // case there's nothing to do. + return; + } + + this._identityBox.className = newMode; + this.setIdentityMessages(newMode); + + // Update the popup too, if it's open + if (this._identityPopup.state == "open") { + this.setPopupMessages(newMode); + } + + this._mode = newMode; + }, + + /** + * Set up the messages for the primary identity UI based on the specified mode, + * and the details of the SSL cert, where applicable + * + * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants. + */ + setIdentityMessages : function(newMode) { + let icon_label = ""; + let tooltip = ""; + let icon_country_label = ""; + let icon_labels_dir = "ltr"; + + if (!this._IDNService) { + this._IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + } + let punyID = gPrefService.getIntPref("browser.identity.display_punycode", 1); + + switch (newMode) { + case this.IDENTITY_MODE_MIXED_CONTENT: + case this.IDENTITY_MODE_DOMAIN_VERIFIED: { + let iData = this.getIdentityData(); + + let label_display = ""; + + // Honor browser.identity.ssl_domain_display + switch (gPrefService.getIntPref("browser.identity.ssl_domain_display")) { + case 2 : // Show full domain + label_display = this._lastLocation.hostname; + break; + case 1 : // Show eTLD. + label_display = this.getEffectiveHost(); + } + + if (punyID >= 1) { + // Display punycode version in identity panel + icon_label = this._IDNService.convertUTF8toACE(label_display); + } else { + icon_label = label_display; + } + + // Verifier is either the CA Org, for a normal cert, or a special string + // for certs that are trusted because of a security exception. + tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier", + [iData.caOrg]); + + // Check whether this site is a security exception. XPConnect does the right + // thing here in terms of converting _lastLocation.port from string to int, but + // the overrideService doesn't like undefined ports, so make sure we have + // something in the default case (bug 432241). + // .hostname can return an empty string in some exceptional cases - + // hasMatchingOverride does not handle that, so avoid calling it. + // Updating the tooltip value in those cases isn't critical. + // FIXME: Fixing bug 646690 would probably makes this check unnecessary + if (this._lastLocation.hostname && + this._overrideService.hasMatchingOverride(this._lastLocation.hostname, + (this._lastLocation.port || 443), + iData.cert, {}, {})) { + tooltip = gNavigatorBundle.getString("identity.identified.verified_by_you"); + } + break; + } + case this.IDENTITY_MODE_IDENTIFIED: { + // If it's identified, then we can populate the dialog with credentials + let iData = this.getIdentityData(); + tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier", + [iData.caOrg]); + icon_label = iData.subjectOrg; + if (iData.country) { + icon_country_label = "(" + iData.country + ")"; + } + + // If the organization name starts with an RTL character, then + // swap the positions of the organization and country code labels. + // The Unicode ranges reflect the definition of the UCS2_CHAR_IS_BIDI + // macro in intl/unicharutil/util/nsBidiUtils.h. When bug 218823 gets + // fixed, this test should be replaced by one adhering to the + // Unicode Bidirectional Algorithm proper (at the paragraph level). + icon_labels_dir = /^[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/.test(icon_label) ? "rtl" : "ltr"; + break; + } + case this.IDENTITY_MODE_CHROMEUI: + break; + default: + tooltip = gNavigatorBundle.getString("identity.unknown.tooltip"); + if (punyID == 2) { + // Check for IDN and display if so... + let rawHost = this._IDNService.convertUTF8toACE(this._lastLocation.hostname); + if (this._IDNService.isACE(rawHost)) { + icon_label = rawHost; + } + } + } + + // Push the appropriate strings out to the UI + this._identityBox.tooltipText = tooltip; + this._identityIconLabel.value = icon_label; + this._identityIconCountryLabel.value = icon_country_label; + // Set cropping and direction + this._identityIconLabel.crop = icon_country_label ? "end" : "center"; + this._identityIconLabel.parentNode.style.direction = icon_labels_dir; + // Hide completely if the organization label is empty + this._identityIconLabel.parentNode.collapsed = icon_label ? false : true; + }, + + /** + * Set up the title and content messages for the identity message popup, + * based on the specified mode, and the details of the SSL cert, where + * applicable + * + * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants. + */ + setPopupMessages : function(newMode) { + + this._identityPopup.className = newMode; + this._identityPopupContentBox.className = newMode; + + // Set the static strings up front + this._identityPopupEncLabel.textContent = this._encryptionLabel[newMode]; + + // Initialize the optional strings to empty values + let supplemental = ""; + let verifier = ""; + let host = ""; + let owner = ""; + + switch (newMode) { + case this.IDENTITY_MODE_DOMAIN_VERIFIED: + host = this.getEffectiveHost(); + owner = gNavigatorBundle.getString("identity.ownerUnknown2"); + verifier = this._identityBox.tooltipText; + break; + case this.IDENTITY_MODE_IDENTIFIED: { + // If it's identified, then we can populate the dialog with credentials + let iData = this.getIdentityData(); + host = this.getEffectiveHost(); + owner = iData.subjectOrg; + verifier = this._identityBox.tooltipText; + + // Build an appropriate supplemental block out of whatever location data we have + if (iData.city) { + supplemental += iData.city + "\n"; + } + if (iData.state && iData.country) { + supplemental += gNavigatorBundle.getFormattedString("identity.identified.state_and_country", + [iData.state, iData.country]); + } else if (iData.state) { + // State only + supplemental += iData.state; + } else if (iData.country) { + // Country only + supplemental += iData.country; + } + break; + } + } + + // Push the appropriate strings out to the UI + this._identityPopupContentHost.textContent = host; + this._identityPopupContentOwner.textContent = owner; + this._identityPopupContentSupp.textContent = supplemental; + this._identityPopupContentVerif.textContent = verifier; + }, + + hideIdentityPopup : function() { + this._identityPopup.hidePopup(); + }, + + /** + * Click handler for the identity-box element in primary chrome. + */ + handleIdentityButtonEvent : function(event) { + event.stopPropagation(); + + if ((event.type == "click" && event.button != 0) || + (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE && + event.keyCode != KeyEvent.DOM_VK_RETURN)) { + return; // Left click, space or enter only + } + + // Don't allow left click, space or enter if the location + // is chrome UI or the location has been modified. + if (this._mode == this.IDENTITY_MODE_CHROMEUI || + gURLBar.getAttribute("pageproxystate") != "valid") { + return; + } + + // Make sure that the display:none style we set in xul is removed now that + // the popup is actually needed + this._identityPopup.hidden = false; + + // Update the popup strings + this.setPopupMessages(this._identityBox.className); + + // Add the "open" attribute to the identity box for styling + this._identityBox.setAttribute("open", "true"); + var self = this; + this._identityPopup.addEventListener("popuphidden", function onPopupHidden(e) { + e.currentTarget.removeEventListener("popuphidden", onPopupHidden, false); + self._identityBox.removeAttribute("open"); + }, false); + + // Now open the popup, anchored off the primary chrome element + this._identityPopup.openPopup(this._identityIcon, "bottomcenter topleft"); + }, + + onPopupShown : function(event) { + document.getElementById('identity-popup-more-info-button').focus(); + }, + + onDragStart: function(event) { + if (gURLBar.getAttribute("pageproxystate") != "valid") { + return; + } + + var value = content.location.href; + var urlString = value + "\n" + content.document.title; + var htmlString = "<a href=\"" + value + "\">" + value + "</a>"; + + var dt = event.dataTransfer; + dt.setData("text/x-moz-url", urlString); + dt.setData("text/uri-list", value); + dt.setData("text/plain", value); + dt.setData("text/html", htmlString); + dt.setDragImage(gProxyFavIcon, 16, 16); + } +}; + +function getNotificationBox(aWindow) { + var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document); + if (foundBrowser) { + return gBrowser.getNotificationBox(foundBrowser) + } + return null; +}; + +function getTabModalPromptBox(aWindow) { + var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document); + if (foundBrowser) { + return gBrowser.getTabModalPromptBox(foundBrowser); + } + return null; +}; + +/* DEPRECATED */ +function getBrowser() { + return gBrowser; +} +function getNavToolbox() { + return gNavToolbox; +} + +var gPrivateBrowsingUI = { + init: function() { + // Do nothing for normal windows + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + return; + } + + // Disable the Clear Recent History... menu item when in PB mode + // temporary fix until bug 463607 is fixed + document.getElementById("Tools:Sanitize").setAttribute("disabled", "true"); + + if (window.location.href == getBrowserURL()) { + // Adjust the window's title + let docElement = document.documentElement; + if (!PrivateBrowsingUtils.permanentPrivateBrowsing) { + docElement.setAttribute("title", docElement.getAttribute("title_privatebrowsing")); + docElement.setAttribute("titlemodifier", docElement.getAttribute("titlemodifier_privatebrowsing")); + } + docElement.setAttribute("privatebrowsingmode", PrivateBrowsingUtils.permanentPrivateBrowsing ? + "permanent" : + "temporary"); + gBrowser.updateTitlebar(); + + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Adjust the New Window menu entries + [ + { normal: "menu_newNavigator", private: "menu_newPrivateWindow" }, + { normal: "appmenu_newNavigator", private: "appmenu_newPrivateWindow" }, + ].forEach(function(menu) { + let newWindow = document.getElementById(menu.normal); + let newPrivateWindow = document.getElementById(menu.private); + if (newWindow && newPrivateWindow) { + newPrivateWindow.hidden = true; + newWindow.label = newPrivateWindow.label; + newWindow.accessKey = newPrivateWindow.accessKey; + newWindow.command = newPrivateWindow.command; + } + }); + } + } + + if (gURLBar && + !PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Disable switch to tab autocompletion for private windows + // (not for "Always use private browsing" mode) + gURLBar.setAttribute("autocompletesearchparam", ""); + } + } +}; + + +/** + * Switch to a tab that has a given URI, and focuses its browser window. + * If a matching tab is in this window, it will be switched to. Otherwise, other + * windows will be searched. + * + * @param aURI + * URI to search for + * @param aOpenNew + * True to open a new tab and switch to it, if no existing tab is found. + * If no suitable window is found, a new one will be opened. + * @return True if an existing tab was found, false otherwise + */ +function switchToTabHavingURI(aURI, aOpenNew) { + // This will switch to the tab in aWindow having aURI, if present. + function switchIfURIInWindow(aWindow) { + // Only switch to the tab if neither the source and desination window are + // private and they are not in permanent private borwsing mode + if ((PrivateBrowsingUtils.isWindowPrivate(window) || + PrivateBrowsingUtils.isWindowPrivate(aWindow)) && + !PrivateBrowsingUtils.permanentPrivateBrowsing) { + return false; + } + + let browsers = aWindow.gBrowser.browsers; + for (let i = 0; i < browsers.length; i++) { + let browser = browsers[i]; + if (browser.currentURI.equals(aURI)) { + // Focus the matching window & tab + aWindow.focus(); + aWindow.gBrowser.tabContainer.selectedIndex = i; + return true; + } + } + return false; + } + + // This can be passed either nsIURI or a string. + if (!(aURI instanceof Ci.nsIURI)) { + aURI = Services.io.newURI(aURI, null, null); + } + + let isBrowserWindow = !!window.gBrowser; + + // Prioritise this window. + if (isBrowserWindow && switchIfURIInWindow(window)) { + return true; + } + + let winEnum = Services.wm.getEnumerator("navigator:browser"); + while (winEnum.hasMoreElements()) { + let browserWin = winEnum.getNext(); + // Skip closed (but not yet destroyed) windows, + // and the current window (which was checked earlier). + if (browserWin.closed || browserWin == window) { + continue; + } + if (switchIfURIInWindow(browserWin)) { + return true; + } + } + + // No opened tab has that url. + if (aOpenNew) { + if (isBrowserWindow && isTabEmpty(gBrowser.selectedTab)) { + gBrowser.selectedBrowser.loadURI(aURI.spec); + } else { + openUILinkIn(aURI.spec, "tab"); + } + } + + return false; +} + +function restoreLastSession() { + let ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore); + ss.restoreLastSession(); +} + +var TabContextMenu = { + contextTab: null, + _updateToggleMuteMenuItem(aTab, aConditionFn) { + ["muted", "soundplaying"].forEach(attr => { + if (!aConditionFn || aConditionFn(attr)) { + if (aTab.hasAttribute(attr)) { + aTab.toggleMuteMenuItem.setAttribute(attr, "true"); + } else { + aTab.toggleMuteMenuItem.removeAttribute(attr); + } + } + }); + }, + updateContextMenu: function updateContextMenu(aPopupMenu) { + this.contextTab = aPopupMenu.triggerNode.localName == "tab" ? + aPopupMenu.triggerNode : + gBrowser.selectedTab; + let disabled = gBrowser.tabs.length == 1; + + // Enable the "Close Tab" menuitem when the window doesn't close with the last tab. + document.getElementById("context_closeTab").disabled = + disabled && gBrowser.tabContainer._closeWindowWithLastTab; + + var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple"); + for (let menuItem of menuItems) { + menuItem.disabled = disabled; + } + + disabled = gBrowser.visibleTabs.length == 1; + menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple-visible"); + for (let menuItem of menuItems) { + menuItem.disabled = disabled; + } + + // Session store + document.getElementById("context_undoCloseTab").disabled = + Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore) + .getClosedTabCount(window) == 0; + + // Only one of pin/unpin should be visible + document.getElementById("context_pinTab").hidden = this.contextTab.pinned; + document.getElementById("context_unpinTab").hidden = !this.contextTab.pinned; + + // Disable "Close Tabs to the Right" if there are no tabs + // following it and hide it when the user rightclicked on a pinned + // tab. + document.getElementById("context_closeTabsToTheEnd").disabled = + gBrowser.getTabsToTheEndFrom(this.contextTab).length == 0; + document.getElementById("context_closeTabsToTheEnd").hidden = this.contextTab.pinned; + + // Disable "Close other Tabs" if there is only one unpinned tab and + // hide it when the user rightclicked on a pinned tab. + let unpinnedTabs = gBrowser.visibleTabs.length - gBrowser._numPinnedTabs; + document.getElementById("context_closeOtherTabs").disabled = unpinnedTabs <= 1; + document.getElementById("context_closeOtherTabs").hidden = this.contextTab.pinned; + + // Hide "Bookmark All Tabs" for a pinned tab. Update its state if visible. + let bookmarkAllTabs = document.getElementById("context_bookmarkAllTabs"); + bookmarkAllTabs.hidden = this.contextTab.pinned; + if (!bookmarkAllTabs.hidden) { + PlacesCommandHook.updateBookmarkAllTabsCommand(); + } + + // Adjust the state of the toggle mute menu item. + let toggleMute = document.getElementById("context_toggleMuteTab"); + if (this.contextTab.hasAttribute("muted")) { + toggleMute.label = gNavigatorBundle.getString("unmuteTab.label"); + toggleMute.accessKey = gNavigatorBundle.getString("unmuteTab.accesskey"); + } else { + toggleMute.label = gNavigatorBundle.getString("muteTab.label"); + toggleMute.accessKey = gNavigatorBundle.getString("muteTab.accesskey"); + } + + this.contextTab.toggleMuteMenuItem = toggleMute; + this._updateToggleMuteMenuItem(this.contextTab); + + this.contextTab.addEventListener("TabAttrModified", this, false); + aPopupMenu.addEventListener("popuphiding", this, false); + }, + handleEvent(aEvent) { + switch (aEvent.type) { + case "popuphiding": + gBrowser.removeEventListener("TabAttrModified", this); + aEvent.target.removeEventListener("popuphiding", this); + break; + case "TabAttrModified": + let tab = aEvent.target; + this._updateToggleMuteMenuItem(tab, attr => aEvent.detail.changed.indexOf(attr) >= 0); + break; + } + } +}; + +#ifdef MOZ_DEVTOOLS +// Note: Do not delete! It is used for: base/content/nsContextMenu.js +XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", + "resource://devtools/client/framework/gDevTools.jsm"); +#endif + +// Prompt user to restart the browser in safe mode or normally +function restart(safeMode) { + let promptTitleString = null; + let promptMessageString = null; + let restartTextString = null; + if (safeMode) { + promptTitleString = "safeModeRestartPromptTitle"; + promptMessageString = "safeModeRestartPromptMessage"; + restartTextString = "safeModeRestartButton"; + } else { + promptTitleString = "restartPromptTitle"; + promptMessageString = "restartPromptMessage"; + restartTextString = "restartButton"; + } + + let flags = Ci.nsIAppStartup.eAttemptQuit; + + // Prompt the user to confirm + let promptTitle = gNavigatorBundle.getString(promptTitleString); + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let promptMessage = gNavigatorBundle.getFormattedString(promptMessageString, [brandShortName]); + let restartText = gNavigatorBundle.getString(restartTextString); + let buttonFlags = (Services.prompt.BUTTON_POS_0 * + Services.prompt.BUTTON_TITLE_IS_STRING) + + (Services.prompt.BUTTON_POS_1 * + Services.prompt.BUTTON_TITLE_CANCEL) + + Services.prompt.BUTTON_POS_0_DEFAULT; + + let rv = Services.prompt.confirmEx(window, promptTitle, promptMessage, + buttonFlags, restartText, null, null, + null, {}); + + if (rv == 0) { + // Notify all windows that an application quit has been requested. + let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + // Something aborted the quit process. + if (cancelQuit.data) { + return; + } + + if (safeMode) { + Services.startup.restartInSafeMode(flags); + } else { + Services.startup.quit(flags | Ci.nsIAppStartup.eRestart); + } + } +} + +/* duplicateTabIn duplicates tab in a place specified by the parameter |where|. + * + * |where| can be: + * "tab" new tab + * "tabshifted" same as "tab" but in background if default is to select new + * tabs, and vice versa + * "window" new window + * + * delta is the offset to the history entry that you want to load. + */ +function duplicateTabIn(aTab, where, delta) { + let newTab = Cc['@mozilla.org/browser/sessionstore;1'] + .getService(Ci.nsISessionStore) + .duplicateTab(window, aTab, delta); + + switch (where) { + case "window": + gBrowser.hideTab(newTab); + gBrowser.replaceTabWithWindow(newTab); + break; + case "tabshifted": + // A background tab has been opened, nothing else to do here. + break; + case "tab": + gBrowser.selectedTab = newTab; + break; + } +} + +function toggleAddonBar() { + let addonBar = document.getElementById("addon-bar"); + setToolbarVisibility(addonBar, addonBar.collapsed); +} + +XPCOMUtils.defineLazyGetter(window, "gShowPageResizers", function() { +#ifdef XP_WIN + // Only show resizers on Windows 2000 and XP + return parseFloat(Services.sysinfo.getProperty("version")) < 6; +#else + return false; +#endif +}); + +var MousePosTracker = { + _listeners: [], + _x: 0, + _y: 0, + get _windowUtils() { + delete this._windowUtils; + return this._windowUtils = window.getInterface(Ci.nsIDOMWindowUtils); + }, + + addListener: function(listener) { + if (this._listeners.indexOf(listener) >= 0) { + return; + } + + listener._hover = false; + this._listeners.push(listener); + + this._callListener(listener); + }, + + removeListener: function(listener) { + var index = this._listeners.indexOf(listener); + if (index < 0) { + return; + } + + this._listeners.splice(index, 1); + }, + + handleEvent: function(event) { + var fullZoom = this._windowUtils.fullZoom; + this._x = event.screenX / fullZoom - window.mozInnerScreenX; + this._y = event.screenY / fullZoom - window.mozInnerScreenY; + + this._listeners.forEach(function(listener) { + try { + this._callListener(listener); + } catch(e) { + Cu.reportError(e); + } + }, this); + }, + + _callListener: function(listener) { + let rect = listener.getMouseTargetRect(); + let hover = this._x >= rect.left && + this._x <= rect.right && + this._y >= rect.top && + this._y <= rect.bottom; + + if (hover == listener._hover) { + return; + } + + listener._hover = hover; + + if (hover) { + if (listener.onMouseEnter) { + listener.onMouseEnter(); + } + } else { + if (listener.onMouseLeave) { + listener.onMouseLeave(); + } + } + } +}; + +var BrowserChromeTest = { + _cb: null, + _ready: false, + markAsReady: function() { + this._ready = true; + if (this._cb) { + this._cb(); + this._cb = null; + } + }, + runWhenReady: function(cb) { + if (this._ready) { + cb(); + } else { + this._cb = cb; + } + } +}; + +var ToolbarIconColor = { + init: function() { + this._initialized = true; + + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); + Services.obs.addObserver(this, "lightweight-theme-styling-update", false); + gPrefService.addObserver("ui.colorChanged", this, false); + + // If the window isn't active now, we assume that it has never been active + // before and will soon become active such that inferFromText will be + // called from the initial activate event. + if (Services.focus.activeWindow == window) { + this.inferFromText(); + } + }, + + uninit: function() { + this._initialized = false; + + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); + Services.obs.removeObserver(this, "lightweight-theme-styling-update"); + gPrefService.removeObserver("ui.colorChanged", this); + }, + + handleEvent: function(event) { + switch (event.type) { + case "activate": + case "deactivate": + this.inferFromText(); + break; + } + }, + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "lightweight-theme-styling-update": + // inferFromText needs to run after LightweightThemeConsumer.jsm's + // lightweight-theme-styling-update observer. + setTimeout(() => { this.inferFromText(); }, 0); + break; + case "nsPref:changed": + // system color change + var colorChangedPref = false; + try { + colorChangedPref = gPrefService.getBoolPref("ui.colorChanged"); + } catch(e) {} + // if pref indicates change, call inferFromText() on a small delay + if (colorChangedPref == true) { + setTimeout(() => { this.inferFromText(); }, 300); + } + break; + default: + console.error("ToolbarIconColor: Uncaught topic " + aTopic); + } + }, + + inferFromText: function() { + if (!this._initialized) { + return; + } + + function parseRGB(aColorString) { + let rgb = aColorString.match(/^rgba?\((\d+), (\d+), (\d+)/); + rgb.shift(); + return rgb.map(x => parseInt(x)); + } + + let toolbarSelector = "toolbar:not([collapsed=true])"; + + // The getComputedStyle calls and setting the brighttext are separated in + // two loops to avoid flushing layout and making it dirty repeatedly. + + let luminances = new Map; + for (let toolbar of document.querySelectorAll(toolbarSelector)) { + let [r, g, b] = parseRGB(getComputedStyle(toolbar).color); + let luminance = (2 * r + 5 * g + b) / 8; + luminances.set(toolbar, luminance); + } + + for (let [toolbar, luminance] of luminances) { + if (luminance <= 128) { + toolbar.removeAttribute("brighttext"); + } else { + toolbar.setAttribute("brighttext", "true"); + } + } + + // Clear pref if set, since we're done applying the color changes. + gPrefService.clearUserPref("ui.colorChanged"); + } +} diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul new file mode 100644 index 000000000..59c726124 --- /dev/null +++ b/browser/base/content/browser.xul @@ -0,0 +1,973 @@ +#filter substitution +<?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://browser/content/browser.css" type="text/css"?> + +# Restore title to AppMenu windowed use +<?xml-stylesheet href="chrome://browser/content/browser-title.css" type="text/css"?> + +<?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?> +#ifdef MOZ_DEVTOOLS +<?xml-stylesheet href="chrome://devtools/skin/devtools-browser.css" type="text/css"?> +#endif +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> + +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +# Padlock feature +<?xul-overlay href="chrome://browser/content/padlock.xul"?> +# Improve bookmark menu dragging +<?xul-overlay href="chrome://browser/content/browser-menudragging.xul"?> +# Automatic browser recovery +<?xul-overlay href="chrome://browser/content/autorecovery.xul"?> + + +# All DTD information is stored in a separate file so that it can be shared by +# hiddenWindow.xul. +#include browser-doctype.inc + +<window id="main-window" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="gBrowserInit.onLoad()" onunload="gBrowserInit.onUnload()" onclose="return WindowIsClosing();" + title="&mainWindow.title;" + title_normal="&mainWindow.title;" + title_privatebrowsing="&mainWindow.titlemodifier; &mainWindow.titlePrivateBrowsingSuffix;" + titlemodifier="&mainWindow.titlemodifier;" + titlemodifier_normal="&mainWindow.titlemodifier;" + titlemodifier_privatebrowsing="&mainWindow.titlemodifier; &mainWindow.titlePrivateBrowsingSuffix;" + titlemenuseparator="&mainWindow.titlemodifiermenuseparator;" + lightweightthemes="true" + lightweightthemesfooter="browser-bottombox" + windowtype="navigator:browser" + macanimationtype="document" + screenX="4" screenY="4" + fullscreenbutton="true" + retargetdocumentfocus="urlbar" + persist="screenX screenY width height sizemode"> + +# All JS files which are not content (only) dependent that browser.xul +# wishes to include *must* go into the global-scripts.inc file +# so that they can be shared by macBrowserOverlay.xul. +#include global-scripts.inc +#ifdef MOZ_DEVTOOLS +#include global-devtools-theme-scripts.inc +#endif +<script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/> + +<script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + +<script type="application/javascript" src="chrome://browser/content/places/editBookmarkOverlay.js"/> + +# All sets except for popupsets (commands, keys, stringbundles and broadcasters) *must* go into the +# browser-sets.inc file for sharing with hiddenWindow.xul. +#define FULL_BROWSER_WINDOW +#include browser-sets.inc +#undef FULL_BROWSER_WINDOW + + <popupset id="mainPopupSet"> + <menupopup id="tabContextMenu" + onpopupshowing="if (event.target == this) TabContextMenu.updateContextMenu(this);" + onpopuphidden="if (event.target == this) TabContextMenu.contextTab = null;"> + <menuitem id="context_reloadTab" label="&reloadTab.label;" accesskey="&reloadTab.accesskey;" + oncommand="gBrowser.reloadTab(TabContextMenu.contextTab);"/> + <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/> + <menuseparator/> + <menuitem id="context_pinTab" label="&pinTab.label;" + accesskey="&pinTab.accesskey;" + oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/> + <menuitem id="context_unpinTab" label="&unpinTab.label;" hidden="true" + accesskey="&unpinTab.accesskey;" + oncommand="gBrowser.unpinTab(TabContextMenu.contextTab);"/> + <menuitem id="context_openTabInWindow" label="&moveToNewWindow.label;" + accesskey="&moveToNewWindow.accesskey;" + tbattr="tabbrowser-multiple" + oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/> + <menuseparator/> + <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;" + tbattr="tabbrowser-multiple-visible" + oncommand="gBrowser.reloadAllTabs();"/> + <menuitem id="context_bookmarkAllTabs" + label="&bookmarkAllTabs.label;" + accesskey="&bookmarkAllTabs.accesskey;" + command="Browser:BookmarkAllTabs"/> + <menuitem id="context_closeTabsToTheEnd" label="&closeTabsToTheEnd.label;" accesskey="&closeTabsToTheEnd.accesskey;" + oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab);"/> + <menuitem id="context_closeOtherTabs" label="&closeOtherTabs.label;" accesskey="&closeOtherTabs.accesskey;" + oncommand="gBrowser.removeAllTabsBut(TabContextMenu.contextTab);"/> + <menuseparator/> + <menuitem id="context_undoCloseTab" + label="&undoCloseTab.label;" + accesskey="&undoCloseTab.accesskey;" + observes="History:UndoCloseTab"/> + <menuitem id="context_closeTab" label="&closeTab.label;" accesskey="&closeTab.accesskey;" + oncommand="gBrowser.removeTab(TabContextMenu.contextTab, { animate: true });"/> + </menupopup> + + <!-- bug 415444/582485: event.stopPropagation is here for the cloned version + of this menupopup --> + <menupopup id="backForwardMenu" + onpopupshowing="return FillHistoryMenu(event.target);" + oncommand="gotoHistoryIndex(event); event.stopPropagation();" + onclick="checkForMiddleClick(this, event);"/> + <tooltip id="aHTMLTooltip" page="true"/> + + <!-- for search and content formfill/pw manager --> + <panel type="private-autocomplete" id="PopupAutoComplete" noautofocus="true" hidden="true"/> + + <!-- for url bar autocomplete --> + <panel type="private-autocomplete-richlistbox" id="PopupAutoCompleteRichResult" noautofocus="true" hidden="true"/> + + <!-- for date/time picker. consumeoutsideclicks is set to never, so that + clicks on the anchored input box are never consumed. --> + <panel id="DateTimePickerPanel" + type="arrow" + hidden="true" + orient="vertical" + noautofocus="true" + norolluponanchor="true" + consumeoutsideclicks="never" + level="parent" + tabspecific="true"> + <iframe id="dateTimePopupFrame"/> + </panel> + + <!-- for invalid form error message --> + <panel id="invalid-form-popup" type="arrow" orient="vertical" noautofocus="true" hidden="true" level="parent"> + <description/> + </panel> + + <panel id="editBookmarkPanel" + type="arrow" + orient="vertical" + ignorekeys="true" + consumeoutsideclicks="true" + hidden="true" + onpopupshown="StarUI.panelShown(event);" + aria-labelledby="editBookmarkPanelTitle"> + <row id="editBookmarkPanelHeader" align="center" hidden="true"> + <vbox align="center"> + <image id="editBookmarkPanelStarIcon"/> + </vbox> + <vbox> + <label id="editBookmarkPanelTitle"/> + <description id="editBookmarkPanelDescription"/> + <hbox> + <button id="editBookmarkPanelRemoveButton" + class="editBookmarkPanelHeaderButton" + oncommand="StarUI.removeBookmarkButtonCommand();" + accesskey="&editBookmark.removeBookmark.accessKey;"/> + </hbox> + </vbox> + </row> + <vbox id="editBookmarkPanelContent" flex="1" hidden="true"/> + <hbox id="editBookmarkPanelBottomButtons" pack="end"> +#ifndef XP_UNIX + <button id="editBookmarkPanelDoneButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.done.label;" + default="true" + oncommand="StarUI.panel.hidePopup();"/> + <button id="editBookmarkPanelDeleteButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.cancel.label;" + oncommand="StarUI.cancelButtonOnCommand();"/> +#else + <button id="editBookmarkPanelDeleteButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.cancel.label;" + oncommand="StarUI.cancelButtonOnCommand();"/> + <button id="editBookmarkPanelDoneButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.done.label;" + default="true" + oncommand="StarUI.panel.hidePopup();"/> +#endif + </hbox> + </panel> + + <menupopup id="toolbar-context-menu" + onpopupshowing="onViewToolbarsPopupShowing(event);"> + <menuseparator/> + <menuitem command="cmd_ToggleTabsOnTop" + type="checkbox" + label="&viewTabsOnTop.label;" + accesskey="&viewTabsOnTop.accesskey;"/> + <menuitem command="cmd_CustomizeToolbars" + label="&viewCustomizeToolbar.label;" + accesskey="&viewCustomizeToolbar.accesskey;"/> + </menupopup> + + <menupopup id="blockedPopupOptions" + onpopupshowing="gPopupBlockerObserver.fillPopupList(event);" + onpopuphiding="gPopupBlockerObserver.onPopupHiding(event);"> + <menuitem observes="blockedPopupAllowSite"/> + <menuitem observes="blockedPopupEditSettings"/> + <menuitem observes="blockedPopupDontShowMessage"/> + <menuseparator observes="blockedPopupsSeparator"/> + </menupopup> + + <menupopup id="autohide-context" + onpopupshowing="FullScreen.getAutohide(this.firstChild);"> + <menuitem type="checkbox" label="&fullScreenAutohide.label;" + accesskey="&fullScreenAutohide.accesskey;" + oncommand="FullScreen.setAutohide();"/> + <menuseparator/> + <menuitem label="&fullScreenExit.label;" + accesskey="&fullScreenExit.accesskey;" + oncommand="BrowserFullScreen();"/> + </menupopup> + + <menupopup id="contentAreaContextMenu" pagemenu="start" + onpopupshowing="if (event.target != this) + return true; + gContextMenu = new nsContextMenu(this, event.shiftKey); + if (gContextMenu.shouldDisplay) + updateEditUIVisibility(); + return gContextMenu.shouldDisplay;" + onpopuphiding="if (event.target != this) + return; + gContextMenu.hiding(); + gContextMenu = null; + updateEditUIVisibility();"> +#include browser-context.inc + </menupopup> + + <menupopup id="placesContext"/> + + + <panel id="ctrlTab-panel" class="KUI-panel" hidden="true" norestorefocus="true" level="top"> + <hbox> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + </hbox> + <hbox pack="center"> + <button id="ctrlTab-showAll" class="ctrlTab-preview" noicon="true"/> + </hbox> + </panel> + + <panel id="allTabs-panel" hidden="true" norestorefocus="true" ignorekeys="true" + onmouseover="allTabs._updateTabCloseButton(event);"> + <hbox id="allTabs-meta" align="center"> + <spacer flex="1"/> + <textbox id="allTabs-filter" + tooltiptext="&allTabs.filter.emptyText;" + type="search" + oncommand="allTabs.filter();"/> + <spacer flex="1"/> + <toolbarbutton class="KUI-panel-closebutton" + oncommand="allTabs.close()" + tooltiptext="&closeCmd.label;"/> + </hbox> + <stack id="allTabs-stack"> + <vbox id="allTabs-container"><hbox/></vbox> + <toolbarbutton id="allTabs-tab-close-button" + class="tabs-closebutton close-icon" + oncommand="allTabs.closeTab(event);" + tooltiptext="&closeCmd.label;" + style="visibility:hidden"/> + </stack> + </panel> + + <!-- Bookmarks and history tooltip --> + <tooltip id="bhTooltip"/> + + <panel id="customizeToolbarSheetPopup" + noautohide="true" + sheetstyle="&dialog.dimensions;"/> + + <tooltip id="tabbrowser-tab-tooltip" onpopupshowing="gBrowser.createTooltip(event);"/> + + <tooltip id="back-button-tooltip"> + <label class="tooltip-label" value="&backButton.tooltip;"/> + <label class="tooltip-label" value="&backForwardButtonMenu.tooltip;"/> + </tooltip> + + <tooltip id="forward-button-tooltip"> + <label class="tooltip-label" value="&forwardButton.tooltip;"/> + <label class="tooltip-label" value="&backForwardButtonMenu.tooltip;"/> + </tooltip> + +#include popup-notifications.inc + + </popupset> + +#ifdef MOZ_CAN_DRAW_IN_TITLEBAR +<vbox id="titlebar"> + <hbox id="titlebar-content"> +#ifdef MENUBAR_CAN_AUTOHIDE + <hbox id="appmenu-button-container"> + <button id="appmenu-button" + type="menu" + label="&brandShortName;" + tooltiptext="&appMenuButton.tooltip;" + style="-moz-user-focus: ignore;"> +#include browser-appmenu.inc + </button> + </hbox> +#endif + <spacer id="titlebar-spacer" flex="1"/> + <hbox id="titlebar-buttonbox-container" align="start"> + <hbox id="titlebar-buttonbox"> + <toolbarbutton class="titlebar-button" id="titlebar-min" oncommand="window.minimize();"/> + <toolbarbutton class="titlebar-button" id="titlebar-max" oncommand="onTitlebarMaxClick();"/> + <toolbarbutton class="titlebar-button" id="titlebar-close" command="cmd_closeWindow"/> + </hbox> + </hbox> + </hbox> +</vbox> +#endif + +<deck flex="1" id="tab-view-deck"> +<vbox flex="1" id="browser-panel"> + + <toolbox id="navigator-toolbox" + defaultmode="icons" mode="icons" + iconsize="large"> + <!-- Menu --> + <toolbar type="menubar" id="toolbar-menubar" class="chromeclass-menubar" customizable="true" + defaultset="menubar-items" + mode="icons" iconsize="small" defaulticonsize="small" + lockiconsize="true" +#ifdef MENUBAR_CAN_AUTOHIDE + toolbarname="&menubarCmd.label;" + accesskey="&menubarCmd.accesskey;" +#endif + context="toolbar-context-menu"> + <toolbaritem id="menubar-items" align="center"> +# The entire main menubar is placed into browser-menubar.inc, so that it can be shared by +# hiddenWindow.xul. +#include browser-menubar.inc + </toolbaritem> + +#ifdef MOZ_CAN_DRAW_IN_TITLEBAR + <hbox class="titlebar-placeholder" type="appmenu-button" ordinal="0"/> + <hbox class="titlebar-placeholder" type="caption-buttons" ordinal="1000"/> +#endif + </toolbar> + + <toolbar id="nav-bar" class="toolbar-primary chromeclass-toolbar" + toolbarname="&navbarCmd.label;" accesskey="&navbarCmd.accesskey;" + fullscreentoolbar="true" mode="icons" customizable="true" + iconsize="large" + defaultset="unified-back-forward-button,reload-button,stop-button,home-button,urlbar-container,search-container,bookmarks-menu-button,history-menu-button,downloads-button,window-controls" + context="toolbar-context-menu"> + + <toolbaritem id="unified-back-forward-button" class="chromeclass-toolbar-additional" + context="backForwardMenu" removable="true" + forwarddisabled="true" + title="&backForwardItem.title;"> + <toolbarbutton id="back-button" class="toolbarbutton-1" + label="&backCmd.label;" + command="Browser:BackOrBackDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltip="back-button-tooltip"/> + <toolbarbutton id="forward-button" class="toolbarbutton-1" + label="&forwardCmd.label;" + command="Browser:ForwardOrForwardDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltip="forward-button-tooltip"/> + <dummyobservertarget hidden="true" + onbroadcast="if (this.getAttribute('disabled') == 'true') + this.parentNode.setAttribute('forwarddisabled', 'true'); + else + this.parentNode.removeAttribute('forwarddisabled');"> + <observes element="Browser:ForwardOrForwardDuplicate" attribute="disabled"/> + </dummyobservertarget> + </toolbaritem> + + <toolbarbutton id="reload-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&reloadCmd.label;" removable="true" + command="Browser:ReloadOrDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltiptext="&reloadButton.tooltip;"/> + + <toolbarbutton id="stop-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&stopCmd.label;" removable="true" + command="Browser:Stop" + tooltiptext="&stopButton.tooltip;"/> + + <toolbarbutton id="home-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + persist="class" removable="true" + label="&homeButton.label;" + ondragover="homeButtonObserver.onDragOver(event)" + ondragenter="homeButtonObserver.onDragOver(event)" + ondrop="homeButtonObserver.onDrop(event)" + ondragexit="homeButtonObserver.onDragExit(event)" + onclick="BrowserGoHome(event);" + aboutHomeOverrideTooltip="&abouthome.pageTitle;"/> + + <toolbaritem id="urlbar-container" align="center" flex="400" persist="width" combined="true" + title="&locationItem.title;" class="chromeclass-location" removable="true"> + <textbox id="urlbar" flex="1" + placeholder="" + type="private-autocomplete" + autocompletesearch="urlinline history" + autocompletesearchparam="enable-actions" + autocompletepopup="PopupAutoCompleteRichResult" + completeselectedindex="true" + tabscrolling="true" + showcommentcolumn="true" + showimagecolumn="true" + enablehistory="true" + maxrows="6" + newlines="stripsurroundingwhitespace" + oninput="gBrowser.userTypedValue = this.value;" + ontextentered="this.handleCommand(param);" + ontextreverted="return this.handleRevert();" + pageproxystate="invalid" + onfocus="document.getElementById('identity-box').style.MozUserFocus= 'normal'" + onblur="setTimeout(function() document.getElementById('identity-box').style.MozUserFocus = '', 0);"> + <box id="notification-popup-box" hidden="true" align="center"> + <image id="default-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="geo-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="addons-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="indexedDB-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="password-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="plugins-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="web-notifications-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="alert-plugins-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="blocked-plugins-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="mixed-content-blocked-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="pointerLock-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="servicesInstall-notification-icon" class="notification-anchor-icon" role="button"/> + </box> + <!-- Use onclick instead of normal popup= syntax since the popup + code fires onmousedown, and hence eats our favicon drag events. + We only add the identity-box button to the tab order when the location bar + has focus, otherwise pressing F6 focuses it instead of the location bar --> + <box id="identity-box" role="button" + align="center" + onclick="gIdentityHandler.handleIdentityButtonEvent(event);" + onkeypress="gIdentityHandler.handleIdentityButtonEvent(event);" + ondragstart="gIdentityHandler.onDragStart(event);"> + <image id="page-proxy-favicon" + onclick="PageProxyClickHandler(event);" + pageproxystate="invalid"/> + <hbox id="identity-icon-labels"> + <label id="identity-icon-label" class="plain" flex="1"/> + <label id="identity-icon-country-label" class="plain"/> + </hbox> + </box> + <box id="urlbar-display-box" align="center"> + <label id="urlbar-display" value="&urlbar.switchToTab.label;"/> + </box> + <hbox id="urlbar-icons"> + <image id="page-report-button" + class="urlbar-icon" + hidden="true" + tooltiptext="&pageReportIcon.tooltip;" + onclick="gPopupBlockerObserver.onReportButtonClick(event);"/> + <button type="menu" + style="-moz-user-focus: none" + class="plain urlbar-icon" + id="ub-feed-button" + collapsed="true" + tooltiptext="&feedButton.tooltip;" + onclick="return FeedHandler.onFeedButtonPMClick(event);"> + <menupopup position="after_end" + id="ub-feed-menu" + onpopupshowing="return FeedHandler.buildFeedList(this);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </button> + <image id="star-button" + class="urlbar-icon" + onclick="BookmarkingUI.onCommand(event);"/> + <image id="go-button" + class="urlbar-icon" + tooltiptext="&goEndCap.tooltip;" + onclick="gURLBar.handleCommand(event);"/> + </hbox> + <toolbarbutton id="urlbar-go-button" + class="chromeclass-toolbar-additional" + onclick="gURLBar.handleCommand(event);" + tooltiptext="&goEndCap.tooltip;"/> + <toolbarbutton id="urlbar-reload-button" + class="chromeclass-toolbar-additional" + command="Browser:ReloadOrDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltiptext="&reloadButton.tooltip;"/> + <toolbarbutton id="urlbar-stop-button" + class="chromeclass-toolbar-additional" + command="Browser:Stop" + tooltiptext="&stopButton.tooltip;"/> + </textbox> + </toolbaritem> + + <toolbaritem id="search-container" title="&searchItem.title;" + align="center" class="chromeclass-toolbar-additional" + flex="100" persist="width" removable="true"> + <searchbar id="searchbar" flex="1"/> + </toolbaritem> + <toolbarbutton id="bookmarks-menu-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + persist="class" + removable="true" + type="menu" + label="&bookmarksMenuButton.label;" + tooltiptext="&bookmarksMenuButton.tooltip;" + onclick="if (event.button == 1) + toggleSidebar('viewBookmarksSidebar');" + ondragenter="PlacesMenuDNDHandler.onDragEnter(event);" + ondragover="PlacesMenuDNDHandler.onDragOver(event);" + ondragleave="PlacesMenuDNDHandler.onDragLeave(event);" + ondrop="PlacesMenuDNDHandler.onDrop(event);"> + <menupopup id="BMB_bookmarksPopup" + placespopup="true" + context="placesContext" + openInTabs="children" + oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);" + onclick="event.stopPropagation(); + BookmarksEventHandler.onClick(event, this.parentNode._placesView);" + onpopupshowing="BookmarkingUI.onPopupShowing(event); + if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=BOOKMARKS_MENU');" + tooltip="bhTooltip" popupsinherittooltip="true"> + <menuitem id="BMB_viewBookmarksToolbar" + placesanonid="view-toolbar" + toolbarId="PersonalToolbar" + type="checkbox" + oncommand="onViewToolbarCommand(event)" + label="&viewBookmarksToolbar.label;"/> + <menuseparator/> + <menuitem id="BMB_bookmarksShowAll" + class="menuitem-iconic" + label="&organizeBookmarks.label;" + command="Browser:ShowAllBookmarks" + key="manBookmarkKb"/> + <menuseparator/> + <menuitem id="BMB_bookmarkThisPage" + class="menuitem-iconic" + label="&bookmarkThisPageCmd.label;" + command="Browser:AddBookmarkAs" + key="addBookmarkAsKb"/> + <menuitem id="BMB_subscribeToPageMenuitem" + class="menuitem-iconic" + label="&subscribeToPageMenuitem.label;" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);" + observes="singleFeedMenuitemState"/> + <menu id="BMB_subscribeToPageMenupopup" + class="menu-iconic" + label="&subscribeToPageMenupopup.label;" + observes="multipleFeedsMenuState"> + <menupopup id="BMB_subscribeToPageSubmenuMenupopup" + onpopupshowing="return FeedHandler.buildFeedList(event.target);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </menu> + <menuseparator/> + <menu id="BMB_bookmarksToolbar" + placesanonid="toolbar-autohide" + class="menu-iconic bookmark-item" + label="&personalbarCmd.label;" + container="true"> + <menupopup id="BMB_bookmarksToolbarPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=TOOLBAR');"/> + </menu> + <menuseparator/> + <!-- Bookmarks menu items --> + <menuseparator builder="end" + class="hide-if-empty-places-result"/> + <menuitem id="BMB_unsortedBookmarks" + class="menuitem-iconic" + label="&bookmarksMenuButton.unsorted.label;" + oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/> + </menupopup> + </toolbarbutton> + + <toolbarbutton id="history-menu-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + persist="class" + removable="true" + type="menu" + label="&historyButton.label;" + tooltiptext="&historyButton.tooltip;" + onclick="if (event.button == 1) + toggleSidebar('viewHistorySidebar');"> + <menupopup id="HMB_historyPopup" + placespopup="true" + context="placesContext" + oncommand="this.parentNode._placesView._onCommand(event);" + onclick="event.stopPropagation(); + checkForMiddleClick(this, event);" + onpopupshowing="if (!this.parentNode._placesView) + new HistoryMenu(event);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <menuitem id="HMB_showAllHistory" + label="&showAllHistoryCmd2.label;" + class="menuitem-iconic" + key="showAllHistoryKb" + command="Browser:ShowAllHistory"/> + <menuitem id="HMB_sanitizeItem" + class="menuitem-iconic" + label="&clearRecentHistory.label;" + key="key_sanitize" + command="Tools:Sanitize"/> + <menuseparator id="HMB_sanitizeSeparator"/> +#ifdef MOZ_SERVICES_SYNC + <menuitem id="HMB_sync-tabs-menuitem" + class="syncTabsMenuItem" + label="&syncTabsMenu2.label;" + oncommand="BrowserOpenSyncTabs();" + disabled="true"/> +#endif + <menuitem id="HMB_historyRestoreLastSession" + label="&historyRestoreLastSession.label;" + command="Browser:RestoreLastSession"/> + <menu id="HMB_historyUndoMenu" + class="recentlyClosedTabsMenu" + label="&historyUndoMenu.label;" + disabled="true"> + <menupopup id="HMB_historyUndoPopup" + placespopup="true" + onpopupshowing="document.getElementById('history-menu-button')._placesView.populateUndoSubmenu();"/> + </menu> + <menu id="HMB_historyUndoWindowMenu" + class="recentlyClosedWindowsMenu" + label="&historyUndoWindowMenu.label;" + disabled="true"> + <menupopup id="HMB_historyUndoWindowPopup" + placespopup="true" + onpopupshowing="document.getElementById('history-menu-button')._placesView.populateUndoWindowSubmenu();"/> + </menu> + <menuseparator id="HMB_startHistorySeparator" + class="hide-if-empty-places-result"/> + <!-- History menu items --> + </menupopup> + </toolbarbutton> + + <hbox id="window-controls" hidden="true" pack="end"> + <toolbarbutton id="minimize-button" + tooltiptext="&fullScreenMinimize.tooltip;" + oncommand="window.minimize();"/> + + <toolbarbutton id="restore-button" + tooltiptext="&fullScreenRestore.tooltip;" + oncommand="BrowserFullScreen();"/> + + <toolbarbutton id="close-button" + tooltiptext="&fullScreenClose.tooltip;" + oncommand="BrowserTryToCloseWindow();"/> + </hbox> + </toolbar> + + <toolbarset id="customToolbars" context="toolbar-context-menu"/> + + <toolbar id="PersonalToolbar" + mode="icons" iconsize="small" defaulticonsize="small" + lockiconsize="true" + class="chromeclass-directories" + context="toolbar-context-menu" + defaultset="personal-bookmarks" + toolbarname="&personalbarCmd.label;" accesskey="&personalbarCmd.accesskey;" + collapsed="false" + customizable="true"> + <toolbaritem flex="1" id="personal-bookmarks" title="&bookmarksItem.title;" + removable="true"> + <hbox flex="1" + id="PlacesToolbar" + context="placesContext" + onclick="BookmarksEventHandler.onClick(event, this._placesView);" + oncommand="BookmarksEventHandler.onCommand(event, this._placesView);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <toolbarbutton class="bookmark-item bookmarks-toolbar-customize" + mousethrough="never" + label="&bookmarksToolbarItem.label;"/> + <hbox flex="1"> + <hbox align="center"> + <image id="PlacesToolbarDropIndicator" + mousethrough="always" + collapsed="true"/> + </hbox> + <scrollbox orient="horizontal" + id="PlacesToolbarItems" + flex="1"/> + <toolbarbutton type="menu" + id="PlacesChevron" + class="chevron" + mousethrough="never" + collapsed="true" + tooltiptext="&bookmarksToolbarChevron.tooltip;" + onpopupshowing="document.getElementById('PlacesToolbar') + ._placesView._onChevronPopupShowing(event);"> + <menupopup id="PlacesChevronPopup" + placespopup="true" + tooltip="bhTooltip" popupsinherittooltip="true" + context="placesContext"/> + </toolbarbutton> + </hbox> + </hbox> + </toolbaritem> + </toolbar> + +#ifdef MENUBAR_CAN_AUTOHIDE +#ifndef MOZ_CAN_DRAW_IN_TITLEBAR +#define APPMENU_ON_TABBAR +#endif +#endif + + + <toolbar id="TabsToolbar" + class="toolbar-primary" + fullscreentoolbar="true" + customizable="true" + mode="icons" lockmode="true" + iconsize="small" defaulticonsize="small" lockiconsize="true" + aria-label="&tabsToolbar.label;" + context="toolbar-context-menu" +#ifdef APPMENU_ON_TABBAR + defaultset="appmenu-toolbar-button,tabbrowser-tabs,new-tab-button,alltabs-button,tabs-closebutton" +#else + defaultset="tabbrowser-tabs,new-tab-button,alltabs-button,tabs-closebutton" +#endif + collapsed="true"> + +#ifdef APPMENU_ON_TABBAR + <toolbarbutton id="appmenu-toolbar-button" + class="chromeclass-toolbar-additional" + type="menu" + label="&brandShortName;" + tooltiptext="&appMenuButton.tooltip;"> +#include browser-appmenu.inc + </toolbarbutton> +#endif + + <tabs id="tabbrowser-tabs" + class="tabbrowser-tabs" + tabbrowser="content" + flex="1" + setfocus="false" + tooltip="tabbrowser-tab-tooltip"> + <tab class="tabbrowser-tab" selected="true" fadein="true"/> + </tabs> + + <toolbarbutton id="new-tab-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&tabCmd.label;" + command="cmd_newNavigatorTab" + onclick="checkForMiddleClick(this, event);" + tooltiptext="&newTabButton.tooltip;" + ondrop="newTabButtonObserver.onDrop(event)" + ondragover="newTabButtonObserver.onDragOver(event)" + ondragenter="newTabButtonObserver.onDragOver(event)" + ondragexit="newTabButtonObserver.onDragExit(event)" + removable="true"/> + + <toolbarbutton id="alltabs-button" + class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button" + type="menu" + label="&listAllTabs.label;" + tooltiptext="&listAllTabs.label;" + removable="true"> + <menupopup id="alltabs-popup" position="after_end"/> + </toolbarbutton> + + <toolbarbutton id="tabs-closebutton" + class="close-button tabs-closebutton close-icon" + command="cmd_close" + label="&closeTab.label;" + tooltiptext="&closeTab.label;"/> + +#ifdef MOZ_CAN_DRAW_IN_TITLEBAR + <hbox class="titlebar-placeholder" type="appmenu-button" ordinal="0"/> + <hbox class="titlebar-placeholder" type="caption-buttons" ordinal="1000"/> +#endif + </toolbar> + + <toolbarpalette id="BrowserToolbarPalette"> + +# Update primaryToolbarButtons in browser/themes/shared/browser.inc when adding +# or removing default items with the toolbarbutton-1 class. + + <toolbarbutton id="print-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&printButton.label;" command="cmd_print" + tooltiptext="&printButton.tooltip;"/> + + <!-- This is a placeholder for the Downloads Indicator. It is visible + during the customization of the toolbar, in the palette, and before + the Downloads Indicator overlay is loaded. --> + <toolbarbutton id="downloads-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + oncommand="DownloadsIndicatorView.onCommand(event);" + ondrop="DownloadsIndicatorView.onDrop(event);" + ondragover="DownloadsIndicatorView.onDragOver(event);" + ondragenter="DownloadsIndicatorView.onDragOver(event);" + label="&downloads.label;" + tooltiptext="&downloads.tooltip;"/> + + <toolbarbutton id="history-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + observes="viewHistorySidebar" label="&historyButton.label;" + tooltiptext="&historyButton.tooltip;"/> + + <toolbarbutton id="bookmarks-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + observes="viewBookmarksSidebar" label="&bookmarksButton.label;" + tooltiptext="&bookmarksButton.tooltip;" + ondrop="bookmarksButtonObserver.onDrop(event)" + ondragover="bookmarksButtonObserver.onDragOver(event)" + ondragenter="bookmarksButtonObserver.onDragOver(event)" + ondragexit="bookmarksButtonObserver.onDragExit(event)"/> + + <toolbarbutton id="new-window-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&newNavigatorCmd.label;" + command="key_newNavigator" + tooltiptext="&newWindowButton.tooltip;" + ondrop="newWindowButtonObserver.onDrop(event)" + ondragover="newWindowButtonObserver.onDragOver(event)" + ondragenter="newWindowButtonObserver.onDragOver(event)" + ondragexit="newWindowButtonObserver.onDragExit(event)"/> + + <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + observes="View:FullScreen" + type="checkbox" + label="&fullScreenCmd.label;" + tooltiptext="&fullScreenButton.tooltip;"/> + + <toolbaritem id="zoom-controls" class="chromeclass-toolbar-additional" + title="&zoomControls.label;"> + <toolbarbutton id="zoom-out-button" class="toolbarbutton-1" + label="&fullZoomReduceCmd.label;" + command="cmd_fullZoomReduce" + tooltiptext="&zoomOutButton.tooltip;"/> + <toolbarbutton id="zoom-in-button" class="toolbarbutton-1" + label="&fullZoomEnlargeCmd.label;" + command="cmd_fullZoomEnlarge" + tooltiptext="&zoomInButton.tooltip;"/> + </toolbaritem> + + <toolbarbutton id="feed-button" + type="menu" + class="toolbarbutton-1 chromeclass-toolbar-additional" + disabled="true" + label="&feedButton.label;" + tooltiptext="&feedButton.tooltip;" + onclick="return FeedHandler.onFeedButtonClick(event);"> + <menupopup position="after_end" + id="feed-menu" + onpopupshowing="return FeedHandler.buildFeedList(this);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </toolbarbutton> + + <toolbarbutton id="cut-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&cutCmd.label;" + command="cmd_cut" + tooltiptext="&cutButton.tooltip;"/> + + <toolbarbutton id="copy-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="©Cmd.label;" + command="cmd_copy" + tooltiptext="©Button.tooltip;"/> + + <toolbarbutton id="paste-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&pasteCmd.label;" + command="cmd_paste" + tooltiptext="&pasteButton.tooltip;"/> + +#ifdef MOZ_SERVICES_SYNC + <toolbarbutton id="sync-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&syncToolbarButton.label;" + oncommand="gSyncUI.handleToolbarButton()"/> +#endif + + <toolbaritem id="navigator-throbber" title="&throbberItem.title;" align="center" pack="center" + mousethrough="always"> + <image/> + </toolbaritem> + </toolbarpalette> + </toolbox> + + <hbox id="fullscr-toggler" hidden="true"/> + + <hbox flex="1" id="browser"> + <vbox id="browser-border-start" hidden="true" layer="true"/> + <vbox id="sidebar-box" hidden="true" class="chromeclass-extrachrome"> + <sidebarheader id="sidebar-header" align="center"> + <label id="sidebar-title" persist="value" flex="1" crop="end" control="sidebar"/> + <image id="sidebar-throbber"/> + <toolbarbutton class="tabs-closebutton close-icon" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="toggleSidebar();"/> + </sidebarheader> + <browser id="sidebar" flex="1" autoscroll="false" disablehistory="true" + style="min-width: 14em; width: 18em; max-width: 36em;"/> + </vbox> + + <splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/> + <vbox id="appcontent" flex="1"> + <tabbrowser id="content" disablehistory="true" + flex="1" contenttooltip="aHTMLTooltip" + tabcontainer="tabbrowser-tabs" + contentcontextmenu="contentAreaContextMenu" + autocompletepopup="PopupAutoComplete" + datetimepicker="DateTimePickerPanel" + authdosprotected="true"/> + <chatbar id="pinnedchats" layer="true" mousethrough="always" hidden="true"/> + <statuspanel id="statusbar-display" inactive="true"/> + </vbox> + <vbox id="browser-border-end" hidden="true" layer="true"/> + </hbox> + + <hbox id="full-screen-warning-container" hidden="true" fadeout="true"> + <hbox style="width: 100%;" pack="center"> <!-- Inner hbox needed due to bug 579776. --> + <vbox id="full-screen-warning-message" align="center"> + <description id="full-screen-domain-text"/> + <description class="full-screen-description" value="&fullscreenExitHint.value;"/> + </vbox> + </hbox> + </hbox> + + <vbox id="browser-bottombox" layer="true"> + <notificationbox id="global-notificationbox"/> + <toolbar id="addon-bar" + toolbarname="&statusBar.label;" accesskey="&statusBar.accesskey;" + collapsed="true" + class="toolbar-primary chromeclass-toolbar" + context="toolbar-context-menu" toolboxid="navigator-toolbox" + mode="icons" iconsize="small" defaulticonsize="small" + lockiconsize="true" + defaultset="addonbar-closebutton,spring,status-bar" + customizable="true" + key="key_toggleAddonBar"> + <toolbarbutton id="addonbar-closebutton" + class="close-icon" + tooltiptext="&addonBarCloseButton.tooltip;" + oncommand="setToolbarVisibility(this.parentNode, false);"/> + <statusbar id="status-bar" ordinal="1000"/> + </toolbar> + </vbox> + +#ifndef XP_UNIX + <svg:svg height="0"> + <svg:clipPath id="windows-keyhole-forward-clip-path" clipPathUnits="objectBoundingBox"> + <svg:path d="M 0,0 C 0.16,0.11 0.28,0.29 0.28,0.5 0.28,0.71 0.16,0.89 0,1 L 1,1 1,0 0,0 z"/> + </svg:clipPath> + <svg:clipPath id="windows-urlbar-back-button-clip-path" clipPathUnits="userSpaceOnUse"> + <svg:path d="M 0,0 0,7.8 C 2.5,11 4,14 4,18 4,22 2.5,25 0,28 l 0,22 10000,0 0,-50 L 0,0 z"/> + </svg:clipPath> + </svg:svg> +#endif + +</vbox> +# <iframe id="tab-view"> is dynamically appended as the 2nd child of #tab-view-deck. +# Introducing the iframe dynamically, as needed, was found to be better than +# starting with an empty iframe here in browser.xul from a Ts standpoint. +</deck> + +</window> diff --git a/browser/base/content/content.js b/browser/base/content/content.js new file mode 100644 index 000000000..33b774f90 --- /dev/null +++ b/browser/base/content/content.js @@ -0,0 +1,178 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", + "resource://gre/modules/LoginManagerContent.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory", + "resource://gre/modules/LoginManagerContent.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils", + "resource://gre/modules/InsecurePasswordUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver", + "resource:///modules/FormSubmitObserver.jsm"); + +// Bug 671101 - directly using webNavigation in this context +// causes docshells to leak +this.__defineGetter__("webNavigation", function () { + return docShell.QueryInterface(Ci.nsIWebNavigation); +}); + +addMessageListener("WebNavigation:LoadURI", function (message) { + let flags = message.json.flags || webNavigation.LOAD_FLAGS_NONE; + + webNavigation.loadURI(message.json.uri, flags, null, null, null); +}); + +// TabChildGlobal +var global = this; + +// Load the form validation popup handler +var formSubmitObserver = new FormSubmitObserver(content, this); + +addMessageListener("Browser:HideSessionRestoreButton", function (message) { + // Hide session restore button on about:home + let doc = content.document; + let container; + if (doc.documentURI.toLowerCase() == "about:home" && + (container = doc.getElementById("sessionRestoreContainer"))) { + container.hidden = true; + } +}); + +addEventListener("DOMFormHasPassword", function(event) { + LoginManagerContent.onDOMFormHasPassword(event, content); + let formLike = LoginFormFactory.createFromForm(event.target); + InsecurePasswordUtils.reportInsecurePasswords(formLike); +}); +addEventListener("DOMAutoComplete", function(event) { + LoginManagerContent.onUsernameInput(event); +}); +addEventListener("blur", function(event) { + LoginManagerContent.onUsernameInput(event); +}); + +// Provide gContextMenuContentData for 'sdk/context-menu' +var handleContentContextMenu = function (event) { + let defaultPrevented = event.defaultPrevented; + if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) { + let plugin = null; + try { + plugin = event.target.QueryInterface(Ci.nsIObjectLoadingContent); + } catch(e) {} + if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) { + // Don't open a context menu for plugins. + return; + } + + defaultPrevented = false; + } + + if (defaultPrevented) { + return; + } + + let addonInfo = {}; + let subject = { + event: event, + addonInfo: addonInfo, + }; + subject.wrappedJSObject = subject; + Services.obs.notifyObservers(subject, "content-contextmenu", null); + + let doc = event.target.ownerDocument; + let docLocation = doc.mozDocumentURIIfNotForErrorPages; + docLocation = docLocation && docLocation.spec; + let charSet = doc.characterSet; + let baseURI = doc.baseURI; + let referrer = doc.referrer; + let referrerPolicy = doc.referrerPolicy; + let frameOuterWindowID = doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + let loginFillInfo = LoginManagerContent.getFieldContext(event.target); + + // The same-origin check will be done in nsContextMenu.openLinkInTab. + let parentAllowsMixedContent = !!docShell.mixedContentChannel; + + // get referrer attribute from clicked link and parse it + // if per element referrer is enabled, the element referrer overrules + // the document wide referrer + if (Services.prefs.getBoolPref("network.http.enablePerElementReferrer")) { + let referrerAttrValue = Services.netUtils.parseAttributePolicyString( + event.target.getAttribute("referrerpolicy")); + if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) { + referrerPolicy = referrerAttrValue; + } + } + + // Media related cache info parent needs for saving + let contentType = null; + let contentDisposition = null; + if (event.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && + event.target instanceof Ci.nsIImageLoadingContent && + event.target.currentURI) { + + try { + let imageCache = + Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools) + .getImgCacheForDocument(doc); + let props = + imageCache.findEntryProperties(event.target.currentURI, doc); + try { + contentType = props.get("type", Ci.nsISupportsCString).data; + } catch(e) {} + try { + contentDisposition = + props.get("content-disposition", Ci.nsISupportsCString).data; + } catch(e) {} + } catch(e) {} + } + + let selectionInfo = BrowserUtils.getSelectionDetails(content); + + let loadContext = docShell.QueryInterface(Ci.nsILoadContext); + + let browser = docShell.chromeEventHandler; + let mainWin = browser.ownerGlobal; + + mainWin.gContextMenuContentData = { + isRemote: false, + event: event, + popupNode: event.target, + browser: browser, + addonInfo: addonInfo, + documentURIObject: doc.documentURIObject, + docLocation: docLocation, + charSet: charSet, + referrer: referrer, + referrerPolicy: referrerPolicy, + contentType: contentType, + contentDisposition: contentDisposition, + selectionInfo: selectionInfo, + loginFillInfo, + parentAllowsMixedContent + }; +} + +Cc["@mozilla.org/eventlistenerservice;1"] + .getService(Ci.nsIEventListenerService) + .addSystemEventListener(global, "contextmenu", handleContentContextMenu, false); + +// Lazily load the finder code +addMessageListener("Finder:Initialize", function() { + let {RemoteFinderListener} = Cu.import("resource://gre/modules/RemoteFinder.jsm", {}); + new RemoteFinderListener(global); +}); + +addEventListener("DOMWebNotificationClicked", function(event) { + sendAsyncMessage("DOMWebNotificationClicked", {}); +}, false); diff --git a/browser/base/content/global-devtools-theme-scripts.inc b/browser/base/content/global-devtools-theme-scripts.inc new file mode 100644 index 000000000..408728ed5 --- /dev/null +++ b/browser/base/content/global-devtools-theme-scripts.inc @@ -0,0 +1,6 @@ +# -*- 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/. + +<script type="application/javascript" src="chrome://browser/content/browser-devtools-theme.js"/> diff --git a/browser/base/content/global-scripts.inc b/browser/base/content/global-scripts.inc new file mode 100644 index 000000000..b4de574ae --- /dev/null +++ b/browser/base/content/global-scripts.inc @@ -0,0 +1,13 @@ +# -*- 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/. + +<script type="application/javascript" src="chrome://global/content/printUtils.js"/> +<script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/> +<script type="application/javascript" src="chrome://browser/content/places/browserPlacesViews.js"/> +<script type="application/javascript" src="chrome://browser/content/browser.js"/> +<script type="application/javascript" src="chrome://browser/content/downloads/downloads.js"/> +<script type="application/javascript" src="chrome://browser/content/downloads/indicator.js"/> +<script type="application/javascript" src="chrome://global/content/inlineSpellCheckUI.js"/> +<script type="application/javascript" src="chrome://global/content/viewSourceUtils.js"/> diff --git a/browser/base/content/hiddenWindow.xul b/browser/base/content/hiddenWindow.xul new file mode 100644 index 000000000..af97928f1 --- /dev/null +++ b/browser/base/content/hiddenWindow.xul @@ -0,0 +1,6 @@ +<?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/. --> + diff --git a/browser/base/content/highlighter.css b/browser/base/content/highlighter.css new file mode 100644 index 000000000..8fb9d8085 --- /dev/null +++ b/browser/base/content/highlighter.css @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.highlighter-container { + pointer-events: none; +} + +.highlighter-controls { + position: absolute; + top: 0; + left: 0; +} + +.highlighter-outline-container { + overflow: hidden; + position: relative; +} + +.highlighter-outline { + position: absolute; +} + +.highlighter-outline[hidden] { + opacity: 0; + pointer-events: none; + display: -moz-box; +} + +.highlighter-outline:not([disable-transitions]) { + transition-property: opacity, top, left, width, height; + transition-duration: 0.1s; + transition-timing-function: linear; +} + +/* + * Node Infobar + */ + +.highlighter-nodeinfobar-container { + position: absolute; + max-width: 95%; +} + +.highlighter-nodeinfobar-container[hidden] { + opacity: 0; + pointer-events: none; + display: -moz-box; +} + +.highlighter-nodeinfobar-container:not([disable-transitions]), +.highlighter-nodeinfobar-container[disable-transitions][force-transitions] { + transition-property: transform, opacity, top, left; + transition-duration: 0.1s; + transition-timing-function: linear; +} + +.highlighter-nodeinfobar-text { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + direction: ltr; +} + +.highlighter-nodeinfobar-button > .toolbarbutton-text { + display: none; +} + +.highlighter-nodeinfobar-container:not([locked]):not(:hover) > .highlighter-nodeinfobar > .highlighter-nodeinfobar-button { + visibility: hidden; +} + +.highlighter-nodeinfobar-container[locked] > .highlighter-nodeinfobar, +.highlighter-nodeinfobar-container:not([locked]):hover > .highlighter-nodeinfobar { + pointer-events: auto; +} + +html|*.highlighter-nodeinfobar-id, +html|*.highlighter-nodeinfobar-classes, +html|*.highlighter-nodeinfobar-pseudo-classes, +html|*.highlighter-nodeinfobar-tagname { + -moz-user-select: text; + -moz-user-focus: normal; + cursor: text; +} + +.highlighter-nodeinfobar-arrow { + display: none; +} + +.highlighter-nodeinfobar-container[position="top"]:not([hide-arrow]) > .highlighter-nodeinfobar-arrow-bottom { + display: block; +} + +.highlighter-nodeinfobar-container[position="bottom"]:not([hide-arrow]) > .highlighter-nodeinfobar-arrow-top { + display: block; +} + +.highlighter-nodeinfobar-container[disabled] { + visibility: hidden; +} + +html|*.highlighter-nodeinfobar-tagname { + text-transform: lowercase; +} diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js new file mode 100644 index 000000000..e848491d1 --- /dev/null +++ b/browser/base/content/nsContextMenu.js @@ -0,0 +1,1602 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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/PrivateBrowsingUtils.jsm"); + +var gContextMenuContentData = null; + +function nsContextMenu(aXulMenu, aIsShift) { + this.shouldDisplay = true; + this.initMenu(aXulMenu, aIsShift); +} + +// Prototype for nsContextMenu "class." +nsContextMenu.prototype = { + initMenu: function(aXulMenu, aIsShift) { + // Get contextual info. + this.setTarget(document.popupNode, document.popupRangeParent, + document.popupRangeOffset); + if (!this.shouldDisplay) { + return; + } + + this.hasPageMenu = false; + if (!aIsShift) { + this.hasPageMenu = PageMenu.maybeBuildAndAttachMenu(this.target, aXulMenu); + } + + this.isFrameImage = document.getElementById("isFrameImage"); + this.ellipsis = "\u2026"; + try { + this.ellipsis = gPrefService.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; + } catch(e) {} + + this.isContentSelected = this.isContentSelection(); + this.onPlainTextLink = false; + + // Initialize (disable/remove) menu items. + this.initItems(); + }, + + hiding: function() { + gContextMenuContentData = null; + InlineSpellCheckerUI.clearSuggestionsFromMenu(); + InlineSpellCheckerUI.clearDictionaryListFromMenu(); + InlineSpellCheckerUI.uninit(); + }, + + initItems: function() { + this.initPageMenuSeparator(); + this.initOpenItems(); + this.initNavigationItems(); + this.initViewItems(); + this.initMiscItems(); + this.initSpellingItems(); + this.initSaveItems(); + this.initClipboardItems(); + this.initMediaPlayerItems(); + this.initLeaveDOMFullScreenItems(); + this.initClickToPlayItems(); + }, + + initPageMenuSeparator: function() { + this.showItem("page-menu-separator", this.hasPageMenu); + }, + + initOpenItems: function() { + var isMailtoInternal = false; + if (this.onMailtoLink) { + var mailtoHandler = Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .getProtocolHandlerInfo("mailto"); + isMailtoInternal = (!mailtoHandler.alwaysAskBeforeHandling && + mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp && + (mailtoHandler.preferredApplicationHandler instanceof Ci.nsIWebHandlerApp)); + } + + // Time to do some bad things and see if we've highlighted a URL that + // isn't actually linked. + if (this.isTextSelected && !this.onLink) { + // Ok, we have some text, let's figure out if it looks like a URL. + let selection = document.commandDispatcher.focusedWindow + .getSelection(); + let linkText = selection.toString().trim(); + let uri; + if (/^(?:https?|ftp):/i.test(linkText)) { + try { + uri = makeURI(linkText); + } catch(ex) {} + } else if (/^[-a-z\d\.]+\.[-a-z\d]{2,}[-_=~:#%&\?\w\/\.]*$/i.test(linkText)) { + // Check if this could be a valid url, just missing the protocol. + let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"] + .getService(Ci.nsIURIFixup); + try { + uri = uriFixup.createFixupURI(linkText, uriFixup.FIXUP_FLAG_NONE); + } catch(ex) {} + } + + if (uri && uri.host) { + this.linkURI = uri; + this.linkURL = this.linkURI.spec; + this.onPlainTextLink = true; + } + } + + var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink; + var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + this.showItem("context-openlink", shouldShow && !isWindowPrivate); + this.showItem("context-openlinkprivate", shouldShow); + this.showItem("context-openlinkintab", shouldShow); + this.showItem("context-openlinkincurrent", shouldShow); + this.showItem("context-sep-open", shouldShow); + }, + + initNavigationItems: function() { + var shouldShow = !(this.isContentSelected || this.onLink || this.onImage || + this.onCanvas || this.onVideo || this.onAudio || + this.onTextInput); + this.showItem("context-back", shouldShow); + this.showItem("context-forward", shouldShow); + + let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true"; + + let stopReloadItem = ""; + if (shouldShow) { + stopReloadItem = stopped ? "reload" : "stop"; + } + + this.showItem("context-reload", stopReloadItem == "reload"); + this.showItem("context-stop", stopReloadItem == "stop"); + this.showItem("context-sep-stop", !!stopReloadItem); + + // XXX: Stop is determined in browser.js; the canStop broadcaster is broken + //this.setItemAttrFromNode( "context-stop", "disabled", "canStop" ); + }, + + initLeaveDOMFullScreenItems: function() { + // only show the option if the user is in DOM fullscreen + var shouldShow = (this.target.ownerDocument.mozFullScreenElement != null); + this.showItem("context-leave-dom-fullscreen", shouldShow); + + // Explicitly show if in DOM fullscreen, but do not hide it has already been shown + if (shouldShow) { + this.showItem("context-media-sep-commands", true); + } + }, + + initSaveItems: function() { + var shouldShow = !(this.onTextInput || this.onLink || + this.isContentSelected || this.onImage || + this.onCanvas || this.onVideo || this.onAudio); + this.showItem("context-savepage", shouldShow); + this.showItem("context-sendpage", shouldShow); + + // Save+Send link depends on whether we're in a link, or selected text matches valid URL pattern. + this.showItem("context-savelink", this.onSaveableLink || this.onPlainTextLink); + this.showItem("context-sendlink", this.onSaveableLink || this.onPlainTextLink); + + // Save image depends on having loaded its content, video and audio don't. + this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas); + this.showItem("context-savevideo", this.onVideo); + this.showItem("context-saveaudio", this.onAudio); + this.showItem("context-video-saveimage", this.onVideo); + this.setItemAttr("context-savevideo", "disabled", !this.mediaURL); + this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL); + // Send media URL (but not for canvas, since it's a big data: URL) + this.showItem("context-sendimage", this.onImage); + this.showItem("context-sendvideo", this.onVideo); + this.showItem("context-sendaudio", this.onAudio); + this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL); + this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL); + }, + + initViewItems: function() { + // View source is always OK, unless in directory listing. + this.showItem("context-viewpartialsource-selection", + this.isContentSelected); + this.showItem("context-viewpartialsource-mathml", + this.onMathML && !this.isContentSelected); + + var shouldShow = !(this.isContentSelected || + this.onImage || this.onCanvas || + this.onVideo || this.onAudio || + this.onLink || this.onTextInput); + this.showItem("context-viewsource", shouldShow); + this.showItem("context-viewinfo", shouldShow); +#ifdef MOZ_DEVTOOLS + var showInspect = gPrefService.getBoolPref("devtools.inspector.enabled"); + this.showItem("inspect-separator", showInspect); + this.showItem("context-inspect", showInspect); +#endif + + this.showItem("context-sep-viewsource", shouldShow); + + // Set as Desktop background depends on whether an image was clicked on, + // and only works if we have a shell service. + var haveSetDesktopBackground = false; +#ifdef HAVE_SHELL_SERVICE + // Only enable Set as Desktop Background if we can get the shell service. + var shell = getShellService(); + if (shell) { + haveSetDesktopBackground = shell.canSetDesktopBackground; + } +#endif + this.showItem("context-setDesktopBackground", + haveSetDesktopBackground && this.onLoadedImage); + + if (haveSetDesktopBackground && this.onLoadedImage) { + document.getElementById("context-setDesktopBackground") + .disabled = this.disableSetDesktopBackground(); + } + + // Reload image depends on an image that's not fully loaded + this.showItem("context-reloadimage", (this.onImage && !this.onCompletedImage)); + + // View image depends on having an image that's not standalone + // (or is in a frame), or a canvas. + this.showItem("context-viewimage", (this.onImage && + (!this.inSyntheticDoc || this.inFrame)) || this.onCanvas); + + // View video depends on not having a standalone video. + this.showItem("context-viewvideo", this.onVideo && (!this.inSyntheticDoc || this.inFrame)); + this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL); + + // View background image depends on whether there is one, but don't make + // background images of a stand-alone media document available. + this.showItem("context-viewbgimage", shouldShow && + !this._hasMultipleBGImages && + !this.inSyntheticDoc); + this.showItem("context-sep-viewbgimage", shouldShow && + !this._hasMultipleBGImages && + !this.inSyntheticDoc); + document.getElementById("context-viewbgimage") + .disabled = !this.hasBGImage; + + this.showItem("context-viewimageinfo", this.onImage); + }, + + initMiscItems: function() { + // Use "Bookmark This Link" if on a link. + this.showItem("context-bookmarkpage", + !(this.isContentSelected || this.onTextInput || this.onLink || + this.onImage || this.onVideo || this.onAudio)); + this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink) || + this.onPlainTextLink); + this.showItem("context-keywordfield", + this.onTextInput && this.onKeywordField); + this.showItem("frame", this.inFrame); + + let showSearchSelect = (this.isTextSelected || this.onLink) && !this.onImage; + this.showItem("context-searchselect", showSearchSelect); + if (showSearchSelect) { + this.formatSearchContextItem(); + } + + // srcdoc cannot be opened separately due to concerns about web + // content with about:srcdoc in location bar masquerading as trusted + // chrome/addon content. + // No need to also test for this.inFrame as this is checked in the parent + // submenu. + this.showItem("context-showonlythisframe", !this.inSrcdocFrame); + this.showItem("context-openframeintab", !this.inSrcdocFrame); + this.showItem("context-openframe", !this.inSrcdocFrame); + this.showItem("context-bookmarkframe", !this.inSrcdocFrame); + this.showItem("open-frame-sep", !this.inSrcdocFrame); + + this.showItem("frame-sep", this.inFrame && this.isTextSelected); + + // Hide menu entries for images, show otherwise + if (this.inFrame) { + if (BrowserUtils.mimeTypeIsTextBased(this.target.ownerDocument.contentType)) { + this.isFrameImage.removeAttribute('hidden'); + } else { + this.isFrameImage.setAttribute('hidden', 'true'); + } + } + + // BiDi UI + this.showItem("context-sep-bidi", top.gBidiUI); + this.showItem("context-bidi-text-direction-toggle", + this.onTextInput && top.gBidiUI); + this.showItem("context-bidi-page-direction-toggle", + !this.onTextInput && top.gBidiUI); + }, + + initSpellingItems: function() { + var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck; + var onMisspelling = InlineSpellCheckerUI.overMisspelling; + var showUndo = canSpell && InlineSpellCheckerUI.canUndo(); + this.showItem("spell-check-enabled", canSpell); + this.showItem("spell-separator", canSpell || this.onEditableArea); + document.getElementById("spell-check-enabled") + .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled); + + this.showItem("spell-add-to-dictionary", onMisspelling); + this.showItem("spell-undo-add-to-dictionary", showUndo); + + // suggestion list + this.showItem("spell-suggestions-separator", onMisspelling || showUndo); + if (onMisspelling) { + var suggestionsSeparator = document.getElementById("spell-add-to-dictionary"); + var numsug = + InlineSpellCheckerUI.addSuggestionsToMenu(suggestionsSeparator.parentNode, + suggestionsSeparator, 5); + this.showItem("spell-no-suggestions", numsug == 0); + } else { + this.showItem("spell-no-suggestions", false); + } + + // dictionary list + this.showItem("spell-dictionaries", canSpell && InlineSpellCheckerUI.enabled); + if (canSpell) { + var dictMenu = document.getElementById("spell-dictionaries-menu"); + var dictSep = document.getElementById("spell-language-separator"); + InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep); + this.showItem("spell-add-dictionaries-main", false); + } else if (this.onEditableArea) { + // when there is no spellchecker but we might be able to spellcheck + // add the add to dictionaries item. This will ensure that people + // with no dictionaries will be able to download them + this.showItem("spell-add-dictionaries-main", true); + } else { + this.showItem("spell-add-dictionaries-main", false); + } + }, + + initClipboardItems: function() { + // Copy depends on whether there is selected text. + // Enabling this context menu item is now done through the global + // command updating system + // this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() ); + goUpdateGlobalEditMenuItems(); + + this.showItem("context-undo", this.onTextInput); + this.showItem("context-sep-undo", this.onTextInput); + this.showItem("context-cut", this.onTextInput); + this.showItem("context-copy", this.isContentSelected || this.onTextInput); + this.showItem("context-paste", this.onTextInput); + this.showItem("context-delete", this.onTextInput); + this.showItem("context-sep-paste", this.onTextInput); + this.showItem("context-selectall", !(this.onLink || this.onImage || + this.onVideo || this.onAudio || + this.inSyntheticDoc) || + this.isDesignMode); + this.showItem("context-sep-selectall", this.isContentSelected ); + + // XXX dr + // ------ + // nsDocumentViewer.cpp has code to determine whether we're + // on a link or an image. we really ought to be using that... + + // Copy email link depends on whether we're on an email link. + this.showItem("context-copyemail", this.onMailtoLink); + + // Copy link location depends on whether we're on a non-mailto link. + this.showItem("context-copylink", this.onLink && !this.onMailtoLink); + this.showItem("context-sep-copylink", this.onLink && + (this.onImage || this.onVideo || this.onAudio)); + +#ifdef CONTEXT_COPY_IMAGE_CONTENTS + // Copy image contents depends on whether we're on an image. + this.showItem("context-copyimage-contents", this.onImage); +#endif + // Copy image location depends on whether we're on an image. + this.showItem("context-copyimage", this.onImage); + this.showItem("context-copyvideourl", this.onVideo); + this.showItem("context-copyaudiourl", this.onAudio); + this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL); + this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL); + this.showItem("context-sep-copyimage", this.onImage || + this.onVideo || + this.onAudio); + }, + + initMediaPlayerItems: function() { + var onMedia = (this.onVideo || this.onAudio); + // Several mutually exclusive items... play/pause, mute/unmute, show/hide + this.showItem("context-media-play", onMedia && (this.target.paused || this.target.ended)); + this.showItem("context-media-pause", onMedia && !this.target.paused && !this.target.ended); + this.showItem("context-media-mute", onMedia && !this.target.muted); + this.showItem("context-media-unmute", onMedia && this.target.muted); + this.showItem("context-media-playbackrate", onMedia); + this.showItem("context-media-loop", onMedia); + this.showItem("context-media-showcontrols", onMedia && !this.target.controls); + this.showItem("context-media-hidecontrols", onMedia && this.target.controls); + this.showItem("context-video-fullscreen", this.onVideo && this.target.ownerDocument.mozFullScreenElement == null); + var statsShowing = this.onVideo && XPCNativeWrapper.unwrap(this.target).mozMediaStatisticsShowing; + this.showItem("context-video-showstats", this.onVideo && this.target.controls && !statsShowing); + this.showItem("context-video-hidestats", this.onVideo && this.target.controls && statsShowing); + + // Disable them when there isn't a valid media source loaded. + if (onMedia) { + this.setItemAttr("context-media-playbackrate-050x", "checked", this.target.playbackRate == 0.5); + this.setItemAttr("context-media-playbackrate-100x", "checked", this.target.playbackRate == 1.0); + this.setItemAttr("context-media-playbackrate-125x", "checked", this.target.playbackRate == 1.25); + this.setItemAttr("context-media-playbackrate-150x", "checked", this.target.playbackRate == 1.5); + this.setItemAttr("context-media-playbackrate-200x", "checked", this.target.playbackRate == 2.0); + this.setItemAttr("context-media-loop", "checked", this.target.loop); + var hasError = this.target.error != null || + this.target.networkState == this.target.NETWORK_NO_SOURCE; + this.setItemAttr("context-media-play", "disabled", hasError); + this.setItemAttr("context-media-pause", "disabled", hasError); + this.setItemAttr("context-media-mute", "disabled", hasError); + this.setItemAttr("context-media-unmute", "disabled", hasError); + this.setItemAttr("context-media-playbackrate", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-125x", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError); + this.setItemAttr("context-media-showcontrols", "disabled", hasError); + this.setItemAttr("context-media-hidecontrols", "disabled", hasError); + if (this.onVideo) { + let canSaveSnapshot = this.target.readyState >= this.target.HAVE_CURRENT_DATA; + this.setItemAttr("context-video-saveimage", "disabled", !canSaveSnapshot); + this.setItemAttr("context-video-fullscreen", "disabled", hasError); + this.setItemAttr("context-video-showstats", "disabled", hasError); + this.setItemAttr("context-video-hidestats", "disabled", hasError); + } + } + this.showItem("context-media-sep-commands", onMedia); + }, + + initClickToPlayItems: function() { + this.showItem("context-ctp-play", this.onCTPPlugin); + this.showItem("context-ctp-hide", this.onCTPPlugin); + this.showItem("context-sep-ctp", this.onCTPPlugin); + }, + + inspectNode: function() { + let {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {}); + let gBrowser = this.browser.ownerDocument.defaultView.gBrowser; + let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); + + return gDevTools.showToolbox(target, "inspector").then(function(toolbox) { + let inspector = toolbox.getCurrentPanel(); + + this.browser.messageManager.sendAsyncMessage("debug:inspect", {}, { node: this.target }); + inspector.walker.findInspectingNode().then(nodeFront => { + inspector.selection.setNodeFront(nodeFront, "browser-context-menu"); + }); + }.bind(this)); + }, + + // Set various context menu attributes based on the state of the world. + setTarget: function(aNode, aRangeParent, aRangeOffset) { + const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + if (aNode.namespaceURI == xulNS || + aNode.nodeType == Node.DOCUMENT_NODE || + this.isDisabledForEvents(aNode)) { + this.shouldDisplay = false; + return; + } + + // Initialize contextual info. + this.onImage = false; + this.onLoadedImage = false; + this.onCompletedImage = false; + this.onCanvas = false; + this.onVideo = false; + this.onAudio = false; + this.onTextInput = false; + this.onKeywordField = false; + this.mediaURL = ""; + this.onLink = false; + this.onMailtoLink = false; + this.onSaveableLink = false; + this.link = null; + this.linkURL = ""; + this.linkURI = null; + this.linkProtocol = ""; + this.linkDownload = ""; + this.onMathML = false; + this.inFrame = false; + this.inSrcdocFrame = false; + this.inSyntheticDoc = false; + this.hasBGImage = false; + this.bgImageURL = ""; + this.onEditableArea = false; + this.isDesignMode = false; + this.onCTPPlugin = false; + this.canSpellCheck = false; + this.textSelected = getBrowserSelection(); + this.isTextSelected = this.textSelected.length != 0; + + // Remember the node that was clicked. + this.target = aNode; + + this.browser = this.target.ownerDocument.defaultView + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + + // Check if we are in a synthetic document (stand alone image, video, etc.). + this.inSyntheticDoc = this.target.ownerDocument.mozSyntheticDocument; + // First, do checks for nodes that never have children. + if (this.target.nodeType == Node.ELEMENT_NODE) { + // See if the user clicked on an image. + if (this.target instanceof Ci.nsIImageLoadingContent && + this.target.currentURI) { + this.onImage = true; + + var request = + this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); + if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)) { + this.onLoadedImage = true; + } + if (request && (request.imageStatus & request.STATUS_LOAD_COMPLETE)) { + this.onCompletedImage = true; + } + + this.mediaURL = this.target.currentURI.spec; + } else if (this.target instanceof HTMLCanvasElement) { + this.onCanvas = true; + } else if (this.target instanceof HTMLVideoElement) { + this.mediaURL = this.target.currentSrc || this.target.src; + // Pale Moon always creates a HTMLVideoElement when loading an ogg file + // directly. If the media is actually audio, be smarter and provide a + // context menu with audio operations. + if (this.target.readyState >= this.target.HAVE_METADATA && + (this.target.videoWidth == 0 || this.target.videoHeight == 0)) { + this.onAudio = true; + } else { + this.onVideo = true; + } + } else if (this.target instanceof HTMLAudioElement) { + this.onAudio = true; + this.mediaURL = this.target.currentSrc || this.target.src; + } else if (this.target instanceof HTMLInputElement ) { + this.onTextInput = this.isTargetATextBox(this.target); + // Allow spellchecking UI on all text and search inputs. + if (this.onTextInput && ! this.target.readOnly && + (this.target.type == "text" || this.target.type == "search")) { + this.onEditableArea = true; + InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor); + InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset); + } + this.onKeywordField = this.isTargetAKeywordField(this.target); + } else if (this.target instanceof HTMLTextAreaElement) { + this.onTextInput = true; + if (!this.target.readOnly) { + this.onEditableArea = true; + InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor); + InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset); + } + } else if (this.target instanceof HTMLHtmlElement) { + var bodyElt = this.target.ownerDocument.body; + if (bodyElt) { + let computedURL; + try { + computedURL = this.getComputedURL(bodyElt, "background-image"); + this._hasMultipleBGImages = false; + } catch(e) { + this._hasMultipleBGImages = true; + } + if (computedURL) { + this.hasBGImage = true; + this.bgImageURL = makeURLAbsolute(bodyElt.baseURI, + computedURL); + } + } + } else if ((this.target instanceof HTMLEmbedElement || + this.target instanceof HTMLObjectElement || + this.target instanceof HTMLAppletElement) && + this.target.displayedType == HTMLObjectElement.TYPE_NULL && + this.target.pluginFallbackType == HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) { + this.onCTPPlugin = true; + } + + this.canSpellCheck = this._isSpellCheckEnabled(this.target); + } else if (this.target.nodeType == Node.TEXT_NODE) { + // For text nodes, look at the parent node to determine the spellcheck attribute. + this.canSpellCheck = this.target.parentNode && + this._isSpellCheckEnabled(this.target); + } + + // Second, bubble out, looking for items of interest that can have childen. + // Always pick the innermost link, background image, etc. + const XMLNS = "http://www.w3.org/XML/1998/namespace"; + var elem = this.target; + while (elem) { + if (elem.nodeType == Node.ELEMENT_NODE) { + // Link? + if (!this.onLink && + // Be consistent with what hrefAndLinkNodeForClickEvent + // does in browser.js + ((elem instanceof HTMLAnchorElement && elem.href) || + (elem instanceof HTMLAreaElement && elem.href) || + elem instanceof HTMLLinkElement || + elem.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple")) { + + // Target is a link or a descendant of a link. + this.onLink = true; + + // Remember corresponding element. + this.link = elem; + this.linkURL = this.getLinkURL(); + this.linkURI = this.getLinkURI(); + this.linkProtocol = this.getLinkProtocol(); + this.onMailtoLink = (this.linkProtocol == "mailto"); + this.onSaveableLink = this.isLinkSaveable( this.link ); + try { + if (elem.download) { + // Ignore download attribute on cross-origin links? + // This shoudn't be an issue because the download link presents + // the originating URL domain and protocol to help user understand + // from where file is downloaded and make right decision. + // If we decide we want this restriction: + // this.principal.checkMayLoad(this.linkURI, false, true); + this.linkDownload = elem.download; + } + } catch(ex) {} + } + + // Background image? Don't bother if we've already found a + // background image further down the hierarchy. Otherwise, + // we look for the computed background-image style. + if (!this.hasBGImage && !this._hasMultipleBGImages) { + let bgImgUrl; + try { + bgImgUrl = this.getComputedURL(elem, "background-image"); + this._hasMultipleBGImages = false; + } catch(e) { + this._hasMultipleBGImages = true; + } + if (bgImgUrl) { + this.hasBGImage = true; + this.bgImageURL = makeURLAbsolute(elem.baseURI, bgImgUrl); + } + } + } + + elem = elem.parentNode; + } + + // See if the user clicked on MathML + const NS_MathML = "http://www.w3.org/1998/Math/MathML"; + if ((this.target.nodeType == Node.TEXT_NODE && + this.target.parentNode.namespaceURI == NS_MathML) + || (this.target.namespaceURI == NS_MathML)) { + this.onMathML = true; + } + + // See if the user clicked in a frame. + var docDefaultView = this.target.ownerDocument.defaultView; + if (docDefaultView != docDefaultView.top) { + this.inFrame = true; + + if (this.target.ownerDocument.isSrcdocDocument) { + this.inSrcdocFrame = true; + } + } + + // if the document is editable, show context menu like in text inputs + if (!this.onEditableArea) { + var win = this.target.ownerDocument.defaultView; + if (win) { + var isEditable = false; + try { + var editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + if (editingSession.windowIsEditable(win) && + this.getComputedStyle(this.target, "-moz-user-modify") == "read-write") { + isEditable = true; + } + } catch(ex) { + // If someone built with composer disabled, we can't get an editing session. + } + + if (isEditable) { + this.onTextInput = true; + this.onKeywordField = false; + this.onImage = false; + this.onLoadedImage = false; + this.onCompletedImage = false; + this.onMathML = false; + this.inFrame = false; + this.inSrcdocFrame = false; + this.hasBGImage = false; + this.isDesignMode = true; + this.onEditableArea = true; + InlineSpellCheckerUI.init(editingSession.getEditorForWindow(win)); + var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck; + InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset); + this.showItem("spell-check-enabled", canSpell); + this.showItem("spell-separator", canSpell); + } + } + } + }, + + // Returns the computed style attribute for the given element. + getComputedStyle: function(aElem, aProp) { + return aElem.ownerDocument + .defaultView + .getComputedStyle(aElem, "") + .getPropertyValue(aProp); + }, + + // Returns a "url"-type computed style attribute value, with the url() stripped. + getComputedURL: function(aElem, aProp) { + var url = aElem.ownerDocument + .defaultView + .getComputedStyle(aElem, "") + .getPropertyCSSValue(aProp); + if (url instanceof CSSValueList) { + if (url.length != 1) { + throw "found multiple URLs"; + } + url = url[0]; + } + return url.primitiveType == CSSPrimitiveValue.CSS_URI ? url.getStringValue() : null; + }, + + // Returns true if clicked-on link targets a resource that can be saved. + isLinkSaveable: function(aLink) { + // We don't do the Right Thing for news/snews yet, so turn them off + // until we do. + return this.linkProtocol && !( + this.linkProtocol == "mailto" || + this.linkProtocol == "javascript" || + this.linkProtocol == "news" || + this.linkProtocol == "snews"); + }, + + _isSpellCheckEnabled: function(aNode) { + // We can always force-enable spellchecking on textboxes + if (this.isTargetATextBox(aNode)) { + return true; + } + // We can never spell check something which is not content editable + var editable = aNode.isContentEditable; + if (!editable && aNode.ownerDocument) { + editable = aNode.ownerDocument.designMode == "on"; + } + if (!editable) { + return false; + } + // Otherwise make sure that nothing in the parent chain disables spellchecking + return aNode.spellcheck; + }, + + // Open linked-to URL in a new window. + openLink : function() { + var doc = this.target.ownerDocument; + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + openLinkIn(this.linkURL, "window", + { charset: doc.characterSet, + referrerURI: doc.documentURIObject, + referrerPolicy: doc.referrerPolicy, + originPrincipal: doc.nodePrincipal, + triggeringPrincipal: doc.nodePrincipal }); + }, + + // Open linked-to URL in a new private window. + openLinkInPrivateWindow : function() { + var doc = this.target.ownerDocument; + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + openLinkIn(this.linkURL, "window", + { charset: doc.characterSet, + referrerURI: doc.documentURIObject, + referrerPolicy: doc.referrerPolicy, + originPrincipal: doc.nodePrincipal, + triggeringPrincipal: doc.nodePrincipal, + private: true }); + }, + + // Open linked-to URL in a new tab. + openLinkInTab: function() { + var doc = this.target.ownerDocument; + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + openLinkIn(this.linkURL, "tab", + { charset: doc.characterSet, + referrerURI: doc.documentURIObject, + referrerPolicy: doc.referrerPolicy, + originPrincipal: doc.nodePrincipal, + triggeringPrincipal: doc.nodePrincipal }); + }, + + // open URL in current tab + openLinkInCurrent: function() { + var doc = this.target.ownerDocument; + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + openLinkIn(this.linkURL, "current", + { charset: doc.characterSet, + referrerURI: doc.documentURIObject, + originPrincipal: doc.nodePrincipal, + triggeringPrincipal: doc.nodePrincipal }); + }, + + // Open frame in a new tab. + openFrameInTab: function() { + var doc = this.target.ownerDocument; + var frameURL = doc.location.href; + var referrer = doc.referrer; + openLinkIn(frameURL, "tab", + { charset: doc.characterSet, + referrerURI: referrer ? makeURI(referrer) : null }); + }, + + // Reload clicked-in frame. + reloadFrame: function() { + this.target.ownerDocument.location.reload(); + }, + + // Open clicked-in frame in its own window. + openFrame: function() { + var doc = this.target.ownerDocument; + var frameURL = doc.location.href; + var referrer = doc.referrer; + openLinkIn(frameURL, "window", + { charset: doc.characterSet, + referrerURI: referrer ? makeURI(referrer) : null }); + }, + + // Open clicked-in frame in the same window. + showOnlyThisFrame: function() { + var doc = this.target.ownerDocument; + var frameURL = doc.location.href; + + urlSecurityCheck(frameURL, this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + var referrer = doc.referrer; + openUILinkIn(frameURL, "current", { disallowInheritPrincipal: true, + referrerURI: referrer ? makeURI(referrer) : null }); + }, + + reload: function(event) { + BrowserReloadOrDuplicate(event); + }, + + // View Partial Source + viewPartialSource: function(aContext) { + let target = aContext == "mathml" ? this.target : null; + top.gViewSourceUtils.viewPartialSourceInBrowser(gBrowser.selectedBrowser, target); + }, + + // Open new "view source" window with the frame's URL. + viewFrameSource: function() { + BrowserViewSourceOfDocument(this.target.ownerDocument); + }, + + viewInfo: function() { + BrowserPageInfo(this.target.ownerDocument.defaultView.top.document); + }, + + viewImageInfo: function() { + BrowserPageInfo(this.target.ownerDocument.defaultView.top.document, + "mediaTab", this.target); + }, + + viewFrameInfo: function() { + BrowserPageInfo(this.target.ownerDocument); + }, + + reloadImage: function(e) { + urlSecurityCheck(this.mediaURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + + if (this.target instanceof Ci.nsIImageLoadingContent) { + this.target.forceReload(); + } + }, + + // Change current window to the URL of the image, video, or audio. + viewMedia: function(e) { + var viewURL; + var doc = this.target.ownerDocument; + if (this.onCanvas) { + var target = this.target; + var win = doc.defaultView; + if (!win) { + Components.utils.reportError( + "View Image (on the <canvas> element):\n" + + "This feature cannot be used, because it hasn't found " + + "an appropriate window."); + } else { + // TODO: This is unreadable. Rewrite it to something more sane. + new Promise.resolve({ then: function(resolve) { + target.toBlob((blob) => { + resolve(win.URL.createObjectURL(blob)); + }) + }}).then(function(blobURL) { + openUILink(blobURL, e, { disallowInheritPrincipal: true, + referrerURI: doc.documentURIObject }); + }, Components.utils.reportError); + } + } else { + viewURL = this.mediaURL; + urlSecurityCheck(viewURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + let doc = this.target.ownerDocument; + openUILink(viewURL, e, { disallowInheritPrincipal: true, + referrerURI: doc.documentURIObject, + forceAllowDataURI: true }); + } + }, + + saveVideoFrameAsImage: function() { + let referrerURI = document.documentURIObject; + let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser); + + urlSecurityCheck(this.mediaURL, this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + let name = ""; + try { + let uri = makeURI(this.mediaURL); + let url = uri.QueryInterface(Ci.nsIURL); + if (url.fileBaseName) + name = decodeURI(url.fileBaseName) + ".jpg"; + } catch(e) {} + if (!name) { + name = "snapshot.jpg"; + } + var video = this.target; + var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + var ctxDraw = canvas.getContext("2d"); + ctxDraw.drawImage(video, 0, 0); + saveImageURL(canvas.toDataURL("image/jpeg", ""), name, "SaveImageTitle", + true, false, referrerURI, null, null, null, + isPrivate); + }, + + fullScreenVideo: function() { + let video = this.target; + if (document.mozFullScreenEnabled) { + video.mozRequestFullScreen(); + } + }, + + leaveDOMFullScreen: function() { + document.mozCancelFullScreen(); + }, + + // Change current window to the URL of the background image. + viewBGImage: function(e) { + urlSecurityCheck(this.bgImageURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + var doc = this.target.ownerDocument; + openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true, + referrerURI: doc.documentURIObject }); + }, + + disableSetDesktopBackground: function() { + // Disable the Set as Desktop Background menu item if we're still trying + // to load the image or the load failed. + if (!(this.target instanceof Ci.nsIImageLoadingContent)) { + return true; + } + + if (("complete" in this.target) && !this.target.complete) { + return true; + } + + if (this.target.currentURI.schemeIs("javascript")) { + return true; + } + + var request = this.target + .QueryInterface(Ci.nsIImageLoadingContent) + .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); + if (!request) { + return true; + } + + return false; + }, + + setDesktopBackground: function() { + // Paranoia: check disableSetDesktopBackground again, in case the + // image changed since the context menu was initiated. + if (this.disableSetDesktopBackground()) { + return; + } + + urlSecurityCheck(this.target.currentURI.spec, + this.target.ownerDocument.nodePrincipal); + + // Confirm since it's annoying if you hit this accidentally. + const kDesktopBackgroundURL = "chrome://browser/content/setDesktopBackground.xul"; + // The Set Wallpaper dialog is modal. + openDialog(kDesktopBackgroundURL, "", + "centerscreen,chrome,dialog,modal,dependent", + this.target); + }, + + // Save URL of clicked-on frame. + saveFrame: function() { + saveDocument(this.target.ownerDocument); + }, + + // Helper function to wait for appropriate MIME-type headers and + // then prompt the user with a file picker + saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc, + linkDownload) { + // canonical def in nsURILoader.h + const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020; + + // an object to proxy the data through to + // nsIExternalHelperAppService.doContent, which will wait for the + // appropriate MIME-type headers and then prompt the user with a + // file picker + function saveAsListener() {} + + saveAsListener.prototype = { + extListener: null, + + onStartRequest: function(aRequest, aContext) { + + // if the timer fired, the error status will have been caused by that, + // and we'll be restarting in onStopRequest, so no reason to notify + // the user + if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT) { + return; + } + + timer.cancel(); + + // some other error occured; notify the user... + if (!Components.isSuccessCode(aRequest.status)) { + try { + const sbs = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService); + const bundle = sbs.createBundle( + "chrome://mozapps/locale/downloads/downloads.properties"); + + const title = bundle.GetStringFromName("downloadErrorAlertTitle"); + const msg = bundle.GetStringFromName("downloadErrorGeneric"); + + const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService); + promptSvc.alert(doc.defaultView, title, msg); + } catch(ex) {} + return; + } + + var extHelperAppSvc = + Cc["@mozilla.org/uriloader/external-helper-app-service;1"]. + getService(Ci.nsIExternalHelperAppService); + var channel = aRequest.QueryInterface(Ci.nsIChannel); + this.extListener = + extHelperAppSvc.doContent(channel.contentType, aRequest, + doc.defaultView, true); + this.extListener.onStartRequest(aRequest, aContext); + }, + + onStopRequest: function(aRequest, aContext, aStatusCode) { + if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) { + // do it the old fashioned way, which will pick the best filename + // it can without waiting. + saveURL(linkURL, linkText, dialogTitle, bypassCache, false, doc.documentURIObject, doc); + } + if (this.extListener) { + this.extListener.onStopRequest(aRequest, aContext, aStatusCode); + } + }, + + onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { + this.extListener.onDataAvailable(aRequest, aContext, aInputStream, + aOffset, aCount); + } + } + + function callbacks() {} + + callbacks.prototype = { + getInterface: function(aIID) { + if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) { + // If the channel demands authentication prompt, we must cancel it + // because the save-as-timer would expire and cancel the channel + // before we get credentials from user. Both authentication dialog + // and save as dialog would appear on the screen as we fall back to + // the old fashioned way after the timeout. + timer.cancel(); + channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT); + } + throw Cr.NS_ERROR_NO_INTERFACE; + } + } + + // if it we don't have the headers after a short time, the user + // won't have received any feedback from their click. that's bad. so + // we give up waiting for the filename. + function timerCallback() {} + + timerCallback.prototype = { + notify: function(aTimer) { + channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT); + return; + } + } + + // setting up a new channel for 'right click - save link as ...' + var channel = NetUtil.newChannel( + { uri: makeURI(linkURL), + loadingPrincipal: this.target.ownerDocument.nodePrincipal, + contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS }); + + if (linkDownload) { + channel.contentDispositionFilename = linkDownload; + } + if (channel instanceof Ci.nsIPrivateBrowsingChannel) { + let docIsPrivate = PrivateBrowsingUtils.isWindowPrivate(doc.defaultView); + channel.setPrivate(docIsPrivate); + } + channel.notificationCallbacks = new callbacks(); + + let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS; + + if (bypassCache) { + flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + } + + if (channel instanceof Ci.nsICachingChannel) { + flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY; + } + + channel.loadFlags |= flags; + + if (channel instanceof Ci.nsIHttpChannel) { + channel.referrer = doc.documentURIObject; + if (channel instanceof Ci.nsIHttpChannelInternal) { + channel.forceAllowThirdPartyCookie = true; + } + } + + // fallback to the old way if we don't see the headers quickly + var timeToWait = gPrefService.getIntPref("browser.download.saveLinkAsFilenameTimeout"); + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(new timerCallback(), timeToWait, timer.TYPE_ONE_SHOT); + + // kick off the channel with our proxy object as the listener + channel.asyncOpen2(new saveAsListener()); + }, + + // Save URL of clicked-on link. + saveLink: function() { + var doc = this.target.ownerDocument; + var linkText; + // If selected text is found to match valid URL pattern. + if (this.onPlainTextLink) { + linkText = document.commandDispatcher.focusedWindow.getSelection().toString().trim(); + } else { + linkText = this.linkText(); + } + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + + this.saveHelper(this.linkURL, linkText, null, true, doc, + this.linkDownload); + }, + + sendLink: function() { + // we don't know the title of the link so pass in an empty string + MailIntegration.sendMessage( this.linkURL, "" ); + }, + + // Backwards-compatibility wrapper + saveImage : function() { + if (this.onCanvas || this.onImage) { + this.saveMedia(); + } + }, + + // Save URL of the clicked upon image, video, or audio. + saveMedia: function() { + var doc = this.target.ownerDocument; + let referrerURI = doc.documentURIObject; + let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser); + if (this.onCanvas) { + // Bypass cache, since it's a blob: URL. + var target = this.target; + var win = doc.defaultView; + if (!win) { + Components.utils.reportError( + "Save Image As (on the <canvas> element):\n" + + "This feature cannot be used, because it hasn't found " + + "an appropriate window."); + } else { + // TODO: This is unreadable. Rewrite it to something more sane. + new Promise.resolve({ then: function(resolve) { + target.toBlob((blob) => { + resolve(win.URL.createObjectURL(blob)); + }) + }}).then(function(blobURL) { + saveImageURL(blobURL, "canvas.png", "SaveImageTitle", + true, false, referrerURI, null, null, null, + isPrivate); + }, Components.utils.reportError); + } + } else if (this.onImage) { + urlSecurityCheck(this.mediaURL, doc.nodePrincipal); + saveImageURL(this.mediaURL, null, "SaveImageTitle", + false, false, referrerURI, doc, null, null, + isPrivate); + } else if (this.onVideo || this.onAudio) { + urlSecurityCheck(this.mediaURL, doc.nodePrincipal); + var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle"; + this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, ""); + } + }, + + // Backwards-compatibility wrapper + sendImage : function() { + if (this.onCanvas || this.onImage) + this.sendMedia(); + }, + + sendMedia: function() { + MailIntegration.sendMessage(this.mediaURL, ""); + }, + + playPlugin: function() { + gPluginHandler._showClickToPlayNotification(this.browser, this.target); + }, + + hidePlugin: function() { + gPluginHandler.hideClickToPlayOverlay(this.target); + }, + + // Generate email address and put it on clipboard. + copyEmail: function() { + // Copy the comma-separated list of email addresses only. + // There are other ways of embedding email addresses in a mailto: + // link, but such complex parsing is beyond us. + var url = this.linkURL; + var qmark = url.indexOf("?"); + var addresses; + + // 7 == length of "mailto:" + addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7); + + // Let's try to unescape it using a character set + // in case the address is not ASCII. + try { + var characterSet = this.target.ownerDocument.characterSet; + const textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"] + .getService(Ci.nsITextToSubURI); + addresses = textToSubURI.unEscapeURIForUI(characterSet, addresses); + } catch(ex) { + // Do nothing. + } + + var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper); + clipboard.copyString(addresses, document); + }, + + /////////////// + // Utilities // + /////////////// + + // Show/hide one item (specified via name or the item element itself). + showItem: function(aItemOrId, aShow) { + var item = aItemOrId.constructor == String ? + document.getElementById(aItemOrId) : + aItemOrId; + if (item) { + item.hidden = !aShow; + } + }, + + // Set given attribute of specified context-menu item. If the + // value is null, then it removes the attribute (which works + // nicely for the disabled attribute). + setItemAttr: function(aID, aAttr, aVal ) { + var elem = document.getElementById(aID); + if (elem) { + if (aVal == null) { + // null indicates attr should be removed. + elem.removeAttribute(aAttr); + } else { + // Set attr=val. + elem.setAttribute(aAttr, aVal); + } + } + }, + + // Set context menu attribute according to like attribute of another node + // (such as a broadcaster). + setItemAttrFromNode: function(aItem_id, aAttr, aOther_id) { + var elem = document.getElementById(aOther_id); + if (elem && elem.getAttribute(aAttr) == "true") { + this.setItemAttr(aItem_id, aAttr, "true"); + } else { + this.setItemAttr(aItem_id, aAttr, null); + } + }, + + // Temporary workaround for DOM api not yet implemented by XUL nodes. + cloneNode: function(aItem) { + // Create another element like the one we're cloning. + var node = document.createElement(aItem.tagName); + + // Copy attributes from argument item to the new one. + var attrs = aItem.attributes; + for (var i = 0; i < attrs.length; i++) { + var attr = attrs.item(i); + node.setAttribute(attr.nodeName, attr.nodeValue); + } + + // Voila! + return node; + }, + + // Generate fully qualified URL for clicked-on link. + getLinkURL: function() { + var href = this.link.href; + if (href) + return href; + + href = this.link.getAttributeNS("http://www.w3.org/1999/xlink", + "href"); + + if (!href || !href.match(/\S/)) { + // Without this we try to save as the current doc, + // for example, HTML case also throws if empty + throw "Empty href"; + } + + return makeURLAbsolute(this.link.baseURI, href); + }, + + getLinkURI: function() { + try { + return makeURI(this.linkURL); + } catch(ex) { + // e.g. empty URL string + } + + return null; + }, + + getLinkProtocol: function() { + if (this.linkURI) { + // can be |undefined| + return this.linkURI.scheme; + } + + return null; + }, + + // Get text of link. + linkText: function() { + var text = gatherTextUnder(this.link); + if (!text || !text.match(/\S/)) { + text = this.link.getAttribute("title"); + if (!text || !text.match(/\S/)) { + text = this.link.getAttribute("alt"); + if (!text || !text.match(/\S/)) { + text = this.linkURL; + } + } + } + + return text; + }, + + // Returns true if anything is selected. + isContentSelection: function() { + return !document.commandDispatcher.focusedWindow.getSelection().isCollapsed; + }, + + toString: function() { + return "contextMenu.target = " + this.target + "\n" + + "contextMenu.onImage = " + this.onImage + "\n" + + "contextMenu.onLink = " + this.onLink + "\n" + + "contextMenu.link = " + this.link + "\n" + + "contextMenu.inFrame = " + this.inFrame + "\n" + + "contextMenu.hasBGImage = " + this.hasBGImage + "\n"; + }, + + isDisabledForEvents: function(aNode) { + let ownerDoc = aNode.ownerDocument; + return ownerDoc.defaultView && + ownerDoc.defaultView + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils) + .isNodeDisabledForEvents(aNode); + }, + + isTargetATextBox: function(node) { + if (node instanceof HTMLInputElement) { + return node.mozIsTextField(false); + } + + return (node instanceof HTMLTextAreaElement); + }, + + isTargetAKeywordField: function(aNode) { + if (!(aNode instanceof HTMLInputElement)) { + return false; + } + + var form = aNode.form; + if (!form || aNode.type == "password") { + return false; + } + + var method = form.method.toUpperCase(); + + // These are the following types of forms we can create keywords for: + // + // method encoding type can create keyword + // GET * YES + // * YES + // POST YES + // POST application/x-www-form-urlencoded YES + // POST text/plain NO (a little tricky to do) + // POST multipart/form-data NO + // POST everything else YES + return (method == "GET" || method == "") || + ((form.enctype != "text/plain") && + (form.enctype != "multipart/form-data")); + }, + + // Determines whether or not the separator with the specified ID should be + // shown or not by determining if there are any non-hidden items between it + // and the previous separator. + shouldShowSeparator: function(aSeparatorID) { + var separator = document.getElementById(aSeparatorID); + if (separator) { + var sibling = separator.previousSibling; + while (sibling && sibling.localName != "menuseparator") { + if (!sibling.hidden) { + return true; + } + sibling = sibling.previousSibling; + } + } + return false; + }, + + addDictionaries: function() { + var uri = formatURL("browser.dictionaries.download.url", true); + + var locale = "-"; + try { + locale = gPrefService.getComplexValue("intl.accept_languages", + Ci.nsIPrefLocalizedString).data; + } catch(e) {} + + var version = "-"; + try { + version = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULAppInfo).version; + } catch(e) {} + + uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version); + + var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow"); + var where = newWindowPref == 3 ? "tab" : "window"; + + openUILinkIn(uri, where); + }, + + bookmarkThisPage: function() { + window.top.PlacesCommandHook.bookmarkPage(this.browser, PlacesUtils.bookmarksMenuFolderId, true); + }, + + bookmarkLink: function() { + var linkText; + // If selected text is found to match valid URL pattern. + if (this.onPlainTextLink) { + linkText = document.commandDispatcher.focusedWindow.getSelection().toString().trim(); + } else { + linkText = this.linkText(); + } + window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId, this.linkURL, linkText); + }, + + addBookmarkForFrame: function() { + var doc = this.target.ownerDocument; + var uri = doc.documentURIObject; + + var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri); + if (itemId == -1) { + var title = doc.title; + var description = PlacesUIUtils.getDescriptionFromDocument(doc); + PlacesUIUtils.showBookmarkDialog({ action: "add", + type: "bookmark", + uri: uri, + title: title, + description: description, + hiddenRows: [ "description" + , "location" + , "loadInSidebar" + , "keyword" ] + }, window.top); + } else { + PlacesUIUtils.showBookmarkDialog({ action: "edit", + type: "bookmark", + itemId: itemId + }, window.top); + } + }, + + savePageAs: function() { + saveDocument(this.browser.contentDocument); + }, + + sendPage: function() { + MailIntegration.sendLinkForWindow(this.browser.contentWindow); + }, + + printFrame: function() { + PrintUtils.print(this.target.ownerDocument.defaultView); + }, + + switchPageDirection: function() { + SwitchDocumentDirection(this.browser.contentWindow); + }, + + mediaCommand : function(command, data) { + var media = this.target; + + switch (command) { + case "play": + media.play(); + break; + case "pause": + media.pause(); + break; + case "loop": + media.loop = !media.loop; + break; + case "mute": + media.muted = true; + break; + case "unmute": + media.muted = false; + break; + case "playbackRate": + media.playbackRate = data; + break; + case "hidecontrols": + media.removeAttribute("controls"); + break; + case "showcontrols": + media.setAttribute("controls", "true"); + break; + case "hidestats": + case "showstats": + var event = media.ownerDocument.createEvent("CustomEvent"); + event.initCustomEvent("media-showStatistics", false, true, command == "showstats"); + media.dispatchEvent(event); + break; + } + }, + + copyMediaLocation : function() { + var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper); + clipboard.copyString(this.mediaURL, document); + }, + + get imageURL() { + if (this.onImage) { + return this.mediaURL; + } + return ""; + }, + + // Formats the 'Search <engine> for "<selection or link text>"' context menu. + formatSearchContextItem: function() { + var menuItem = document.getElementById("context-searchselect"); + var selectedText = this.isTextSelected ? this.textSelected : this.linkText(); + + // Store searchTerms in context menu item so we know what to search onclick + menuItem.searchTerms = selectedText; + + if (selectedText.length > 15) { + selectedText = selectedText.substr(0,15) + this.ellipsis; + } + + // Use the current engine if the search bar is visible, the default + // engine otherwise. + var engineName = ""; + var ss = Cc["@mozilla.org/browser/search-service;1"] + .getService(Ci.nsIBrowserSearchService); + if (isElementVisible(BrowserSearch.searchBar)) { + engineName = ss.currentEngine.name; + } else { + engineName = ss.defaultEngine.name; + } + + // format "Search <engine> for <selection>" string to show in menu + var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch", + [engineName, + selectedText]); + menuItem.label = menuLabel; + menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey"); + } +}; diff --git a/browser/base/content/openLocation.js b/browser/base/content/openLocation.js new file mode 100644 index 000000000..1263d68d0 --- /dev/null +++ b/browser/base/content/openLocation.js @@ -0,0 +1,148 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var browser; +var dialog = {}; +var pref = null; +var openLocationModule = {}; +try { + pref = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); +} catch(ex) { + // not critical, remain silent +} + +Components.utils.import("resource:///modules/openLocationLastURL.jsm", openLocationModule); +var gOpenLocationLastURL = new openLocationModule.OpenLocationLastURL(window.opener); + +function onLoad() { + dialog.input = document.getElementById("dialog.input"); + dialog.open = document.documentElement.getButton("accept"); + dialog.openWhereList = document.getElementById("openWhereList"); + dialog.openTopWindow = document.getElementById("currentWindow"); + dialog.bundle = document.getElementById("openLocationBundle"); + + if ("arguments" in window && window.arguments.length >= 1) { + browser = window.arguments[0]; + } + + dialog.openWhereList.selectedItem = dialog.openTopWindow; + + if (pref) { + try { + var useAutoFill = pref.getBoolPref("browser.urlbar.autoFill"); + if (useAutoFill) { + dialog.input.setAttribute("completedefaultindex", "true"); + } + } catch(ex) {} + + try { + var value = pref.getIntPref("general.open_location.last_window_choice"); + var element = dialog.openWhereList.getElementsByAttribute("value", value)[0]; + if (element) { + dialog.openWhereList.selectedItem = element; + } + dialog.input.value = gOpenLocationLastURL.value; + } catch(ex) {} + + if (dialog.input.value) { + // XXX should probably be done automatically + dialog.input.select(); + } + } + + doEnabling(); +} + +function doEnabling() { + dialog.open.disabled = !dialog.input.value; +} + +function open() { + var openData = { + "url": null, + "postData": null, + "mayInheritPrincipal": false + }; + if (browser) { + browser.getShortcutOrURIAndPostData(dialog.input.value).then(data => { + openData.url = data.url; + openData.postData = data.postData; + openData.mayInheritPrincipal = data.mayInheritPrincipal; + + openLocation(openData); + }); + } else { + openData.url = dialog.input.value; + + openLocation(openData); + } + + return false; +} + +function openLocation(openData) { + try { + // Whichever target we use for the load, we allow third-party services to + // fix up the URI + switch (dialog.openWhereList.value) { + case "0": + var webNav = Components.interfaces.nsIWebNavigation; + var flags = webNav.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | + webNav.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + if (!openData.mayInheritPrincipal) { + flags |= webNav.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; + } + browser.gBrowser.loadURIWithFlags(openData.url, flags, null, null, openData.postData); + break; + case "1": + window.opener.delayedOpenWindow(getBrowserURL(), "all,dialog=no", + openData.url, openData.postData, + null, null, true); + break; + case "3": + browser.delayedOpenTab(openData.url, null, null, openData.postData, true); + break; + } + } catch(ex) {} + + if (pref) { + gOpenLocationLastURL.value = dialog.input.value; + pref.setIntPref("general.open_location.last_window_choice", dialog.openWhereList.value); + } + + window.close(); +} + +function createInstance(contractid, iidName) +{ + var iid = Components.interfaces[iidName]; + return Components.classes[contractid].createInstance(iid); +} + +const nsIFilePicker = Components.interfaces.nsIFilePicker; +function onChooseFile() +{ + try { + let fp = Components.classes["@mozilla.org/filepicker;1"] + .createInstance(nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK && + fp.fileURL.spec && + fp.fileURL.spec.length > 0) { + dialog.input.value = fp.fileURL.spec; + } + doEnabling(); + }; + + fp.init(window, dialog.bundle.getString("chooseFileDialogTitle"), + nsIFilePicker.modeOpen); + fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText | + nsIFilePicker.filterImages | nsIFilePicker.filterXML | + nsIFilePicker.filterHTML); + fp.open(fpCallback); + } catch(ex) {} +} diff --git a/browser/base/content/openLocation.xul b/browser/base/content/openLocation.xul new file mode 100644 index 000000000..7bafed0fe --- /dev/null +++ b/browser/base/content/openLocation.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"?> + +<!DOCTYPE dialog SYSTEM "chrome://browser/locale/openLocation.dtd"> + +<dialog id="openLocation" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&caption.label;" + onload="onLoad()" + buttonlabelaccept="&openBtn.label;" + buttoniconaccept="open" + ondialogaccept="open()" + style="width: 40em;" + persist="screenX screenY" + screenX="24" screenY="24"> + + <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" src="chrome://browser/content/openLocation.js"/> + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + + <stringbundle id="openLocationBundle" src="chrome://browser/locale/openLocation.properties"/> + + <hbox> + <separator orient="vertical" class="thin"/> + <vbox flex="1"> + <description>&enter.label;</description> + <separator class="thin"/> + + <hbox align="center"> + <textbox id="dialog.input" flex="1" type="autocomplete" + completeselectedindex="true" + autocompletesearch="urlinline history" + enablehistory="true" + class="uri-element" + oninput="doEnabling();"/> + <button label="&chooseFile.label;" oncommand="onChooseFile();"/> + </hbox> + <hbox align="center"> + <label value="&openWhere.label;"/> + <menulist id="openWhereList"> + <menupopup> + <menuitem value="0" id="currentWindow" label="&topTab.label;"/> + <menuitem value="3" label="&newTab.label;"/> + <menuitem value="1" label="&newWindow.label;"/> + </menupopup> + </menulist> + <spacer flex="1"/> + </hbox> + </vbox> + </hbox> + +</dialog> diff --git a/browser/base/content/overrides/app-license.html b/browser/base/content/overrides/app-license.html new file mode 100644 index 000000000..2d2e3d55e --- /dev/null +++ b/browser/base/content/overrides/app-license.html @@ -0,0 +1,6 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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><b>Binaries</b> of this product have been made available to you by the + <a href="http://www.palemoon.org/">Pale Moon Project</a> under the Pale Moon + Binary <a href="http://www.palemoon.org/redist.shtml">redistribution license</a>.</p>
\ No newline at end of file diff --git a/browser/base/content/padlock.css b/browser/base/content/padlock.css new file mode 100644 index 000000000..7503790a3 --- /dev/null +++ b/browser/base/content/padlock.css @@ -0,0 +1,227 @@ +#padlock-ib { + -moz-appearance: none; + min-width: 0px; + margin-right: 1px !important; + background-repeat: no-repeat; + background-position: center; + z-index: 1000 !important; + padding: 0px; + margin: 0px; + border: 0px; +} + +#padlock-ib[padshow="ib-trans-bg"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + background-color: transparent; +} + +#padlock-ib[padshow="ib-trans-bg"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + background-color: transparent; +} + +#padlock-ib[padshow="ib-trans-bg"][level="low"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + background-color: transparent; +} + +#padlock-ib[padshow="ib-trans-bg"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_mixed.png"); + background-color: transparent; +} + +#padlock-ib[padshow="ib-trans-bg"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + background-color: transparent; +} + +#padlock-ib-left { + -moz-appearance: none; + min-width: 0px; + margin-right: 1px !important; + background-repeat: no-repeat; + background-position: center; + z-index: 1000 !important; + padding: 0px; + margin: 0px; + border: 0px; +} + +#padlock-ib-left[padshow="ib-left"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + padding: 2px; + background-color: transparent; +} + +#padlock-ib-left[padshow="ib-left"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + padding: 2px; + background-color: transparent; +} + +#padlock-ib-left[padshow="ib-left"][level="low"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + padding: 2px; + background-color: transparent; +} + +#padlock-ib-left[padshow="ib-left"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_mixed.png"); + padding: 2px; + background-color: transparent; +} + +#padlock-ib-left[padshow="ib-left"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + padding: 2px; + background-color: transparent; +} + +#padlock-ub-right { + -moz-appearance: none; + min-width: 0px; + margin-right: 1px !important; + background-repeat: no-repeat; + background-position: center; + z-index: 1000 !important; + padding: 0px; + margin: 0px; + border: 0px; +} + +#padlock-ub-right[padshow="ub-right"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + background-color: transparent; +} + +#padlock-ub-right[padshow="ub-right"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + background-color: transparent; +} + +#padlock-ub-right[padshow="ub-right"][level="low"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + background-color: transparent; +} + +#padlock-ub-right[padshow="ub-right"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_mixed.png"); + background-color: transparent; +} + +#padlock-ub-right[padshow="ub-right"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + background-color: transparent; +} + +#padlock-sb { + -moz-appearance: none; + background-repeat: no-repeat; + background-position: center; +} + +#padlock-sb[padshow="statbar"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + background-color: transparent; +} + +#padlock-sb[padshow="statbar"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + background-color: transparent; +} + +#padlock-sb[padshow="statbar"][level="low"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + background-color: transparent; +} + +#padlock-sb[padshow="statbar"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_mixed.png"); + background-color: transparent; +} + +#padlock-sb[padshow="statbar"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + background-color: transparent; +} + +#padlock-tab { + -moz-appearance: none; + background-repeat: no-repeat; + background-position: center; +} + +#padlock-tab[padshow="tabs-bar"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + background-color: transparent; +} + +#padlock-tab[padshow="tabs-bar"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + background-color: transparent; +} + +#padlock-tab[padshow="tabs-bar"][level="low"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + background-color: transparent; +} + +#padlock-tab[padshow="tabs-bar"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_mixed.png"); + background-color: transparent; +} + +#padlock-tab[padshow="tabs-bar"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + background-color: transparent; +} + +/* Classic style */ +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="ev"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="ev"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="ev"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="ev"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_classic_ev.png"); +} + +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="high"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="high"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="high"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="high"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_classic_https.png"); +} + +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="low"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="low"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="low"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="low"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="low"] { + list-style-image: url("chrome://browser/content/padlock_classic_low.png"); +} + +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="mixed"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="mixed"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="mixed"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="mixed"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_classic_mixed.png"); +} + +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="broken"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="broken"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="broken"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="broken"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_classic_broken.png"); +} + +/* Remove a few px of dead space for disabled locations */ +#padlock-ib:not([padshow="ib-trans-bg"]), +#padlock-ib-left:not([padshow="ib-left"]), +#padlock-ub-right:not([padshow="ub-right"]), +#padlock-sb:not([padshow="statbar"]), +#padlock-tab:not([padshow="tabs-bar"]) { + visibility: collapse; +}
\ No newline at end of file diff --git a/browser/base/content/padlock.js b/browser/base/content/padlock.js new file mode 100644 index 000000000..f57f5075e --- /dev/null +++ b/browser/base/content/padlock.js @@ -0,0 +1,282 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var padlock_PadLock = +{ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + onButtonClick: function(event) { + event.stopPropagation(); + gIdentityHandler.handleMoreInfoClick(event); + }, + onStateChange: function() {}, + onProgressChange: function() {}, + onLocationChange: function() {}, + onStatusChange: function() {}, + onSecurityChange: function(aCallerWebProgress, aRequestWithState, aState) { + const wpl = Ci.nsIWebProgressListener; + var level; + var highlight_urlbar = false; + var secUI = gBrowser.securityUI; + var secState = secUI.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus; + if (secState == null) { + level = null; + } else { + highlight_urlbar = true; + secState.QueryInterface(Ci.nsISSLStatus); + // Step 1: Check EV + if (secState.isExtendedValidation) { + // Step 1 TRUE: Extended Validation + // Normal "ev" + // Mixed Content "broken" + if ((aState & wpl.STATE_LOADED_MIXED_ACTIVE_CONTENT) || + (aState & wpl.STATE_LOADED_MIXED_DISPLAY_CONTENT)) + level = "broken"; + else + level = "ev"; + } else { + // Step 1 FALSE: Domain Validation + // Normal "high" + // Mixed Passive Content "mixed" + // Mixed Active Content "broken" + if (aState & wpl.STATE_LOADED_MIXED_ACTIVE_CONTENT) + level = "broken"; + else if (aState & wpl.STATE_LOADED_MIXED_DISPLAY_CONTENT) + level = "mixed"; + else + level = "high"; + } + // Step 2: Check Protocol + if (level != "broken") { + // SSL 3 "broken" + // TLS 1.0 "low" + // TLS 1.1 "low" + var proto = secState.protocolVersion; + if (proto == Ci.nsISSLStatus.SSL_VERSION_3) + level = "broken"; + else if (proto == Ci.nsISSLStatus.TLS_VERSION_1 || + proto == Ci.nsISSLStatus.TLS_VERSION_1_1) { + level = "low"; + } + } + // Step 3: Check Bad Ciphers + if (level != "broken") { + // EXPORT "broken" + // RC2 "broken" + // RC4 + MD5 "broken" + // RC4 + SHA1 "low" + // 3DES "low" + var aCipher = secState.cipherSuite; + if (aCipher.indexOf("_EXPORT") > -1) { + level = "broken"; + } else if (aCipher.indexOf("_RC2_") > -1) { + level = "broken"; + } else if (aCipher.indexOf("_RC4_") > -1) { + if (aCipher.indexOf("_MD5") > -1) { + level = "broken"; + } else if (aCipher.indexOf("_SHA") > -1) { + level = "low"; + } + } else if (aCipher.indexOf("_3DES_") > -1) { + level = "low"; + } + } + // Step 4: Check Boolean Problems + if (level != "broken") { + // Untrusted "broken" + // Domain Mismatch "broken" + // Expired (or too new) "broken" + if (secState.isUntrusted || secState.isDomainMismatch || + secState.isNotValidAtThisTime) + level = "broken"; + } + } + + let ub = document.getElementById("urlbar"); + if (ub) { + // Only call if URL bar is present. + if (highlight_urlbar) { + ub.setAttribute("security_level", level); + } else { + ub.removeAttribute("security_level"); + } + } + + try { // URL bar may be hidden + padlock_PadLock.setPadlockLevel("padlock-ib", level); + padlock_PadLock.setPadlockLevel("padlock-ib-left", level); + padlock_PadLock.setPadlockLevel("padlock-ub-right", level); + } catch(e) {} + + padlock_PadLock.setPadlockLevel("padlock-sb", level); + padlock_PadLock.setPadlockLevel("padlock-tab", level); + }, + + setPadlockLevel: function(item, level) { + let secbut = document.getElementById(item); + var sectooltip = ""; + + if (level) { + secbut.setAttribute("level", level); + secbut.hidden = false; + } else { + secbut.hidden = true; + secbut.removeAttribute("level"); + } + + let s_ev = "Extended Validated"; + let s_hi = "Secure"; + let s_mx = "Mixed content"; + let s_lo = "Weak security"; + let s_no = "Not secure"; + let gLocale = document.getElementById("bundle_browser"); + if(!!gLocale) { + let n_ev = gLocale.getString("identity.padlock.ev"); + if(n_ev != null) + s_ev = n_ev; + let n_hi = gLocale.getString("identity.padlock.high"); + if(n_hi != null) + s_hi = n_hi; + let n_mx = gLocale.getString("identity.padlock.mixed"); + if(n_mx != null) + s_mx = n_mx; + let n_lo = gLocale.getString("identity.padlock.low"); + if(n_lo != null) + s_lo = n_lo; + let n_no = gLocale.getString("identity.padlock.broken"); + if(n_no != null) + s_no = n_no; + } + switch (level) { + case "ev": + sectooltip = s_ev; + break; + case "high": + sectooltip = s_hi; + break; + case "low": + sectooltip = s_lo; + break; + case "mixed": + sectooltip = s_mx; + break; + case "broken": + sectooltip = s_no; + break; + default: + sectooltip = ""; + } + secbut.setAttribute("tooltiptext", sectooltip); + }, + + prefbranch : null, + + onLoad: function() { + gBrowser.addProgressListener(padlock_PadLock); + + var prefService = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService); + padlock_PadLock.prefbranch = prefService.getBranch("browser.padlock."); + padlock_PadLock.prefbranch.QueryInterface(Components.interfaces.nsIPrefBranch2); + padlock_PadLock.usePrefs(); + padlock_PadLock.prefbranch.addObserver("", padlock_PadLock, false); + }, + onUnLoad: function() { + padlock_PadLock.prefbranch.removeObserver("", padlock_PadLock); + }, + observe: function(subject, topic, data) + { + if (topic != "nsPref:changed") + return; + if (data != "style" && data != "urlbar_background" && data != "shown") + return; + padlock_PadLock.usePrefs(); + }, + usePrefs: function() { + var prefval = padlock_PadLock.prefbranch.getIntPref("style"); + var position; + var padstyle; + if (prefval == 2) { + position = "ib-left"; + padstyle = "modern"; + } else if (prefval == 3) { + position = "ub-right"; + padstyle = "modern"; + } else if (prefval == 4) { + position = "statbar"; + padstyle = "modern"; + } else if (prefval == 5) { + position = "tabs-bar"; + padstyle = "modern"; + } else if (prefval == 6) { + position = "ib-trans-bg"; + padstyle = "classic"; + } else if (prefval == 7) { + position = "ib-left"; + padstyle = "classic"; + } else if (prefval == 8) { + position = "ub-right"; + padstyle = "classic"; + } else if (prefval == 9) { + position = "statbar"; + padstyle = "classic"; + } else if (prefval == 10) { + position = "tabs-bar"; + padstyle = "classic"; + } else { + // 1 or anything else_ default + position = "ib-trans-bg"; + padstyle = "modern"; + } + + var colshow; + var colprefval = padlock_PadLock.prefbranch.getIntPref("urlbar_background"); + switch (colprefval) { + case 3: + colshow = "all"; + break; + case 2: + colshow = "secure-mixed"; + break; + case 1: + colshow = "secure-only"; + break; + default: + // 0 or anything else: no shading + colshow = ""; + } + try { + // XXX should probably be done automatically + document.getElementById("urlbar").setAttribute("https_color", colshow); + } catch(e) {} + + var lockenabled = padlock_PadLock.prefbranch.getBoolPref("shown"); + var padshow = ""; + if (lockenabled) { + padshow = position; + } + + try { // URL bar may be hidden + document.getElementById("padlock-ib").setAttribute("padshow", padshow); + document.getElementById("padlock-ib-left").setAttribute("padshow", padshow); + document.getElementById("padlock-ub-right").setAttribute("padshow", padshow); + } catch(e) {} + + document.getElementById("padlock-sb").setAttribute("padshow", padshow); + document.getElementById("padlock-tab").setAttribute("padshow", padshow); + + try { // URL bar may be hidden + document.getElementById("padlock-ib").setAttribute("padstyle", padstyle); + document.getElementById("padlock-ib-left").setAttribute("padstyle", padstyle); + document.getElementById("padlock-ub-right").setAttribute("padstyle", padstyle); + } catch(e) {} + + document.getElementById("padlock-sb").setAttribute("padstyle", padstyle); + document.getElementById("padlock-tab").setAttribute("padstyle", padstyle); + + } +}; + +window.addEventListener("load", padlock_PadLock.onLoad, false ); +window.addEventListener("unload", padlock_PadLock.onUnLoad, false ); diff --git a/browser/base/content/padlock.xul b/browser/base/content/padlock.xul new file mode 100644 index 000000000..e820c19c7 --- /dev/null +++ b/browser/base/content/padlock.xul @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://browser/content/padlock.css" type="text/css"?> + +<overlay + id="padlock" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/x-javascript" src="chrome://browser/content/padlock.js"/> + + <hbox id="identity-box"> + <image id="padlock-ib" insertafter="identity-icon-labels" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </hbox> + + <hbox id="identity-box"> + <image id="padlock-ib-left" insertbefore="identity-icon-labels" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </hbox> + + <hbox id="urlbar-icons"> + <image id="padlock-ub-right" insertbefore="star-button" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </hbox> + + <statusbar id="status-bar"> + <statusbarpanel insertafter="security-button" + id="padlock-sb-panel" + class="statusbar-iconic-text"> + <image id="padlock-sb" insertbefore="star-button" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </statusbarpanel> + </statusbar> + + <toolbar id="TabsToolbar"> + <toolbaritem insertafter="tabs-closebutton" id="tabs-padlock-tbitem" + align="center" pack="center"> + <image id="padlock-tab" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </toolbaritem> + </toolbar> + + +</overlay> diff --git a/browser/base/content/padlock_classic_broken.png b/browser/base/content/padlock_classic_broken.png Binary files differnew file mode 100644 index 000000000..437036fe8 --- /dev/null +++ b/browser/base/content/padlock_classic_broken.png diff --git a/browser/base/content/padlock_classic_ev.png b/browser/base/content/padlock_classic_ev.png Binary files differnew file mode 100644 index 000000000..b3f80c0da --- /dev/null +++ b/browser/base/content/padlock_classic_ev.png diff --git a/browser/base/content/padlock_classic_https.png b/browser/base/content/padlock_classic_https.png Binary files differnew file mode 100644 index 000000000..86026c04f --- /dev/null +++ b/browser/base/content/padlock_classic_https.png diff --git a/browser/base/content/padlock_classic_low.png b/browser/base/content/padlock_classic_low.png Binary files differnew file mode 100644 index 000000000..652ad091f --- /dev/null +++ b/browser/base/content/padlock_classic_low.png diff --git a/browser/base/content/padlock_classic_mixed.png b/browser/base/content/padlock_classic_mixed.png Binary files differnew file mode 100644 index 000000000..a8be5d4fe --- /dev/null +++ b/browser/base/content/padlock_classic_mixed.png diff --git a/browser/base/content/padlock_mod_broken.png b/browser/base/content/padlock_mod_broken.png Binary files differnew file mode 100644 index 000000000..33a6c0645 --- /dev/null +++ b/browser/base/content/padlock_mod_broken.png diff --git a/browser/base/content/padlock_mod_ev.png b/browser/base/content/padlock_mod_ev.png Binary files differnew file mode 100644 index 000000000..3dfdcbde5 --- /dev/null +++ b/browser/base/content/padlock_mod_ev.png diff --git a/browser/base/content/padlock_mod_https.png b/browser/base/content/padlock_mod_https.png Binary files differnew file mode 100644 index 000000000..d494b42b0 --- /dev/null +++ b/browser/base/content/padlock_mod_https.png diff --git a/browser/base/content/padlock_mod_low.png b/browser/base/content/padlock_mod_low.png Binary files differnew file mode 100644 index 000000000..29179efaf --- /dev/null +++ b/browser/base/content/padlock_mod_low.png diff --git a/browser/base/content/padlock_mod_mixed.png b/browser/base/content/padlock_mod_mixed.png Binary files differnew file mode 100644 index 000000000..137c338b4 --- /dev/null +++ b/browser/base/content/padlock_mod_mixed.png diff --git a/browser/base/content/palemoon.xhtml b/browser/base/content/palemoon.xhtml new file mode 100644 index 000000000..f145550a9 --- /dev/null +++ b/browser/base/content/palemoon.xhtml @@ -0,0 +1,66 @@ +<!DOCTYPE html +[ + <!ENTITY % mozillaDTD SYSTEM "chrome://browser/locale/palemoon.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>&chronicles.title.66.1;</title> + +<style type="text/css"> +html { + background: #333399 radial-gradient( circle at 75% 25%, #6666b0 0%, #333399 40%, #111177 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"> + &chronicles.quote.66.1; + </p> + + <p id="from"> + &chronicles.from.66.1; + </p> +</section> + +</body> +</html> diff --git a/browser/base/content/popup-notifications.inc b/browser/base/content/popup-notifications.inc new file mode 100644 index 000000000..945c24dcd --- /dev/null +++ b/browser/base/content/popup-notifications.inc @@ -0,0 +1,82 @@ +# to be included inside a popupset element + + <panel id="notification-popup" + type="arrow" + position="after_start" + hidden="true" + orient="vertical" + role="alert"/> + + <!-- Popup for site identity information --> + <panel id="identity-popup" + type="arrow" + hidden="true" + noautofocus="true" + consumeoutsideclicks="true" + onpopupshown="gIdentityHandler.onPopupShown(event);" + level="top"> + <hbox id="identity-popup-container" align="top"> + <image id="identity-popup-icon"/> + <vbox id="identity-popup-content-box"> + <label id="identity-popup-connectedToLabel" + class="identity-popup-label" + value="&identity.connectedTo;"/> + <label id="identity-popup-connectedToLabel2" + class="identity-popup-label" + value="&identity.unverifiedsite2;"/> + <description id="identity-popup-content-host" + class="identity-popup-description"/> + <label id="identity-popup-runByLabel" + class="identity-popup-label" + value="&identity.runBy;"/> + <description id="identity-popup-content-owner" + class="identity-popup-description"/> + <description id="identity-popup-content-supplemental" + class="identity-popup-description"/> + <description id="identity-popup-content-verifier" + class="identity-popup-description"/> + <hbox id="identity-popup-encryption" flex="1"> + <vbox> + <image id="identity-popup-encryption-icon"/> + </vbox> + <description id="identity-popup-encryption-label" flex="1" + class="identity-popup-description"/> + </hbox> + <!-- Footer button to open security page info --> + <hbox id="identity-popup-button-container" pack="end"> + <button id="identity-popup-more-info-button" + label="&identity.moreInfoLinkText;" + onblur="gIdentityHandler.hideIdentityPopup();" + oncommand="gIdentityHandler.handleMoreInfoClick(event);"/> + </hbox> + </vbox> + </hbox> + </panel> + + <popupnotification id="servicesInstall-notification" hidden="true"> + <popupnotificationcontent orient="vertical" align="start"> + <!-- XXX bug 974146, tests are looking for this, can't remove yet. --> + </popupnotificationcontent> + </popupnotification> + + <popupnotification id="pointerLock-notification" hidden="true"> + <popupnotificationcontent orient="vertical" align="start"> + <separator class="thin"/> + <label id="pointerLock-cancel" value="&pointerLock.notification.message;"/> + </popupnotificationcontent> + </popupnotification> + + <popupnotification id="password-notification" hidden="true"> + <popupnotificationcontent orient="vertical"> + <textbox id="password-notification-username"/> + <textbox id="password-notification-password" type="password" show-content=""/> + <checkbox id="password-notification-visibilityToggle" hidden="true"/> + </popupnotificationcontent> + </popupnotification> + + <popupnotification id="mixed-content-blocked-notification" hidden="true"> + <popupnotificationcontent orient="vertical" align="start"> + <separator/> + <description id="mixed-content-blocked-moreinfo">&mixedContentBlocked.moreinfo;</description> + </popupnotificationcontent> + </popupnotification> diff --git a/browser/base/content/safeMode.css b/browser/base/content/safeMode.css new file mode 100644 index 000000000..4f093a452 --- /dev/null +++ b/browser/base/content/safeMode.css @@ -0,0 +1,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/. */ + +#resetProfileFooter { + font-weight: bold; +} + diff --git a/browser/base/content/safeMode.js b/browser/base/content/safeMode.js new file mode 100644 index 000000000..b64595a35 --- /dev/null +++ b/browser/base/content/safeMode.js @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.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.classes, + Ci = Components.interfaces, + Cu = Components.utils; + +Cu.import("resource://gre/modules/AddonManager.jsm"); + +function restartApp() { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit | Ci.nsIAppStartup.eRestart); +} + +function clearAllPrefs() { + var prefService = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService); + prefService.resetUserPrefs(); + + // Remove the pref-overrides dir, if it exists + try { + var fileLocator = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + const NS_APP_PREFS_OVERRIDE_DIR = "PrefDOverride"; + var prefOverridesDir = fileLocator.get(NS_APP_PREFS_OVERRIDE_DIR, Ci.nsIFile); + prefOverridesDir.remove(true); + } catch(ex) { + Components.utils.reportError(ex); + } +} + +function restoreDefaultBookmarks() { + var prefBranch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + prefBranch.setBoolPref("browser.bookmarks.restore_default_bookmarks", true); +} + +function deleteLocalstore() { + const nsIDirectoryServiceContractID = "@mozilla.org/file/directory_service;1"; + const nsIProperties = Ci.nsIProperties; + var directoryService = Cc[nsIDirectoryServiceContractID] + .getService(nsIProperties); + // Local store file + var localstoreFile = directoryService.get("LStoreS", Components.interfaces.nsIFile); + // XUL store file + var xulstoreFile = directoryService.get("ProfD", Components.interfaces.nsIFile); + xulstoreFile.append("xulstore.json"); + try { + xulstoreFile.remove(false); + if (localstoreFile.exists()) { + localstoreFile.remove(false); + } + } catch(e) { + Components.utils.reportError(e); + } +} + +function disableAddons() { + AddonManager.getAllAddons(function(aAddons) { + aAddons.forEach(function(aAddon) { + if (aAddon.type == "theme") { + // Setting userDisabled to false on the default theme activates it, + // disables all other themes and deactivates the applied persona, if + // any. + const DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}"; + if (aAddon.id == DEFAULT_THEME_ID) + aAddon.userDisabled = false; + } else { + aAddon.userDisabled = true; + } + }); + + restartApp(); + }); +} + +function restoreDefaultSearchEngines() { + var searchService = Cc["@mozilla.org/browser/search-service;1"] + .getService(Ci.nsIBrowserSearchService); + + searchService.restoreDefaultEngines(); +} + +function onOK() { + try { + if (document.getElementById("resetUserPrefs").checked) { + clearAllPrefs(); + } + if (document.getElementById("deleteBookmarks").checked) { + restoreDefaultBookmarks(); + } + if (document.getElementById("resetToolbars").checked) { + deleteLocalstore(); + } + if (document.getElementById("restoreSearch").checked) { + restoreDefaultSearchEngines(); + } + if (document.getElementById("disableAddons").checked) { + disableAddons(); + // disableAddons will asynchronously restart the application + return false; + } + } catch(e) {} + + restartApp(); + return false; +} + +function onCancel() { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit); +} + +function onLoad() { + document.getElementById("tasks") + .addEventListener("CheckboxStateChange", UpdateOKButtonState, false); +} + +function UpdateOKButtonState() { + document.documentElement.getButton("accept").disabled = + !document.getElementById("resetUserPrefs").checked && + !document.getElementById("deleteBookmarks").checked && + !document.getElementById("resetToolbars").checked && + !document.getElementById("disableAddons").checked && + !document.getElementById("restoreSearch").checked; +} diff --git a/browser/base/content/safeMode.xul b/browser/base/content/safeMode.xul new file mode 100644 index 000000000..656df6eaf --- /dev/null +++ b/browser/base/content/safeMode.xul @@ -0,0 +1,55 @@ +<?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 % safeModeDTD SYSTEM "chrome://browser/locale/safeMode.dtd" > +%safeModeDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" > +%browserDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/"?> + +<dialog id="safeModeDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&safeModeDialog.title;" + buttons="accept,cancel,extra1" + buttonlabelaccept="&changeAndRestartButton.label;" +#ifdef XP_WIN + buttonlabelcancel="&quitApplicationCmdWin.label;" +#else + buttonlabelcancel="&quitApplicationCmd.label;" +#endif + buttonlabelextra1="&continueButton.label;" + width="&window.width;" + ondialogaccept="return onOK()" + ondialogcancel="onCancel()" + ondialogextra1="window.close()" + onload="onLoad();" + buttondisabledaccept="true"> + + <script type="application/javascript" src="chrome://browser/content/safeMode.js"/> + + <stringbundle id="preferencesBundle" src="chrome://browser/locale/preferences/preferences.properties"/> + + <description>&safeModeDescription.label;</description> + + <separator class="thin"/> + + <label value="&safeModeDescription2.label;"/> + <vbox id="tasks"> + <checkbox id="disableAddons" label="&disableAddons.label;" accesskey="&disableAddons.accesskey;"/> + <checkbox id="resetToolbars" label="&resetToolbars.label;" accesskey="&resetToolbars.accesskey;"/> + <checkbox id="deleteBookmarks" label="&deleteBookmarks.label;" accesskey="&deleteBookmarks.accesskey;"/> + <checkbox id="resetUserPrefs" label="&resetUserPrefs.label;" accesskey="&resetUserPrefs.accesskey;"/> + <checkbox id="restoreSearch" label="&restoreSearch.label;" accesskey="&restoreSearch.accesskey;"/> + </vbox> + + <separator class="thin"/> +</dialog> diff --git a/browser/base/content/sanitize.js b/browser/base/content/sanitize.js new file mode 100644 index 000000000..2be68878f --- /dev/null +++ b/browser/base/content/sanitize.js @@ -0,0 +1,517 @@ +// -*- 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/. + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/Console.jsm"); + +function Sanitizer() {} + +Sanitizer.prototype = { + // warning to the caller: this one may raise an exception (e.g. bug #265028) + clearItem: function (aItemName) { + if (this.items[aItemName].canClear) { + this.items[aItemName].clear(); + } + }, + + canClearItem: function (aItemName, aCallback, aArg) { + let canClear = this.items[aItemName].canClear; + if (typeof canClear == "function") { + canClear(aCallback, aArg); + return false; + } + + aCallback(aItemName, canClear, aArg); + return canClear; + }, + + prefDomain: "", + isShutDown: false, + + getNameFromPreference: function (aPreferenceName) { + return aPreferenceName.substr(this.prefDomain.length); + }, + + /** + * Deletes privacy sensitive data in a batch, according to user preferences. + * Returns a promise which is resolved if no errors occurred. If an error + * occurs, a message is reported to the console and all other items are still + * cleared before the promise is finally rejected. + */ + sanitize: function () { + var deferred = Promise.defer(); + var psvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + var branch = psvc.getBranch(this.prefDomain); + var seenError = false; + + // Cache the range of times to clear + if (this.ignoreTimespan) { + // If we ignore timespan, clear everything + var range = null; + } else { + range = this.range || Sanitizer.getClearRange(); + } + + let itemCount = Object.keys(this.items).length; + let onItemComplete = function() { + if (!--itemCount) { + seenError ? deferred.reject() : deferred.resolve(); + } + }; + for (var itemName in this.items) { + let item = this.items[itemName]; + item.range = range; + item.isShutDown = this.isShutDown; + if ("clear" in item && branch.getBoolPref(itemName)) { + let clearCallback = (itemName, aCanClear) => { + // Some of these clear() may raise exceptions (see bug #265028) + // to sanitize as much as possible, we catch and store them, + // rather than fail fast. + // Callers should check returned errors and give user feedback + // about items that could not be sanitized + let item = this.items[itemName]; + try { + if (aCanClear) + item.clear(); + } catch(er) { + seenError = true; + console.error("Error sanitizing " + itemName + ": " + er + "\n"); + } + onItemComplete(); + }; + this.canClearItem(itemName, clearCallback); + } else { + onItemComplete(); + } + } + + return deferred.promise; + }, + + // Time span only makes sense in certain cases. Consumers who want + // to only clear some private data can opt in by setting this to false, + // and can optionally specify a specific range. If timespan is not ignored, + // and range is not set, sanitize() will use the value of the timespan + // pref to determine a range + ignoreTimespan : true, + range : null, + + items: { + cache: { + clear: function() { + var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Ci.nsICacheStorageService); + try { + // Cache doesn't consult timespan, nor does it have the + // facility for timespan-based eviction. Wipe it. + cache.clear(); + } catch(er) {} + + var imageCache = Cc["@mozilla.org/image/tools;1"]. + getService(Ci.imgITools).getImgCacheForDocument(null); + try { + imageCache.clearCache(false); // true=chrome, false=content + } catch(er) {} + }, + + get canClear() { + return true; + } + }, + + cookies: { + clear: function () { + var cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"] + .getService(Ci.nsICookieManager); + if (this.range) { + // Iterate through the cookies and delete any created after our cutoff. + var cookiesEnum = cookieMgr.enumerator; + while (cookiesEnum.hasMoreElements()) { + var cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2); + + if (cookie.creationTime > this.range[0]) { + // This cookie was created after our cutoff, clear it + cookieMgr.remove(cookie.host, cookie.name, cookie.path, + false, cookie.originAttributes); + } + } + } else { + // Remove everything + cookieMgr.removeAll(); + } + + // Clear plugin data. + const phInterface = Ci.nsIPluginHost; + const FLAG_CLEAR_ALL = phInterface.FLAG_CLEAR_ALL; + let ph = Cc["@mozilla.org/plugin/host;1"].getService(phInterface); + + // Determine age range in seconds. (-1 means clear all.) We don't know + // that this.range[1] is actually now, so we compute age range based + // on the lower bound. If this.range results in a negative age, do + // nothing. + let age = this.range ? + (Date.now() / 1000 - this.range[0] / 1000000) : + -1; + if (!this.range || age >= 0) { + let tags = ph.getPluginTags(); + for (let i = 0; i < tags.length; i++) { + try { + ph.clearSiteData(tags[i], null, FLAG_CLEAR_ALL, age); + } catch(e) { + // If the plugin doesn't support clearing by age, clear everything. + if (e.result == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) { + try { + ph.clearSiteData(tags[i], null, FLAG_CLEAR_ALL, -1); + } catch(e) { + // Ignore errors from the plugin + } + } + } + } + } + }, + + get canClear() { + return true; + } + }, + + offlineApps: { + clear: function () { + Components.utils.import("resource:///modules/offlineAppCache.jsm"); + OfflineAppCacheHelper.clear(); + if (!this.range || this.isShutDown) { + Components.utils.import("resource:///modules/QuotaManager.jsm"); + QuotaManagerHelper.clear(this.isShutDown); + } + }, + + get canClear() { + return true; + } + }, + + history: { + clear: function () { + if (this.range) { + PlacesUtils.history.removeVisitsByFilter({ + beginDate: new Date(this.range[0] / 1000), + endDate: new Date(this.range[1] / 1000) + }).catch(Components.utils.reportError);; + } else { + // Remove everything. + PlacesUtils.history.clear() + .catch(Components.utils.reportError); + } + + try { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.notifyObservers(null, "browser:purge-session-history", ""); + } catch(e) {} + + // Clear last URL of the Open Web Location dialog + var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + try { + prefs.clearUserPref("general.open_location.last_url"); + } catch(e) {} + }, + + get canClear() + { + // bug 347231: Always allow clearing history due to dependencies on + // the browser:purge-session-history notification. (like error console) + return true; + } + }, + + formdata: { + clear: function () { + // Clear undo history of all searchBars + var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1'] + .getService(Components.interfaces.nsIWindowMediator); + var windows = windowManager.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + let currentDocument = windows.getNext().document; + let searchBar = currentDocument.getElementById("searchbar"); + if (searchBar) { + searchBar.textbox.reset(); + } + let findBar = currentDocument.getElementById("FindToolbar"); + if (findBar) { + findBar.clear(); + } + } + + let change = { op: "remove" }; + if (this.range) { + [ change.firstUsedStart, change.firstUsedEnd ] = this.range; + } + FormHistory.update(change); + }, + + canClear: function(aCallback, aArg) { + var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1'] + .getService(Components.interfaces.nsIWindowMediator); + var windows = windowManager.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + let currentDocument = windows.getNext().document; + let searchBar = currentDocument.getElementById("searchbar"); + if (searchBar) { + let transactionMgr = searchBar.textbox.editor.transactionManager; + if (searchBar.value || + transactionMgr.numberOfUndoItems || + transactionMgr.numberOfRedoItems) { + aCallback("formdata", true, aArg); + return false; + } + } + let findBar = currentDocument.getElementById("FindToolbar"); + if (findBar && findBar.canClear) { + aCallback("formdata", true, aArg); + return false; + } + } + + let count = 0; + let countDone = { + handleResult: function(aResult) { + count = aResult; + }, + handleError: function(aError) { + Components.utils.reportError(aError); + }, + handleCompletion: function(aReason) { + aCallback("formdata", aReason == 0 && count > 0, aArg); + } + }; + FormHistory.count({}, countDone); + return false; + } + }, + + downloads: { + clear: Task.async(function* (range) { + let refObj = {}; + try { + let filterByTime = null; + if (range) { + // Convert microseconds back to milliseconds for date comparisons. + let rangeBeginMs = range[0] / 1000; + let rangeEndMs = range[1] / 1000; + filterByTime = download => { + return download.startTime >= rangeBeginMs && + download.startTime <= rangeEndMs; + } + } + + // Clear all completed/cancelled downloads + let list = yield Downloads.getList(Downloads.ALL); + list.removeFinished(filterByTime); + } finally {} + }), + + get canClear() { + //Clearing is always possible with JSTransfers + return true; + } + }, + + passwords: { + clear: function () { + var pwmgr = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + // Passwords are timeless, and don't respect the timeSpan setting + pwmgr.removeAllLogins(); + }, + + get canClear() { + var pwmgr = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + // count all logins + var count = pwmgr.countLogins("", "", ""); + return (count > 0); + } + }, + + sessions: { + clear: function () { + // clear all auth tokens + var sdr = Components.classes["@mozilla.org/security/sdr;1"] + .getService(Components.interfaces.nsISecretDecoderRing); + sdr.logoutAndTeardown(); + + // clear FTP and plain HTTP auth sessions + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.notifyObservers(null, "net:clear-active-logins", null); + }, + + get canClear() { + return true; + } + }, + + siteSettings: { + clear: function () { + // Clear site-specific permissions like "Allow this site to open popups" + var pm = Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager); + pm.removeAll(); + + // Clear site-specific settings like page-zoom level + var cps = Components.classes["@mozilla.org/content-pref/service;1"] + .getService(Components.interfaces.nsIContentPrefService2); + cps.removeAllDomains(null); + + // Clear "Never remember passwords for this site", which is not handled by + // the permission manager + var pwmgr = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + var hosts = pwmgr.getAllDisabledHosts(); + for each (var host in hosts) { + pwmgr.setLoginSavingEnabled(host, true); + } + }, + + get canClear() { + return true; + } + }, + + connectivityData: { + clear: function () { + // Clear site security settings + var sss = Components.classes["@mozilla.org/ssservice;1"] + .getService(Components.interfaces.nsISiteSecurityService); + sss.clearAll(); + }, + + get canClear() { + return true; + } + } + } +}; + + + +// "Static" members +Sanitizer.prefDomain = "privacy.sanitize."; +Sanitizer.prefShutdown = "sanitizeOnShutdown"; +Sanitizer.prefDidShutdown = "didShutdownSanitize"; + +// Time span constants corresponding to values of the privacy.sanitize.timeSpan +// pref. Used to determine how much history to clear, for various items +Sanitizer.TIMESPAN_EVERYTHING = 0; +Sanitizer.TIMESPAN_HOUR = 1; +Sanitizer.TIMESPAN_2HOURS = 2; +Sanitizer.TIMESPAN_4HOURS = 3; +Sanitizer.TIMESPAN_TODAY = 4; + +Sanitizer.IS_SHUTDOWN = true; + +// Return a 2 element array representing the start and end times, +// in the uSec-since-epoch format that PRTime likes. If we should +// clear everything, return null. Use ts if it is defined; otherwise +// use the timeSpan pref. +Sanitizer.getClearRange = function(ts) { + if (ts === undefined) { + ts = Sanitizer.prefs.getIntPref("timeSpan"); + } + if (ts === Sanitizer.TIMESPAN_EVERYTHING) { + return null; + } + + // PRTime is microseconds while JS time is milliseconds + var endDate = Date.now() * 1000; + switch (ts) { + case Sanitizer.TIMESPAN_HOUR : + var startDate = endDate - 3600000000; // 1*60*60*1000000 + break; + case Sanitizer.TIMESPAN_2HOURS : + startDate = endDate - 7200000000; // 2*60*60*1000000 + break; + case Sanitizer.TIMESPAN_4HOURS : + startDate = endDate - 14400000000; // 4*60*60*1000000 + break; + case Sanitizer.TIMESPAN_TODAY : + var d = new Date(); // Start with today + d.setHours(0); // zero us back to midnight... + d.setMinutes(0); + d.setSeconds(0); + startDate = d.valueOf() * 1000; // convert to epoch usec + break; + default: + throw "Invalid time span for clear private data: " + ts; + } + return [startDate, endDate]; +}; + +Sanitizer._prefs = null; +Sanitizer.__defineGetter__("prefs", function() { + return Sanitizer._prefs ? + Sanitizer._prefs : + Sanitizer._prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService) + .getBranch(Sanitizer.prefDomain); +}); + +// Shows sanitization UI +Sanitizer.showUI = function(aParentWindow) { + var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIWindowWatcher); + ww.openWindow(aParentWindow, + "chrome://browser/content/sanitize.xul", + "Sanitize", + "chrome,titlebar,dialog,centerscreen,modal", + null); +}; + +/** + * Deletes privacy sensitive data in a batch, optionally showing the + * sanitize UI, according to user preferences + */ +Sanitizer.sanitize = function(aParentWindow) { + Sanitizer.showUI(aParentWindow); +}; + +Sanitizer.onStartup = function() { + // we check for unclean exit with pending sanitization + Sanitizer._checkAndSanitize(); +}; + +Sanitizer.onShutdown = function() { + // we check if sanitization is needed and perform it + Sanitizer._checkAndSanitize(Sanitizer.IS_SHUTDOWN); +}; + +// this is called on startup and shutdown, to perform pending sanitizations +Sanitizer._checkAndSanitize = function(isShutDown) { + const prefs = Sanitizer.prefs; + if (prefs.getBoolPref(Sanitizer.prefShutdown) && + !prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) { + // this is a shutdown or a startup after an unclean exit + var s = new Sanitizer(); + s.prefDomain = "privacy.clearOnShutdown."; + s.isShutDown = isShutDown; + s.sanitize().then(function() { + prefs.setBoolPref(Sanitizer.prefDidShutdown, true); + }); + } +}; diff --git a/browser/base/content/sanitize.xul b/browser/base/content/sanitize.xul new file mode 100644 index 000000000..691be926e --- /dev/null +++ b/browser/base/content/sanitize.xul @@ -0,0 +1,190 @@ +<?xml version="1.0"?> + +# -*- 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/. + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/sanitizeDialog.css"?> + +#ifdef CRH_DIALOG_TREE_VIEW +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> +#endif + +<?xml-stylesheet href="chrome://browser/content/sanitizeDialog.css"?> + +<!DOCTYPE prefwindow [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % sanitizeDTD SYSTEM "chrome://browser/locale/sanitize.dtd"> + %brandDTD; + %sanitizeDTD; +]> + +<prefwindow id="SanitizeDialog" type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + dlgbuttons="accept,cancel" + title="&sanitizeDialog2.title;" + noneverythingtitle="&sanitizeDialog2.title;" + style="width: &dialog.width2;;" + ondialogaccept="return gSanitizePromptDialog.sanitize();"> + + <prefpane id="SanitizeDialogPane" onpaneload="gSanitizePromptDialog.init();"> + <stringbundle id="bundleBrowser" + src="chrome://browser/locale/browser.properties"/> + + <script type="application/javascript" + src="chrome://browser/content/sanitize.js"/> + +#ifdef CRH_DIALOG_TREE_VIEW + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" + src="chrome://browser/content/places/treeView.js"/> + <script type="application/javascript"><![CDATA[ + Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + Components.utils.import("resource:///modules/PlacesUIUtils.jsm"); + ]]></script> +#endif + + <script type="application/javascript" + src="chrome://browser/content/sanitizeDialog.js"/> + + <preferences id="sanitizePreferences"> + <preference id="privacy.cpd.history" name="privacy.cpd.history" type="bool"/> + <preference id="privacy.cpd.formdata" name="privacy.cpd.formdata" type="bool"/> + <preference id="privacy.cpd.downloads" name="privacy.cpd.downloads" type="bool" disabled="true"/> + <preference id="privacy.cpd.cookies" name="privacy.cpd.cookies" type="bool"/> + <preference id="privacy.cpd.cache" name="privacy.cpd.cache" type="bool"/> + <preference id="privacy.cpd.sessions" name="privacy.cpd.sessions" type="bool"/> + <preference id="privacy.cpd.offlineApps" name="privacy.cpd.offlineApps" type="bool"/> + <preference id="privacy.cpd.siteSettings" name="privacy.cpd.siteSettings" type="bool"/> + <preference id="privacy.cpd.connectivityData" name="privacy.cpd.connectivityData" type="bool"/> + </preferences> + + <preferences id="nonItemPreferences"> + <preference id="privacy.sanitize.timeSpan" + name="privacy.sanitize.timeSpan" + type="int"/> + </preferences> + + <hbox id="SanitizeDurationBox" align="center"> + <label value="&clearTimeDuration.label;" + accesskey="&clearTimeDuration.accesskey;" + control="sanitizeDurationChoice" + id="sanitizeDurationLabel"/> + <menulist id="sanitizeDurationChoice" + preference="privacy.sanitize.timeSpan" + onselect="gSanitizePromptDialog.selectByTimespan();" + flex="1"> + <menupopup id="sanitizeDurationPopup"> +#ifdef CRH_DIALOG_TREE_VIEW + <menuitem label="" value="-1" id="sanitizeDurationCustom"/> +#endif + <menuitem label="&clearTimeDuration.lastHour;" value="1"/> + <menuitem label="&clearTimeDuration.last2Hours;" value="2"/> + <menuitem label="&clearTimeDuration.last4Hours;" value="3"/> + <menuitem label="&clearTimeDuration.today;" value="4"/> + <menuseparator/> + <menuitem label="&clearTimeDuration.everything;" value="0"/> + </menupopup> + </menulist> + <label id="sanitizeDurationSuffixLabel" + value="&clearTimeDuration.suffix;"/> + </hbox> + + <separator class="thin"/> + +#ifdef CRH_DIALOG_TREE_VIEW + <deck id="durationDeck"> + <tree id="placesTree" flex="1" hidecolumnpicker="true" rows="10" + disabled="true" disableKeyNavigation="true"> + <treecols> + <treecol id="date" label="&clearTimeDuration.dateColumn;" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="title" label="&clearTimeDuration.nameColumn;" flex="5"/> + </treecols> + <treechildren id="placesTreechildren" + ondragstart="gSanitizePromptDialog.grippyMoved('ondragstart', event);" + ondragover="gSanitizePromptDialog.grippyMoved('ondragover', event);" + onkeypress="gSanitizePromptDialog.grippyMoved('onkeypress', event);" + onmousedown="gSanitizePromptDialog.grippyMoved('onmousedown', event);"/> + </tree> +#endif + + <vbox id="sanitizeEverythingWarningBox"> + <spacer flex="1"/> + <hbox align="center"> + <image id="sanitizeEverythingWarningIcon"/> + <vbox id="sanitizeEverythingWarningDescBox" flex="1"> + <description id="sanitizeEverythingWarning"/> + <description id="sanitizeEverythingUndoWarning">&sanitizeEverythingUndoWarning;</description> + </vbox> + </hbox> + <spacer flex="1"/> + </vbox> + +#ifdef CRH_DIALOG_TREE_VIEW + </deck> +#endif + + <separator class="thin"/> + + <hbox id="detailsExpanderWrapper" align="center"> + <button type="image" + id="detailsExpander" + class="expander-down" + persist="class" + oncommand="gSanitizePromptDialog.toggleItemList();"/> + <label id="detailsExpanderLabel" + value="&detailsProgressiveDisclosure.label;" + accesskey="&detailsProgressiveDisclosure.accesskey;" + control="detailsExpander"/> + </hbox> + <listbox id="itemList" rows="8" collapsed="true" persist="collapsed"> + <listitem label="&itemHistoryAndDownloads.label;" + type="checkbox" + accesskey="&itemHistoryAndDownloads.accesskey;" + preference="privacy.cpd.history" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemFormSearchHistory.label;" + type="checkbox" + accesskey="&itemFormSearchHistory.accesskey;" + preference="privacy.cpd.formdata" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemCookies.label;" + type="checkbox" + accesskey="&itemCookies.accesskey;" + preference="privacy.cpd.cookies" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemCache.label;" + type="checkbox" + accesskey="&itemCache.accesskey;" + preference="privacy.cpd.cache" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemActiveLogins.label;" + type="checkbox" + accesskey="&itemActiveLogins.accesskey;" + preference="privacy.cpd.sessions" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemOfflineApps.label;" + type="checkbox" + accesskey="&itemOfflineApps.accesskey;" + preference="privacy.cpd.offlineApps" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemSitePreferences.label;" + type="checkbox" + accesskey="&itemSitePreferences.accesskey;" + preference="privacy.cpd.siteSettings" + noduration="true" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemConnectivityData.label;" + type="checkbox" + accesskey="&itemConnectivityData.accesskey;" + preference="privacy.cpd.connectivityData" + noduration="true" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + </listbox> + + </prefpane> +</prefwindow> diff --git a/browser/base/content/sanitizeDialog.css b/browser/base/content/sanitizeDialog.css new file mode 100644 index 000000000..27c3c0866 --- /dev/null +++ b/browser/base/content/sanitizeDialog.css @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Places tree */ + +#placesTreechildren { + -moz-user-focus: normal; +} + +#placesTreechildren::-moz-tree-cell(grippyRow), +#placesTreechildren::-moz-tree-cell-text(grippyRow), +#placesTreechildren::-moz-tree-image(grippyRow) { + cursor: -moz-grab; +} + + +/* Sanitize everything warnings */ + +#sanitizeEverythingWarning, +#sanitizeEverythingUndoWarning { + white-space: pre-wrap; +} diff --git a/browser/base/content/sanitizeDialog.js b/browser/base/content/sanitizeDialog.js new file mode 100644 index 000000000..900b81324 --- /dev/null +++ b/browser/base/content/sanitizeDialog.js @@ -0,0 +1,886 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; + +var gSanitizePromptDialog = { + + get bundleBrowser() { + if (!this._bundleBrowser) { + this._bundleBrowser = document.getElementById("bundleBrowser"); + } + return this._bundleBrowser; + }, + + get selectedTimespan() { + var durList = document.getElementById("sanitizeDurationChoice"); + return parseInt(durList.value); + }, + + get sanitizePreferences() { + if (!this._sanitizePreferences) { + this._sanitizePreferences = document.getElementById("sanitizePreferences"); + } + return this._sanitizePreferences; + }, + + get warningBox() { + return document.getElementById("sanitizeEverythingWarningBox"); + }, + + init: function() { + // This is used by selectByTimespan() to determine if the window has loaded. + this._inited = true; + + var s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + + let sanitizeItemList = document.querySelectorAll("#itemList > [preference]"); + for (let i = 0; i < sanitizeItemList.length; i++) { + let prefItem = sanitizeItemList[i]; + let name = s.getNameFromPreference(prefItem.getAttribute("preference")); + s.canClearItem(name, function canClearCallback(aItem, aCanClear, aPrefItem) { + if (!aCanClear) { + aPrefItem.preference = null; + aPrefItem.checked = false; + aPrefItem.disabled = true; + } + }, prefItem); + } + + document.documentElement.getButton("accept").label = + this.bundleBrowser.getString("sanitizeButtonOK"); + + if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) { + this.prepareWarning(); + this.warningBox.hidden = false; + document.title = this.bundleBrowser.getString("sanitizeDialog2.everything.title"); + } else { + this.warningBox.hidden = true; + } + }, + + selectByTimespan: function() { + // This method is the onselect handler for the duration dropdown. As a + // result it's called a couple of times before onload calls init(). + if (!this._inited) { + return; + } + + var warningBox = this.warningBox; + + // If clearing everything + if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) { + this.prepareWarning(); + if (warningBox.hidden) { + warningBox.hidden = false; + window.resizeBy(0, warningBox.boxObject.height); + } + window.document.title = this.bundleBrowser.getString("sanitizeDialog2.everything.title"); + return; + } + + // If clearing a specific time range + if (!warningBox.hidden) { + window.resizeBy(0, -warningBox.boxObject.height); + warningBox.hidden = true; + } + window.document.title = window.document.documentElement.getAttribute("noneverythingtitle"); + }, + + sanitize: function() { + // Update pref values before handing off to the sanitizer (bug 453440) + this.updatePrefs(); + var s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + + s.range = Sanitizer.getClearRange(this.selectedTimespan); + s.ignoreTimespan = !s.range; + + // As the sanitize is async, we disable the buttons, update the label on + // the 'accept' button to indicate things are happening and return false - + // once the async operation completes (either with or without errors) + // we close the window. + let docElt = document.documentElement; + let acceptButton = docElt.getButton("accept"); + acceptButton.disabled = true; + acceptButton.setAttribute("label", this.bundleBrowser.getString("sanitizeButtonClearing")); + docElt.getButton("cancel").disabled = true; + try { + s.sanitize().then(window.close, window.close); + } catch(er) { + Components.utils.reportError("Exception during sanitize: " + er); + return true; // We *do* want to close immediately on error. + } + return false; + }, + + /** + * If the panel that displays a warning when the duration is "Everything" is + * not set up, sets it up. Otherwise does nothing. + * + * @param aDontShowItemList Whether only the warning message should be updated. + * True means the item list visibility status should not + * be changed. + */ + prepareWarning: function(aDontShowItemList) { + // If the date and time-aware locale warning string is ever used again, + // initialize it here. Currently we use the no-visits warning string, + // which does not include date and time. See bug 480169 comment 48. + + var warningStringID; + if (this.hasNonSelectedItems()) { + warningStringID = "sanitizeSelectedWarning"; + if (!aDontShowItemList) { + this.showItemList(); + } + } else { + warningStringID = "sanitizeEverythingWarning2"; + } + + var warningDesc = document.getElementById("sanitizeEverythingWarning"); + warningDesc.textContent = this.bundleBrowser.getString(warningStringID); + }, + + /** + * Called when the value of a preference element is synced from the actual + * pref. Enables or disables the OK button appropriately. + */ + onReadGeneric: function() { + var found = false; + + // Find any other pref that's checked and enabled. + var i = 0; + while (!found && i < this.sanitizePreferences.childNodes.length) { + var preference = this.sanitizePreferences.childNodes[i]; + + found = !!preference.value && + !preference.disabled; + i++; + } + + try { + document.documentElement.getButton("accept").disabled = !found; + } catch(e) {} + + // Update the warning prompt if needed + this.prepareWarning(true); + + return undefined; + }, + + /** + * Sanitizer.prototype.sanitize() requires the prefs to be up-to-date. + * Because the type of this prefwindow is "child" -- and that's needed because + * without it the dialog has no OK and Cancel buttons -- the prefs are not + * updated on dialogaccept on platforms that don't support instant-apply + * (i.e., Windows). We must therefore manually set the prefs from their + * corresponding preference elements. + */ + updatePrefs : function() + { + var tsPref = document.getElementById("privacy.sanitize.timeSpan"); + Sanitizer.prefs.setIntPref("timeSpan", this.selectedTimespan); + + // Keep the pref for the download history in sync with the history pref. + document.getElementById("privacy.cpd.downloads").value = + document.getElementById("privacy.cpd.history").value; + + // Now manually set the prefs from their corresponding preference + // elements. + var prefs = this.sanitizePreferences.rootBranch; + for (let i = 0; i < this.sanitizePreferences.childNodes.length; ++i) { + var p = this.sanitizePreferences.childNodes[i]; + prefs.setBoolPref(p.name, p.value); + } + }, + + /** + * Check if all of the history items have been selected like the default status. + */ + hasNonSelectedItems: function() { + let checkboxes = document.querySelectorAll("#itemList > [preference]"); + for (let i = 0; i < checkboxes.length; ++i) { + let pref = document.getElementById(checkboxes[i].getAttribute("preference")); + if (!pref.value) { + return true; + } + } + return false; + }, + + /** + * Show the history items list. + */ + showItemList: function() { + var itemList = document.getElementById("itemList"); + var expanderButton = document.getElementById("detailsExpander"); + + if (itemList.collapsed) { + expanderButton.className = "expander-up"; + itemList.setAttribute("collapsed", "false"); + if (document.documentElement.boxObject.height) { + window.resizeBy(0, itemList.boxObject.height); + } + } + }, + + /** + * Hide the history items list. + */ + hideItemList: function() { + var itemList = document.getElementById("itemList"); + var expanderButton = document.getElementById("detailsExpander"); + + if (!itemList.collapsed) { + expanderButton.className = "expander-down"; + window.resizeBy(0, -itemList.boxObject.height); + itemList.setAttribute("collapsed", "true"); + } + }, + + /** + * Called by the item list expander button to toggle the list's visibility. + */ + toggleItemList: function() { + var itemList = document.getElementById("itemList"); + + if (itemList.collapsed) { + this.showItemList(); + } else { + this.hideItemList(); + } + } + +#ifdef CRH_DIALOG_TREE_VIEW + // A duration value; used in the same context as Sanitizer.TIMESPAN_HOUR, + // Sanitizer.TIMESPAN_2HOURS, et al. This should match the value attribute + // of the sanitizeDurationCustom menuitem. + get TIMESPAN_CUSTOM() { + return -1; + }, + + get placesTree() { + if (!this._placesTree) { + this._placesTree = document.getElementById("placesTree"); + } + return this._placesTree; + }, + + init: function() { + // This is used by selectByTimespan() to determine if the window has loaded. + this._inited = true; + + var s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + + let sanitizeItemList = document.querySelectorAll("#itemList > [preference]"); + for (let i = 0; i < sanitizeItemList.length; i++) { + let prefItem = sanitizeItemList[i]; + let name = s.getNameFromPreference(prefItem.getAttribute("preference")); + s.canClearItem(name, function canClearCallback(aCanClear) { + if (!aCanClear) { + prefItem.preference = null; + prefItem.checked = false; + prefItem.disabled = true; + } + }); + } + + document.documentElement.getButton("accept").label = + this.bundleBrowser.getString("sanitizeButtonOK"); + + this.selectByTimespan(); + }, + + /** + * Sets up the hashes this.durationValsToRows, which maps duration values + * to rows in the tree, this.durationRowsToVals, which maps rows in + * the tree to duration values, and this.durationStartTimes, which maps + * duration values to their corresponding start times. + */ + initDurationDropdown: function() { + // First, calculate the start times for each duration. + this.durationStartTimes = {}; + var durVals = []; + var durPopup = document.getElementById("sanitizeDurationPopup"); + var durMenuitems = durPopup.childNodes; + for (let i = 0; i < durMenuitems.length; i++) { + let durMenuitem = durMenuitems[i]; + let durVal = parseInt(durMenuitem.value); + if (durMenuitem.localName === "menuitem" && + durVal !== Sanitizer.TIMESPAN_EVERYTHING && + durVal !== this.TIMESPAN_CUSTOM) { + durVals.push(durVal); + let durTimes = Sanitizer.getClearRange(durVal); + this.durationStartTimes[durVal] = durTimes[0]; + } + } + + // Sort the duration values ascending. Because one tree index can map to + // more than one duration, this ensures that this.durationRowsToVals maps + // a row index to the largest duration possible in the code below. + durVals.sort(); + + // Now calculate the rows in the tree of the durations' start times. For + // each duration, we are looking for the node in the tree whose time is the + // smallest time greater than or equal to the duration's start time. + this.durationRowsToVals = {}; + this.durationValsToRows = {}; + var view = this.placesTree.view; + // For all rows in the tree except the grippy row... + for (let i = 0; i < view.rowCount - 1; i++) { + let unfoundDurVals = []; + let nodeTime = view.QueryInterface(Ci.nsINavHistoryResultTreeViewer) + .nodeForTreeIndex(i).time; + // For all durations whose rows have not yet been found in the tree, see + // if index i is their index. An index may map to more than one duration, + // in which case the final duration (the largest) wins. + for (let j = 0; j < durVals.length; j++) { + let durVal = durVals[j]; + let durStartTime = this.durationStartTimes[durVal]; + if (nodeTime < durStartTime) { + this.durationValsToRows[durVal] = i - 1; + this.durationRowsToVals[i - 1] = durVal; + } else { + unfoundDurVals.push(durVal); + } + } + durVals = unfoundDurVals; + } + + // If any durations were not found above, then every node in the tree has a + // time greater than or equal to the duration. In other words, those + // durations include the entire tree (except the grippy row). + for (let i = 0; i < durVals.length; i++) { + let durVal = durVals[i]; + this.durationValsToRows[durVal] = view.rowCount - 2; + this.durationRowsToVals[view.rowCount - 2] = durVal; + } + }, + + /** + * If the Places tree is not set up, sets it up. Otherwise does nothing. + */ + ensurePlacesTreeIsInited: function() { + if (this._placesTreeIsInited) { + return; + } + + this._placesTreeIsInited = true; + + // Either "Last Four Hours" or "Today" will have the most history. If + // it's been more than 4 hours since today began, "Today" will. Otherwise + // "Last Four Hours" will. + var times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_TODAY); + + // If it's been less than 4 hours since today began, use the past 4 hours. + if (times[1] - times[0] < 14400000000) { // 4*60*60*1000000 + times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_4HOURS); + } + + var histServ = Cc["@mozilla.org/browser/nav-history-service;1"] + .getService(Ci.nsINavHistoryService); + var query = histServ.getNewQuery(); + query.beginTimeReference = query.TIME_RELATIVE_EPOCH; + query.beginTime = times[0]; + query.endTimeReference = query.TIME_RELATIVE_EPOCH; + query.endTime = times[1]; + var opts = histServ.getNewQueryOptions(); + opts.sortingMode = opts.SORT_BY_DATE_DESCENDING; + opts.queryType = opts.QUERY_TYPE_HISTORY; + var result = histServ.executeQuery(query, opts); + + var view = gContiguousSelectionTreeHelper.setTree(this.placesTree, + new PlacesTreeView()); + result.addObserver(view, false); + this.initDurationDropdown(); + }, + + /** + * Called on select of the duration dropdown and when grippyMoved() sets a + * duration based on the location of the grippy row. Selects all the nodes in + * the tree that are contained in the selected duration. If clearing + * everything, the warning panel is shown instead. + */ + selectByTimespan: function() { + // This method is the onselect handler for the duration dropdown. As a + // result it's called a couple of times before onload calls init(). + if (!this._inited) { + return; + } + + var durDeck = document.getElementById("durationDeck"); + var durList = document.getElementById("sanitizeDurationChoice"); + var durVal = parseInt(durList.value); + var durCustom = document.getElementById("sanitizeDurationCustom"); + + // If grippy row is not at a duration boundary, show the custom menuitem; + // otherwise, hide it. Since the user cannot specify a custom duration by + // using the dropdown, this conditional is true only when this method is + // called onselect from grippyMoved(), so no selection need be made. + if (durVal === this.TIMESPAN_CUSTOM) { + durCustom.hidden = false; + return; + } + durCustom.hidden = true; + + // If clearing everything, show the warning and change the dialog's title. + if (durVal === Sanitizer.TIMESPAN_EVERYTHING) { + this.prepareWarning(); + durDeck.selectedIndex = 1; + window.document.title = this.bundleBrowser.getString("sanitizeDialog2.everything.title"); + document.documentElement.getButton("accept").disabled = false; + return; + } + + // Otherwise -- if clearing a specific time range -- select that time range in the tree. + this.ensurePlacesTreeIsInited(); + durDeck.selectedIndex = 0; + window.document.title = window.document.documentElement.getAttribute("noneverythingtitle"); + var durRow = this.durationValsToRows[durVal]; + gContiguousSelectionTreeHelper.rangedSelect(durRow); + gContiguousSelectionTreeHelper.scrollToGrippy(); + + // If duration is empty (there are no selected rows), disable the dialog's OK button. + document.documentElement.getButton("accept").disabled = durRow < 0; + }, + + sanitize: function() { + // Update pref values before handing off to the sanitizer (bug 453440) + this.updatePrefs(); + var s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + + var durList = document.getElementById("sanitizeDurationChoice"); + var durValue = parseInt(durList.value); + s.ignoreTimespan = durValue === Sanitizer.TIMESPAN_EVERYTHING; + + // Set the sanitizer's time range if we're not clearing everything. + if (!s.ignoreTimespan) { + // If user selected a custom timespan, use that. + if (durValue === this.TIMESPAN_CUSTOM) { + var view = this.placesTree.view; + var now = Date.now() * 1000; + // We disable the dialog's OK button if there's no selection, but we'll + // handle that case just in... case. + if (view.selection.getRangeCount() === 0) { + s.range = [now, now]; + } else { + var startIndexRef = {}; + // Tree sorted by visit date DEscending, so start time time comes last. + view.selection.getRangeAt(0, {}, startIndexRef); + view.QueryInterface(Ci.nsINavHistoryResultTreeViewer); + var startNode = view.nodeForTreeIndex(startIndexRef.value); + s.range = [startNode.time, now]; + } + } else { + // Use the predetermined range. + s.range = [this.durationStartTimes[durValue], Date.now() * 1000]; + } + } + + try { + s.sanitize(); + } catch(er) { + Components.utils.reportError("Exception during sanitize: " + er); + } + return true; + }, + + /** + * In order to mark the custom Places tree view and its nsINavHistoryResult + * for garbage collection, we need to break the reference cycle between the + * two. + */ + unload: function() { + let result = this.placesTree.getResult(); + result.removeObserver(this.placesTree.view); + this.placesTree.view = null; + }, + + /** + * Called when the user moves the grippy by dragging it, clicking in the tree, + * or on keypress. Updates the duration dropdown so that it displays the + * appropriate specific or custom duration. + * + * @param aEventName + * The name of the event whose handler called this method, e.g., + * "ondragstart", "onkeypress", etc. + * @param aEvent + * The event captured in the event handler. + */ + grippyMoved: function(aEventName, aEvent) { + gContiguousSelectionTreeHelper[aEventName](aEvent); + var lastSelRow = gContiguousSelectionTreeHelper.getGrippyRow() - 1; + var durList = document.getElementById("sanitizeDurationChoice"); + var durValue = parseInt(durList.value); + + // Multiple durations can map to the same row. Don't update the dropdown + // if the current duration is valid for lastSelRow. + if ((durValue !== this.TIMESPAN_CUSTOM || + lastSelRow in this.durationRowsToVals) && + (durValue === this.TIMESPAN_CUSTOM || + this.durationValsToRows[durValue] !== lastSelRow)) { + // Setting durList.value causes its onselect handler to fire, which calls + // selectByTimespan(). + if (lastSelRow in this.durationRowsToVals) { + durList.value = this.durationRowsToVals[lastSelRow]; + } else { + durList.value = this.TIMESPAN_CUSTOM; + } + } + + // If there are no selected rows, disable the dialog's OK button. + document.documentElement.getButton("accept").disabled = lastSelRow < 0; + } +#endif + +}; + +#ifdef CRH_DIALOG_TREE_VIEW +/** + * A helper for handling contiguous selection in the tree. + */ +var gContiguousSelectionTreeHelper = { + + /** + * Gets the tree associated with this helper. + */ + get tree() { + return this._tree; + }, + + /** + * Sets the tree that this module handles. The tree is assigned a new view + * that is equipped to handle contiguous selection. You can pass in an + * object that will be used as the prototype of the new view. Otherwise + * the tree's current view is used as the prototype. + * + * @param aTreeElement + * The tree element + * @param aProtoTreeView + * If defined, this will be used as the prototype of the tree's new + * view + * @return The new view + */ + setTree: function(aTreeElement, aProtoTreeView) { + this._tree = aTreeElement; + var newView = this._makeTreeView(aProtoTreeView || aTreeElement.view); + aTreeElement.view = newView; + return newView; + }, + + /** + * The index of the row that the grippy occupies. Note that the index of the + * last selected row is getGrippyRow() - 1. If getGrippyRow() is 0, then + * no selection exists. + * + * @return The row index of the grippy + */ + getGrippyRow: function() { + var sel = this.tree.view.selection; + var rangeCount = sel.getRangeCount(); + if (rangeCount === 0) { + return 0; + } + if (rangeCount !== 1) { + throw "contiguous selection tree helper: getGrippyRow called with " + + "multiple selection ranges"; + } + var max = {}; + sel.getRangeAt(0, {}, max); + return max.value + 1; + }, + + /** + * Helper function for the dragover event. Your dragover listener should + * call this. It updates the selection in the tree under the mouse. + * + * @param aEvent + * The observed dragover event + */ + ondragover: function(aEvent) { + // Without this when dragging on Windows the mouse cursor is a "no" sign. + // This makes it a drop symbol. + var ds = Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService) + .getCurrentSession(); + ds.canDrop = true; + ds.dragAction = 0; + + var tbo = this.tree.treeBoxObject; + aEvent.QueryInterface(Ci.nsIDOMMouseEvent); + var hoverRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY); + + if (hoverRow < 0) { + return; + } + + this.rangedSelect(hoverRow - 1); + }, + + /** + * Helper function for the dragstart event. Your dragstart listener should + * call this. It starts a drag session. + * + * @param aEvent + * The observed dragstart event + */ + ondragstart: function(aEvent) { + var tbo = this.tree.treeBoxObject; + var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY); + + if (clickedRow !== this.getGrippyRow()) { + return; + } + + // This part is a hack. What we really want is a grab and slide, not + // drag and drop. Start a move drag session with dummy data and a + // dummy region. Set the region's coordinates to (Infinity, Infinity) + // so it's drawn offscreen and its size to (1, 1). + var arr = Cc["@mozilla.org/supports-array;1"] + .createInstance(Ci.nsISupportsArray); + var trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + trans.init(null); + trans.setTransferData('dummy-flavor', null, 0); + arr.AppendElement(trans); + var reg = Cc["@mozilla.org/gfx/region;1"] + .createInstance(Ci.nsIScriptableRegion); + reg.setToRect(Infinity, Infinity, 1, 1); + var ds = Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService); + ds.invokeDragSession(aEvent.target, arr, reg, ds.DRAGDROP_ACTION_MOVE); + }, + + /** + * Helper function for the keypress event. Your keypress listener should + * call this. Users can use Up, Down, Page Up/Down, Home, and End to move + * the bottom of the selection window. + * + * @param aEvent + * The observed keypress event + */ + onkeypress: function(aEvent) { + var grippyRow = this.getGrippyRow(); + var tbo = this.tree.treeBoxObject; + var rangeEnd; + switch (aEvent.keyCode) { + case aEvent.DOM_VK_HOME: + rangeEnd = 0; + break; + case aEvent.DOM_VK_PAGE_UP: + rangeEnd = grippyRow - tbo.getPageLength(); + break; + case aEvent.DOM_VK_UP: + rangeEnd = grippyRow - 2; + break; + case aEvent.DOM_VK_DOWN: + rangeEnd = grippyRow; + break; + case aEvent.DOM_VK_PAGE_DOWN: + rangeEnd = grippyRow + tbo.getPageLength(); + break; + case aEvent.DOM_VK_END: + rangeEnd = this.tree.view.rowCount - 2; + break; + default: + return; + break; + } + + aEvent.stopPropagation(); + + // First, clip rangeEnd. this.rangedSelect() doesn't clip the range if we + // select past the ends of the tree. + if (rangeEnd < 0) { + rangeEnd = -1; + } else if (this.tree.view.rowCount - 2 < rangeEnd) { + rangeEnd = this.tree.view.rowCount - 2; + } + + // Next, (de)select. + this.rangedSelect(rangeEnd); + + // Finally, scroll the tree. We always want one row above and below the + // grippy row to be visible if possible. + if (rangeEnd < grippyRow) { + // moved up + tbo.ensureRowIsVisible(rangeEnd < 0 ? 0 : rangeEnd); + } else { + // moved down + if (rangeEnd + 2 < this.tree.view.rowCount) { + tbo.ensureRowIsVisible(rangeEnd + 2); + } else if (rangeEnd + 1 < this.tree.view.rowCount) { + tbo.ensureRowIsVisible(rangeEnd + 1); + } + } + }, + + /** + * Helper function for the mousedown event. Your mousedown listener should + * call this. Users can click on individual rows to make the selection + * jump to them immediately. + * + * @param aEvent + * The observed mousedown event + */ + onmousedown: function(aEvent) { + var tbo = this.tree.treeBoxObject; + var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY); + + if (clickedRow < 0 || clickedRow >= this.tree.view.rowCount) { + return; + } + + if (clickedRow < this.getGrippyRow()) { + this.rangedSelect(clickedRow); + } else if (clickedRow > this.getGrippyRow()) { + this.rangedSelect(clickedRow - 1); + } + }, + + /** + * Selects range [0, aEndRow] in the tree. The grippy row will then be at + * index aEndRow + 1. aEndRow may be -1, in which case the selection is + * cleared and the grippy row will be at index 0. + * + * @param aEndRow + * The range [0, aEndRow] will be selected. + */ + rangedSelect: function(aEndRow) { + var tbo = this.tree.treeBoxObject; + if (aEndRow < 0) { + this.tree.view.selection.clearSelection(); + } else { + this.tree.view.selection.rangedSelect(0, aEndRow, false); + } + tbo.invalidateRange(tbo.getFirstVisibleRow(), tbo.getLastVisibleRow()); + }, + + /** + * Scrolls the tree so that the grippy row is in the center of the view. + */ + scrollToGrippy: function() { + var rowCount = this.tree.view.rowCount; + var tbo = this.tree.treeBoxObject; + var pageLen = tbo.getPageLength() || + parseInt(this.tree.getAttribute("rows")) || + 10; + + if (rowCount <= pageLen) { + // All rows fit on a single page. + return; + } + + var scrollToRow = this.getGrippyRow() - Math.ceil(pageLen / 2.0); + + if (scrollToRow < 0) { + // Grippy row is in first half of first page. + scrollToRow = 0; + } else if (rowCount < scrollToRow + pageLen) { + // Grippy row is in last half of last page. + scrollToRow = rowCount - pageLen; + } + + tbo.scrollToRow(scrollToRow); + }, + + /** + * Creates a new tree view suitable for contiguous selection. If + * aProtoTreeView is specified, it's used as the new view's prototype. + * Otherwise the tree's current view is used as the prototype. + * + * @param aProtoTreeView + * Used as the new view's prototype if specified + */ + _makeTreeView: function(aProtoTreeView) { + var view = aProtoTreeView; + var that = this; + + //XXXadw: When Alex gets the grippy icon done, this may or may not change, + // depending on how we style it. + view.isSeparator = function(aRow) { + return aRow === that.getGrippyRow(); + }; + + // rowCount includes the grippy row. + view.__defineGetter__("_rowCount", view.__lookupGetter__("rowCount")); + view.__defineGetter__("rowCount", function() { + return this._rowCount + 1; + }); + + // This has to do with visual feedback in the view itself, e.g., drawing + // a small line underneath the dropzone. Not what we want. + view.canDrop = function() { + return false; + }; + + // No clicking headers to sort the tree or sort feedback on columns. + view.cycleHeader = function() {}; + view.sortingChanged = function() {}; + + // Override a bunch of methods to account for the grippy row. + + view._getCellProperties = view.getCellProperties; + view.getCellProperties = function (aRow, aCol) { + var grippyRow = that.getGrippyRow(); + if (aRow === grippyRow) { + return "grippyRow"; + } + if (aRow < grippyRow) { + return this._getCellProperties(aRow, aCol); + } + + return this._getCellProperties(aRow - 1, aCol); + }; + + view._getRowProperties = view.getRowProperties; + view.getRowProperties = function (aRow) { + var grippyRow = that.getGrippyRow(); + if (aRow === grippyRow) { + return "grippyRow"; + } + + if (aRow < grippyRow) { + return this._getRowProperties(aRow); + } + + return this._getRowProperties(aRow - 1); + }; + + view._getCellText = view.getCellText; + view.getCellText = function (aRow, aCol) { + var grippyRow = that.getGrippyRow(); + if (aRow === grippyRow) { + return ""; + } + aRow = aRow < grippyRow ? aRow : aRow - 1; + return this._getCellText(aRow, aCol); + }; + + view._getImageSrc = view.getImageSrc; + view.getImageSrc = function(aRow, aCol) { + var grippyRow = that.getGrippyRow(); + if (aRow === grippyRow) { + return ""; + } + aRow = aRow < grippyRow ? aRow : aRow - 1; + return this._getImageSrc(aRow, aCol); + }; + + view.isContainer = function(aRow) { return false; }; + view.getParentIndex = function(aRow) { return -1; }; + view.getLevel = function(aRow) { return 0; }; + view.hasNextSibling = function(aRow, aAfterIndex) { + return aRow < this.rowCount - 1; + }; + + return view; + } +}; +#endif diff --git a/browser/base/content/tabbrowser.css b/browser/base/content/tabbrowser.css new file mode 100644 index 000000000..43536b27a --- /dev/null +++ b/browser/base/content/tabbrowser.css @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.tabbrowser-tabbox { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabbox"); + /* Make the content area follow the system colors before load */ + background: Menu; + color: MenuText; +} + +.tabbrowser-arrowscrollbox { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-arrowscrollbox"); +} + +.tab-close-button { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-close-tab-button"); + display: none; +} + +.tabbrowser-tabs[closebuttons="activetab"] > * > * > * > .tab-close-button:not([pinned])[selected="true"], +.tabbrowser-tabs[closebuttons="alltabs"] > * > * > * > .tab-close-button:not([pinned]) { + display: -moz-box; +} + +.tab-label[pinned] { + width: 0; + margin-left: 0 !important; + margin-right: 0 !important; + padding-left: 0 !important; + padding-right: 0 !important; +} + +.tab-stack { + vertical-align: top; /* for pinned tabs */ +} + +tabpanels { + background-color: transparent; +} + +.tab-drop-indicator { + position: relative; + z-index: 2; +} + +.tab-throbber:not([busy]), +.tab-throbber[busy] + .tab-icon-image, +.tab-icon-sound:not([soundplaying]):not([muted]):not([blocked]), +.tab-icon-sound[pinned], +.tab-icon-overlay { + display: none; +} + +.tab-icon-overlay[soundplaying][pinned], +.tab-icon-overlay[muted][pinned], +.tab-icon-overlay[blocked][pinned] { + display: -moz-box; +} + +.closing-tabs-spacer { + pointer-events: none; +} + +.tabbrowser-tabs:not(:hover) > .tabbrowser-arrowscrollbox > .closing-tabs-spacer { + transition: width .15s ease-out; +} + +/** + * Optimization for tabs that are restored lazily. We can save a good amount of + * memory that to-be-restored tabs would otherwise consume simply by setting + * their browsers to 'display: none' as that will prevent them from having to + * create a presentation and the like. + */ +browser[pending] { + display: none; +} diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml new file mode 100644 index 000000000..f9cb73fa1 --- /dev/null +++ b/browser/base/content/tabbrowser.xml @@ -0,0 +1,5340 @@ +<?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 % tabBrowserDTD SYSTEM "chrome://browser/locale/tabbrowser.dtd" > +%tabBrowserDTD; +]> + +<bindings id="tabBrowserBindings" + 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="tabbrowser"> + <resources> + <stylesheet src="chrome://browser/content/tabbrowser.css"/> + </resources> + + <content> + <xul:stringbundle anonid="tbstringbundle" src="chrome://browser/locale/tabbrowser.properties"/> + <xul:tabbox anonid="tabbox" class="tabbrowser-tabbox" + flex="1" eventnode="document" xbl:inherits="handleCtrlPageUpDown" + onselect="if (event.target.localName == 'tabpanels') this.parentNode.updateCurrentBrowser();"> + <xul:tabpanels flex="1" class="plain" selectedIndex="0" anonid="panelcontainer"> + <xul:notificationbox flex="1"> + <xul:hbox flex="1" class="browserSidebarContainer"> + <xul:vbox flex="1" class="browserContainer"> + <xul:stack flex="1" class="browserStack" anonid="browserStack"> + <xul:browser anonid="initialBrowser" type="content-primary" message="true" disablehistory="true" + xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,datetimepicker,authdosprotected"/> + </xul:stack> + </xul:vbox> + </xul:hbox> + </xul:notificationbox> + </xul:tabpanels> + </xul:tabbox> + <children/> + </content> + <implementation implements="nsIDOMEventListener, nsIMessageListener"> + + <property name="tabContextMenu" readonly="true" + onget="return this.tabContainer.contextMenu;"/> + + <field name="tabContainer" readonly="true"> + document.getElementById(this.getAttribute("tabcontainer")); + </field> + <field name="tabs" readonly="true"> + this.tabContainer.childNodes; + </field> + + <property name="visibleTabs" readonly="true"> + <getter><![CDATA[ + if (!this._visibleTabs) + this._visibleTabs = Array.filter(this.tabs, + function (tab) { return !tab.hidden && !tab.closing; }); + return this._visibleTabs; + ]]></getter> + </property> + + <field name="closingTabsEnum" readonly="true">({ ALL: 0, OTHER: 1, TO_END: 2 });</field> + + <field name="_visibleTabs">null</field> + + <field name="mURIFixup" readonly="true"> + Components.classes["@mozilla.org/docshell/urifixup;1"] + .getService(Components.interfaces.nsIURIFixup); + </field> + <field name="mFaviconService" readonly="true"> + Components.classes["@mozilla.org/browser/favicon-service;1"] + .getService(Components.interfaces.nsIFaviconService); + </field> + <field name="_placesAutocomplete" readonly="true"> + Components.classes["@mozilla.org/autocomplete/search;1?name=history"] + .getService(Components.interfaces.mozIPlacesAutoComplete); + </field> + <field name="mTabBox" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "tabbox"); + </field> + <field name="mPanelContainer" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer"); + </field> + <field name="mStringBundle"> + document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle"); + </field> + <field name="mCurrentTab"> + null + </field> + <field name="_lastRelatedTab"> + null + </field> + <field name="mCurrentBrowser"> + null + </field> + <field name="mProgressListeners"> + [] + </field> + <field name="mTabsProgressListeners"> + [] + </field> + <field name="mTabListeners"> + [] + </field> + <field name="mTabFilters"> + [] + </field> + <field name="mIsBusy"> + false + </field> + <field name="_outerWindowIDBrowserMap"> + new Map(); + </field> + <field name="arrowKeysShouldWrap" readonly="true"> + false + </field> + + <field name="_autoScrollPopup"> + null + </field> + + <field name="_previewMode"> + false + </field> + + <property name="_numPinnedTabs" readonly="true"> + <getter><![CDATA[ + for (var i = 0; i < this.tabs.length; i++) { + if (!this.tabs[i].pinned) + break; + } + return i; + ]]></getter> + </property> + + <property name="popupAnchor" readonly="true"> + <getter><![CDATA[ + if (this.mCurrentTab._popupAnchor) { + return this.mCurrentTab._popupAnchor; + } + let stack = this.mCurrentBrowser.parentNode; + // Create an anchor for the popup + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let popupAnchor = document.createElementNS(NS_XUL, "hbox"); + popupAnchor.className = "popup-anchor"; + popupAnchor.hidden = true; + stack.appendChild(popupAnchor); + return this.mCurrentTab._popupAnchor = popupAnchor; + ]]></getter> + </property> + + <method name="updateWindowResizers"> + <body><![CDATA[ + if (!window.gShowPageResizers) + return; + + var show = document.getElementById("addon-bar").collapsed && + window.windowState == window.STATE_NORMAL; + for (let i = 0; i < this.browsers.length; i++) { + this.browsers[i].showWindowResizer = show; + } + ]]></body> + </method> + + <method name="_setCloseKeyState"> + <parameter name="aEnabled"/> + <body><![CDATA[ + let keyClose = document.getElementById("key_close"); + let closeKeyEnabled = keyClose.getAttribute("disabled") != "true"; + if (closeKeyEnabled == aEnabled) + return; + + if (aEnabled) + keyClose.removeAttribute("disabled"); + else + keyClose.setAttribute("disabled", "true"); + + // We also want to remove the keyboard shortcut from the file menu + // when the shortcut is disabled, and bring it back when it's + // renabled. + // + // Fixing bug 630826 could make that happen automatically. + // Fixing bug 630830 could avoid the ugly hack below. + + let closeMenuItem = document.getElementById("menu_close"); + let parentPopup = closeMenuItem.parentNode; + let nextItem = closeMenuItem.nextSibling; + let clonedItem = closeMenuItem.cloneNode(true); + + parentPopup.removeChild(closeMenuItem); + + if (aEnabled) + clonedItem.setAttribute("key", "key_close"); + else + clonedItem.removeAttribute("key"); + + parentPopup.insertBefore(clonedItem, nextItem); + ]]></body> + </method> + + <method name="pinTab"> + <parameter name="aTab"/> + <body><![CDATA[ + if (aTab.pinned) + return; + + if (aTab.hidden) + this.showTab(aTab); + + this.moveTabTo(aTab, this._numPinnedTabs); + aTab.setAttribute("pinned", "true"); + this.tabContainer._unlockTabSizing(); + this.tabContainer._positionPinnedTabs(); + this.tabContainer.adjustTabstrip(); + + this.getBrowserForTab(aTab).docShell.isAppTab = true; + + if (aTab.selected) + this._setCloseKeyState(false); + + let event = document.createEvent("Events"); + event.initEvent("TabPinned", true, false); + aTab.dispatchEvent(event); + ]]></body> + </method> + + <method name="unpinTab"> + <parameter name="aTab"/> + <body><![CDATA[ + if (!aTab.pinned) + return; + + this.moveTabTo(aTab, this._numPinnedTabs - 1); + aTab.setAttribute("fadein", "true"); + aTab.removeAttribute("pinned"); + aTab.style.MozMarginStart = ""; + this.tabContainer._unlockTabSizing(); + this.tabContainer._positionPinnedTabs(); + this.tabContainer.adjustTabstrip(); + + this.getBrowserForTab(aTab).docShell.isAppTab = false; + + if (aTab.selected) + this._setCloseKeyState(true); + + let event = document.createEvent("Events"); + event.initEvent("TabUnpinned", true, false); + aTab.dispatchEvent(event); + ]]></body> + </method> + + <method name="previewTab"> + <parameter name="aTab"/> + <parameter name="aCallback"/> + <body> + <![CDATA[ + let currentTab = this.selectedTab; + try { + // Suppress focus, ownership and selected tab changes + this._previewMode = true; + this.selectedTab = aTab; + aCallback(); + } finally { + this.selectedTab = currentTab; + this._previewMode = false; + } + ]]> + </body> + </method> + + <method name="getBrowserAtIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + return this.browsers[aIndex]; + ]]> + </body> + </method> + + <method name="getBrowserIndexForDocument"> + <parameter name="aDocument"/> + <body> + <![CDATA[ + var tab = this._getTabForContentWindow(aDocument.defaultView); + return tab ? tab._tPos : -1; + ]]> + </body> + </method> + + <method name="getBrowserForDocument"> + <parameter name="aDocument"/> + <body> + <![CDATA[ + var tab = this._getTabForContentWindow(aDocument.defaultView); + return tab ? tab.linkedBrowser : null; + ]]> + </body> + </method> + + <method name="getBrowserForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + var tab = this._getTabForContentWindow(aWindow); + return tab ? tab.linkedBrowser : null; + ]]> + </body> + </method> + + <method name="getBrowserForOuterWindowID"> + <parameter name="aID"/> + <body> + <![CDATA[ + return this._outerWindowIDBrowserMap.get(aID); + ]]> + </body> + </method> + + <method name="_getTabForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + for (let i = 0; i < this.browsers.length; i++) { + if (this.browsers[i].contentWindow == aWindow) + return this.tabs[i]; + } + return null; + ]]> + </body> + </method> + + <!-- Binding from browser to tab --> + <field name="_tabForBrowser" readonly="true"> + <![CDATA[ + new WeakMap(); + ]]> + </field> + + <method name="_getTabForBrowser"> + <parameter name="aBrowser" /> + <body> + <![CDATA[ + let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated; + let text = "_getTabForBrowser` is now deprecated, please use `getTabForBrowser"; + let url = "https://developer.mozilla.org/docs/Mozilla/Tech/XUL/Method/getTabForBrowser"; + Deprecated.warning(text, url); + return this.getTabForBrowser(aBrowser); + ]]> + </body> + </method> + + <method name="getTabForBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return this._tabForBrowser.get(aBrowser); + ]]> + </body> + </method> + + <method name="getNotificationBox"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return this.getSidebarContainer(aBrowser).parentNode; + ]]> + </body> + </method> + + <method name="getSidebarContainer"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return this.getBrowserContainer(aBrowser).parentNode; + ]]> + </body> + </method> + + <method name="getBrowserContainer"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return (aBrowser || this.mCurrentBrowser).parentNode.parentNode; + ]]> + </body> + </method> + + <method name="getTabModalPromptBox"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let browser = (aBrowser || this.mCurrentBrowser); + let stack = browser.parentNode; + let self = this; + + let promptBox = { + appendPrompt : function(args, onCloseCallback) { + let newPrompt = document.createElementNS(XUL_NS, "tabmodalprompt"); + // stack.appendChild(newPrompt); + stack.insertBefore(newPrompt, browser.nextSibling); + browser.setAttribute("tabmodalPromptShowing", true); + + newPrompt.clientTop; // style flush to assure binding is attached + + let prompts = this.listPrompts(); + if (prompts.length > 1) { + // Let's hide ourself behind the current prompt. + newPrompt.hidden = true; + } + + let tab = self._getTabForContentWindow(browser.contentWindow); + newPrompt.init(args, tab, onCloseCallback); + return newPrompt; + }, + + removePrompt : function(aPrompt) { + stack.removeChild(aPrompt); + + let prompts = this.listPrompts(); + if (prompts.length) { + let prompt = prompts[prompts.length - 1]; + prompt.hidden = false; + prompt.Dialog.setDefaultFocus(); + } else { + browser.removeAttribute("tabmodalPromptShowing"); + browser.focus(); + } + }, + + listPrompts : function(aPrompt) { + let els = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"); + // NodeList --> real JS array + let prompts = Array.slice(els); + return prompts; + }, + }; + + return promptBox; + ]]> + </body> + </method> + + <method name="getTabFromAudioEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") || + !aEvent.isTrusted) { + return null; + } + + var browser = aEvent.originalTarget; + var tab = this.getTabForBrowser(browser); + return tab; + ]]> + </body> + </method> + + <method name="_callProgressListeners"> + <parameter name="aBrowser"/> + <parameter name="aMethod"/> + <parameter name="aArguments"/> + <parameter name="aCallGlobalListeners"/> + <parameter name="aCallTabsListeners"/> + <body><![CDATA[ + var rv = true; + + if (!aBrowser) + aBrowser = this.mCurrentBrowser; + + if (aCallGlobalListeners != false && + aBrowser == this.mCurrentBrowser) { + this.mProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + if (!p[aMethod].apply(p, aArguments)) + rv = false; + } catch (e) { + // don't inhibit other listeners + Components.utils.reportError(e); + } + } + }); + } + + if (aCallTabsListeners != false) { + aArguments.unshift(aBrowser); + + this.mTabsProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + if (!p[aMethod].apply(p, aArguments)) + rv = false; + } catch (e) { + // don't inhibit other listeners + Components.utils.reportError(e); + } + } + }); + } + + return rv; + ]]></body> + </method> + + <!-- A web progress listener object definition for a given tab. --> + <method name="mTabProgressListener"> + <parameter name="aTab"/> + <parameter name="aBrowser"/> + <parameter name="aStartsBlank"/> + <body> + <![CDATA[ + return ({ + mTabBrowser: this, + mTab: aTab, + mBrowser: aBrowser, + mBlank: aStartsBlank, + + // cache flags for correct status UI update after tab switching + mStateFlags: 0, + mStatus: 0, + mMessage: "", + mTotalProgress: 0, + + // count of open requests (should always be 0 or 1) + mRequestCount: 0, + + destroy: function () { + delete this.mTab; + delete this.mBrowser; + delete this.mTabBrowser; + }, + + _callProgressListeners: function () { + Array.unshift(arguments, this.mBrowser); + return this.mTabBrowser._callProgressListeners.apply(this.mTabBrowser, arguments); + }, + + _shouldShowProgress: function (aRequest) { + if (this.mBlank) + return false; + + // Don't show progress indicators in tabs for about: URIs + // pointing to local resources. + try { + let channel = aRequest.QueryInterface(Ci.nsIChannel); + if (channel.originalURI.schemeIs("about") && + (channel.URI.schemeIs("jar") || channel.URI.schemeIs("file"))) + return false; + } catch (e) {} + + return true; + }, + + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0; + + if (!this._shouldShowProgress(aRequest)) + return; + + if (this.mTotalProgress) + this.mTab.setAttribute("progress", "true"); + + this._callProgressListeners("onProgressChange", + [aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress]); + }, + + onProgressChange64: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + return this.onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress); + }, + + onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { + if (!aRequest) + return; + + var oldBlank = this.mBlank; + + const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + const nsIChannel = Components.interfaces.nsIChannel; + let location, originalLocation; + try { + aRequest.QueryInterface(nsIChannel) + location = aRequest.URI; + originalLocation = aRequest.originalURI; + } catch (ex) {} + + if (aStateFlags & nsIWebProgressListener.STATE_START) { + this.mRequestCount++; + } + else if (aStateFlags & nsIWebProgressListener.STATE_STOP) { + const NS_ERROR_UNKNOWN_HOST = 2152398878; + if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) { + // to prevent bug 235825: wait for the request handled + // by the automatic keyword resolver + return; + } + // since we (try to) only handle STATE_STOP of the last request, + // the count of open requests should now be 0 + this.mRequestCount = 0; + } + + if (aStateFlags & nsIWebProgressListener.STATE_START && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + if (aWebProgress.isTopLevel) + this.mBrowser.urlbarChangeTracker.startedLoad(); + + if (this._shouldShowProgress(aRequest)) { + if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { + this.mTab.setAttribute("busy", "true"); + if (aWebProgress.isTopLevel && + !(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD)) + this.mTabBrowser.setTabTitleLoading(this.mTab); + } + + if (this.mTab.selected) + this.mTabBrowser.mIsBusy = true; + } + } + else if (aStateFlags & nsIWebProgressListener.STATE_STOP && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + + if (this.mTab.hasAttribute("busy")) { + this.mTab.removeAttribute("busy"); + this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]); + if (!this.mTab.selected) + this.mTab.setAttribute("unread", "true"); + } + this.mTab.removeAttribute("progress"); + + if (aWebProgress.isTopLevel) { + let isSuccessful = Components.isSuccessCode(aStatus); + if (!isSuccessful && !isTabEmpty(this.mTab)) { + // Restore the current document's location in case the + // request was stopped (possibly from a content script) + // before the location changed. + + this.mBrowser.userTypedValue = null; + + if (this.mTab.selected && gURLBar) + URLBarSetURI(); + } else if (isSuccessful) { + this.mBrowser.urlbarChangeTracker.finishedLoad(); + } + + if (!this.mBrowser.mIconURL) + this.mTabBrowser.useDefaultIcon(this.mTab); + } + + if (this.mBlank) + this.mBlank = false; + + // For keyword URIs clear the user typed value since they will be changed into real URIs + if (location.scheme == "keyword") + this.mBrowser.userTypedValue = null; + + if (this.mTab.label == this.mTabBrowser.mStringBundle.getString("tabs.connecting")) + this.mTabBrowser.setTabTitle(this.mTab); + + if (this.mTab.selected) + this.mTabBrowser.mIsBusy = false; + } + + if (oldBlank) { + this._callProgressListeners("onUpdateCurrentBrowser", + [aStateFlags, aStatus, "", 0], + true, false); + } else { + this._callProgressListeners("onStateChange", + [aWebProgress, aRequest, aStateFlags, aStatus], + true, false); + } + + this._callProgressListeners("onStateChange", + [aWebProgress, aRequest, aStateFlags, aStatus], + false); + + if (aStateFlags & (nsIWebProgressListener.STATE_START | + nsIWebProgressListener.STATE_STOP)) { + // reset cached temporary values at beginning and end + this.mMessage = ""; + this.mTotalProgress = 0; + } + this.mStateFlags = aStateFlags; + this.mStatus = aStatus; + }, + + onLocationChange: function (aWebProgress, aRequest, aLocation, + aFlags) { + // OnLocationChange is called for both the top-level content + // and the subframes. + let topLevel = aWebProgress.isTopLevel; + + if (topLevel) { + let isSameDocument = + !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); + // We need to clear the typed value + // if the document failed to load, to make sure the urlbar reflects the + // failed URI (particularly for SSL errors). However, don't clear the value + // if the error page's URI is about:blank, because that causes complete + // loss of urlbar contents for invalid URI errors (see bug 867957). + if (this.mBrowser.didStartLoadSinceLastUserTyping() || + ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) && + aLocation.spec != "about:blank")) + this.mBrowser.userTypedValue = null; + + // If the browser was playing audio, we should remove the playing state. + if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) { + clearTimeout(this.mTab._soundPlayingAttrRemovalTimer); + this.mTab._soundPlayingAttrRemovalTimer = 0; + this.mTab.removeAttribute("soundplaying"); + this.mTabBrowser._tabAttrModified(this.mTab, ["soundplaying"]); + } + + // If the browser was previously muted, we should restore the muted state. + if (this.mTab.hasAttribute("muted")) { + this.mTab.linkedBrowser.mute(); + } + + // Don't clear the favicon if this onLocationChange was + // triggered by a pushState or a replaceState. See bug 550565. + if (aWebProgress.isLoadingDocument && + !(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) + this.mBrowser.mIconURL = null; + + let autocomplete = this.mTabBrowser._placesAutocomplete; + if (this.mBrowser.registeredOpenURI) { + autocomplete.unregisterOpenPage(this.mBrowser.registeredOpenURI); + delete this.mBrowser.registeredOpenURI; + } + // Tabs in private windows aren't registered as "Open" so + // that they don't appear as switch-to-tab candidates. + if (!isBlankPageURL(aLocation.spec) && + (!PrivateBrowsingUtils.isWindowPrivate(window) || + PrivateBrowsingUtils.permanentPrivateBrowsing)) { + autocomplete.registerOpenPage(aLocation); + this.mBrowser.registeredOpenURI = aLocation; + } + } + + if (!this.mBlank) { + this._callProgressListeners("onLocationChange", + [aWebProgress, aRequest, aLocation, + aFlags]); + } + + if (topLevel) { + this.mBrowser.lastURI = aLocation; + this.mBrowser.lastLocationChange = Date.now(); + } + }, + + onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) { + if (this.mBlank) + return; + + this._callProgressListeners("onStatusChange", + [aWebProgress, aRequest, aStatus, aMessage]); + + this.mMessage = aMessage; + }, + + onSecurityChange: function (aWebProgress, aRequest, aState) { + this._callProgressListeners("onSecurityChange", + [aWebProgress, aRequest, aState]); + }, + + onRefreshAttempted: function (aWebProgress, aURI, aDelay, aSameURI) { + return this._callProgressListeners("onRefreshAttempted", + [aWebProgress, aURI, aDelay, aSameURI]); + }, + + QueryInterface: function (aIID) { + if (aIID.equals(Components.interfaces.nsIWebProgressListener) || + aIID.equals(Components.interfaces.nsIWebProgressListener2) || + aIID.equals(Components.interfaces.nsISupportsWeakReference) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + } + }); + ]]> + </body> + </method> + + <method name="setIcon"> + <parameter name="aTab"/> + <parameter name="aURI"/> + <parameter name="aLoadingPrincipal"/> + <body> + <![CDATA[ + let browser = this.getBrowserForTab(aTab); + browser.mIconURL = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + + if (aURI && this.mFaviconService) { + if (!(aURI instanceof Ci.nsIURI)) { + aURI = makeURI(aURI); + } + // We do not serialize the principal from within SessionStore.jsm, + // hence if aLoadingPrincipal is null we default to the + // systemPrincipal which will allow the favicon to load. + let loadingPrincipal = aLoadingPrincipal + ? aLoadingPrincipal + : Services.scriptSecurityManager.getSystemPrincipal(); + let loadType = PrivateBrowsingUtils.isWindowPrivate(window) + ? this.mFaviconService.FAVICON_LOAD_PRIVATE + : this.mFaviconService.FAVICON_LOAD_NON_PRIVATE; + + this.mFaviconService.setAndFetchFaviconForPage( + browser.currentURI, aURI, false, loadType, null, loadingPrincipal); + } + + let sizedIconUrl = browser.mIconURL || ""; + if (sizedIconUrl != aTab.getAttribute("image")) { + if (sizedIconUrl) + aTab.setAttribute("image", sizedIconUrl); + else + aTab.removeAttribute("image"); + this._tabAttrModified(aTab, ["image"]); + } + + if (Services.prefs.getBoolPref("browser.chrome.favicons.process")) { + let favImage = new Image; + favImage.src = browser.mIconURL; + var tabBrowser = this; + favImage.onload = function () { + try { + // Draw the icon on a hidden canvas + var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + var tabImg = document.getAnonymousElementByAttribute(aTab, "anonid", "tab-icon"); + var w = tabImg.boxObject.width; + var h = tabImg.boxObject.height; + canvas.width = w; + canvas.height = h; + var ctx = canvas.getContext('2d'); + ctx.drawImage(favImage, 0, 0, w, h); + icon = canvas.toDataURL(); + browser.mIconURL = icon; + aTab.setAttribute("image", icon); + } + catch (e) { + console.warn("Processing of favicon failed."); + // Canvas failed: icon remains as it was + } + tabBrowser._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]); + } + } + + this._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]); + ]]> + </body> + </method> + + <method name="getIcon"> + <parameter name="aTab"/> + <body> + <![CDATA[ + let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser; + return browser.mIconURL; + ]]> + </body> + </method> + + <method name="shouldLoadFavIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + return (aURI && + Services.prefs.getBoolPref("browser.chrome.site_icons") && + Services.prefs.getBoolPref("browser.chrome.favicons") && + ("schemeIs" in aURI) && (aURI.schemeIs("http") || aURI.schemeIs("https"))); + ]]> + </body> + </method> + + <method name="useDefaultIcon"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var browser = this.getBrowserForTab(aTab); + var docURIObject = browser.contentDocument.documentURIObject; + var icon = null; + <!-- Pale Moon: new image icon method, see bug #305986 --> + let req = browser.contentDocument.imageRequest; + let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size"); + if (browser.contentDocument instanceof ImageDocument && + req && req.image) { + if (Services.prefs.getBoolPref("browser.chrome.site_icons") && sz) { + try { + <!-- Main method: draw on a hidden canvas --> + var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + var tabImg = document.getAnonymousElementByAttribute(aTab, "anonid", "tab-icon"); + var w = tabImg.boxObject.width; + var h = tabImg.boxObject.height; + canvas.width = w; + canvas.height = h; + var ctx = canvas.getContext('2d'); + ctx.drawImage(browser.contentDocument.body.firstChild, 0, 0, w, h); + icon = canvas.toDataURL(); + } + catch (e) { + <!-- Fallback method in case canvas method fails, restricted by sz --> + try { + + if (req && + req.image && + req.image.width <= sz && + req.image.height <= sz) + icon = browser.currentURI; + } + catch (e) { + <!-- Both methods fail (very large or corrupt image): icon remains null --> + } + } + } + } + // Use documentURIObject in the check for shouldLoadFavIcon so that we + // do the right thing with about:-style error pages. Bug 453442 + else if (this.shouldLoadFavIcon(docURIObject)) { + let url = docURIObject.prePath + "/favicon.ico"; + if (!this.isFailedIcon(url)) + icon = url; + } + this.setIcon(aTab, icon, browser.contentPrincipal); + ]]> + </body> + </method> + + <method name="isFailedIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + if (this.mFaviconService) { + if (!(aURI instanceof Ci.nsIURI)) + aURI = makeURI(aURI); + return this.mFaviconService.isFailedFavicon(aURI); + } + return null; + ]]> + </body> + </method> + + <method name="getWindowTitleForBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + var newTitle = ""; + var docElement = this.ownerDocument.documentElement; + var sep = docElement.getAttribute("titlemenuseparator"); + + // Strip out any null bytes in the content title, since the + // underlying widget implementations of nsWindow::SetTitle pass + // null-terminated strings to system APIs. + var docTitle = aBrowser.contentTitle.replace("\0", "", "g"); + + if (!docTitle) + docTitle = docElement.getAttribute("titledefault"); + + var modifier = docElement.getAttribute("titlemodifier"); + if (docTitle) { + newTitle += docElement.getAttribute("titlepreface"); + newTitle += docTitle; + if (modifier) + newTitle += sep; + } + newTitle += modifier; + + // If location bar is hidden and the URL type supports a host, + // add the scheme and host to the title to prevent spoofing. + // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239 + try { + if (docElement.getAttribute("chromehidden").includes("location")) { + var uri = this.mURIFixup.createExposableURI( + aBrowser.currentURI); + if (uri.scheme == "about") + newTitle = uri.spec + sep + newTitle; + else + newTitle = uri.prePath + sep + newTitle; + } + } catch (e) {} + + return newTitle; + ]]> + </body> + </method> + + <method name="freezeTitlebar"> + <parameter name="aTitle"/> + <body> + <![CDATA[ + this._frozenTitle = aTitle || ""; + this.updateTitlebar(); + ]]> + </body> + </method> + + <method name="unfreezeTitlebar"> + <body> + <![CDATA[ + this._frozenTitle = ""; + this.updateTitlebar(); + ]]> + </body> + </method> + + <method name="updateTitlebar"> + <body> + <![CDATA[ + this.ownerDocument.title = this._frozenTitle || + this.getWindowTitleForBrowser(this.mCurrentBrowser); + ]]> + </body> + </method> + + <method name="updateCurrentBrowser"> + <parameter name="aForceUpdate"/> + <body> + <![CDATA[ + var newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex); + if (this.mCurrentBrowser == newBrowser && !aForceUpdate) + return; + + if (!aForceUpdate) { + window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils) + .beginTabSwitch(); + } + + var oldTab = this.mCurrentTab; + + // Preview mode should not reset the owner + if (!this._previewMode && !oldTab.selected) + oldTab.owner = null; + + if (this._lastRelatedTab) { + if (!this._lastRelatedTab.selected) + this._lastRelatedTab.owner = null; + this._lastRelatedTab = null; + } + + var oldBrowser = this.mCurrentBrowser; + if (oldBrowser) { + oldBrowser.setAttribute("type", "content-targetable"); + oldBrowser.docShellIsActive = false; + this.finder.mListeners.forEach(l => oldBrowser.finder.removeResultListener(l)); + } + + var updateBlockedPopups = false; + if (!oldBrowser || + (oldBrowser.blockedPopups && !newBrowser.blockedPopups) || + (!oldBrowser.blockedPopups && newBrowser.blockedPopups)) + updateBlockedPopups = true; + + newBrowser.setAttribute("type", "content-primary"); + newBrowser.docShellIsActive = + (window.windowState != window.STATE_MINIMIZED); + this.mCurrentBrowser = newBrowser; + this.mCurrentTab = this.tabContainer.selectedItem; + this.finder.mListeners.forEach(l => this.mCurrentBrowser.finder.addResultListener(l)); + this.showTab(this.mCurrentTab); + + var backForwardContainer = document.getElementById("unified-back-forward-button"); + if (backForwardContainer) { + backForwardContainer.setAttribute("switchingtabs", "true"); + window.addEventListener("MozAfterPaint", function removeSwitchingtabsAttr() { + window.removeEventListener("MozAfterPaint", removeSwitchingtabsAttr); + backForwardContainer.removeAttribute("switchingtabs"); + }); + } + + if (updateBlockedPopups) + this.mCurrentBrowser.updateBlockedPopups(); + + // Update the URL bar. + var loc = this.mCurrentBrowser.currentURI; + + // Bug 666809 - SecurityUI support for e10s + var webProgress = this.mCurrentBrowser.webProgress; + var securityUI = this.mCurrentBrowser.securityUI; + + // Update global findbar with new content browser + if (gFindBarInitialized) { + gFindBar.browser = newBrowser; + } + + this._callProgressListeners(null, "onLocationChange", + [webProgress, null, loc, 0], true, + false); + + if (securityUI) { + this._callProgressListeners(null, "onSecurityChange", + [webProgress, null, securityUI.state], true, false); + } + + var listener = this.mTabListeners[this.tabContainer.selectedIndex] || null; + if (listener && listener.mStateFlags) { + this._callProgressListeners(null, "onUpdateCurrentBrowser", + [listener.mStateFlags, listener.mStatus, + listener.mMessage, listener.mTotalProgress], + true, false); + } + + if (!this._previewMode) { + this.mCurrentTab.removeAttribute("unread"); + this.selectedTab.lastAccessed = Date.now(); + + this.updateTitlebar(); + + this.mCurrentTab.removeAttribute("titlechanged"); + } + + // If the new tab is busy, and our current state is not busy, then + // we need to fire a start to all progress listeners. + const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + if (this.mCurrentTab.hasAttribute("busy") && !this.mIsBusy) { + this.mIsBusy = true; + this._callProgressListeners(null, "onStateChange", + [webProgress, null, + nsIWebProgressListener.STATE_START | + nsIWebProgressListener.STATE_IS_NETWORK, 0], + true, false); + } + + // If the new tab is not busy, and our current state is busy, then + // we need to fire a stop to all progress listeners. + if (!this.mCurrentTab.hasAttribute("busy") && this.mIsBusy) { + this.mIsBusy = false; + this._callProgressListeners(null, "onStateChange", + [webProgress, null, + nsIWebProgressListener.STATE_STOP | + nsIWebProgressListener.STATE_IS_NETWORK, 0], + true, false); + } + + this._setCloseKeyState(!this.mCurrentTab.pinned); + + // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code + // that might rely upon the other changes suppressed. + // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window + if (!this._previewMode) { + // We've selected the new tab, so go ahead and notify listeners. + let event = new CustomEvent("TabSelect", { + bubbles: true, + cancelable: false, + detail: { + previousTab: oldTab + } + }); + this.mCurrentTab.dispatchEvent(event); + + this._tabAttrModified(oldTab, ["selected"]); + this._tabAttrModified(this.mCurrentTab, ["selected"]); + + // Adjust focus + oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused); + do { + // When focus is in the tab bar, retain it there. + if (document.activeElement == oldTab) { + // We need to explicitly focus the new tab, because + // tabbox.xml does this only in some cases. + this.mCurrentTab.focus(); + break; + } + + // If there's a tabmodal prompt showing, focus it. + if (newBrowser.hasAttribute("tabmodalPromptShowing")) { + let XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let prompts = newBrowser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"); + let prompt = prompts[prompts.length - 1]; + prompt.Dialog.setDefaultFocus(); + break; + } + + // Focus the location bar if it was previously focused for that tab. + // In full screen mode, only bother making the location bar visible + // if the tab is a blank one. + if (newBrowser._urlbarFocused && gURLBar) { + + // Explicitly close the popup if the URL bar retains focus + gURLBar.closePopup(); + + if (!window.fullScreen) { + gURLBar.focus(); + break; + } else if (isTabEmpty(this.mCurrentTab)) { + focusAndSelectUrlBar(); + break; + } + } + + // If the find bar is focused, keep it focused. + if (gFindBarInitialized && + !gFindBar.hidden && + gFindBar.getElement("findbar-textbox").getAttribute("focused") == "true") + break; + + // Otherwise, focus the content area. + let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + let focusFlags = fm.FLAG_NOSCROLL; + + let newFocusedElement = fm.getFocusedElementForWindow(window.content, true, {}); + + // for anchors, use FLAG_SHOWRING so that it is clear what link was + // last clicked when switching back to that tab + if (newFocusedElement && + (newFocusedElement instanceof HTMLAnchorElement || + newFocusedElement.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple")) + focusFlags |= fm.FLAG_SHOWRING; + fm.setFocus(newBrowser, focusFlags); + } while (false); + } + + this.tabContainer._setPositionalAttributes(); + ]]> + </body> + </method> + + <method name="_tabAttrModified"> + <parameter name="aTab"/> + <parameter name="aChanged"/> + <body><![CDATA[ + if (aTab.closing) + return; + + let event = new CustomEvent("TabAttrModified", { + bubbles: true, + cancelable: false, + detail: { + changed: aChanged, + } + }); + aTab.dispatchEvent(event); + ]]></body> + </method> + + <method name="setTabTitleLoading"> + <parameter name="aTab"/> + <body> + <![CDATA[ + aTab.label = this.mStringBundle.getString("tabs.connecting"); + aTab.crop = "end"; + this._tabAttrModified(aTab, ["label", "crop"]); + ]]> + </body> + </method> + + <method name="setTabTitle"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var browser = this.getBrowserForTab(aTab); + var crop = "end"; + var title = browser.contentTitle; + + if (!title) { + if (browser.currentURI.spec) { + try { + title = this.mURIFixup.createExposableURI(browser.currentURI).spec; + } catch(ex) { + title = browser.currentURI.spec; + } + } + + if (title && !isBlankPageURL(title)) { + // At this point, we now have a URI. + // Let's try to unescape it using a character set + // in case the URI is not ASCII. + try { + var characterSet = browser.characterSet; + const textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"] + .getService(Components.interfaces.nsITextToSubURI); + title = textToSubURI.unEscapeNonAsciiURI(characterSet, title); + } catch(ex) { /* Do nothing. */ } + + crop = "center"; + + } else // Still no title? Fall back to our untitled string. + title = this.mStringBundle.getString("tabs.emptyTabTitle"); + } + + if (aTab.label == title && + aTab.crop == crop) + return false; + + aTab.label = title; + aTab.crop = crop; + this._tabAttrModified(aTab, ["label", "crop"]); + + if (aTab.selected) + this.updateTitlebar(); + + return true; + ]]> + </body> + </method> + + <method name="loadOneTab"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <parameter name="aLoadInBackground"/> + <parameter name="aAllowThirdPartyFixup"/> + <body> + <![CDATA[ + var aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aRelatedToCurrent; + var aOriginPrincipal; + var aOpener; + if (arguments.length == 2 && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aTriggeringPrincipal = params.triggeringPrincipal; + aReferrerURI = params.referrerURI; + aReferrerPolicy = params.referrerPolicy; + aCharset = params.charset; + aPostData = params.postData; + aLoadInBackground = params.inBackground; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + aOriginPrincipal = params.originPrincipal; + aOpener = params.opener; + } + + var bgLoad = (aLoadInBackground != null) ? aLoadInBackground : + Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + var owner = bgLoad ? null : this.selectedTab; + var tab = this.addTab(aURI, { + triggeringPrincipal: aTriggeringPrincipal, + referrerURI: aReferrerURI, + referrerPolicy: aReferrerPolicy, + charset: aCharset, + postData: aPostData, + ownerTab: owner, + allowThirdPartyFixup: aAllowThirdPartyFixup, + fromExternal: aFromExternal, + originPrincipal: aOriginPrincipal, + relatedToCurrent: aRelatedToCurrent, + opener: aOpener }); + if (!bgLoad) + this.selectedTab = tab; + + return tab; + ]]> + </body> + </method> + + <method name="loadTabs"> + <parameter name="aURIs"/> + <parameter name="aLoadInBackground"/> + <parameter name="aReplace"/> + <body><![CDATA[ + let aAllowThirdPartyFixup; + let aTargetTab; + let aNewIndex = -1; + let aPostDatas = []; + if (arguments.length == 2 && + typeof arguments[1] == "object") { + let params = arguments[1]; + aLoadInBackground = params.inBackground; + aReplace = params.replace; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aTargetTab = params.targetTab; + aNewIndex = typeof params.newIndex === "number" ? + params.newIndex : aNewIndex; + aPostDatas = params.postDatas || aPostDatas; + } + + if (!aURIs.length) + return; + + // The tab selected after this new tab is closed (i.e. the new tab's + // "owner") is the next adjacent tab (i.e. not the previously viewed tab) + // when several urls are opened here (i.e. closing the first should select + // the next of many URLs opened) or if the pref to have UI links opened in + // the background is set (i.e. the link is not being opened modally) + // + // i.e. + // Number of URLs Load UI Links in BG Focus Last Viewed? + // == 1 false YES + // == 1 true NO + // > 1 false/true NO + var multiple = aURIs.length > 1; + var owner = multiple || aLoadInBackground ? null : this.selectedTab; + var firstTabAdded = null; + var targetTabIndex = -1; + + if (aReplace) { + let browser; + if (aTargetTab) { + browser = this.getBrowserForTab(aTargetTab); + targetTabIndex = aTargetTab._tPos; + } else { + browser = this.mCurrentBrowser; + targetTabIndex = this.tabContainer.selectedIndex; + } + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (aAllowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | + Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + try { + browser.loadURIWithFlags(aURIs[0], { + flags, postData: aPostDatas[0] + }); + } catch (e) { + // Ignore failure in case a URI is wrong, so we can continue + // opening the next ones. + } + } else { + firstTabAdded = this.addTab(aURIs[0], { + ownerTab: owner, + skipAnimation: multiple, + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[0] + }); + if (aNewIndex !== -1) { + this.moveTabTo(firstTabAdded, aNewIndex); + targetTabIndex = firstTabAdded._tPos; + } + } + + let tabNum = targetTabIndex; + for (let i = 1; i < aURIs.length; ++i) { + let tab = this.addTab(aURIs[i], { + skipAnimation: true, + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[i] + }); + if (targetTabIndex !== -1) + this.moveTabTo(tab, ++tabNum); + } + + if (!aLoadInBackground) { + if (firstTabAdded) { + // .selectedTab setter focuses the content area + this.selectedTab = firstTabAdded; + } + else + this.selectedBrowser.focus(); + } + ]]></body> + </method> + + <method name="addTab"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <parameter name="aOwner"/> + <parameter name="aAllowThirdPartyFixup"/> + <body> + <![CDATA[ + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aRelatedToCurrent; + var aSkipAnimation; + var aOriginPrincipal; + var aSkipBackgroundNotify; + var aOpener; + if (arguments.length == 2 && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aTriggeringPrincipal = params.triggeringPrincipal; + aReferrerURI = params.referrerURI; + aReferrerPolicy = params.referrerPolicy; + aCharset = params.charset; + aPostData = params.postData; + aOwner = params.ownerTab; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + aSkipAnimation = params.skipAnimation; + aOriginPrincipal = params.originPrincipal; + aOpener = params.opener; + aSkipBackgroundNotify = params.skipBackgroundNotify; + } + + // if we're adding tabs, we're past interrupt mode, ditch the owner + if (this.mCurrentTab.owner) + this.mCurrentTab.owner = null; + + var t = document.createElementNS(NS_XUL, "tab"); + + let aURIObject = null; + try { + aURIObject = Services.io.newURI(aURI || "about:blank"); + } catch (ex) { /* we'll try to fix up this URL later */ } + + var uriIsAboutBlank = !aURI || aURI == "about:blank"; + + if (!aURI || isBlankPageURL(aURI)) + t.setAttribute("label", this.mStringBundle.getString("tabs.emptyTabTitle")); + else + t.setAttribute("label", aURI); + + t.setAttribute("crop", "end"); + t.setAttribute("validate", "never"); //PMed + t.setAttribute("onerror", "this.removeAttribute('image');"); + + if (aSkipBackgroundNotify) { + t.setAttribute("skipbackgroundnotify", true); + } + + t.className = "tabbrowser-tab"; + + this.tabContainer._unlockTabSizing(); + + // When overflowing, new tabs are scrolled into view smoothly, which + // doesn't go well together with the width transition. So we skip the + // transition in that case. + let animate = !aSkipAnimation && + this.tabContainer.getAttribute("overflow") != "true" && + Services.prefs.getBoolPref("browser.tabs.animate"); + if (!animate) { + t.setAttribute("fadein", "true"); + setTimeout(function (tabContainer) { + tabContainer._handleNewTab(t); + }, 0, this.tabContainer); + } + + // invalidate caches + this._browsers = null; + this._visibleTabs = null; + + this.tabContainer.appendChild(t); + + // If this new tab is owned by another, assert that relationship + if (aOwner) + t.owner = aOwner; + + var b = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "browser"); + b.setAttribute("type", "content-targetable"); + b.setAttribute("message", "true"); + b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu")); + b.setAttribute("tooltip", this.getAttribute("contenttooltip")); + + if (window.gShowPageResizers && document.getElementById("addon-bar").collapsed && + window.windowState == window.STATE_NORMAL) { + b.setAttribute("showresizer", "true"); + } + + if (aOpener) { + b.QueryInterface(Ci.nsIFrameLoaderOwner).presetOpenerWindow(aOpener); + } + + if (this.hasAttribute("autocompletepopup")) + b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup")); + b.setAttribute("autoscrollpopup", this._autoScrollPopup.id); + + if (this.hasAttribute("datetimepicker")) { + b.setAttribute("datetimepicker", this.getAttribute("datetimepicker")); + } + + if (this.hasAttribute("authdosprotected")) { + b.setAttribute("authdosprotected", this.getAttribute("authdosprotected")); + } + + // Create the browserStack container + var stack = document.createElementNS(NS_XUL, "stack"); + stack.className = "browserStack"; + stack.appendChild(b); + stack.setAttribute("flex", "1"); + + // Create the browserContainer + var browserContainer = document.createElementNS(NS_XUL, "vbox"); + browserContainer.className = "browserContainer"; + browserContainer.appendChild(stack); + browserContainer.setAttribute("flex", "1"); + + // Create the sidebar container + var browserSidebarContainer = document.createElementNS(NS_XUL, + "hbox"); + browserSidebarContainer.className = "browserSidebarContainer"; + browserSidebarContainer.appendChild(browserContainer); + browserSidebarContainer.setAttribute("flex", "1"); + + // Add the Message and the Browser to the box + var notificationbox = document.createElementNS(NS_XUL, + "notificationbox"); + notificationbox.setAttribute("flex", "1"); + notificationbox.appendChild(browserSidebarContainer); + + var position = this.tabs.length - 1; + var uniqueId = "panel" + Date.now() + position; + notificationbox.id = uniqueId; + t.linkedPanel = uniqueId; + t.linkedBrowser = b; + this._tabForBrowser.set(b, t); + t._tPos = position; + this.tabContainer._setPositionalAttributes(); + + // Prevent the superfluous initial load of a blank document + // if we're going to load something other than about:blank. + if (!uriIsAboutBlank) { + b.setAttribute("nodefaultsrc", "true"); + } + + // NB: this appendChild call causes us to run constructors for the + // browser element, which fires off a bunch of notifications. Some + // of those notifications can cause code to run that inspects our + // state, so it is important that the tab element is fully + // initialized by this point. + this.mPanelContainer.appendChild(notificationbox); + + this.tabContainer.updateVisibility(); + + // wire up a progress listener for the new browser object. + var tabListener = this.mTabProgressListener(t, b, uriIsAboutBlank); + const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Components.interfaces.nsIWebProgress); + filter.addProgressListener(tabListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL); + b.webProgress.addProgressListener(filter, Components.interfaces.nsIWebProgress.NOTIFY_ALL); + this.mTabListeners[position] = tabListener; + this.mTabFilters[position] = filter; + + b._fastFind = this.fastFind; + b.droppedLinkHandler = handleDroppedLink; + + // If we just created a new tab that loads the default + // newtab url, swap in a preloaded page if possible. + // Do nothing if we're a private window. + let docShellsSwapped = false; + if (aURI == BROWSER_NEW_TAB_URL && + !PrivateBrowsingUtils.isWindowPrivate(window)) { + docShellsSwapped = gBrowserNewTabPreloader.newTab(t); + } + + // Dispatch a new tab notification. We do this once we're + // entirely done, so that things are in a consistent state + // even if the event listener opens or closes tabs. + var evt = document.createEvent("Events"); + evt.initEvent("TabOpen", true, false); + t.dispatchEvent(evt); + + if (aOriginPrincipal && aURI) { + let {URI_INHERITS_SECURITY_CONTEXT} = Ci.nsIProtocolHandler; + // Unless we know for sure we're not inheriting principals, + // force the about:blank viewer to have the right principal: + if (!aURIObject || + (Services.io.getProtocolFlags(aURIObject.scheme) & URI_INHERITS_SECURITY_CONTEXT)) { + b.createAboutBlankContentViewer(aOriginPrincipal); + } + } + + // If we didn't swap docShells with a preloaded browser + // then let's just continue loading the page normally. + if (!docShellsSwapped && !uriIsAboutBlank) { + // pretend the user typed this so it'll be available till + // the document successfully loads + if (aURI && gInitialPages.indexOf(aURI) == -1) + b.userTypedValue = aURI; + + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (aAllowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + if (aFromExternal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL; + try { + b.loadURIWithFlags(aURI, { + flags: flags, + triggeringPrincipal: aTriggeringPrincipal, + referrerURI: aReferrerURI, + referrerPolicy: aReferrerPolicy, + charset: aCharset, + postData: aPostData, + }); + } catch (ex) { + Cu.reportError(ex); + } + } + + // We start our browsers out as inactive, and then maintain + // activeness in the tab switcher. + b.docShellIsActive = false; + + // When addTab() is called with an URL that is not "about:blank" we + // set the "nodefaultsrc" attribute that prevents a frameLoader + // from being created as soon as the linked <browser> is inserted + // into the DOM. We thus have to register the new outerWindowID + // for non-remote browsers after we have called browser.loadURI(). + // + // Note: Only do this of we still have a docShell. The TabOpen + // event was dispatched above and a gBrowser.removeTab() call from + // one of its listeners could cause us to fail here. + if (b.docShell) { + this._outerWindowIDBrowserMap.set(b.outerWindowID, b); + } + + // Check if the user prefers inserting all new tabs after the current tab. + // If not, check if we're opening a tab related to the current tab. + // If either condition is met, move the new tab to after the current tab. + // Note: aReferrerURI is null or undefined if the tab is opened from + // an external application or bookmark, i.e. somewhere other + // than the current tab. + if (Services.prefs.getBoolPref("browser.tabs.insertAllAfterCurrent", false) || + ((aRelatedToCurrent == null ? aReferrerURI : aRelatedToCurrent) && + Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent"))) { + let newTabPos = (this._lastRelatedTab || + this.selectedTab)._tPos + 1; + if (this._lastRelatedTab) + this._lastRelatedTab.owner = null; + else + t.owner = this.selectedTab; + this.moveTabTo(t, newTabPos); + this._lastRelatedTab = t; + } + + if (animate) { + requestAnimationFrame(function () { + this.tabContainer._handleTabTelemetryStart(t, aURI); + + // kick the animation off + t.setAttribute("fadein", "true"); + + // This call to adjustTabstrip is redundant but needed so that + // when opening a second tab, the first tab's close buttons + // appears immediately rather than when the transition ends. + if (this.tabs.length - this._removingTabs.length == 2) + this.tabContainer.adjustTabstrip(); + }.bind(this)); + } + + return t; + ]]> + </body> + </method> + + <method name="warnAboutClosingTabs"> + <parameter name="aCloseTabs"/> + <parameter name="aTab"/> + <body> + <![CDATA[ + var tabsToClose; + switch (aCloseTabs) { + case this.closingTabsEnum.ALL: + // If there are multiple windows, pinned tabs will be closed, so + // we warn about them, too; if there is just one window, pinned + // tabs should come back on restart, so exclude them from warning. + var numberOfWindows = 0; + var browserEnum = Services.wm.getEnumerator("navigator:browser"); + while (browserEnum.hasMoreElements() && numberOfWindows < 2) { + numberOfWindows++; + browserEnum.getNext(); + } + if (numberOfWindows > 1) { + tabsToClose = this.tabs.length - this._removingTabs.length + } + else { + tabsToClose = this.tabs.length - this._removingTabs.length - + gBrowser._numPinnedTabs; + } + break; + case this.closingTabsEnum.OTHER: + tabsToClose = this.visibleTabs.length - 1 - gBrowser._numPinnedTabs; + break; + case this.closingTabsEnum.TO_END: + if (!aTab) + throw new Error("Required argument missing: aTab"); + + tabsToClose = this.getTabsToTheEndFrom(aTab).length; + break; + default: + throw new Error("Invalid argument: " + aCloseTabs); + } + + if (tabsToClose <= 1) + return true; + + const pref = aCloseTabs == this.closingTabsEnum.ALL ? + "browser.tabs.warnOnClose" : "browser.tabs.warnOnCloseOtherTabs"; + var shouldPrompt = Services.prefs.getBoolPref(pref); + if (!shouldPrompt) + return true; + + var ps = Services.prompt; + + // default to true: if it were false, we wouldn't get this far + var warnOnClose = { value: true }; + var bundle = this.mStringBundle; + + // focus the window before prompting. + // this will raise any minimized window, which will + // make it obvious which window the prompt is for and will + // solve the problem of windows "obscuring" the prompt. + // see bug #350299 for more details + window.focus(); + var buttonPressed = + ps.confirmEx(window, + bundle.getString("tabs.closeWarningTitle"), + bundle.getFormattedString("tabs.closeWarningMultipleTabs", + [tabsToClose]), + (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) + + (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1), + bundle.getString("tabs.closeButtonMultiple"), + null, null, + aCloseTabs == this.closingTabsEnum.ALL ? + bundle.getString("tabs.closeWarningPromptMe") : null, + warnOnClose); + var reallyClose = (buttonPressed == 0); + + // don't set the prefs unless they press OK and have unchecked the box + if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value) { + Services.prefs.setBoolPref("browser.tabs.warnOnClose", false); + Services.prefs.setBoolPref("browser.tabs.warnOnCloseOtherTabs", false); + } + return reallyClose; + ]]> + </body> + </method> + + <method name="getTabsToTheEndFrom"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var tabsToEnd = []; + let tabs = this.visibleTabs; + for (let i = tabs.length - 1; tabs[i] != aTab && i >= 0; --i) { + tabsToEnd.push(tabs[i]); + } + return tabsToEnd.reverse(); + ]]> + </body> + </method> + + <method name="removeTabsToTheEndFrom"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (this.warnAboutClosingTabs(this.closingTabsEnum.TO_END, aTab)) { + let tabs = this.getTabsToTheEndFrom(aTab); + for (let i = tabs.length - 1; i >= 0; --i) { + this.removeTab(tabs[i], {animate: true}); + } + } + ]]> + </body> + </method> + + <method name="removeAllTabsBut"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (aTab.pinned) + return; + + if (this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) { + let tabs = this.visibleTabs; + this.selectedTab = aTab; + + for (let i = tabs.length - 1; i >= 0; --i) { + if (tabs[i] != aTab && !tabs[i].pinned) + this.removeTab(tabs[i]); + } + } + ]]> + </body> + </method> + + <method name="removeCurrentTab"> + <parameter name="aParams"/> + <body> + <![CDATA[ + this.removeTab(this.mCurrentTab, aParams); + ]]> + </body> + </method> + + <field name="_removingTabs"> + [] + </field> + + <method name="removeTab"> + <parameter name="aTab"/> + <parameter name="aParams"/> + <body> + <![CDATA[ + if (aParams) { + var animate = aParams.animate; + var byMouse = aParams.byMouse; + } + + // Handle requests for synchronously removing an already + // asynchronously closing tab. + if (!animate && + aTab.closing) { + this._endRemoveTab(aTab); + return; + } + + var isLastTab = (this.tabs.length - this._removingTabs.length == 1); + + if (!this._beginRemoveTab(aTab, false, null, true)) + return; + + if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse) + this.tabContainer._lockTabSizing(aTab); + else + this.tabContainer._unlockTabSizing(); + + if (!animate /* the caller didn't opt in */ || + isLastTab || + aTab.pinned || + aTab.hidden || + this._removingTabs.length > 3 /* don't want lots of concurrent animations */ || + aTab.getAttribute("fadein") != "true" /* fade-in transition hasn't been triggered yet */ || + window.getComputedStyle(aTab).maxWidth == "0.1px" /* fade-in transition hasn't moved yet */ || + !Services.prefs.getBoolPref("browser.tabs.animate")) { + this._endRemoveTab(aTab); + return; + } + + this.tabContainer._handleTabTelemetryStart(aTab); + + this._blurTab(aTab); + aTab.style.maxWidth = ""; // ensure that fade-out transition happens + aTab.removeAttribute("fadein"); + + if (this.tabs.length - this._removingTabs.length == 1) { + // The second tab just got closed and we will end up with a single + // one. Remove the first tab's close button immediately (if needed) + // rather than after the tabclose animation ends. + this.tabContainer.adjustTabstrip(); + } + + setTimeout(function (tab, tabbrowser) { + if (tab.parentNode && + window.getComputedStyle(tab).maxWidth == "0.1px") { + NS_ASSERT(false, "Giving up waiting for the tab closing animation to finish (bug 608589)"); + tabbrowser._endRemoveTab(tab); + } + }, 3000, aTab, this); + ]]> + </body> + </method> + + <!-- Tab close requests are ignored if the window is closing anyway, + e.g. when holding Ctrl+W. --> + <field name="_windowIsClosing"> + false + </field> + + <method name="_beginRemoveTab"> + <parameter name="aTab"/> + <parameter name="aTabWillBeMoved"/> + <parameter name="aCloseWindowWithLastTab"/> + <parameter name="aCloseWindowFastpath"/> + <body> + <![CDATA[ + if (aTab.closing || + aTab._pendingPermitUnload || + this._windowIsClosing) + return false; + + var browser = this.getBrowserForTab(aTab); + + if (!aTabWillBeMoved) { + let ds = browser.docShell; + if (ds && ds.contentViewer) { + // We need to block while calling permitUnload() because it + // processes the event queue and may lead to another removeTab() + // call before permitUnload() even returned. + aTab._pendingPermitUnload = true; + let permitUnload = ds.contentViewer.permitUnload(); + delete aTab._pendingPermitUnload; + + if (!permitUnload) + return false; + } + } + + var closeWindow = false; + var newTab = false; + if (this.tabs.length - this._removingTabs.length == 1) { + closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab : + !window.toolbar.visible || + this.tabContainer._closeWindowWithLastTab; + + // Closing the tab and replacing it with a blank one is notably slower + // than closing the window right away. If the caller opts in, take + // the fast path. + if (closeWindow && + aCloseWindowFastpath && + this._removingTabs.length == 0 && + (this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow))) + return null; + + newTab = true; + } + + aTab.closing = true; + this._removingTabs.push(aTab); + this._visibleTabs = null; // invalidate cache + if (newTab) + this.addTab(BROWSER_NEW_TAB_URL, {skipAnimation: true}); + else + this.tabContainer.updateVisibility(); + + // We're committed to closing the tab now. + // Dispatch a notification. + // We dispatch it before any teardown so that event listeners can + // inspect the tab that's about to close. + var evt = document.createEvent("UIEvent"); + evt.initUIEvent("TabClose", true, false, window, aTabWillBeMoved ? 1 : 0); + aTab.dispatchEvent(evt); + + // Prevent this tab from showing further dialogs, since we're closing it + var windowUtils = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + windowUtils.disableDialogs(); + + // Remove the tab's filter and progress listener. + const filter = this.mTabFilters[aTab._tPos]; + + browser.webProgress.removeProgressListener(filter); + + filter.removeProgressListener(this.mTabListeners[aTab._tPos]); + this.mTabListeners[aTab._tPos].destroy(); + + if (browser.registeredOpenURI && !aTabWillBeMoved) { + this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI); + delete browser.registeredOpenURI; + } + + // We are no longer the primary content area. + browser.setAttribute("type", "content-targetable"); + + // Remove this tab as the owner of any other tabs, since it's going away. + Array.forEach(this.tabs, function (tab) { + if ("owner" in tab && tab.owner == aTab) + // |tab| is a child of the tab we're removing, make it an orphan + tab.owner = null; + }); + + aTab._endRemoveArgs = [closeWindow, newTab]; + return true; + ]]> + </body> + </method> + + <method name="_endRemoveTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (!aTab || !aTab._endRemoveArgs) + return; + + var [aCloseWindow, aNewTab] = aTab._endRemoveArgs; + aTab._endRemoveArgs = null; + + if (this._windowIsClosing) { + aCloseWindow = false; + aNewTab = false; + } + + this._lastRelatedTab = null; + + // update the UI early for responsiveness + aTab.collapsed = true; + this.tabContainer._fillTrailingGap(); + this._blurTab(aTab); + + this._removingTabs.splice(this._removingTabs.indexOf(aTab), 1); + + if (aCloseWindow) { + this._windowIsClosing = true; + while (this._removingTabs.length) + this._endRemoveTab(this._removingTabs[0]); + } else if (!this._windowIsClosing) { + if (aNewTab) + focusAndSelectUrlBar(); + + // workaround for bug 345399 + this.tabContainer.mTabstrip._updateScrollButtonsDisabledState(); + } + + // We're going to remove the tab and the browser now. + // Clean up mTabFilters and mTabListeners now rather than in + // _beginRemoveTab, so that their size is always in sync with the + // number of tabs and browsers (the xbl destructor depends on this). + this.mTabFilters.splice(aTab._tPos, 1); + this.mTabListeners.splice(aTab._tPos, 1); + + var browser = this.getBrowserForTab(aTab); + this._outerWindowIDBrowserMap.delete(browser.outerWindowID); + + // Because of the way XBL works (fields just set JS + // properties on the element) and the code we have in place + // to preserve the JS objects for any elements that have + // JS properties set on them, the browser element won't be + // destroyed until the document goes away. So we force a + // cleanup ourselves. + // This has to happen before we remove the child so that the + // XBL implementation of nsIObserver still works. + browser.destroy(); + + if (browser == this.mCurrentBrowser) + this.mCurrentBrowser = null; + + var wasPinned = aTab.pinned; + + // Invalidate browsers cache, as the tab is removed from the + // tab container. + this._browsers = null; + + // Remove the tab ... + this.tabContainer.removeChild(aTab); + + // ... and fix up the _tPos properties immediately. + for (let i = aTab._tPos; i < this.tabs.length; i++) + this.tabs[i]._tPos = i; + + if (!this._windowIsClosing) { + if (wasPinned) + this.tabContainer._positionPinnedTabs(); + + // update tab close buttons state + this.tabContainer.adjustTabstrip(); + + setTimeout(function(tabs) { + tabs._lastTabClosedByMouse = false; + }, 0, this.tabContainer); + } + + // Pale Moon: if resizing immediately, select the tab immediately to the left + // instead of the right (if not leftmost) to prevent focus swap and + // "selected tab not under cursor" + // FIXME: Tabs must be sliding in from the left for this, or it'd unfocus + // in the other direction! Disabled for now. Is there an easier way? :hover? + // Is this even needed when resizing immediately?... + + //if (Services.prefs.getBoolPref("browser.tabs.resize_immediately")) { + // if (this.selectedTab._tPos > 1) { + // let newPos = this.selectedTab._tPos - 1; + // this.selectedTab = this.tabs[newPos]; + // } + //} + + // update tab positional properties and attributes + this.selectedTab._selected = true; + this.tabContainer._setPositionalAttributes(); + + // Removing the panel requires fixing up selectedPanel immediately + // (see below), which would be hindered by the potentially expensive + // browser removal. So we remove the browser and the panel in two + // steps. + + var panel = this.getNotificationBox(browser); + + // This will unload the document. An unload handler could remove + // dependant tabs, so it's important that the tabbrowser is now in + // a consistent state (tab removed, tab positions updated, etc.). + browser.parentNode.removeChild(browser); + + // Release the browser in case something is erroneously holding a + // reference to the tab after its removal. + this._tabForBrowser.delete(aTab.linkedBrowser); + aTab.linkedBrowser = null; + + // As the browser is removed, the removal of a dependent document can + // cause the whole window to close. So at this point, it's possible + // that the binding is destructed. + if (this.mTabBox) { + let selectedPanel = this.mTabBox.selectedPanel; + + this.mPanelContainer.removeChild(panel); + + // Under the hood, a selectedIndex attribute controls which panel + // is displayed. Removing a panel A which precedes the selected + // panel B makes selectedIndex point to the panel next to B. We + // need to explicitly preserve B as the selected panel. + this.mTabBox.selectedPanel = selectedPanel; + } + + if (aCloseWindow) + this._windowIsClosing = closeWindow(true, window.warnAboutClosingWindow); + ]]> + </body> + </method> + + <method name="_blurTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (!aTab.selected) + return; + + if (aTab.owner && + !aTab.owner.hidden && + !aTab.owner.closing && + Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) { + this.selectedTab = aTab.owner; + return; + } + + // Switch to a visible tab unless there aren't any others remaining + let remainingTabs = this.visibleTabs; + let numTabs = remainingTabs.length; + if (numTabs == 0 || numTabs == 1 && remainingTabs[0] == aTab) { + remainingTabs = Array.filter(this.tabs, function(tab) { + return !tab.closing; + }, this); + } + + // Try to find a remaining tab that comes after the given tab + var tab = aTab; + do { + tab = tab.nextSibling; + } while (tab && remainingTabs.indexOf(tab) == -1); + + if (!tab) { + tab = aTab; + + do { + tab = tab.previousSibling; + } while (tab && remainingTabs.indexOf(tab) == -1); + } + + this.selectedTab = tab; + ]]> + </body> + </method> + + <method name="swapNewTabWithBrowser"> + <parameter name="aNewTab"/> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + // The browser must be standalone. + if (aBrowser.getTabBrowser()) + throw Cr.NS_ERROR_INVALID_ARG; + + // The tab is definitely not loading. + aNewTab.removeAttribute("busy"); + if (aNewTab.selected) { + this.mIsBusy = false; + } + + this._swapBrowserDocShells(aNewTab, aBrowser); + + // Update the new tab's title. + this.setTabTitle(aNewTab); + + if (aNewTab.selected) { + this.updateCurrentBrowser(true); + } + ]]> + </body> + </method> + + <method name="swapBrowsersAndCloseOther"> + <parameter name="aOurTab"/> + <parameter name="aOtherTab"/> + <body> + <![CDATA[ + // Do not allow transfering a private tab to a non-private window + // and vice versa. + if (PrivateBrowsingUtils.isWindowPrivate(window) != + PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerDocument.defaultView)) + return; + + // That's gBrowser for the other window, not the tab's browser! + var remoteBrowser = aOtherTab.ownerDocument.defaultView.gBrowser; + var isPending = aOtherTab.hasAttribute("pending"); + + // Expedite the removal of the icon if it was already scheduled. + if (aOtherTab._soundPlayingAttrRemovalTimer) { + clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer); + aOtherTab._soundPlayingAttrRemovalTimer = 0; + aOtherTab.removeAttribute("soundplaying"); + remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]); + } + + // First, start teardown of the other browser. Make sure to not + // fire the beforeunload event in the process. Close the other + // window if this was its last tab. + if (!remoteBrowser._beginRemoveTab(aOtherTab, true, true)) + return; + + let ourBrowser = this.getBrowserForTab(aOurTab); + let otherBrowser = aOtherTab.linkedBrowser; + + let modifiedAttrs = []; + if (aOtherTab.hasAttribute("muted")) { + aOurTab.setAttribute("muted", "true"); + aOurTab.muteReason = aOtherTab.muteReason; + ourBrowser.mute(); + modifiedAttrs.push("muted"); + } + if (aOtherTab.hasAttribute("soundplaying")) { + aOurTab.setAttribute("soundplaying", "true"); + modifiedAttrs.push("soundplaying"); + } + + // If the other tab is pending (i.e. has not been restored, yet) + // then do not switch docShells but retrieve the other tab's state + // and apply it to our tab. + if (isPending) { + let ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore) + ss.setTabState(aOurTab, ss.getTabState(aOtherTab)); + + // Make sure to unregister any open URIs. + this._swapRegisteredOpenURIs(ourBrowser, otherBrowser); + } else { + // Workarounds for bug 458697 + // Icon might have been set on DOMLinkAdded, don't override that. + if (!ourBrowser.mIconURL && otherBrowser.mIconURL) + this.setIcon(aOurTab, otherBrowser.mIconURL, otherBrowser.contentPrincipal); + var isBusy = aOtherTab.hasAttribute("busy"); + if (isBusy) { + aOurTab.setAttribute("busy", "true"); + modifiedAttrs.push("busy"); + if (aOurTab.selected) + this.mIsBusy = true; + } + + this._swapBrowserDocShells(aOurTab, otherBrowser); + } + + // Finish tearing down the tab that's going away. + remoteBrowser._endRemoveTab(aOtherTab); + + if (isBusy) + this.setTabTitleLoading(aOurTab); + else + this.setTabTitle(aOurTab); + + // If the tab was already selected (this happpens in the scenario + // of replaceTabWithWindow), notify onLocationChange, etc. + if (aOurTab.selected) + this.updateCurrentBrowser(true); + + if (modifiedAttrs.length) { + this._tabAttrModified(aOurTab, modifiedAttrs); + } + ]]> + </body> + </method> + + <method name="_swapBrowserDocShells"> + <parameter name="aOurTab"/> + <parameter name="aOtherBrowser"/> + <body> + <![CDATA[ + // Unhook our progress listener + let index = aOurTab._tPos; + const filter = this.mTabFilters[index]; + let tabListener = this.mTabListeners[index]; + let ourBrowser = this.getBrowserForTab(aOurTab); + ourBrowser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(tabListener); + + // Make sure to unregister any open URIs. + this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser); + + // Unmap old outerWindowIDs. + this._outerWindowIDBrowserMap.delete(ourBrowser.outerWindowID); + let remoteBrowser = aOtherBrowser.ownerDocument.defaultView.gBrowser; + if (remoteBrowser) { + remoteBrowser._outerWindowIDBrowserMap.delete(aOtherBrowser.outerWindowID); + } + // Swap the docshells + ourBrowser.swapDocShells(aOtherBrowser); + + // Register new outerWindowIDs. + this._outerWindowIDBrowserMap.set(ourBrowser.outerWindowID, ourBrowser); + if (remoteBrowser) { + remoteBrowser._outerWindowIDBrowserMap.set(aOtherBrowser.outerWindowID, aOtherBrowser); + } + // Restore the progress listener + this.mTabListeners[index] = tabListener = + this.mTabProgressListener(aOurTab, ourBrowser, false); + + const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL; + filter.addProgressListener(tabListener, notifyAll); + ourBrowser.webProgress.addProgressListener(filter, notifyAll); + ]]> + </body> + </method> + + <method name="_swapRegisteredOpenURIs"> + <parameter name="aOurBrowser"/> + <parameter name="aOtherBrowser"/> + <body> + <![CDATA[ + // If the current URI is registered as open remove it from the list. + if (aOurBrowser.registeredOpenURI) { + this._placesAutocomplete.unregisterOpenPage(aOurBrowser.registeredOpenURI); + delete aOurBrowser.registeredOpenURI; + } + + // If the other/new URI is registered as open then copy it over. + if (aOtherBrowser.registeredOpenURI) { + aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI; + delete aOtherBrowser.registeredOpenURI; + } + ]]> + </body> + </method> + + <method name="reloadAllTabs"> + <body> + <![CDATA[ + let tabs = this.visibleTabs; + let l = tabs.length; + for (var i = 0; i < l; i++) { + try { + this.getBrowserForTab(tabs[i]).reload(); + } catch (e) { + // ignore failure to reload so others will be reloaded + } + } + ]]> + </body> + </method> + + <method name="reloadTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + let browser = this.getBrowserForTab(aTab); + // Reset DOS mitigation for basic auth prompt + delete browser.authPromptCounter; + browser.reload(); + ]]> + </body> + </method> + + <method name="addProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (arguments.length != 1) { + Components.utils.reportError("gBrowser.addProgressListener was " + + "called with a second argument, " + + "which is not supported. See bug " + + "608628."); + } + + this.mProgressListeners.push(aListener); + ]]> + </body> + </method> + + <method name="removeProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + this.mProgressListeners = + this.mProgressListeners.filter(function (l) l != aListener); + ]]> + </body> + </method> + + <method name="addTabsProgressListener"> + <parameter name="aListener"/> + <body> + this.mTabsProgressListeners.push(aListener); + </body> + </method> + + <method name="removeTabsProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + this.mTabsProgressListeners = + this.mTabsProgressListeners.filter(function (l) l != aListener); + ]]> + </body> + </method> + + <method name="getBrowserForTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + return aTab.linkedBrowser; + ]]> + </body> + </method> + + <method name="showOnlyTheseTabs"> + <parameter name="aTabs"/> + <body> + <![CDATA[ + Array.forEach(this.tabs, function(tab) { + if (aTabs.indexOf(tab) == -1) + this.hideTab(tab); + else + this.showTab(tab); + }, this); + + this.tabContainer._handleTabSelect(false); + ]]> + </body> + </method> + + <method name="showTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (aTab.hidden) { + aTab.removeAttribute("hidden"); + this._visibleTabs = null; // invalidate cache + + this.tabContainer.adjustTabstrip(); + + this.tabContainer._setPositionalAttributes(); + + let event = document.createEvent("Events"); + event.initEvent("TabShow", true, false); + aTab.dispatchEvent(event); + } + ]]> + </body> + </method> + + <method name="hideTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (!aTab.hidden && !aTab.pinned && !aTab.selected && + !aTab.closing) { + aTab.setAttribute("hidden", "true"); + this._visibleTabs = null; // invalidate cache + + this.tabContainer.adjustTabstrip(); + + this.tabContainer._setPositionalAttributes(); + + let event = document.createEvent("Events"); + event.initEvent("TabHide", true, false); + aTab.dispatchEvent(event); + } + ]]> + </body> + </method> + + <method name="selectTabAtIndex"> + <parameter name="aIndex"/> + <parameter name="aEvent"/> + <body> + <![CDATA[ + let tabs = this.visibleTabs; + + // count backwards for aIndex < 0 + if (aIndex < 0) + aIndex += tabs.length; + + if (aIndex >= 0 && aIndex < tabs.length) + this.selectedTab = tabs[aIndex]; + + if (aEvent) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + } + ]]> + </body> + </method> + + <property name="selectedTab"> + <getter> + return this.mCurrentTab; + </getter> + <setter> + <![CDATA[ + if (gNavToolbox.collapsed) { + return this.mTabBox.selectedTab; + } + // Update the tab + this.mTabBox.selectedTab = val; + return val; + ]]> + </setter> + </property> + + <property name="selectedBrowser" + onget="return this.mCurrentBrowser;" + readonly="true"/> + + <property name="browsers" readonly="true"> + <getter> + <![CDATA[ + return this._browsers || + (this._browsers = Array.map(this.tabs, function (tab) tab.linkedBrowser)); + ]]> + </getter> + </property> + <field name="_browsers">null</field> + + <!-- Moves a tab to a new browser window, unless it's already the only tab + in the current window, in which case this will do nothing. --> + <method name="replaceTabWithWindow"> + <parameter name="aTab"/> + <parameter name="aOptions"/> + <body> + <![CDATA[ + if (this.tabs.length == 1) + return null; + + var options = "chrome,dialog=no,all"; + for (var name in aOptions) + options += "," + name + "=" + aOptions[name]; + + // tell a new window to take the "dropped" tab + return window.openDialog(getBrowserURL(), "_blank", options, aTab); + ]]> + </body> + </method> + + <method name="moveTabTo"> + <parameter name="aTab"/> + <parameter name="aIndex"/> + <body> + <![CDATA[ + var oldPosition = aTab._tPos; + if (oldPosition == aIndex) + return; + + // Don't allow mixing pinned and unpinned tabs. + if (aTab.pinned) + aIndex = Math.min(aIndex, this._numPinnedTabs - 1); + else + aIndex = Math.max(aIndex, this._numPinnedTabs); + if (oldPosition == aIndex) + return; + + this._lastRelatedTab = null; + + this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(aTab._tPos, 1)[0]); + this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(aTab._tPos, 1)[0]); + + let wasFocused = (document.activeElement == this.mCurrentTab); + + aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1; + this.mCurrentTab._selected = false; + + // invalidate caches + this._browsers = null; + this._visibleTabs = null; + + // use .item() instead of [] because dragging to the end of the strip goes out of + // bounds: .item() returns null (so it acts like appendChild), but [] throws + this.tabContainer.insertBefore(aTab, this.tabs.item(aIndex)); + + for (let i = 0; i < this.tabs.length; i++) { + this.tabs[i]._tPos = i; + this.tabs[i]._selected = false; + } + this.mCurrentTab._selected = true; + + if (wasFocused) + this.mCurrentTab.focus(); + + this.tabContainer._handleTabSelect(false); + + if (aTab.pinned) + this.tabContainer._positionPinnedTabs(); + + this.tabContainer._setPositionalAttributes(); + + var evt = document.createEvent("UIEvents"); + evt.initUIEvent("TabMove", true, false, window, oldPosition); + aTab.dispatchEvent(evt); + ]]> + </body> + </method> + + <method name="moveTabForward"> + <body> + <![CDATA[ + let nextTab = this.mCurrentTab.nextSibling; + while (nextTab && nextTab.hidden) + nextTab = nextTab.nextSibling; + + if (nextTab) + this.moveTabTo(this.mCurrentTab, nextTab._tPos); + else if (this.arrowKeysShouldWrap) + this.moveTabToStart(); + ]]> + </body> + </method> + + <method name="moveTabBackward"> + <body> + <![CDATA[ + let previousTab = this.mCurrentTab.previousSibling; + while (previousTab && previousTab.hidden) + previousTab = previousTab.previousSibling; + + if (previousTab) + this.moveTabTo(this.mCurrentTab, previousTab._tPos); + else if (this.arrowKeysShouldWrap) + this.moveTabToEnd(); + ]]> + </body> + </method> + + <method name="moveTabToStart"> + <body> + <![CDATA[ + var tabPos = this.mCurrentTab._tPos; + if (tabPos > 0) + this.moveTabTo(this.mCurrentTab, 0); + ]]> + </body> + </method> + + <method name="moveTabToEnd"> + <body> + <![CDATA[ + var tabPos = this.mCurrentTab._tPos; + if (tabPos < this.browsers.length - 1) + this.moveTabTo(this.mCurrentTab, this.browsers.length - 1); + ]]> + </body> + </method> + + <method name="moveTabOver"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var direction = window.getComputedStyle(this.parentNode, null).direction; + if ((direction == "ltr" && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) || + (direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT)) + this.moveTabForward(); + else + this.moveTabBackward(); + ]]> + </body> + </method> + + <method name="duplicateTab"> + <parameter name="aTab"/><!-- can be from a different window as well --> + <body> + <![CDATA[ + return Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore) + .duplicateTab(window, aTab); + ]]> + </body> + </method> + + <!-- BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT + MAKE SURE TO ADD IT HERE AS WELL. --> + <property name="canGoBack" + onget="return this.mCurrentBrowser.canGoBack;" + readonly="true"/> + + <property name="canGoForward" + onget="return this.mCurrentBrowser.canGoForward;" + readonly="true"/> + + <method name="goBack"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goBack(); + ]]> + </body> + </method> + + <method name="goForward"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goForward(); + ]]> + </body> + </method> + + <method name="reload"> + <body> + <![CDATA[ + return this.mCurrentBrowser.reload(); + ]]> + </body> + </method> + + <method name="reloadWithFlags"> + <parameter name="aFlags"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.reloadWithFlags(aFlags); + ]]> + </body> + </method> + + <method name="stop"> + <body> + <![CDATA[ + return this.mCurrentBrowser.stop(); + ]]> + </body> + </method> + + <!-- throws exception for unknown schemes --> + <method name="loadURI"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.loadURI(aURI, 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[ + // Note - the callee understands both: + // (a) loadURIWithFlags(aURI, aFlags, ...) + // (b) loadURIWithFlags(aURI, { flags: aFlags, ... }) + // Forwarding it as (a) here actually supports both (a) and (b), + // so you can call us either way too. + return this.mCurrentBrowser.loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData); + ]]> + </body> + </method> + + <method name="goHome"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goHome(); + ]]> + </body> + </method> + + <property name="homePage"> + <getter> + <![CDATA[ + return this.mCurrentBrowser.homePage; + ]]> + </getter> + <setter> + <![CDATA[ + this.mCurrentBrowser.homePage = val; + return val; + ]]> + </setter> + </property> + + <method name="gotoIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.gotoIndex(aIndex); + ]]> + </body> + </method> + + <method name="attachFormFill"> + <body><![CDATA[ + for (var i = 0; i < this.mPanelContainer.childNodes.length; ++i) { + var cb = this.getBrowserAtIndex(i); + cb.attachFormFill(); + } + ]]></body> + </method> + + <method name="detachFormFill"> + <body><![CDATA[ + for (var i = 0; i < this.mPanelContainer.childNodes.length; ++i) { + var cb = this.getBrowserAtIndex(i); + cb.detachFormFill(); + } + ]]></body> + </method> + + <property name="currentURI" + onget="return this.mCurrentBrowser.currentURI;" + readonly="true"/> + + <field name="_fastFind">null</field> + <property name="fastFind" + readonly="true"> + <getter> + <![CDATA[ + if (!this._fastFind) { + 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> + <field name="_lastSearchHighlight">false</field> + + <field name="finder"><![CDATA[ + ({ + mTabBrowser: this, + mListeners: new Set(), + get finder() { + return this.mTabBrowser.mCurrentBrowser.finder; + }, + destroy: function() { + this.finder.destroy(); + }, + addResultListener: function(aListener) { + this.mListeners.add(aListener); + this.finder.addResultListener(aListener); + }, + removeResultListener: function(aListener) { + this.mListeners.delete(aListener); + this.finder.removeResultListener(aListener); + }, + get searchString() { + return this.finder.searchString; + }, + get clipboardSearchString() { + return this.finder.clipboardSearchString; + }, + set clipboardSearchString(val) { + return this.finder.clipboardSearchString = val; + }, + set caseSensitive(val) { + return this.finder.caseSensitive = val; + }, + set entireWord(val) { + return this.finder.entireWord = val; + }, + get highlighter() { + return this.finder.highlighter; + }, + get matchesCountLimit() { + return this.finder.matchesCountLimit; + }, + fastFind: function(...args) { + this.finder.fastFind(...args); + }, + findAgain: function(...args) { + this.finder.findAgain(...args); + }, + setSearchStringToSelection: function() { + return this.finder.setSearchStringToSelection(); + }, + highlight: function(...args) { + this.finder.highlight(...args); + }, + getInitialSelection: function() { + this.finder.getInitialSelection(); + }, + getActiveSelectionText: function() { + return this.finder.getActiveSelectionText(); + }, + enableSelection: function() { + this.finder.enableSelection(); + }, + removeSelection: function() { + this.finder.removeSelection(); + }, + focusContent: function() { + this.finder.focusContent(); + }, + onFindbarClose: function() { + this.finder.onFindbarClose(); + }, + onFindbarOpen: function() { + this.finder.onFindbarOpen(); + }, + onModalHighlightChange: function(...args) { + return this.finder.onModalHighlightChange(...args); + }, + onHighlightAllChange: function(...args) { + return this.finder.onHighlightAllChange(...args); + }, + keyPress: function(aEvent) { + this.finder.keyPress(aEvent); + }, + requestMatchesCount: function(...args) { + this.finder.requestMatchesCount(...args); + }, + onIteratorRangeFound: function(...args) { + this.finder.onIteratorRangeFound(...args); + }, + onIteratorReset: function() { + this.finder.onIteratorReset(); + }, + onIteratorRestart: function(...args) { + this.finder.onIteratorRestart(...args); + }, + onIteratorStart: function(...args) { + this.finder.onIteratorStart(...args); + }, + onLocationChange: function(...args) { + this.finder.onLocationChange(...args); + } + }) + ]]></field> + + <property name="docShell" + onget="return this.mCurrentBrowser.docShell" + readonly="true"/> + + <property name="webNavigation" + onget="return this.mCurrentBrowser.webNavigation" + readonly="true"/> + + <property name="webBrowserFind" + readonly="true" + onget="return this.mCurrentBrowser.webBrowserFind"/> + + <property name="webProgress" + readonly="true" + onget="return this.mCurrentBrowser.webProgress"/> + + <property name="contentWindow" + readonly="true" + onget="return this.mCurrentBrowser.contentWindow"/> + + <property name="contentWindowAsCPOW" + readonly="true" + onget="return this.mCurrentBrowser.contentWindow;"/> + + <property name="sessionHistory" + onget="return this.mCurrentBrowser.sessionHistory;" + readonly="true"/> + + <property name="markupDocumentViewer" + onget="return this.mCurrentBrowser.markupDocumentViewer;" + readonly="true"/> + + <property name="contentViewerEdit" + onget="return this.mCurrentBrowser.contentViewerEdit;" + readonly="true"/> + + <property name="contentViewerFile" + onget="return this.mCurrentBrowser.contentViewerFile;" + readonly="true"/> + + <property name="contentDocument" + onget="return this.mCurrentBrowser.contentDocument;" + readonly="true"/> + + <property name="contentDocumentAsCPOW" + onget="return this.mCurrentBrowser.contentDocument;" + readonly="true"/> + + <property name="contentTitle" + onget="return this.mCurrentBrowser.contentTitle;" + readonly="true"/> + + <property name="contentPrincipal" + onget="return this.mCurrentBrowser.contentPrincipal;" + readonly="true"/> + + <property name="securityUI" + onget="return this.mCurrentBrowser.securityUI;" + readonly="true"/> + + <property name="fullZoom" + onget="return this.mCurrentBrowser.fullZoom;" + onset="this.mCurrentBrowser.fullZoom = val;"/> + + <property name="textZoom" + onget="return this.mCurrentBrowser.textZoom;" + onset="this.mCurrentBrowser.textZoom = val;"/> + + <property name="isSyntheticDocument" + onget="return this.mCurrentBrowser.isSyntheticDocument;" + readonly="true"/> + + <method name="_handleKeyEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!aEvent.isTrusted) { + // Don't let untrusted events mess with tabs. + return; + } + + if (aEvent.altKey) + return; + + if (aEvent.ctrlKey && aEvent.shiftKey && !aEvent.metaKey) { + switch (aEvent.keyCode) { + case aEvent.DOM_VK_PAGE_UP: + this.moveTabBackward(); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + case aEvent.DOM_VK_PAGE_DOWN: + this.moveTabForward(); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + } + } + + if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey && + aEvent.keyCode == KeyEvent.DOM_VK_F4 && + !this.mCurrentTab.pinned) { + this.removeCurrentTab({animate: true}); + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + ]]></body> + </method> + + <property name="userTypedValue" + onget="return this.mCurrentBrowser.userTypedValue;" + onset="return this.mCurrentBrowser.userTypedValue = val;"/> + + <method name="createTooltip"> + <parameter name="event"/> + <body><![CDATA[ + event.stopPropagation(); + var tab = document.tooltipNode; + if (tab.localName != "tab") { + event.preventDefault(); + return; + } + + var stringID, label; + if (tab.mOverCloseButton) { + stringID = "tabs.closeTab"; + } else if (tab._overPlayingIcon) { + if (tab.linkedBrowser.audioBlocked) { + stringID = "tabs.unblockAudio.tooltip"; + } else { + stringID = tab.linkedBrowser.audioMuted ? + "tabs.unmuteAudio.tooltip" : + "tabs.muteAudio.tooltip"; + } + } else { + label = tab.getAttribute("label"); + } + if (stringID && !label) { + label = this.mStringBundle.getString(stringID); + } + event.target.setAttribute("label", label); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "keypress": + this._handleKeyEvent(aEvent); + break; + case "sizemodechange": + if (aEvent.target == window) { + this.mCurrentBrowser.docShellIsActive = + (window.windowState != window.STATE_MINIMIZED); + } + break; + } + ]]></body> + </method> + + <method name="receiveMessage"> + <parameter name="aMessage"/> + <body><![CDATA[ + let json = aMessage.json; + let browser = aMessage.target; + + switch (aMessage.name) { + case "DOMTitleChanged": { + let tab = this.getTabForBrowser(browser); + if (!tab) + return; + let titleChanged = this.setTabTitle(tab); + if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) + tab.setAttribute("titlechanged", "true"); + break; + } + case "DOMWebNotificationClicked": { + let tab = this.getTabForBrowser(browser); + if (!tab) + return; + this.selectedTab = tab; + window.focus(); + break; + } + } + ]]></body> + </method> + + <constructor> + <![CDATA[ + let browserStack = document.getAnonymousElementByAttribute(this, "anonid", "browserStack"); + this.mCurrentBrowser = document.getAnonymousElementByAttribute(this, "anonid", "initialBrowser"); + + this.mCurrentTab = this.tabContainer.firstChild; + document.addEventListener("keypress", this, false); + window.addEventListener("sizemodechange", this, false); + + var uniqueId = "panel" + Date.now(); + this.mPanelContainer.childNodes[0].id = uniqueId; + this.mCurrentTab.linkedPanel = uniqueId; + this.mCurrentTab._tPos = 0; + this.mCurrentTab._fullyOpen = true; + this.mCurrentTab.linkedBrowser = this.mCurrentBrowser; + this._tabForBrowser.set(this.mCurrentBrowser, this.mCurrentTab); + + // set up the shared autoscroll popup + this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup(); + this._autoScrollPopup.id = "autoscroller"; + this.appendChild(this._autoScrollPopup); + this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id); + this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink; + this.updateWindowResizers(); + + // Hook up the event listeners to the first browser + var tabListener = this.mTabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true); + const nsIWebProgress = Components.interfaces.nsIWebProgress; + const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(nsIWebProgress); + filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL); + this.mTabListeners[0] = tabListener; + this.mTabFilters[0] = filter; + + try { + // We assume this can only fail because mCurrentBrowser's docShell + // hasn't been created, yet. This may be caused by code accessing + // gBrowser before the window has finished loading. + this._addProgressListenerForInitialTab(); + } catch (e) { + // The binding was constructed too early, wait until the initial + // tab's document is ready, then add the progress listener. + this._waitForInitialContentDocument(); + } + + this.style.backgroundColor = + Services.prefs.getBoolPref("browser.display.use_system_colors") ? + "-moz-default-background-color" : + Services.prefs.getCharPref("browser.display.background_color"); + + this._outerWindowIDBrowserMap.set(this.mCurrentBrowser.outerWindowID, + this.mCurrentBrowser); + + messageManager.addMessageListener("DOMWebNotificationClicked", this); + ]]> + </constructor> + + <method name="_addProgressListenerForInitialTab"> + <body><![CDATA[ + this.webProgress.addProgressListener(this.mTabFilters[0], Ci.nsIWebProgress.NOTIFY_ALL); + ]]></body> + </method> + + <method name="_waitForInitialContentDocument"> + <body><![CDATA[ + let obs = (subject, topic) => { + if (this.browsers[0].contentWindow == subject) { + Services.obs.removeObserver(obs, topic); + this._addProgressListenerForInitialTab(); + } + }; + + // We use content-document-global-created as an approximation for + // "docShell is initialized". We can do this because in the + // mTabProgressListener we care most about the STATE_STOP notification + // that will reset mBlank. That means it's important to at least add + // the progress listener before the initial about:blank load stops + // if we can't do it before the load starts. + Services.obs.addObserver(obs, "content-document-global-created", false); + ]]></body> + </method> + + <destructor> + <![CDATA[ + for (var i = 0; i < this.mTabListeners.length; ++i) { + let browser = this.getBrowserAtIndex(i); + if (browser.registeredOpenURI) { + this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI); + delete browser.registeredOpenURI; + } + browser.webProgress.removeProgressListener(this.mTabFilters[i]); + this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]); + this.mTabFilters[i] = null; + this.mTabListeners[i].destroy(); + this.mTabListeners[i] = null; + } + document.removeEventListener("keypress", this, false); + window.removeEventListener("sizemodechange", this, false); + ]]> + </destructor> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <method name="enterTabbedMode"> + <body> + console.log("enterTabbedMode is an obsolete method and " + + "will be removed in a future release."); + </body> + </method> + <field name="mTabbedMode" readonly="true">true</field> + <method name="setStripVisibilityTo"> + <parameter name="aShow"/> + <body> + this.tabContainer.visible = aShow; + </body> + </method> + <method name="getStripVisibility"> + <body> + return this.tabContainer.visible; + </body> + </method> + <property name="mContextTab" readonly="true" + onget="return TabContextMenu.contextTab;"/> + <property name="mPrefs" readonly="true" + onget="return Services.prefs;"/> + <property name="mTabContainer" readonly="true" + onget="return this.tabContainer;"/> + <property name="mTabs" readonly="true" + onget="return this.tabs;"/> + <!-- + - Compatibility hack: several extensions depend on this property to + - access the tab context menu or tab container, so keep that working for + - now. Ideally we can remove this once extensions are using + - tabbrowser.tabContextMenu and tabbrowser.tabContainer directly. + --> + <property name="mStrip" readonly="true"> + <getter> + <![CDATA[ + return ({ + self: this, + childNodes: [null, this.tabContextMenu, this.tabContainer], + firstChild: { nextSibling: this.tabContextMenu }, + getElementsByAttribute: function (attr, attrValue) { + if (attr == "anonid" && attrValue == "tabContextMenu") + return [this.self.tabContextMenu]; + return []; + }, + // Also support adding event listeners (forward to the tab container) + addEventListener: function (a,b,c) { this.self.tabContainer.addEventListener(a,b,c); }, + removeEventListener: function (a,b,c) { this.self.tabContainer.removeEventListener(a,b,c); } + }); + ]]> + </getter> + </property> + <field name="_soundPlayingAttrRemovalTimer">0</field> + </implementation> + + <handlers> + <handler event="DOMWindowClose" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + if (this.tabs.length == 1) + return; + + var tab = this._getTabForContentWindow(event.target); + if (tab) { + this.removeTab(tab); + event.preventDefault(); + } + ]]> + </handler> + <handler event="DOMWillOpenModalDialog" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + // We're about to open a modal dialog, make sure the opening + // tab is brought to the front. + this.selectedTab = this._getTabForContentWindow(event.target.top); + ]]> + </handler> + <handler event="DOMTitleChanged"> + <![CDATA[ + if (!event.isTrusted) + return; + + var contentWin = event.target.defaultView; + if (contentWin != contentWin.top) + return; + + var tab = this._getTabForContentWindow(contentWin); + if (!tab) { + return; + } + + var titleChanged = this.setTabTitle(tab); + if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) + tab.setAttribute("titlechanged", "true"); + ]]> + </handler> + <handler event="DOMAudioPlaybackStarted"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + clearTimeout(tab._soundPlayingAttrRemovalTimer); + tab._soundPlayingAttrRemovalTimer = 0; + + let modifiedAttrs = []; + if (tab.hasAttribute("soundplaying-scheduledremoval")) { + tab.removeAttribute("soundplaying-scheduledremoval"); + modifiedAttrs.push("soundplaying-scheduledremoval"); + } + + if (!tab.hasAttribute("soundplaying")) { + tab.setAttribute("soundplaying", true); + modifiedAttrs.push("soundplaying"); + } + + this._tabAttrModified(tab, modifiedAttrs); + ]]> + </handler> + <handler event="DOMAudioPlaybackStopped"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (tab.hasAttribute("soundplaying")) { + let removalDelay = Services.prefs.getIntPref("browser.tabs.delayHidingAudioPlayingIconMS"); + + tab.style.setProperty("--soundplaying-removal-delay", `${removalDelay - 300}ms`); + tab.setAttribute("soundplaying-scheduledremoval", "true"); + this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]); + + tab._soundPlayingAttrRemovalTimer = setTimeout(() => { + tab.removeAttribute("soundplaying-scheduledremoval"); + tab.removeAttribute("soundplaying"); + this._tabAttrModified(tab, ["soundplaying", "soundplaying-scheduledremoval"]); + }, removalDelay); + } + ]]> + </handler> + <handler event="DOMAudioPlaybackBlockStarted"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (!tab.hasAttribute("blocked")) { + tab.setAttribute("blocked", true); + this._tabAttrModified(tab, ["blocked"]); + } + ]]> + </handler> + <handler event="DOMAudioPlaybackBlockStopped"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (tab.hasAttribute("blocked")) { + tab.removeAttribute("blocked"); + this._tabAttrModified(tab, ["blocked"]); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-tabbox" + extends="chrome://global/content/bindings/tabbox.xml#tabbox"> + <implementation> + <property name="tabs" readonly="true" + onget="return document.getBindingParent(this).tabContainer;"/> + </implementation> + </binding> + + <binding id="tabbrowser-arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll"> + <implementation> + <!-- Override scrollbox.xml method, since our scrollbox's children are + inherited from the binding parent --> + <method name="_getScrollableElements"> + <body><![CDATA[ + return Array.filter(document.getBindingParent(this).childNodes, + this._canScrollToElement, this); + ]]></body> + </method> + <method name="_canScrollToElement"> + <parameter name="tab"/> + <body><![CDATA[ + return !tab.pinned && !tab.hidden; + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="underflow" phase="capturing"><![CDATA[ + if (event.detail == 0) + return; // Ignore vertical events + + var tabs = document.getBindingParent(this); + tabs.removeAttribute("overflow"); + + if (tabs._lastTabClosedByMouse) + tabs._expandSpacerBy(this._scrollButtonDown.clientWidth); + + tabs.tabbrowser._removingTabs.forEach(tabs.tabbrowser.removeTab, + tabs.tabbrowser); + + tabs._positionPinnedTabs(); + ]]></handler> + <handler event="overflow"><![CDATA[ + if (event.detail == 0) + return; // Ignore vertical events + + var tabs = document.getBindingParent(this); + tabs.setAttribute("overflow", "true"); + tabs._positionPinnedTabs(); + tabs._handleTabSelect(false); + ]]></handler> + </handlers> + </binding> + + <binding id="tabbrowser-tabs" + extends="chrome://global/content/bindings/tabbox.xml#tabs"> + <resources> + <stylesheet src="chrome://browser/content/tabbrowser.css"/> + </resources> + + <content> + <xul:hbox align="end"> + <xul:image class="tab-drop-indicator" anonid="tab-drop-indicator" collapsed="true"/> + </xul:hbox> + <xul:arrowscrollbox anonid="arrowscrollbox" orient="horizontal" flex="1" + style="min-width: 1px;" + clicktoscroll="true" + class="tabbrowser-arrowscrollbox"> +# This is a hack to circumvent bug 472020, otherwise the tabs show up on the +# right of the newtab button. + <children includes="tab"/> +# This is to ensure anything extensions put here will go before the newtab +# button, necessary due to the previous hack. + <children/> + <xul:toolbarbutton class="tabs-newtab-button" + command="cmd_newNavigatorTab" + onclick="checkForMiddleClick(this, event);" + onmouseover="document.getBindingParent(this)._enterNewTab();" + onmouseout="document.getBindingParent(this)._leaveNewTab();" + tooltiptext="&newTabButton.tooltip;"/> + <xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer" + style="width: 0;"/> + </xul:arrowscrollbox> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor> + <![CDATA[ + this.mTabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth"); + this.mCloseButtons = Services.prefs.getIntPref("browser.tabs.closeButtons"); + this._closeWindowWithLastTab = Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab"); + + var tab = this.firstChild; + tab.setAttribute("label", + this.tabbrowser.mStringBundle.getString("tabs.emptyTabTitle")); + tab.setAttribute("crop", "end"); + tab.setAttribute("onerror", "this.removeAttribute('image');"); + this.adjustTabstrip(); + + Services.prefs.addObserver("browser.tabs.", this._prefObserver, false); + window.addEventListener("resize", this, false); + window.addEventListener("load", this, false); + + this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled", false); + this._browserNewtabpageEnabled = Services.prefs.getBoolPref("browser.newtabpage.enabled"); + ]]> + </constructor> + + <destructor> + <![CDATA[ + Services.prefs.removeObserver("browser.tabs.", this._prefObserver); + ]]> + </destructor> + + <field name="tabbrowser" readonly="true"> + document.getElementById(this.getAttribute("tabbrowser")); + </field> + + <field name="tabbox" readonly="true"> + this.tabbrowser.mTabBox; + </field> + + <field name="contextMenu" readonly="true"> + document.getElementById("tabContextMenu"); + </field> + + <field name="mTabstripWidth">0</field> + + <field name="mTabstrip"> + document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox"); + </field> + + <field name="_firstTab">null</field> + <field name="_lastTab">null</field> + <field name="_afterSelectedTab">null</field> + <field name="_beforeHoveredTab">null</field> + <field name="_afterHoveredTab">null</field> + + <method name="_setPositionalAttributes"> + <body><![CDATA[ + let visibleTabs = this.tabbrowser.visibleTabs; + + if (!visibleTabs.length) + return; + + let selectedIndex = visibleTabs.indexOf(this.selectedItem); + + let lastVisible = visibleTabs.length - 1; + + if (this._afterSelectedTab) + this._afterSelectedTab.removeAttribute("afterselected-visible"); + if (this.selectedItem.closing || selectedIndex == lastVisible) { + this._afterSelectedTab = null; + } else { + this._afterSelectedTab = visibleTabs[selectedIndex + 1]; + this._afterSelectedTab.setAttribute("afterselected-visible", + "true"); + } + + if (this._firstTab) + this._firstTab.removeAttribute("first-visible-tab"); + this._firstTab = visibleTabs[0]; + this._firstTab.setAttribute("first-visible-tab", "true"); + if (this._lastTab) + this._lastTab.removeAttribute("last-visible-tab"); + this._lastTab = visibleTabs[lastVisible]; + this._lastTab.setAttribute("last-visible-tab", "true"); + ]]></body> + </method> + + <field name="_prefObserver"><![CDATA[({ + tabContainer: this, + + observe: function (subject, topic, data) { + switch (data) { + case "browser.tabs.closeButtons": + this.tabContainer.mCloseButtons = Services.prefs.getIntPref(data); + this.tabContainer.adjustTabstrip(); + break; + case "browser.tabs.autoHide": + this.tabContainer.updateVisibility(); + break; + case "browser.tabs.closeWindowWithLastTab": + this.tabContainer._closeWindowWithLastTab = Services.prefs.getBoolPref(data); + this.tabContainer.adjustTabstrip(); + break; + } + } + });]]></field> + <field name="_blockDblClick">false</field> + + <field name="_tabDropIndicator"> + document.getAnonymousElementByAttribute(this, "anonid", "tab-drop-indicator"); + </field> + + <field name="_dragOverDelay">350</field> + <field name="_dragTime">0</field> + + <field name="_container" readonly="true"><![CDATA[ + this.parentNode && this.parentNode.localName == "toolbar" ? this.parentNode : this; + ]]></field> + + <field name="_propagatedVisibilityOnce">false</field> + + <property name="visible" + onget="return !this._container.collapsed;"> + <setter><![CDATA[ + if (val == this.visible && + this._propagatedVisibilityOnce) + return val; + + this._container.collapsed = !val; + + this._propagateVisibility(); + this._propagatedVisibilityOnce = true; + + return val; + ]]></setter> + </property> + + <method name="_enterNewTab"> + <body><![CDATA[ + let visibleTabs = this.tabbrowser.visibleTabs; + let candidate = visibleTabs[visibleTabs.length - 1]; + if (!candidate.selected) { + this._beforeHoveredTab = candidate; + candidate.setAttribute("beforehovered", "true"); + } + ]]></body> + </method> + + <method name="_leaveNewTab"> + <body><![CDATA[ + if (this._beforeHoveredTab) { + this._beforeHoveredTab.removeAttribute("beforehovered"); + this._beforeHoveredTab = null; + } + ]]></body> + </method> + + <method name="_propagateVisibility"> + <body><![CDATA[ + let visible = this.visible; + + document.getElementById("menu_closeWindow").hidden = !visible; + document.getElementById("menu_close").setAttribute("label", + this.tabbrowser.mStringBundle.getString(visible ? "tabs.closeTab" : "tabs.close")); + + goSetCommandEnabled("cmd_ToggleTabsOnTop", visible); + + TabsOnTop.syncUI(); + + TabsInTitlebar.allowedBy("tabs-visible", visible); + ]]></body> + </method> + + <method name="updateVisibility"> + <body><![CDATA[ + if (this.childNodes.length - this.tabbrowser._removingTabs.length == 1) + this.visible = window.toolbar.visible && + !Services.prefs.getBoolPref("browser.tabs.autoHide"); + else + this.visible = true; + ]]></body> + </method> + + <method name="adjustTabstrip"> + <body><![CDATA[ + let numTabs = this.childNodes.length - + this.tabbrowser._removingTabs.length; + // modes for tabstrip + // 0 - button on active tab only + // 1 - close buttons on all tabs, if available space allows + // 2 - no close buttons at all + // 3 - close button at the end of the tabstrip + switch (this.mCloseButtons) { + case 0: + // If we decide we want to hide the close tab button on the last tab + // when closing the window with the last tab, then we should check + // if (numTabs == 1 && this._closeWindowWithLastTab) here and set + // this.setAttribute("closebuttons", "hidden") appropriately + this.setAttribute("closebuttons", "activetab"); + break; + case 1: + if (numTabs == 1) { + // See remark about potentially hiding the close tab button, above. + this.setAttribute("closebuttons", "alltabs"); + } else if (numTabs == 2) { + // This is an optimization to avoid layout flushes by calling + // getBoundingClientRect() when we just opened a second tab. In + // this case it's highly unlikely that the tab width is smaller + // than mTabClipWidth and the tab close button obscures too much + // of the tab's label. In the edge case of the window being too + // narrow (or if tabClipWidth has been set to a way higher value), + // we'll correct the 'closebuttons' attribute after the tabopen + // animation has finished. + this.setAttribute("closebuttons", "alltabs"); + } else { + let tab = this.tabbrowser.visibleTabs[this.tabbrowser._numPinnedTabs]; + if (tab && tab.getBoundingClientRect().width > this.mTabClipWidth) + this.setAttribute("closebuttons", "alltabs"); + else + this.setAttribute("closebuttons", "activetab"); + } + break; + case 2: + case 3: + this.setAttribute("closebuttons", "never"); + break; + } + var tabstripClosebutton = document.getElementById("tabs-closebutton"); + if (tabstripClosebutton && tabstripClosebutton.parentNode == this._container) + tabstripClosebutton.collapsed = this.mCloseButtons != 3; + ]]></body> + </method> + + <method name="_handleTabSelect"> + <parameter name="aSmoothScroll"/> + <body><![CDATA[ + if (this.getAttribute("overflow") == "true") + this.mTabstrip.ensureElementIsVisible(this.selectedItem, aSmoothScroll); + ]]></body> + </method> + + <method name="_fillTrailingGap"> + <body><![CDATA[ + try { + // if we're at the right side (and not the logical end, + // which is why this works for both LTR and RTL) + // of the tabstrip, we need to ensure that we stay + // completely scrolled to the right side + var tabStrip = this.mTabstrip; + if (tabStrip.scrollPosition + tabStrip.scrollClientSize > + tabStrip.scrollSize) + tabStrip.scrollByPixels(-1); + } catch (e) {} + ]]></body> + </method> + + <field name="_closingTabsSpacer"> + document.getAnonymousElementByAttribute(this, "anonid", "closing-tabs-spacer"); + </field> + + <field name="_tabDefaultMaxWidth">NaN</field> + <field name="_lastTabClosedByMouse">false</field> + <field name="_hasTabTempMaxWidth">false</field> + + <!-- Try to keep the active tab's close button under the mouse cursor --> + <method name="_lockTabSizing"> + <parameter name="aTab"/> + <body><![CDATA[ + var tabs = this.tabbrowser.visibleTabs; + if (!tabs.length) + return; + + var isEndTab = (aTab._tPos > tabs[tabs.length-1]._tPos); + var tabWidth = aTab.getBoundingClientRect().width; + + if (!this._tabDefaultMaxWidth) + this._tabDefaultMaxWidth = + parseFloat(window.getComputedStyle(aTab).maxWidth); + this._lastTabClosedByMouse = true; + + if (this.getAttribute("overflow") == "true") { +#ifdef XP_WIN + // Don't need to do anything if we're in overflow mode and we're closing + // the last tab. + if (isEndTab) +#else + // Don't need to do anything if we're in overflow mode and aren't scrolled + // all the way to the right, or if we're closing the last tab. + if (isEndTab || !this.mTabstrip._scrollButtonDown.disabled) +#endif + return; + + // If the tab has an owner that will become the active tab, the owner will + // be to the left of it, so we actually want the left tab to slide over. + // This can't be done as easily in non-overflow mode, so we don't bother. + if (aTab.owner) + return; + + // Resize immediately if preffed + // XXX: we may want to make this a three-state pref to disable this early + // exit if people prefer a mix of behavior (don't resize in overflow, + // but resize if not overflowing) + if (Services.prefs.getBoolPref("browser.tabs.resize_immediately")) + return; + + this._expandSpacerBy(tabWidth); + } else { // non-overflow mode + // Locking is neither in effect nor needed, so let tabs expand normally. + if (isEndTab && !this._hasTabTempMaxWidth) + return; + + // Resize immediately if preffed + // XXX: we may want to make this a three-state pref to disable this early + // exit if people prefer a mix of behavior (don't resize in overflow, + // but resize if not overflowing) + if (Services.prefs.getBoolPref("browser.tabs.resize_immediately")) + return; + + let numPinned = this.tabbrowser._numPinnedTabs; + // Force tabs to stay the same width, unless we're closing the last tab, + // which case we need to let them expand just enough so that the overall + // tabbar width is the same. + if (isEndTab) { + let numNormalTabs = tabs.length - numPinned; + tabWidth = tabWidth * (numNormalTabs + 1) / numNormalTabs; + if (tabWidth > this._tabDefaultMaxWidth) + tabWidth = this._tabDefaultMaxWidth; + } + tabWidth += "px"; + for (let i = numPinned; i < tabs.length; i++) { + let tab = tabs[i]; + tab.style.setProperty("max-width", tabWidth, "important"); + if (!isEndTab) { // keep tabs the same width + tab.style.transition = "none"; + tab.clientTop; // flush styles to skip animation; see bug 649247 + tab.style.transition = ""; + } + } + this._hasTabTempMaxWidth = true; + this.tabbrowser.addEventListener("mousemove", this, false); + window.addEventListener("mouseout", this, false); + } + ]]></body> + </method> + + <method name="_expandSpacerBy"> + <parameter name="pixels"/> + <body><![CDATA[ + let spacer = this._closingTabsSpacer; + spacer.style.width = parseFloat(spacer.style.width) + pixels + "px"; + this.setAttribute("using-closing-tabs-spacer", "true"); + this.tabbrowser.addEventListener("mousemove", this, false); + window.addEventListener("mouseout", this, false); + ]]></body> + </method> + + <method name="_unlockTabSizing"> + <body><![CDATA[ + this.tabbrowser.removeEventListener("mousemove", this, false); + window.removeEventListener("mouseout", this, false); + + if (this._hasTabTempMaxWidth) { + this._hasTabTempMaxWidth = false; + let tabs = this.tabbrowser.visibleTabs; + for (let i = 0; i < tabs.length; i++) + tabs[i].style.maxWidth = ""; + } + + if (this.hasAttribute("using-closing-tabs-spacer")) { + this.removeAttribute("using-closing-tabs-spacer"); + this._closingTabsSpacer.style.width = 0; + } + ]]></body> + </method> + + <field name="_lastNumPinned">0</field> + <method name="_positionPinnedTabs"> + <body><![CDATA[ + var numPinned = this.tabbrowser._numPinnedTabs; + var doPosition = this.getAttribute("overflow") == "true" && + numPinned > 0; + + if (doPosition) { + this.setAttribute("positionpinnedtabs", "true"); + + let scrollButtonWidth = this.mTabstrip._scrollButtonDown.getBoundingClientRect().width; + let paddingStart = this.mTabstrip.scrollboxPaddingStart; + let width = 0; + + for (let i = numPinned - 1; i >= 0; i--) { + let tab = this.childNodes[i]; + width += tab.getBoundingClientRect().width; + tab.style.MozMarginStart = - (width + scrollButtonWidth + paddingStart) + "px"; + } + + this.style.MozPaddingStart = width + paddingStart + "px"; + + } else { + this.removeAttribute("positionpinnedtabs"); + + for (let i = 0; i < numPinned; i++) { + let tab = this.childNodes[i]; + tab.style.MozMarginStart = ""; + } + + this.style.MozPaddingStart = ""; + } + + if (this._lastNumPinned != numPinned) { + this._lastNumPinned = numPinned; + this._handleTabSelect(false); + } + ]]></body> + </method> + + <method name="_animateTabMove"> + <parameter name="event"/> + <body><![CDATA[ + let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); + + if (this.getAttribute("movingtab") != "true") { + this.setAttribute("movingtab", "true"); + this.selectedItem = draggedTab; + } + + if (!("animLastScreenX" in draggedTab._dragData)) + draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX; + + let screenX = event.screenX; + if (screenX == draggedTab._dragData.animLastScreenX) + return; + + let draggingRight = screenX > draggedTab._dragData.animLastScreenX; + draggedTab._dragData.animLastScreenX = screenX; + + let rtl = (window.getComputedStyle(this).direction == "rtl"); + let pinned = draggedTab.pinned; + let numPinned = this.tabbrowser._numPinnedTabs; + let tabs = this.tabbrowser.visibleTabs + .slice(pinned ? 0 : numPinned, + pinned ? numPinned : undefined); + if (rtl) + tabs.reverse(); + let tabWidth = draggedTab.getBoundingClientRect().width; + + // Move the dragged tab based on the mouse position. + + let leftTab = tabs[0]; + let rightTab = tabs[tabs.length - 1]; + let tabScreenX = draggedTab.boxObject.screenX; + let translateX = screenX - draggedTab._dragData.screenX; + if (!pinned) + translateX += this.mTabstrip.scrollPosition - draggedTab._dragData.scrollX; + let leftBound = leftTab.boxObject.screenX - tabScreenX; + let rightBound = (rightTab.boxObject.screenX + rightTab.boxObject.width) - + (tabScreenX + tabWidth); + translateX = Math.max(translateX, leftBound); + translateX = Math.min(translateX, rightBound); + draggedTab.style.transform = "translateX(" + translateX + "px)"; + + // Determine what tab we're dragging over. + // * Point of reference is the center of the dragged tab. If that + // point touches a background tab, the dragged tab would take that + // tab's position when dropped. + // * We're doing a binary search in order to reduce the amount of + // tabs we need to check. + + let tabCenter = tabScreenX + translateX + tabWidth / 2; + let newIndex = -1; + let oldIndex = "animDropIndex" in draggedTab._dragData ? + draggedTab._dragData.animDropIndex : draggedTab._tPos; + let low = 0; + let high = tabs.length - 1; + while (low <= high) { + let mid = Math.floor((low + high) / 2); + if (tabs[mid] == draggedTab && + ++mid > high) + break; + let boxObject = tabs[mid].boxObject; + let screenX = boxObject.screenX + getTabShift(tabs[mid], oldIndex); + if (screenX > tabCenter) { + high = mid - 1; + } else if (screenX + boxObject.width < tabCenter) { + low = mid + 1; + } else { + newIndex = tabs[mid]._tPos; + break; + } + } + if (newIndex >= oldIndex) + newIndex++; + if (newIndex < 0 || newIndex == oldIndex) + return; + draggedTab._dragData.animDropIndex = newIndex; + + // Shift background tabs to leave a gap where the dragged tab + // would currently be dropped. + + for (let tab of tabs) { + if (tab != draggedTab) { + let shift = getTabShift(tab, newIndex); + tab.style.transform = shift ? "translateX(" + shift + "px)" : ""; + } + } + + function getTabShift(tab, dropIndex) { + if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex) + return rtl ? -tabWidth : tabWidth; + if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) + return rtl ? tabWidth : -tabWidth; + return 0; + } + ]]></body> + </method> + + <method name="_finishAnimateTabMove"> + <body><![CDATA[ + if (this.getAttribute("movingtab") != "true") + return; + + for (let tab of this.tabbrowser.visibleTabs) + tab.style.transform = ""; + + this.removeAttribute("movingtab"); + + this._handleTabSelect(); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "load": + this.updateVisibility(); + break; + case "resize": + if (aEvent.target != window) + break; + + let sizemode = document.documentElement.getAttribute("sizemode"); + TabsInTitlebar.allowedBy("sizemode", + sizemode == "maximized" || sizemode == "fullscreen"); + + var width = this.mTabstrip.boxObject.width; + if (width != this.mTabstripWidth) { + this.adjustTabstrip(); + this._fillTrailingGap(); + this._handleTabSelect(); + this.mTabstripWidth = width; + } + + this.tabbrowser.updateWindowResizers(); + break; + case "mouseout": + // If the "related target" (the node to which the pointer went) is not + // a child of the current document, the mouse just left the window. + let relatedTarget = aEvent.relatedTarget; + if (relatedTarget && relatedTarget.ownerDocument == document) + break; + case "mousemove": + if (document.getElementById("tabContextMenu").state != "open") + this._unlockTabSizing(); + break; + } + ]]></body> + </method> + + <field name="_animateElement"> + this.mTabstrip._scrollButtonDown; + </field> + + <method name="_notifyBackgroundTab"> + <parameter name="aTab"/> + <body><![CDATA[ + if (aTab.pinned) + return; + + var scrollRect = this.mTabstrip.scrollClientRect; + var tab = aTab.getBoundingClientRect(); + + // Is the new tab already completely visible? + if (scrollRect.left <= tab.left && tab.right <= scrollRect.right) + return; + + if (this.mTabstrip.smoothScroll) { + let selected = !this.selectedItem.pinned && + this.selectedItem.getBoundingClientRect(); + + // Can we make both the new tab and the selected tab completely visible? + if (!selected || + Math.max(tab.right - selected.left, selected.right - tab.left) <= + scrollRect.width) { + this.mTabstrip.ensureElementIsVisible(aTab); + return; + } + + this.mTabstrip._smoothScrollByPixels(this.mTabstrip._isRTLScrollbox ? + selected.right - scrollRect.right : + selected.left - scrollRect.left); + } + + if (!this._animateElement.hasAttribute("notifybgtab")) { + this._animateElement.setAttribute("notifybgtab", "true"); + setTimeout(function (ele) { + ele.removeAttribute("notifybgtab"); + }, 150, this._animateElement); + } + ]]></body> + </method> + + <method name="_getDragTargetTab"> + <parameter name="event"/> + <body><![CDATA[ + let tab = event.target.localName == "tab" ? event.target : null; + if (tab && + (event.type == "drop" || event.type == "dragover") && + event.dataTransfer.dropEffect == "link") { + let boxObject = tab.boxObject; + if (event.screenX < boxObject.screenX + boxObject.width * .25 || + event.screenX > boxObject.screenX + boxObject.width * .75) + return null; + } + return tab; + ]]></body> + </method> + + <method name="_getDropIndex"> + <parameter name="event"/> + <body><![CDATA[ + var tabs = this.childNodes; + var tab = this._getDragTargetTab(event); + if (window.getComputedStyle(this, null).direction == "ltr") { + for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) + if (event.screenX < tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2) + return i; + } else { + for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) + if (event.screenX > tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2) + return i; + } + return tabs.length; + ]]></body> + </method> + + <method name="_setEffectAllowedForDataTransfer"> + <parameter name="event"/> + <body><![CDATA[ + var dt = event.dataTransfer; + // Disallow dropping multiple items + if (dt.mozItemCount > 1) + return dt.effectAllowed = "none"; + + var types = dt.mozTypesAt(0); + var sourceNode = null; + // tabs are always added as the first type + if (types[0] == TAB_DROP_TYPE) { + var sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0); + if (sourceNode instanceof XULElement && + sourceNode.localName == "tab" && + sourceNode.ownerDocument.defaultView instanceof ChromeWindow && + sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser" && + sourceNode.ownerDocument.defaultView.gBrowser.tabContainer == sourceNode.parentNode) { + // Do not allow transfering a private tab to a non-private window + // and vice versa. + if (PrivateBrowsingUtils.isWindowPrivate(window) != + PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerDocument.defaultView)) + return dt.effectAllowed = "none"; + + return dt.effectAllowed = event.ctrlKey ? "copy" : "move"; + } + } + + if (browserDragAndDrop.canDropLink(event)) { + // Here we need to do this manually + return dt.effectAllowed = dt.dropEffect = "link"; + } + return dt.effectAllowed = "none"; + ]]></body> + </method> + + <method name="_handleNewTab"> + <parameter name="tab"/> + <body><![CDATA[ + if (tab.parentNode != this) + return; + tab._fullyOpen = true; + + this.adjustTabstrip(); + + if (tab.getAttribute("selected") == "true") { + this._fillTrailingGap(); + this._handleTabSelect(); + } else { + if (tab.hasAttribute("skipbackgroundnotify")) { + tab.removeAttribute("skipbackgroundnotify"); + } else { + this._notifyBackgroundTab(tab); + } + } + + // XXXmano: this is a temporary workaround for bug 345399 + // We need to manually update the scroll buttons disabled state + // if a tab was inserted to the overflow area or removed from it + // without any scrolling and when the tabbar has already + // overflowed. + this.mTabstrip._updateScrollButtonsDisabledState(); + ]]></body> + </method> + + <method name="_canAdvanceToTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + return !aTab.closing; + ]]> + </body> + </method> + + <method name="_handleTabTelemetryStart"> + <parameter name="aTab"/> + <parameter name="aURI"/> + <body> + <![CDATA[ + // Animation-smoothness telemetry/logging + if (this._tabAnimationLoggingEnabled) { + if (aURI == "about:newtab" && (aTab._tPos == 1 || aTab._tPos == 2)) { + // Indicate newtab page animation where other tabs are unaffected + // (for which case, the 2nd or 3rd tabs are good representatives, even if not absolute) + aTab._recordingTabOpenPlain = true; + } + aTab._recordingHandle = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .startFrameTimeRecording(); + } + + // Overall animation duration + aTab._animStartTime = Date.now(); + ]]> + </body> + </method> + + <method name="_handleTabTelemetryEnd"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (!aTab._animStartTime) { + return; + } + + aTab._animStartTime = 0; + + // Handle tab animation smoothness telemetry/logging of frame intervals and paint times + if (!("_recordingHandle" in aTab)) { + return; + } + + let paints = {}; + let intervals = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .stopFrameTimeRecording(aTab._recordingHandle, paints); + delete aTab._recordingHandle; + paints = paints.value; // The result array itself. + let frameCount = intervals.length; + + if (this._tabAnimationLoggingEnabled) { + let msg = "Tab " + (aTab.closing ? "close" : "open") + " (Frame-interval / paint-processing):\n"; + for (let i = 0; i < frameCount; i++) { + msg += Math.round(intervals[i]) + " / " + Math.round(paints[i]) + "\n"; + } + Services.console.logStringMessage(msg); + } + + // For telemetry, the first frame interval is not useful since it may represent an interval + // to a relatively old frame (prior to recording start). So we'll ignore it for the average. + // But if we recorded only 1 frame (very rare), then the first paint duration is a good + // representative of the first frame interval for our cause (indicates very bad animation). + // First paint duration is always useful for us. + if (frameCount > 0) { + let averageInterval = 0; + let averagePaint = paints[0]; + for (let i = 1; i < frameCount; i++) { + averageInterval += intervals[i]; + averagePaint += paints[i]; + }; + averagePaint /= frameCount; + averageInterval = (frameCount == 1) + ? averagePaint + : averageInterval / (frameCount - 1); + + if (aTab._recordingTabOpenPlain) { + delete aTab._recordingTabOpenPlain; + } + } + ]]> + </body> + </method> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <property name="mTabstripClosebutton" readonly="true" + onget="return document.getElementById('tabs-closebutton');"/> + <property name="mAllTabsPopup" readonly="true" + onget="return document.getElementById('alltabs-popup');"/> + </implementation> + + <handlers> + <handler event="TabSelect" action="this._handleTabSelect();"/> + + <handler event="transitionend"><![CDATA[ + if (event.propertyName != "max-width") + return; + + var tab = event.target; + + this._handleTabTelemetryEnd(tab); + + if (tab.getAttribute("fadein") == "true") { + if (tab._fullyOpen) + this.adjustTabstrip(); + else + this._handleNewTab(tab); + } else if (tab.closing) { + this.tabbrowser._endRemoveTab(tab); + } + ]]></handler> + + <handler event="dblclick"><![CDATA[ + // When the tabbar has an unified appearance with the titlebar + // and menubar, a double-click in it should have the same behavior + // as double-clicking the titlebar + if (TabsInTitlebar.enabled || + (TabsOnTop.enabled && this.parentNode._dragBindingAlive)) + return; + + if (event.button != 0 || + event.originalTarget.localName != "box") + return; + + // See comments in the "mousedown" and "click" event handlers of the + // tabbrowser-tabs binding. + if (!this._blockDblClick) + BrowserOpenTab(); + + event.preventDefault(); + ]]></handler> + + <!-- Consider that the in-tab close button is only shown on the active + tab. When clicking on an inactive tab at the position where the + close button will appear during the click, no "click" event will be + dispatched, because the mousedown and mouseup events don't have the + same event target. For that reason use "mousedown" instead of "click" + to implement in-tab close button behavior. (Pale Moon UXP issue #775) + --> + <handler event="mousedown" button="0" phase="capturing"><![CDATA[ + /* The only sequence in which a second click event (i.e. dblclik) + * can be dispatched on an in-tab close button is when it is shown + * after the first click (i.e. the first click event was dispatched + * on the tab). This happens when we show the close button only on + * the active tab. (bug 352021) + * The only sequence in which a third click event can be dispatched + * on an in-tab close button is when the tab was opened with a + * double click on the tabbar. (bug 378344) + * In both cases, it is most likely that the close button area has + * been accidentally clicked, therefore we do not close the tab. + * + * We don't want to ignore processing of more than one click event, + * though, since the user might actually be repeatedly clicking to + * close many tabs at once. + * + * Also prevent errant doubleclick on the close button from opening + * a new tab (bug 343628): + * Since we're removing the event target, if the user double-clicks + * the button, the dblclick event will be dispatched with the tabbar + * as its event target (and explicit/originalTarget), which treats + * that as a mouse gesture for opening a new tab. + * In this context, we're manually blocking the dblclick event. + */ + + // Reset flags at the beginning of a series of clicks: + if (event.detail == 1) { + this.flagClickOnCloseButton = false; + this.flagActivateTabOrClickOnTabbar = false; + } + + this.blockCloseButtonOnDblclick = this.flagActivateTabOrClickOnTabbar; + this._blockDblClick = this.flagClickOnCloseButton; + + // Set flags: + let eventTargetIsCloseButton = + event.originalTarget.classList.contains("tab-close-button"); + this.flagClickOnCloseButton = eventTargetIsCloseButton; + this.flagActivateTabOrClickOnTabbar = + ((!eventTargetIsCloseButton && event.detail == 1) || + event.originalTarget.localName == "box"); + ]]></handler> + + <handler event="click" button="0"><![CDATA[ + // See comment in the "mousedown" event handler of the + // tabbrowser-tabs binding. + if (event.originalTarget.classList.contains("tab-close-button") && + !this.blockCloseButtonOnDblclick) { + gBrowser.removeTab(document.getBindingParent(event.originalTarget), + {animate: true, byMouse: true,}); + this._blockDblClick = true; + } + ]]></handler> + + <handler event="click" button="1"><![CDATA[ + if (event.target.localName == "tab") { + if (this.childNodes.length > 1 || !this._closeWindowWithLastTab) + this.tabbrowser.removeTab(event.target, {animate: true, byMouse: true}); + } else if (event.originalTarget.localName == "box") { + BrowserOpenTab(); + } else { + return; + } + + event.stopPropagation(); + ]]></handler> + + <handler event="keypress"><![CDATA[ + if (event.altKey || event.shiftKey || + !event.ctrlKey || event.metaKey) + return; + + switch (event.keyCode) { + case KeyEvent.DOM_VK_UP: + this.tabbrowser.moveTabBackward(); + break; + case KeyEvent.DOM_VK_DOWN: + this.tabbrowser.moveTabForward(); + break; + case KeyEvent.DOM_VK_RIGHT: + case KeyEvent.DOM_VK_LEFT: + this.tabbrowser.moveTabOver(event); + break; + case KeyEvent.DOM_VK_HOME: + this.tabbrowser.moveTabToStart(); + break; + case KeyEvent.DOM_VK_END: + this.tabbrowser.moveTabToEnd(); + break; + default: + // Stop the keypress event for the above keyboard + // shortcuts only. + return; + } + event.stopPropagation(); + event.preventDefault(); + ]]></handler> + + <handler event="dragstart"><![CDATA[ + var tab = this._getDragTargetTab(event); + if (!tab) + return; + + let dt = event.dataTransfer; + dt.mozSetDataAt(TAB_DROP_TYPE, tab, 0); + let browser = tab.linkedBrowser; + + // We must not set text/x-moz-url or text/plain data here, + // otherwise trying to deatch the tab by dropping it on the desktop + // may result in an "internet shortcut" + dt.mozSetDataAt("text/x-moz-text-internal", browser.currentURI.spec, 0); + + // Set the cursor to an arrow during tab drags. + dt.mozCursor = "default"; + + // Create a canvas to which we capture the current tab. + // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired + // canvas size (in CSS pixels) to the window's backing resolution in order + // to get a full-resolution drag image for use on HiDPI displays. + let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils); + let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom; + let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.mozOpaque = true; + canvas.width = 160 * scale; + canvas.height = 90 * scale; + PageThumbs.captureToCanvas(browser, canvas); + dt.setDragImage(canvas, -16 * scale, -16 * scale); + + // _dragData.offsetX/Y give the coordinates that the mouse should be + // positioned relative to the corner of the new window created upon + // dragend such that the mouse appears to have the same position + // relative to the corner of the dragged tab. + function clientX(ele) ele.getBoundingClientRect().left; + let tabOffsetX = clientX(tab) - clientX(this); + tab._dragData = { + offsetX: event.screenX - window.screenX - tabOffsetX, + offsetY: event.screenY - window.screenY, + scrollX: this.mTabstrip.scrollPosition, + screenX: event.screenX + }; + + event.stopPropagation(); + ]]></handler> + + <handler event="dragover"><![CDATA[ + var effects = this._setEffectAllowedForDataTransfer(event); + + var ind = this._tabDropIndicator; + if (effects == "" || effects == "none") { + ind.collapsed = true; + return; + } + event.preventDefault(); + event.stopPropagation(); + + var tabStrip = this.mTabstrip; + var ltr = (window.getComputedStyle(this, null).direction == "ltr"); + + // autoscroll the tab strip if we drag over the scroll + // buttons, even if we aren't dragging a tab, but then + // return to avoid drawing the drop indicator + var pixelsToScroll = 0; + if (this.getAttribute("overflow") == "true") { + var targetAnonid = event.originalTarget.getAttribute("anonid"); + switch (targetAnonid) { + case "scrollbutton-up": + pixelsToScroll = tabStrip.scrollIncrement * -1; + break; + case "scrollbutton-down": + pixelsToScroll = tabStrip.scrollIncrement; + break; + } + if (pixelsToScroll) + tabStrip.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll); + } + + if (effects == "move" && + this == event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0).parentNode) { + ind.collapsed = true; + this._animateTabMove(event); + return; + } + + this._finishAnimateTabMove(); + + if (effects == "link") { + let tab = this._getDragTargetTab(event); + if (tab) { + if (!this._dragTime) + this._dragTime = Date.now(); + if (Date.now() >= this._dragTime + this._dragOverDelay) + this.selectedItem = tab; + ind.collapsed = true; + return; + } + } + + var rect = tabStrip.getBoundingClientRect(); + var newMargin; + if (pixelsToScroll) { + // if we are scrolling, put the drop indicator at the edge + // so that it doesn't jump while scrolling + let scrollRect = tabStrip.scrollClientRect; + let minMargin = scrollRect.left - rect.left; + let maxMargin = Math.min(minMargin + scrollRect.width, + scrollRect.right); + if (!ltr) + [minMargin, maxMargin] = [this.clientWidth - maxMargin, + this.clientWidth - minMargin]; + newMargin = (pixelsToScroll > 0) ? maxMargin : minMargin; + } + else { + let newIndex = this._getDropIndex(event); + if (newIndex == this.childNodes.length) { + let tabRect = this.childNodes[newIndex-1].getBoundingClientRect(); + if (ltr) + newMargin = tabRect.right - rect.left; + else + newMargin = rect.right - tabRect.left; + } + else { + let tabRect = this.childNodes[newIndex].getBoundingClientRect(); + if (ltr) + newMargin = tabRect.left - rect.left; + else + newMargin = rect.right - tabRect.right; + } + } + + ind.collapsed = false; + + newMargin += ind.clientWidth / 2; + if (!ltr) + newMargin *= -1; + + ind.style.transform = "translate(" + Math.round(newMargin) + "px)"; + ind.style.MozMarginStart = (-ind.clientWidth) + "px"; + ]]></handler> + + <handler event="drop"><![CDATA[ + var dt = event.dataTransfer; + var dropEffect = dt.dropEffect; + var draggedTab; + if (dropEffect != "link") { // copy or move + draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0); + // not our drop then + if (!draggedTab) + return; + } + + this._tabDropIndicator.collapsed = true; + event.stopPropagation(); + if (draggedTab && dropEffect == "copy") { + // copy the dropped tab (wherever it's from) + let newIndex = this._getDropIndex(event); + let newTab = this.tabbrowser.duplicateTab(draggedTab); + this.tabbrowser.moveTabTo(newTab, newIndex); + if (draggedTab.parentNode != this || event.shiftKey) + this.selectedItem = newTab; + } else if (draggedTab && draggedTab.parentNode == this) { + this._finishAnimateTabMove(); + + // actually move the dragged tab + if ("animDropIndex" in draggedTab._dragData) { + let newIndex = draggedTab._dragData.animDropIndex; + if (newIndex > draggedTab._tPos) + newIndex--; + this.tabbrowser.moveTabTo(draggedTab, newIndex); + } + } else if (draggedTab) { + // swap the dropped tab with a new one we create and then close + // it in the other window (making it seem to have moved between + // windows) + let newIndex = this._getDropIndex(event); + let newTab = this.tabbrowser.addTab("about:blank"); + let newBrowser = this.tabbrowser.getBrowserForTab(newTab); + // Stop the about:blank load + newBrowser.stop(); + // make sure it has a docshell + newBrowser.docShell; + + let numPinned = this.tabbrowser._numPinnedTabs; + if (newIndex < numPinned || draggedTab.pinned && newIndex == numPinned) + this.tabbrowser.pinTab(newTab); + this.tabbrowser.moveTabTo(newTab, newIndex); + + // We need to select the tab before calling swapBrowsersAndCloseOther + // so that window.content in chrome windows points to the right tab + // when pagehide/show events are fired. + this.tabbrowser.selectedTab = newTab; + + draggedTab.parentNode._finishAnimateTabMove(); + this.tabbrowser.swapBrowsersAndCloseOther(newTab, draggedTab); + + // Call updateCurrentBrowser to make sure the URL bar is up to date + // for our new tab after we've done swapBrowsersAndCloseOther. + this.tabbrowser.updateCurrentBrowser(true); + } else { + // Pass true to disallow dropping javascript: or data: urls + let links; + try { + links = browserDragAndDrop.dropLinks(event, true); + } catch (ex) {} + +// // valid urls don't contain spaces ' '; if we have a space it isn't a valid url. +// if (!url || url.includes(" ")) //PMed + if (!links || links.length === 0) //FF + return; + + let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + + if (event.shiftKey) + inBackground = !inBackground; + + let targetTab = this._getDragTargetTab(event); + let replace = !(!targetTab || dropEffect == "copy"); + let newIndex = this._getDropIndex(event); + let urls = links.map(link => link.url); + this.tabbrowser.loadTabs(urls, { + inBackground, + replace, + allowThirdPartyFixup: true, + targetTab, + newIndex, + }); + } + + if (draggedTab) { + delete draggedTab._dragData; + } + ]]></handler> + + <handler event="dragend"><![CDATA[ + // Note: while this case is correctly handled here, this event + // isn't dispatched when the tab is moved within the tabstrip, + // see bug 460801. + + this._finishAnimateTabMove(); + + var dt = event.dataTransfer; + var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0); + if (dt.mozUserCancelled || dt.dropEffect != "none") { + delete draggedTab._dragData; + return; + } + + // Check if tab detaching is enabled + if (!Services.prefs.getBoolPref("browser.tabs.allowTabDetach")) { + return; + } + + // Disable detach within the browser toolbox + var eX = event.screenX; + var eY = event.screenY; + var wX = window.screenX; + // check if the drop point is horizontally within the window + if (eX > wX && eX < (wX + window.outerWidth)) { + let bo = this.mTabstrip.boxObject; + // also avoid detaching if the the tab was dropped too close to + // the tabbar (half a tab) + let endScreenY = bo.screenY + 1.5 * bo.height; + if (eY < endScreenY && eY > window.screenY) + return; + } + + // screen.availLeft et. al. only check the screen that this window is on, + // but we want to look at the screen the tab is being dropped onto. + var sX = {}, sY = {}, sWidth = {}, sHeight = {}; + Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager) + .screenForRect(eX, eY, 1, 1) + .GetAvailRect(sX, sY, sWidth, sHeight); + // ensure new window entirely within screen + var winWidth = Math.min(window.outerWidth, sWidth.value); + var winHeight = Math.min(window.outerHeight, sHeight.value); + var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, sX.value), + sX.value + sWidth.value - winWidth); + var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, sY.value), + sY.value + sHeight.value - winHeight); + + delete draggedTab._dragData; + + if (this.tabbrowser.tabs.length == 1) { + // resize _before_ move to ensure the window fits the new screen. if + // the window is too large for its screen, the window manager may do + // automatic repositioning. + window.resizeTo(winWidth, winHeight); + window.moveTo(left, top); + window.focus(); + } else { + this.tabbrowser.replaceTabWithWindow(draggedTab, { screenX: left, + screenY: top, +#ifndef XP_WIN + outerWidth: winWidth, + outerHeight: winHeight +#endif + }); + } + event.stopPropagation(); + ]]></handler> + + <handler event="dragexit"><![CDATA[ + this._dragTime = 0; + + // This does not work at all (see bug 458613) + var target = event.relatedTarget; + while (target && target != this) + target = target.parentNode; + if (target) + return; + + this._tabDropIndicator.collapsed = true; + event.stopPropagation(); + ]]></handler> + </handlers> + </binding> + + <!-- close-tab-button binding + This binding relies on the structure of the tabbrowser binding. + Therefore it should only be used as a child of the tab or the tabs + element (in both cases, when they are anonymous nodes of <tabbrowser>). + --> + <binding id="tabbrowser-close-tab-button" + extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-image"> + <handlers> + <handler event="dragstart"> + event.stopPropagation(); + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-tab" display="xul:hbox" + extends="chrome://global/content/bindings/tabbox.xml#tab"> + <resources> + <stylesheet src="chrome://browser/content/tabbrowser.css"/> + </resources> + + <content context="tabContextMenu" closetabtext="&closeTab.label;"> + <xul:stack class="tab-stack" flex="1"> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background"> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background-start"/> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background-middle"/> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background-end"/> + </xul:hbox> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-content" align="center"> + <xul:image xbl:inherits="fadein,pinned,busy,progress,selected" + class="tab-throbber" + role="presentation" + layer="true" /> + <xul:image xbl:inherits="validate,src=image,fadein,pinned,selected" + class="tab-icon-image" + role="presentation" + anonid="tab-icon"/> + <xul:image xbl:inherits="busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected" + anonid="overlay-icon" + class="tab-icon-overlay" + role="presentation"/> + <xul:label flex="1" + xbl:inherits="value=label,crop,accesskey,fadein,pinned,selected" + class="tab-text tab-label" + role="presentation"/> + <xul:image xbl:inherits="soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected" + anonid="soundplaying-icon" + class="tab-icon-sound" + role="presentation"/> + <xul:toolbarbutton anonid="close-button" + xbl:inherits="fadein,pinned,selected" + class="tab-close-button close-icon"/> + </xul:hbox> + </xul:stack> + </content> + + <implementation> + <property name="pinned" readonly="true"> + <getter> + return this.getAttribute("pinned") == "true"; + </getter> + </property> + <property name="hidden" readonly="true"> + <getter> + return this.getAttribute("hidden") == "true"; + </getter> + </property> + + <field name="mOverCloseButton">false</field> + <property name="_overPlayingIcon" readonly="true"> + <getter><![CDATA[ + let iconVisible = this.hasAttribute("soundplaying") || + this.hasAttribute("muted") || + this.hasAttribute("blocked"); + let soundPlayingIcon = + document.getAnonymousElementByAttribute(this, "anonid", "soundplaying-icon"); + let overlayIcon = + document.getAnonymousElementByAttribute(this, "anonid", "overlay-icon"); + + return soundPlayingIcon && soundPlayingIcon.matches(":hover") || + (overlayIcon && overlayIcon.matches(":hover") && iconVisible); + ]]></getter> + </property> + <field name="mCorrespondingMenuitem">null</field> + <field name="closing">false</field> + <field name="lastAccessed">0</field> + + <method name="toggleMuteAudio"> + <parameter name="aMuteReason"/> + <body> + <![CDATA[ + let tabContainer = this.parentNode; + let browser = this.linkedBrowser; + let modifiedAttrs = []; + if (browser.audioBlocked) { + this.removeAttribute("blocked"); + modifiedAttrs.push("blocked"); + + // We don't want sound icon flickering between "blocked", "none" and + // "sound-playing", here adding the "soundplaying" is to keep the + // transition smoothly. + if (!this.hasAttribute("soundplaying")) { + this.setAttribute("soundplaying", true); + modifiedAttrs.push("soundplaying"); + } + + browser.resumeMedia(); + } else { + if (browser.audioMuted) { + browser.unmute(); + this.removeAttribute("muted"); + } else { + browser.mute(); + this.setAttribute("muted", "true"); + } + this.muteReason = aMuteReason || null; + modifiedAttrs.push("muted"); + } + tabContainer.tabbrowser._tabAttrModified(this, modifiedAttrs); + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="mouseover"><![CDATA[ + let anonid = event.originalTarget.getAttribute("anonid"); + if (anonid == "close-button") + this.mOverCloseButton = true; + + let tab = event.target; + if (tab.closing) + return; + + let tabContainer = this.parentNode; + let visibleTabs = tabContainer.tabbrowser.visibleTabs; + let tabIndex = visibleTabs.indexOf(tab); + if (tabIndex == 0) { + tabContainer._beforeHoveredTab = null; + } else { + let candidate = visibleTabs[tabIndex - 1]; + if (!candidate.selected) { + tabContainer._beforeHoveredTab = candidate; + candidate.setAttribute("beforehovered", "true"); + } + } + + if (tabIndex == visibleTabs.length - 1) { + tabContainer._afterHoveredTab = null; + } else { + let candidate = visibleTabs[tabIndex + 1]; + if (!candidate.selected) { + tabContainer._afterHoveredTab = candidate; + candidate.setAttribute("afterhovered", "true"); + } + } + ]]></handler> + <handler event="mouseout"><![CDATA[ + let anonid = event.originalTarget.getAttribute("anonid"); + if (anonid == "close-button") + this.mOverCloseButton = false; + + let tabContainer = this.parentNode; + if (tabContainer._beforeHoveredTab) { + tabContainer._beforeHoveredTab.removeAttribute("beforehovered"); + tabContainer._beforeHoveredTab = null; + } + if (tabContainer._afterHoveredTab) { + tabContainer._afterHoveredTab.removeAttribute("afterhovered"); + tabContainer._afterHoveredTab = null; + } + ]]></handler> + <handler event="dragstart" phase="capturing"> + this.style.MozUserFocus = ''; + </handler> + <handler event="mousedown" phase="capturing"> + <![CDATA[ + if (this.selected) { + this.style.MozUserFocus = 'ignore'; + this.clientTop; // just using this to flush style updates + } else if (this.mOverCloseButton || + this._overPlayingIcon) { + // Prevent tabbox.xml from selecting the tab. + event.stopPropagation(); + } + ]]> + </handler> + <handler event="mouseup"> + this.style.MozUserFocus = ''; + </handler> + <handler event="click"> + <![CDATA[ + if (event.button != 0) { + return; + } + + if (this._overPlayingIcon) { + this.toggleMuteAudio(); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-alltabs-popup" + extends="chrome://global/content/bindings/popup.xml#popup"> + <implementation implements="nsIDOMEventListener"> + <method name="_tabOnAttrModified"> + <parameter name="aEvent"/> + <body><![CDATA[ + var tab = aEvent.target; + if (tab.mCorrespondingMenuitem) + this._setMenuitemAttributes(tab.mCorrespondingMenuitem, tab); + ]]></body> + </method> + + <method name="_tabOnTabClose"> + <parameter name="aEvent"/> + <body><![CDATA[ + var tab = aEvent.target; + if (tab.mCorrespondingMenuitem) + this.removeChild(tab.mCorrespondingMenuitem); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "TabAttrModified": + this._tabOnAttrModified(aEvent); + break; + case "TabClose": + this._tabOnTabClose(aEvent); + break; + case "scroll": + this._updateTabsVisibilityStatus(); + break; + } + ]]></body> + </method> + + <method name="_updateTabsVisibilityStatus"> + <body><![CDATA[ + var tabContainer = gBrowser.tabContainer; + // We don't want menu item decoration unless there is overflow. + if (tabContainer.getAttribute("overflow") != "true") + return; + + var tabstripBO = tabContainer.mTabstrip.scrollBoxObject; + for (var i = 0; i < this.childNodes.length; i++) { + let curTab = this.childNodes[i].tab; + let curTabBO = curTab.boxObject; + if (curTabBO.screenX >= tabstripBO.screenX && + curTabBO.screenX + curTabBO.width <= tabstripBO.screenX + tabstripBO.width) + this.childNodes[i].setAttribute("tabIsVisible", "true"); + else + this.childNodes[i].removeAttribute("tabIsVisible"); + } + ]]></body> + </method> + + <method name="_createTabMenuItem"> + <parameter name="aTab"/> + <body><![CDATA[ + var menuItem = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "menuitem"); + + menuItem.setAttribute("class", "menuitem-iconic alltabs-item menuitem-with-favicon"); + + this._setMenuitemAttributes(menuItem, aTab); + + if (!aTab.mCorrespondingMenuitem) { + aTab.mCorrespondingMenuitem = menuItem; + menuItem.tab = aTab; + + this.appendChild(menuItem); + } + ]]></body> + </method> + + <method name="_setMenuitemAttributes"> + <parameter name="aMenuitem"/> + <parameter name="aTab"/> + <body><![CDATA[ + aMenuitem.setAttribute("label", aTab.label); + aMenuitem.setAttribute("crop", aTab.getAttribute("crop")); + + if (aTab.hasAttribute("busy")) { + aMenuitem.setAttribute("busy", aTab.getAttribute("busy")); + aMenuitem.removeAttribute("image"); + } else { + aMenuitem.setAttribute("image", aTab.getAttribute("image")); + aMenuitem.removeAttribute("busy"); + } + + if (aTab.hasAttribute("pending")) + aMenuitem.setAttribute("pending", aTab.getAttribute("pending")); + else + aMenuitem.removeAttribute("pending"); + + if (aTab.selected) + aMenuitem.setAttribute("selected", "true"); + else + aMenuitem.removeAttribute("selected"); + + function addEndImage() { + let endImage = document.createElement("image"); + endImage.setAttribute("class", "allTabs-endimage"); + let endImageContainer = document.createElement("hbox"); + endImageContainer.setAttribute("align", "center"); + endImageContainer.setAttribute("pack", "center"); + endImageContainer.appendChild(endImage); + aMenuitem.appendChild(endImageContainer); + return endImage; + } + + if (aMenuitem.firstChild) + aMenuitem.firstChild.remove(); + if (aTab.hasAttribute("muted")) + addEndImage().setAttribute("muted", "true"); + else if (aTab.hasAttribute("soundplaying")) + addEndImage().setAttribute("soundplaying", "true"); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="popupshowing"> + <![CDATA[ + var tabcontainer = gBrowser.tabContainer; + + // Listen for changes in the tab bar. + tabcontainer.addEventListener("TabAttrModified", this, false); + tabcontainer.addEventListener("TabClose", this, false); + tabcontainer.mTabstrip.addEventListener("scroll", this, false); + + let tabs = gBrowser.visibleTabs; + for (var i = 0; i < tabs.length; i++) { + if (!tabs[i].pinned) + this._createTabMenuItem(tabs[i]); + } + this._updateTabsVisibilityStatus(); + ]]></handler> + + <handler event="popuphidden"> + <![CDATA[ + // clear out the menu popup and remove the listeners + for (let i = this.childNodes.length - 1; i >= 0; i--) { + let menuItem = this.childNodes[i]; + if (menuItem.tab) { + menuItem.tab.mCorrespondingMenuitem = null; + this.removeChild(menuItem); + } + } + var tabcontainer = gBrowser.tabContainer; + tabcontainer.mTabstrip.removeEventListener("scroll", this, false); + tabcontainer.removeEventListener("TabAttrModified", this, false); + tabcontainer.removeEventListener("TabClose", this, false); + ]]></handler> + + <handler event="DOMMenuItemActive"> + <![CDATA[ + var tab = event.target.tab; + if (tab) { + let overLink = tab.linkedBrowser.currentURI.spec; + if (overLink == "about:blank") + overLink = ""; + XULBrowserWindow.setOverLink(overLink, null); + } + ]]></handler> + + <handler event="DOMMenuItemInactive"> + <![CDATA[ + XULBrowserWindow.setOverLink("", null); + ]]></handler> + + <handler event="command"><![CDATA[ + if (event.target.tab) + gBrowser.selectedTab = event.target.tab; + ]]></handler> + + </handlers> + </binding> + + <binding id="statuspanel" display="xul:hbox"> + <content> + <xul:hbox class="statuspanel-inner"> + <xul:label class="statuspanel-label" + role="status" + aria-live="off" + xbl:inherits="value=label,crop,mirror" + flex="1" + crop="end"/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor><![CDATA[ + window.addEventListener("resize", this, false); + ]]></constructor> + + <destructor><![CDATA[ + window.removeEventListener("resize", this, false); + MousePosTracker.removeListener(this); + ]]></destructor> + + <property name="label"> + <setter><![CDATA[ + if (!this.label) { + this.removeAttribute("mirror"); + this.removeAttribute("sizelimit"); + } + + this.style.minWidth = this.getAttribute("type") == "status" && + this.getAttribute("previoustype") == "status" + ? getComputedStyle(this).width : ""; + + if (val) { + this.setAttribute("label", val); + this.removeAttribute("inactive"); + this._calcMouseTargetRect(); + MousePosTracker.addListener(this); + } else { + this.setAttribute("inactive", "true"); + MousePosTracker.removeListener(this); + } + + return val; + ]]></setter> + <getter> + return this.hasAttribute("inactive") ? "" : this.getAttribute("label"); + </getter> + </property> + + <method name="getMouseTargetRect"> + <body><![CDATA[ + return this._mouseTargetRect; + ]]></body> + </method> + + <method name="onMouseEnter"> + <body> + this._mirror(); + </body> + </method> + + <method name="onMouseLeave"> + <body> + this._mirror(); + </body> + </method> + + <method name="handleEvent"> + <parameter name="event"/> + <body><![CDATA[ + if (!this.label) + return; + + switch (event.type) { + case "resize": + this._calcMouseTargetRect(); + break; + } + ]]></body> + </method> + + <method name="_calcMouseTargetRect"> + <body><![CDATA[ + let alignRight = false; + + if (getComputedStyle(document.documentElement).direction == "rtl") + alignRight = !alignRight; + + let rect = this.getBoundingClientRect(); + this._mouseTargetRect = { + top: rect.top, + bottom: rect.bottom, + left: alignRight ? window.innerWidth - rect.width : 0, + right: alignRight ? window.innerWidth : rect.width + }; + ]]></body> + </method> + + <method name="_mirror"> + <body> + if (this.hasAttribute("mirror")) + this.removeAttribute("mirror"); + else + this.setAttribute("mirror", "true"); + + if (!this.hasAttribute("sizelimit")) { + this.setAttribute("sizelimit", "true"); + this._calcMouseTargetRect(); + } + </body> + </method> + </implementation> + </binding> + +</bindings> diff --git a/browser/base/content/test/general/audio.ogg b/browser/base/content/test/general/audio.ogg Binary files differnew file mode 100644 index 000000000..7e6ef77ec --- /dev/null +++ b/browser/base/content/test/general/audio.ogg diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml new file mode 100644 index 000000000..a17304cfa --- /dev/null +++ b/browser/base/content/urlbarBindings.xml @@ -0,0 +1,1798 @@ +<?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 bindings [ +<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd"> +%notificationDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> +%browserDTD; +]> + +<bindings id="urlbarBindings" 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="urlbar" extends="chrome://browser/content/autocomplete.xml#private-autocomplete"> + + <content sizetopopup="pref"> + <xul:hbox anonid="textbox-container" + class="private-autocomplete-textbox-container urlbar-textbox-container" + flex="1" xbl:inherits="focused"> + <children includes="image|deck|stack|box"> + <xul:image class="private-autocomplete-icon" allowevents="true"/> + </children> + <xul:hbox anonid="textbox-input-box" + class="textbox-input-box urlbar-input-box" + flex="1" xbl:inherits="tooltiptext=inputtooltiptext"> + <children/> + <html:input anonid="input" + class="private-autocomplete-textbox urlbar-input textbox-input uri-element-right-align" + allowevents="true" + xbl:inherits="tooltiptext=inputtooltiptext,value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/> + </xul:hbox> + <children includes="hbox"/> + </xul:hbox> + <xul:dropmarker anonid="historydropmarker" + class="private-autocomplete-history-dropmarker urlbar-history-dropmarker" + allowevents="true" + xbl:inherits="open,enablehistory,parentfocused=focused"/> + <xul:popupset anonid="popupset" + class="private-autocomplete-result-popupset"/> + <children includes="toolbarbutton"/> + </content> + + <implementation implements="nsIObserver, nsIDOMEventListener"> + <constructor><![CDATA[ + this._prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService) + .getBranch("browser.urlbar."); + + this._prefs.addObserver("", this, false); + this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll"); + this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll"); + this.completeDefaultIndex = this._prefs.getBoolPref("autoFill"); + this.timeout = this._prefs.getIntPref("delay"); + this._formattingEnabled = this._prefs.getBoolPref("formatting.enabled"); + this._mayTrimURLs = this._prefs.getBoolPref("trimURLs"); + + this.inputField.controllers.insertControllerAt(0, this._copyCutController); + this.inputField.addEventListener("mousedown", this, false); + this.inputField.addEventListener("mousemove", this, false); + this.inputField.addEventListener("mouseout", this, false); + this.inputField.addEventListener("overflow", this, false); + this.inputField.addEventListener("underflow", this, false); + + const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var textBox = document.getAnonymousElementByAttribute(this, + "anonid", "textbox-input-box"); + var cxmenu = document.getAnonymousElementByAttribute(textBox, + "anonid", "input-box-contextmenu"); + var pasteAndGo; + cxmenu.addEventListener("popupshowing", function() { + if (!pasteAndGo) + return; + var controller = document.commandDispatcher.getControllerForCommand("cmd_paste"); + var enabled = controller.isCommandEnabled("cmd_paste"); + if (enabled) + pasteAndGo.removeAttribute("disabled"); + else + pasteAndGo.setAttribute("disabled", "true"); + }, false); + + var insertLocation = cxmenu.firstChild; + while (insertLocation.nextSibling && + insertLocation.getAttribute("cmd") != "cmd_paste") + insertLocation = insertLocation.nextSibling; + if (insertLocation) { + pasteAndGo = document.createElement("menuitem"); + let label = Services.strings.createBundle("chrome://browser/locale/browser.properties"). + GetStringFromName("pasteAndGo.label"); + pasteAndGo.setAttribute("label", label); + pasteAndGo.setAttribute("anonid", "paste-and-go"); + pasteAndGo.setAttribute("oncommand", + "gURLBar.select(); goDoCommand('cmd_paste'); gURLBar.handleCommand();"); + cxmenu.insertBefore(pasteAndGo, insertLocation.nextSibling); + } + ]]></constructor> + + <destructor><![CDATA[ + this._prefs.removeObserver("", this); + this._prefs = null; + this.inputField.controllers.removeController(this._copyCutController); + this.inputField.removeEventListener("mousedown", this, false); + this.inputField.removeEventListener("mousemove", this, false); + this.inputField.removeEventListener("mouseout", this, false); + this.inputField.removeEventListener("overflow", this, false); + this.inputField.removeEventListener("underflow", this, false); + ]]></destructor> + + <field name="_value">""</field> + + <!-- + onBeforeValueGet is called by the base-binding's .value getter. + It can return an object with a "value" property, to override the + return value of the getter. + --> + <method name="onBeforeValueGet"> + <body><![CDATA[ + if (this.hasAttribute("actiontype")) + return {value: this._value}; + return null; + ]]></body> + </method> + + <!-- + onBeforeValueSet is called by the base-binding's .value setter. + It should return the value that the setter should use. + --> + <method name="onBeforeValueSet"> + <parameter name="aValue"/> + <body><![CDATA[ + this._value = aValue; + var returnValue = aValue; + var action = this._parseActionUrl(aValue); + + if (action) { + returnValue = action.param; + } + + // Set the actiontype only if the user is not overriding actions. + if (action && this._noActionsKeys.size == 0) { + this.setAttribute("actiontype", action.type); + } else { + this.removeAttribute("actiontype"); + } + return returnValue; + ]]></body> + </method> + + <field name="_mayTrimURLs">true</field> + <method name="trimValue"> + <parameter name="aURL"/> + <body><![CDATA[ + // This method must not modify the given URL such that calling + // nsIURIFixup::createFixupURI with the result will produce a different URI. + return this._mayTrimURLs ? trimURL(aURL) : aURL; + ]]></body> + </method> + + <field name="_formattingEnabled">true</field> + <method name="formatValue"> + <body><![CDATA[ + if (!this._formattingEnabled || this.focused) + return; + + let controller = this.editor.selectionController; + let selection = controller.getSelection(controller.SELECTION_URLSECONDARY); + selection.removeAllRanges(); + + let textNode = this.editor.rootElement.firstChild; + let value = textNode.textContent; + + let protocol = value.match(/^[a-z\d.+\-]+:(?=[^\d])/); + if (protocol && + ["http:", "https:", "ftp:"].indexOf(protocol[0]) == -1) + return; + let matchedURL = value.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/); + if (!matchedURL) + return; + + let [, preDomain, domain] = matchedURL; + let baseDomain = domain; + let subDomain = ""; + // getBaseDomainFromHost doesn't recognize IPv6 literals in brackets as IPs (bug 667159) + if (domain[0] != "[") { + try { + baseDomain = Services.eTLD.getBaseDomainFromHost(domain); + if (!domain.endsWith(baseDomain)) { + // getBaseDomainFromHost converts its resultant to ACE. + let IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + baseDomain = IDNService.convertACEtoUTF8(baseDomain); + } + } catch (e) {} + } + if (baseDomain != domain) { + subDomain = domain.slice(0, -baseDomain.length); + } + + let rangeLength = preDomain.length + subDomain.length; + if (rangeLength) { + let range = document.createRange(); + range.setStart(textNode, 0); + range.setEnd(textNode, rangeLength); + selection.addRange(range); + } + + let startRest = preDomain.length + domain.length; + if (startRest < value.length) { + let range = document.createRange(); + range.setStart(textNode, startRest); + range.setEnd(textNode, value.length); + selection.addRange(range); + } + ]]></body> + </method> + + <method name="_clearFormatting"> + <body><![CDATA[ + if (!this._formattingEnabled) + return; + + let controller = this.editor.selectionController; + let selection = controller.getSelection(controller.SELECTION_URLSECONDARY); + selection.removeAllRanges(); + ]]></body> + </method> + + <method name="handleRevert"> + <body><![CDATA[ + var isScrolling = this.popupOpen; + + gBrowser.userTypedValue = null; + + // don't revert to last valid url unless page is NOT loading + // and user is NOT key-scrolling through autocomplete list + if (!XULBrowserWindow.isBusy && !isScrolling) { + URLBarSetURI(); + + // If the value isn't empty and the urlbar has focus, select the value. + if (this.value && this.hasAttribute("focused")) + this.select(); + } + + // tell widget to revert to last typed text only if the user + // was scrolling when they hit escape + return !isScrolling; + ]]></body> + </method> + + <method name="handleCommand"> + <parameter name="aTriggeringEvent"/> + <body><![CDATA[ + if (aTriggeringEvent instanceof MouseEvent && aTriggeringEvent.button == 2) + return; // Do nothing for right clicks + + var url = this.value; + var mayInheritPrincipal = false; + var postData = null; + + var action = this._parseActionUrl(url); + let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange; + + let matchLastLocationChange = true; + if (action) { + url = action.param; + if (this.hasAttribute("actiontype")) { + if (action.type == "switchtab") { + this.handleRevert(); + let prevTab = gBrowser.selectedTab; + if (switchToTabHavingURI(url) && + isTabEmpty(prevTab)) + gBrowser.removeTab(prevTab); + } + return; + } + continueOperation.call(this); + } + else { + this._canonizeURL(aTriggeringEvent, response => { + [url, postData, mayInheritPrincipal] = response; + if (url) { + matchLastLocationChange = (lastLocationChange == + gBrowser.selectedBrowser.lastLocationChange); + continueOperation.call(this); + } + }); + } + + function continueOperation() + { + this.value = url; + gBrowser.userTypedValue = url; + try { + addToUrlbarHistory(url); + } catch (ex) { + // Things may go wrong when adding url to session history, + // but don't let that interfere with the loading of the url. + Cu.reportError(ex); + } + + // Reset DOS mitigations for the basic auth prompt. + let browser = gBrowser.selectedBrowser; + delete browser.authPromptCounter; + + function loadCurrent() { + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + // Pass LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL to prevent any loads from + // inheriting the currently loaded document's principal, unless this + // URL is marked as safe to inherit (e.g. came from a bookmark + // keyword). + if (!mayInheritPrincipal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; + // If the value wasn't typed, we know that we decoded the value as + // UTF-8 (see losslessDecodeURI) + if (!this.valueIsTyped) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_URI_IS_UTF8; + gBrowser.loadURIWithFlags(url, flags, null, null, postData); + } + + // Focus the content area before triggering loads, since if the load + // occurs in a new tab, we want focus to be restored to the content + // area when the current tab is re-selected. + gBrowser.selectedBrowser.focus(); + + let isMouseEvent = aTriggeringEvent instanceof MouseEvent; + + // If the current tab is empty, ignore Alt+Enter (just reuse this tab) + let altEnter = !isMouseEvent && aTriggeringEvent && + aTriggeringEvent.altKey && !isTabEmpty(gBrowser.selectedTab); + + if (isMouseEvent || altEnter) { + // Use the standard UI link behaviors for clicks or Alt+Enter + let where = "tab"; + if (isMouseEvent) + where = whereToOpenLink(aTriggeringEvent, false, false); + + if (where == "current") { + if (matchLastLocationChange) { + loadCurrent(); + } + } else { + this.handleRevert(); + let params = { allowThirdPartyFixup: true, + postData: postData, + initiatingDoc: document }; + if (!this.valueIsTyped) + params.isUTF8 = true; + openUILinkIn(url, where, params); + } + } else { + if (matchLastLocationChange) { + loadCurrent(); + } + } + } + ]]></body> + </method> + + <method name="_canonizeURL"> + <parameter name="aTriggeringEvent"/> + <parameter name="aCallback"/> + <body><![CDATA[ + var url = this.value; + if (!url) { + aCallback(["", null, false]); + return; + } + + // Only add the suffix when the URL bar value isn't already "URL-like", + // and only if we get a keyboard event, to match user expectations. + if (/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(url) && + (aTriggeringEvent instanceof KeyEvent)) { + let accel = aTriggeringEvent.ctrlKey; + let shift = aTriggeringEvent.shiftKey; + + let suffix = ""; + + switch (true) { + case (accel && shift): + suffix = ".org/"; + break; + case (shift): + suffix = ".net/"; + break; + case (accel): + try { + suffix = gPrefService.getCharPref("browser.fixup.alternate.suffix"); + if (suffix.charAt(suffix.length - 1) != "/") + suffix += "/"; + } catch(e) { + suffix = ".com/"; + } + break; + } + + if (suffix) { + // trim leading/trailing spaces (bug 233205) + url = url.trim(); + + // Tack www. and suffix on. If user has appended directories, insert + // suffix before them (bug 279035). Be careful not to get two slashes. + + let firstSlash = url.indexOf("/"); + + if (firstSlash >= 0) { + url = url.substring(0, firstSlash) + suffix + + url.substring(firstSlash + 1); + } else { + url = url + suffix; + } + + url = "http://www." + url; + } + } + + getShortcutOrURIAndPostData(url).then(data => { + aCallback([data.url, data.postData, data.mayInheritPrincipal]); + }); + ]]></body> + </method> + + <field name="_contentIsCropped">false</field> + + <method name="_initURLTooltip"> + <body><![CDATA[ + if (this.focused || !this._contentIsCropped) + return; + this.inputField.setAttribute("tooltiptext", this.value); + ]]></body> + </method> + + <method name="_hideURLTooltip"> + <body><![CDATA[ + this.inputField.removeAttribute("tooltiptext"); + ]]></body> + </method> + + <method name="onDragOver"> + <parameter name="aEvent"/> + <body> + var types = aEvent.dataTransfer.types; + if (types.contains("application/x-moz-file") || + types.contains("text/x-moz-url") || + types.contains("text/uri-list") || + types.contains("text/unicode")) + aEvent.preventDefault(); + </body> + </method> + + <method name="onDrop"> + <parameter name="aEvent"/> + <body><![CDATA[ + let links = browserDragAndDrop.dropLinks(aEvent); + + // The URL bar automatically handles inputs with newline characters, + // so we can get away with treating text/x-moz-url flavours as text/plain. + if (links.length > 0 && links[0].url) { + let url = links[0].url; + aEvent.preventDefault(); + this.value = url; + SetPageProxyState("invalid"); + this.focus(); + try { + urlSecurityCheck(url, + gBrowser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + } catch (ex) { + return; + } + this.handleCommand(); + // Force not showing the dropped URI immediately. + gBrowser.userTypedValue = null; + URLBarSetURI(); + } + ]]></body> + </method> + + <method name="_getSelectedValueForClipboard"> + <body><![CDATA[ + // Grab the actual input field's value, not our value, which could include moz-action: + var inputVal = this.inputField.value; + var selectedVal = inputVal.substring(this.selectionStart, this.selectionEnd); + + // If the selection doesn't start at the beginning or doesn't span the full domain or + // the URL bar is modified, nothing else to do here. + if (this.selectionStart > 0 || this.valueIsTyped) + return selectedVal; + // The selection doesn't span the full domain if it doesn't contain a slash and is + // followed by some character other than a slash. + if (!selectedVal.includes("/")) { + let remainder = inputVal.replace(selectedVal, ""); + if (remainder != "" && remainder[0] != "/") + return selectedVal; + } + + let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); + + let uri; + if (this.getAttribute("pageproxystate") == "valid") { + uri = gBrowser.currentURI; + } else { + // We're dealing with an autocompleted value, create a new URI from that. + try { + uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_NONE); + } catch (e) {} + if (!uri) + return selectedVal; + } + + // Only copy exposable URIs + try { + uri = uriFixup.createExposableURI(uri); + } catch (ex) {} + + // If the entire URL is selected, just use the actual loaded URI, + // unless we want a decoded URI, or it's a data: or javascript: URI, + // since those are hard to read when encoded. + if (inputVal == selectedVal && + !uri.schemeIs("javascript") && !uri.schemeIs("data") && + !Services.prefs.getBoolPref("browser.urlbar.decodeURLsOnCopy")) { + return uri.spec; + } + + // Just the beginning of the URL is selected, or we want a decoded + // url. First check for a trimmed value. + let spec = uri.spec; + let trimmedSpec = this.trimValue(spec); + if (spec != trimmedSpec) { + // Prepend the portion that trimValue removed from the beginning. + // This assumes trimValue will only truncate the URL at + // the beginning or end (or both). + let trimmedSegments = spec.split(trimmedSpec); + selectedVal = trimmedSegments[0] + selectedVal; + } + + return selectedVal; + ]]></body> + </method> + + <field name="_copyCutController"><![CDATA[ + ({ + urlbar: this, + doCommand: function(aCommand) { + var urlbar = this.urlbar; + var val = urlbar._getSelectedValueForClipboard(); + if (!val) + return; + + if (aCommand == "cmd_cut" && this.isCommandEnabled(aCommand)) { + let start = urlbar.selectionStart; + let end = urlbar.selectionEnd; + urlbar.inputField.value = urlbar.inputField.value.substring(0, start) + + urlbar.inputField.value.substring(end); + urlbar.selectionStart = urlbar.selectionEnd = start; + urlbar.removeAttribute("actiontype"); + SetPageProxyState("invalid"); + } + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(val, document); + }, + supportsCommand: function(aCommand) { + switch (aCommand) { + case "cmd_copy": + case "cmd_cut": + return true; + } + return false; + }, + isCommandEnabled: function(aCommand) { + return this.supportsCommand(aCommand) && + (aCommand != "cmd_cut" || !this.urlbar.readOnly) && + this.urlbar.selectionStart < this.urlbar.selectionEnd; + }, + onEvent: function(aEventName) {} + }) + ]]></field> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + if (aTopic == "nsPref:changed") { + switch (aData) { + case "clickSelectsAll": + case "doubleClickSelectsAll": + this[aData] = this._prefs.getBoolPref(aData); + break; + case "autoFill": + this.completeDefaultIndex = this._prefs.getBoolPref(aData); + break; + case "delay": + this.timeout = this._prefs.getIntPref(aData); + break; + case "formatting.enabled": + this._formattingEnabled = this._prefs.getBoolPref(aData); + break; + case "trimURLs": + this._mayTrimURLs = this._prefs.getBoolPref(aData); + break; + } + } + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "mousedown": + if (this.doubleClickSelectsAll && + aEvent.button == 0 && aEvent.detail == 2) { + this.editor.selectAll(); + aEvent.preventDefault(); + } + break; + case "mousemove": + this._initURLTooltip(); + break; + case "mouseout": + this._hideURLTooltip(); + break; + case "overflow": + this._contentIsCropped = true; + break; + case "underflow": + this._contentIsCropped = false; + this._hideURLTooltip(); + break; + } + ]]></body> + </method> + + <property name="textValue" + onget="return this.value;"> + <setter> + <![CDATA[ + try { + val = losslessDecodeURI(makeURI(val)); + } catch (ex) { } + + // Trim popup selected values, but never trim results coming from + // autofill. + if (this.popup.selectedIndex == -1) + this._disableTrim = true; + this.value = val; + this._disableTrim = false; + + // 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="_parseActionUrl"> + <parameter name="aUrl"/> + <body><![CDATA[ + if (!aUrl.startsWith("moz-action:")) + return null; + + // url is in the format moz-action:ACTION,PARAM + let [, action, param] = aUrl.match(/^moz-action:([^,]+),(.*)$/); + return {type: action, param: param}; + ]]></body> + </method> + + <field name="_noActionsKeys"><![CDATA[ + new Set(); + ]]></field> + + <method name="_clearNoActions"> + <parameter name="aURL"/> + <body><![CDATA[ + this._noActionsKeys.clear(); + this.popup.removeAttribute("noactions"); + let action = this._parseActionUrl(this._value); + if (action) + this.setAttribute("actiontype", action.type); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="keydown"><![CDATA[ + if ((event.keyCode === KeyEvent.DOM_VK_ALT || + event.keyCode === KeyEvent.DOM_VK_SHIFT) && + this.popup.selectedIndex >= 0 && + !this._noActionsKeys.has(event.keyCode)) { + if (this._noActionsKeys.size == 0) { + this.popup.setAttribute("noactions", "true"); + this.removeAttribute("actiontype"); + } + this._noActionsKeys.add(event.keyCode); + } + ]]></handler> + + <handler event="keyup"><![CDATA[ + if ((event.keyCode === KeyEvent.DOM_VK_ALT || + event.keyCode === KeyEvent.DOM_VK_SHIFT) && + this._noActionsKeys.has(event.keyCode)) { + this._noActionsKeys.delete(event.keyCode); + if (this._noActionsKeys.size == 0) + this._clearNoActions(); + } + ]]></handler> + + <handler event="blur"><![CDATA[ + if (event.originalTarget != this.inputField) + return; + this._clearNoActions(); + this.formatValue(); + ]]></handler> + + <handler event="dragstart" phase="capturing"><![CDATA[ + // Drag only if the gesture starts from the input field. + if (event.originalTarget != this.inputField) + return; + + // Drag only if the entire value is selected and it's a valid URI. + var isFullSelection = this.selectionStart == 0 && + this.selectionEnd == this.textLength; + if (!isFullSelection || + this.getAttribute("pageproxystate") != "valid") + return; + + var urlString = content.location.href; + var title = content.document.title || urlString; + var htmlString = "<a href=\"" + urlString + "\">" + urlString + "</a>"; + + var dt = event.dataTransfer; + dt.setData("text/x-moz-url", urlString + "\n" + title); + dt.setData("text/unicode", urlString); + dt.setData("text/html", htmlString); + + dt.effectAllowed = "copyLink"; + event.stopPropagation(); + ]]></handler> + + <handler event="focus" phase="capturing"><![CDATA[ + if (event.originalTarget != this.inputField) + return; + this._hideURLTooltip(); + this._clearFormatting(); + ]]></handler> + + <handler event="dragover" phase="capturing" action="this.onDragOver(event, this);"/> + <handler event="drop" phase="capturing" action="this.onDrop(event, this);"/> + <handler event="select"><![CDATA[ + if (!Cc["@mozilla.org/widget/clipboard;1"] + .getService(Ci.nsIClipboard) + .supportsSelectionClipboard()) + return; + + // Check if this selection was actually user-generated, and exit if not + // to prevent copying the selection (e.g autofill) to clipboard/primary + if (!window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .isHandlingUserInput) + return; + + var val = this._getSelectedValueForClipboard(); + if (!val) + return; + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyStringToClipboard(val, Ci.nsIClipboard.kSelectionClipboard, document); + ]]></handler> + </handlers> + + </binding> + + <!-- Note: this binding is applied to the autocomplete popup used in the Search bar and in web page content --> + <binding id="browser-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-result-popup"> + <implementation> + <method name="openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body> + <![CDATA[ + // initially the panel is hidden + // to avoid impacting startup / new window performance + aInput.popup.hidden = false; + + // this method is defined on the base binding + this._openAutocompletePopup(aInput, aElement); + ]]></body> + </method> + + <method name="onPopupClick"> + <parameter name="aEvent"/> + <body><![CDATA[ + // Ignore all right-clicks + if (aEvent.button == 2) + return; + + var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); + + // Check for unmodified left-click, and use default behavior + if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey && + !aEvent.altKey && !aEvent.metaKey) { + controller.handleEnter(true); + return; + } + + // Check for middle-click or modified clicks on the search bar + var searchBar = BrowserSearch.searchBar; + if (searchBar && searchBar.textbox == this.mInput) { + // Handle search bar popup clicks + var search = controller.getValueAt(this.selectedIndex); + + // close the autocomplete popup and revert the entered search term + this.closePopup(); + controller.handleEscape(); + + // Fill in the search bar's value + searchBar.value = search; + + // open the search results according to the clicking subtlety + var where = whereToOpenLink(aEvent, false, true); + searchBar.doSearch(search, where); + } + ]]></body> + </method> + </implementation> + </binding> + + <binding id="urlbar-rich-result-popup" extends="chrome://browser/content/autocomplete.xml#private-autocomplete-rich-result-popup"> + <implementation> + <field name="_maxResults">0</field> + + <field name="_bundle" readonly="true"> + Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle("chrome://browser/locale/places/places.properties"); + </field> + + <property name="maxResults" readonly="true"> + <getter> + <![CDATA[ + if (!this._maxResults) { + var prefService = + Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + this._maxResults = prefService.getIntPref("browser.urlbar.maxRichResults"); + } + return this._maxResults; + ]]> + </getter> + </property> + + <method name="openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body> + <![CDATA[ + // initially the panel is hidden + // to avoid impacting startup / new window performance + aInput.popup.hidden = false; + + // this method is defined on the base binding + this._openAutocompletePopup(aInput, aElement); + ]]></body> + </method> + + <method name="onPopupClick"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + // Ignore right-clicks + if (aEvent.button == 2) + return; + + var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); + + // Check for unmodified left-click, and use default behavior + if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey && + !aEvent.altKey && !aEvent.metaKey) { + controller.handleEnter(true); + return; + } + + // Check for middle-click or modified clicks on the URL bar + if (gURLBar && this.mInput == gURLBar) { + var url = controller.getValueAt(this.selectedIndex); + + // close the autocomplete popup and revert the entered address + this.closePopup(); + controller.handleEscape(); + + // Check if this is meant to be an action + let action = this.mInput._parseActionUrl(url); + if (action) { + if (action.type == "switchtab") + url = action.param; + else + return; + } + + // respect the usual clicking subtleties + openUILink(url, aEvent); + } + ]]> + </body> + </method> + + <method name="createResultLabel"> + <parameter name="aTitle"/> + <parameter name="aUrl"/> + <parameter name="aType"/> + <body> + <![CDATA[ + var label = aTitle + " " + aUrl; + // convert aType (ex: "ac-result-type-<aType>") to text to be spoke aloud + // by screen readers. convert "tag" and "bookmark" to the localized versions, + // but don't do anything for "favicon" (the default) + if (aType != "favicon") { + label += " " + this._bundle.GetStringFromName(aType + "ResultLabel"); + } + return label; + ]]> + </body> + </method> + + </implementation> + </binding> + + <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification"> + <content align="start"> + <xul:image class="popup-notification-icon" + xbl:inherits="popupid,src=icon"/> + <xul:vbox flex="1"> + <xul:description class="popup-notification-description addon-progress-description" + xbl:inherits="xbl:text=label"/> + <xul:spacer flex="1"/> + <xul:hbox align="center"> + <xul:progressmeter anonid="progressmeter" flex="1" mode="undetermined" class="popup-progress-meter"/> + <xul:button anonid="cancel" class="popup-progress-cancel" oncommand="document.getBindingParent(this).cancel()"/> + </xul:hbox> + <xul:label anonid="progresstext" class="popup-progress-label"/> + <xul:hbox class="popup-notification-button-container" + pack="end" align="center"> + <xul:button anonid="button" + class="popup-notification-menubutton" + type="menu-button" + xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey"> + <xul:menupopup anonid="menupopup" + xbl:inherits="oncommand=menucommand"> + <children/> + <xul:menuitem class="menuitem-iconic popup-notification-closeitem close-icon" + label="&closeNotificationItem.label;" + xbl:inherits="oncommand=closeitemcommand"/> + </xul:menupopup> + </xul:button> + </xul:hbox> + </xul:vbox> + <xul:vbox pack="start"> + <xul:toolbarbutton anonid="closebutton" + class="messageCloseButton close-icon popup-notification-closebutton tabbable" + xbl:inherits="oncommand=closebuttoncommand" + tooltiptext="&closeNotification.tooltip;"/> + </xul:vbox> + </content> + <implementation> + <constructor><![CDATA[ + this.cancelbtn.setAttribute("tooltiptext", gNavigatorBundle.getString("addonDownloadCancelTooltip")); + + this.notification.options.installs.forEach(function(aInstall) { + aInstall.addListener(this); + }, this); + + // Calling updateProgress can sometimes cause this notification to be + // removed in the middle of refreshing the notification panel which + // makes the panel get refreshed again. Just initialise to the + // undetermined state and then schedule a proper check at the next + // opportunity + this.setProgress(0, -1); + this._updateProgressTimeout = setTimeout(this.updateProgress.bind(this), 0); + ]]></constructor> + + <destructor><![CDATA[ + this.destroy(); + ]]></destructor> + + <field name="progressmeter" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "progressmeter"); + </field> + <field name="progresstext" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "progresstext"); + </field> + <field name="cancelbtn" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "cancel"); + </field> + <field name="DownloadUtils" readonly="true"> + { + let utils = {}; + Components.utils.import("resource://gre/modules/DownloadUtils.jsm", utils); + utils.DownloadUtils; + } + </field> + + <method name="destroy"> + <body><![CDATA[ + this.notification.options.installs.forEach(function(aInstall) { + aInstall.removeListener(this); + }, this); + clearTimeout(this._updateProgressTimeout); + ]]></body> + </method> + + <method name="setProgress"> + <parameter name="aProgress"/> + <parameter name="aMaxProgress"/> + <body><![CDATA[ + if (aMaxProgress == -1) { + this.progressmeter.mode = "undetermined"; + } + else { + this.progressmeter.mode = "determined"; + this.progressmeter.value = (aProgress * 100) / aMaxProgress; + } + + let now = Date.now(); + + if (!this.notification.lastUpdate) { + this.notification.lastUpdate = now; + this.notification.lastProgress = aProgress; + return; + } + + let delta = now - this.notification.lastUpdate; + if ((delta < 400) && (aProgress < aMaxProgress)) + return; + + delta /= 1000; + + // This code is taken from nsDownloadManager.cpp + let speed = (aProgress - this.notification.lastProgress) / delta; + if (this.notification.speed) + speed = speed * 0.9 + this.notification.speed * 0.1; + + this.notification.lastUpdate = now; + this.notification.lastProgress = aProgress; + this.notification.speed = speed; + + let status = null; + [status, this.notification.last] = this.DownloadUtils.getDownloadStatus(aProgress, aMaxProgress, speed, this.notification.last); + this.progresstext.value = status; + ]]></body> + </method> + + <method name="cancel"> + <body><![CDATA[ + // Cache these as cancelling the installs will remove this + // notification which will drop these references + let browser = this.notification.browser; + let contentWindow = this.notification.options.contentWindow; + let sourceURI = this.notification.options.sourceURI; + + let installs = this.notification.options.installs; + installs.forEach(function(aInstall) { + try { + aInstall.cancel(); + } + catch (e) { + // Cancel will throw if the download has already failed + } + }, this); + + let anchorID = "addons-notification-icon"; + let notificationID = "addon-install-cancelled"; + let messageString = gNavigatorBundle.getString("addonDownloadCancelled"); + messageString = PluralForm.get(installs.length, messageString); + let buttonText = gNavigatorBundle.getString("addonDownloadRestart"); + buttonText = PluralForm.get(installs.length, buttonText); + + let action = { + label: buttonText, + accessKey: gNavigatorBundle.getString("addonDownloadRestart.accessKey"), + callback: function() { + let weblistener = Cc["@mozilla.org/addons/web-install-listener;1"]. + getService(Ci.amIWebInstallListener); + if (weblistener.onWebInstallRequested(contentWindow, sourceURI, + installs, installs.length)) { + installs.forEach(function(aInstall) { + aInstall.install(); + }); + } + } + }; + + PopupNotifications.show(browser, notificationID, messageString, + anchorID, action); + ]]></body> + </method> + + <method name="updateProgress"> + <body><![CDATA[ + let downloadingCount = 0; + let progress = 0; + let maxProgress = 0; + + this.notification.options.installs.forEach(function(aInstall) { + if (aInstall.maxProgress == -1) + maxProgress = -1; + progress += aInstall.progress; + if (maxProgress >= 0) + maxProgress += aInstall.maxProgress; + if (aInstall.state < AddonManager.STATE_DOWNLOADED) + downloadingCount++; + }); + + if (downloadingCount == 0) { + this.destroy(); + PopupNotifications.remove(this.notification); + } + else { + this.setProgress(progress, maxProgress); + } + ]]></body> + </method> + + <method name="onDownloadProgress"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + + <method name="onDownloadFailed"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + + <method name="onDownloadCancelled"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + + <method name="onDownloadEnded"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + </implementation> + </binding> + + <binding id="plugin-popupnotification-center-item"> + <content align="center"> + <xul:vbox pack="center" anonid="itemBox" class="itemBox"> + <xul:description anonid="center-item-label" class="center-item-label" /> + <xul:hbox flex="1" pack="start" align="center" anonid="center-item-warning"> + <xul:image anonid="center-item-warning-icon" class="center-item-warning-icon"/> + <xul:label anonid="center-item-warning-label"/> + <xul:label anonid="center-item-link" value="&checkForUpdates;" class="text-link"/> + </xul:hbox> + </xul:vbox> + <xul:vbox pack="center"> + <xul:menulist class="center-item-menulist" + anonid="center-item-menulist"> + <xul:menupopup> + <xul:menuitem anonid="allownow" value="allownow" + label="&pluginActivateNow.label;" /> + <xul:menuitem anonid="allowalways" value="allowalways" + label="&pluginActivateAlways.label;" /> + <xul:menuitem anonid="block" value="block" + label="&pluginBlockNow.label;" /> + </xul:menupopup> + </xul:menulist> + </xul:vbox> + </content> + <resources> + <stylesheet src="chrome://global/skin/notification.css"/> + </resources> + <implementation> + <constructor><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "center-item-label").value = this.action.pluginName; + + let curState = "block"; + if (this.action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) { + if (this.action.pluginPermissionType == Ci.nsIPermissionManager.EXPIRE_SESSION) { + curState = "allownow"; + } + else { + curState = "allowalways"; + } + } + document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").value = curState; + + let warningString = ""; + let linkString = ""; + + let link = document.getAnonymousElementByAttribute(this, "anonid", "center-item-link"); + + let url; + let linkHandler; + + if (this.action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) { + document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true; + warningString = gNavigatorBundle.getString("pluginActivateDisabled.label"); + linkString = gNavigatorBundle.getString("pluginActivateDisabled.manage"); + linkHandler = function(event) { + event.preventDefault(); + gPluginHandler.managePlugins(); + }; + document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-icon").hidden = true; + } + else { + url = this.action.detailsLink; + + switch (this.action.blocklistState) { + case Ci.nsIBlocklistService.STATE_NOT_BLOCKED: + document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning").hidden = true; + break; + case Ci.nsIBlocklistService.STATE_BLOCKED: + document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true; + warningString = gNavigatorBundle.getString("pluginActivateBlocked.label"); + linkString = gNavigatorBundle.getString("pluginActivate.learnMore"); + break; + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + warningString = gNavigatorBundle.getString("pluginActivateOutdated.label"); + linkString = gNavigatorBundle.getString("pluginActivate.updateLabel"); + break; + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + warningString = gNavigatorBundle.getString("pluginActivateVulnerable.label"); + linkString = gNavigatorBundle.getString("pluginActivate.riskLabel"); + break; + } + } + document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-label").value = warningString; + + if (url || linkHandler) { + link.value = linkString; + if (url) { + link.href = url; + } + if (linkHandler) { + link.addEventListener("click", linkHandler, false); + } + } + else { + link.hidden = true; + } + ]]></constructor> + <property name="value"> + <getter> + return document.getAnonymousElementByAttribute(this, "anonid", + "center-item-menulist").value; + </getter> + <setter><!-- This should be used only in automated tests --> + document.getAnonymousElementByAttribute(this, "anonid", + "center-item-menulist").value = val; + </setter> + </property> + </implementation> + </binding> + + <binding id="click-to-play-plugins-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification"> + <content align="start" class="click-to-play-plugins-notification-content"> + <xul:vbox flex="1" align="stretch" class="popup-notification-main-box" + xbl:inherits="popupid"> + <xul:hbox class="click-to-play-plugins-notification-description-box" flex="1" align="start"> + <xul:description class="click-to-play-plugins-outer-description" flex="1"> + <html:span anonid="click-to-play-plugins-notification-description" /> + <xul:label class="text-link click-to-play-plugins-notification-link" anonid="click-to-play-plugins-notification-link" /> + </xul:description> + <xul:toolbarbutton anonid="closebutton" + class="messageCloseButton close-icon popup-notification-closebutton tabbable" + xbl:inherits="oncommand=closebuttoncommand" + tooltiptext="&closeNotification.tooltip;"/> + </xul:hbox> + <xul:grid anonid="click-to-play-plugins-notification-center-box" + class="click-to-play-plugins-notification-center-box"> + <xul:columns> + <xul:column flex="1"/> + <xul:column/> + </xul:columns> + <xul:rows> + <children includes="row"/> + <xul:hbox pack="start" anonid="plugin-notification-showbox"> + <xul:button label="&pluginNotification.showAll.label;" + accesskey="&pluginNotification.showAll.accesskey;" + class="plugin-notification-showbutton" + oncommand="document.getBindingParent(this)._setState(2)"/> + </xul:hbox> + </xul:rows> + </xul:grid> + <xul:hbox anonid="button-container" + class="click-to-play-plugins-notification-button-container" + pack="center" align="center"> + <xul:button anonid="primarybutton" + class="click-to-play-popup-button primary-button" + oncommand="document.getBindingParent(this)._onButton(this)" + flex="1"/> + <xul:button anonid="secondarybutton" + class="click-to-play-popup-button" + oncommand="document.getBindingParent(this)._onButton(this);" + flex="1"/> + </xul:hbox> + <xul:box hidden="true"> + <children/> + </xul:box> + </xul:vbox> + </content> + <resources> + <stylesheet src="chrome://global/skin/notification.css"/> + </resources> + <implementation> + <field name="_states"> + ({SINGLE: 0, MULTI_COLLAPSED: 1, MULTI_EXPANDED: 2}) + </field> + <field name="_primaryButton"> + document.getAnonymousElementByAttribute(this, "anonid", "primarybutton"); + </field> + <field name="_secondaryButton"> + document.getAnonymousElementByAttribute(this, "anonid", "secondarybutton") + </field> + <field name="_buttonContainer"> + document.getAnonymousElementByAttribute(this, "anonid", "button-container") + </field> + <field name="_brandShortName"> + document.getElementById("bundle_brand").getString("brandShortName") + </field> + <field name="_items">[]</field> + <constructor><![CDATA[ + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + for (let action of this.notification.options.centerActions) { + let item = document.createElementNS(XUL_NS, "row"); + item.setAttribute("class", "plugin-popupnotification-centeritem"); + item.action = action; + this.appendChild(item); + this._items.push(item); + } + switch (this.notification.options.centerActions.length) { + case 0: + PopupNotifications._dismiss(); + break; + case 1: + this._setState(this._states.SINGLE); + break; + default: + if (this.notification.options.primaryPlugin) { + this._setState(this._states.MULTI_COLLAPSED); + } else { + this._setState(this._states.MULTI_EXPANDED); + } + } + ]]></constructor> + <method name="_setState"> + <parameter name="state" /> + <body><![CDATA[ + var grid = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-center-box"); + + if (this._states.SINGLE == state) { + grid.hidden = true; + this._setupSingleState(); + return; + } + + let prePath = this.notification.browser.contentWindow.document.nodePrincipal.URI.prePath; + this._setupDescription("pluginActivateMultiple.message", null, prePath); + + var showBox = document.getAnonymousElementByAttribute(this, "anonid", "plugin-notification-showbox"); + + var dialogStrings = Services.strings.createBundle("chrome://global/locale/dialog.properties"); + this._primaryButton.label = dialogStrings.GetStringFromName("button-accept"); + this._primaryButton.setAttribute("default", "true"); + + this._secondaryButton.label = dialogStrings.GetStringFromName("button-cancel"); + this._primaryButton.setAttribute("action", "_multiAccept"); + this._secondaryButton.setAttribute("action", "_cancel"); + + grid.hidden = false; + + if (this._states.MULTI_COLLAPSED == state) { + for (let child of this.childNodes) { + if (child.tagName != "row") { + continue; + } + child.hidden = this.notification.options.primaryPlugin != + child.action.permissionString; + } + showBox.hidden = false; + } + else { + for (let child of this.childNodes) { + if (child.tagName != "row") { + continue; + } + child.hidden = false; + } + showBox.hidden = true; + } + this._setupLink(null); + ]]></body> + </method> + <method name="_setupSingleState"> + <body><![CDATA[ + var action = this.notification.options.centerActions[0]; + var prePath = action.pluginPermissionPrePath; + + let label, linkLabel, linkUrl, button1, button2; + + if (action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) { + button1 = { + label: "pluginBlockNow.label", + accesskey: "pluginBlockNow.accesskey", + action: "_singleBlock" + }; + button2 = { + label: "pluginContinue.label", + accesskey: "pluginContinue.accesskey", + action: "_singleContinue", + default: true + }; + switch (action.blocklistState) { + case Ci.nsIBlocklistService.STATE_NOT_BLOCKED: + label = "pluginEnabled.message"; + linkLabel = "pluginActivate.learnMore"; + break; + + case Ci.nsIBlocklistService.STATE_BLOCKED: + Cu.reportError(Error("Cannot happen!")); + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + label = "pluginEnabledOutdated.message"; + linkLabel = "pluginActivate.updateLabel"; + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + label = "pluginEnabledVulnerable.message"; + linkLabel = "pluginActivate.riskLabel" + break; + + default: + Cu.reportError(Error("Unexpected blocklist state")); + } + } + else if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) { + let linkElement = + document.getAnonymousElementByAttribute( + this, "anonid", "click-to-play-plugins-notification-link"); + linkElement.textContent = gNavigatorBundle.getString("pluginActivateDisabled.manage"); + linkElement.setAttribute("onclick", "gPluginHandler.managePlugins()"); + + let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + descElement.textContent = gNavigatorBundle.getFormattedString( + "pluginActivateDisabled.message", [action.pluginName, this._brandShortName]) + " "; + this._buttonContainer.hidden = true; + return; + } + else if (action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { + let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + descElement.textContent = gNavigatorBundle.getFormattedString( + "pluginActivateBlocked.message", [action.pluginName, this._brandShortName]) + " "; + this._setupLink("pluginActivate.learnMore", action.detailsLink); + this._buttonContainer.hidden = true; + return; + } + else { + button1 = { + label: "pluginActivateNow.label", + accesskey: "pluginActivateNow.accesskey", + action: "_singleActivateNow" + }; + button2 = { + label: "pluginActivateAlways.label", + accesskey: "pluginActivateAlways.accesskey", + action: "_singleActivateAlways" + }; + switch (action.blocklistState) { + case Ci.nsIBlocklistService.STATE_NOT_BLOCKED: + label = "pluginActivateNew.message"; + linkLabel = "pluginActivate.learnMore"; + button2.default = true; + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + label = "pluginActivateOutdated.message"; + linkLabel = "pluginActivate.updateLabel"; + button1.default = true; + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + label = "pluginActivateVulnerable.message"; + linkLabel = "pluginActivate.riskLabel" + button1.default = true; + break; + + default: + Cu.reportError(Error("Unexpected blocklist state")); + } + } + this._setupDescription(label, action.pluginName, prePath); + this._setupLink(linkLabel, action.detailsLink); + + this._primaryButton.label = gNavigatorBundle.getString(button1.label); + this._primaryButton.accesskey = gNavigatorBundle.getString(button1.accesskey); + this._primaryButton.setAttribute("action", button1.action); + + this._secondaryButton.label = gNavigatorBundle.getString(button2.label); + this._secondaryButton.accesskey = gNavigatorBundle.getString(button2.accesskey); + this._secondaryButton.setAttribute("action", button2.action); + if (button1.default) { + this._primaryButton.setAttribute("default", "true"); + } + else if (button2.default) { + this._secondaryButton.setAttribute("default", "true"); + } + ]]></body> + </method> + <method name="_setupDescription"> + <parameter name="baseString" /> + <parameter name="pluginName" /> <!-- null for the multiple-plugin case --> + <parameter name="prePath" /> + <body><![CDATA[ + var bsn = this._brandShortName; + var span = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + while (span.lastChild) { + span.removeChild(span.lastChild); + } + + var args = ["__prepath__", this._brandShortName]; + if (pluginName) { + args.unshift(pluginName); + } + var bases = gNavigatorBundle.getFormattedString(baseString, args). + split("__prepath__", 2); + + span.appendChild(document.createTextNode(bases[0])); + var prePathSpan = document.createElementNS("http://www.w3.org/1999/xhtml", "em"); + prePathSpan.appendChild(document.createTextNode(prePath)); + span.appendChild(prePathSpan); + span.appendChild(document.createTextNode(bases[1] + " ")); + ]]></body> + </method> + <method name="_setupLink"> + <parameter name="linkString"/> + <parameter name="linkUrl" /> + <body><![CDATA[ + var link = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-link"); + if (!linkString || !linkUrl) { + link.hidden = true; + return; + } + + link.hidden = false; + link.textContent = gNavigatorBundle.getString(linkString); + link.href = linkUrl; + ]]></body> + </method> + <method name="_onButton"> + <parameter name="aButton" /> + <body><![CDATA[ + let methodName = aButton.getAttribute("action"); + this[methodName](); + ]]></body> + </method> + <method name="_singleActivateNow"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this.notification.options.centerActions[0], + "allownow"); + this._cancel(); + ]]></body> + </method> + <method name="_singleBlock"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this.notification.options.centerActions[0], + "block"); + this._cancel(); + ]]></body> + </method> + <method name="_singleActivateAlways"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this.notification.options.centerActions[0], + "allowalways"); + this._cancel(); + ]]></body> + </method> + <method name="_singleContinue"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this.notification.options.centerActions[0], + "continue"); + this._cancel(); + ]]></body> + </method> + <method name="_multiAccept"> + <body><![CDATA[ + for (let item of this._items) { + let action = item.action; + if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED || + action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { + continue; + } + gPluginHandler._updatePluginPermission(this.notification, + item.action, item.value); + } + this._cancel(); + ]]></body> + </method> + <method name="_cancel"> + <body><![CDATA[ + PopupNotifications._dismiss(); + ]]></body> + </method> + <method name="_accept"> + <parameter name="aEvent" /> + <body><![CDATA[ + if (aEvent.defaultPrevented) + return; + aEvent.preventDefault(); + if (this._primaryButton.getAttribute("default") == "true") { + this._primaryButton.click(); + } + else if (this._secondaryButton.getAttribute("default") == "true") { + this._secondaryButton.click(); + } + ]]></body> + </method> + </implementation> + <handlers> + <!-- The _accept method checks for .defaultPrevented so that if focus is in a button, + enter activates the button and not this default action --> + <handler event="keypress" keycode="VK_ENTER" group="system" action="this._accept(event);"/> + <handler event="keypress" keycode="VK_RETURN" group="system" action="this._accept(event);"/> + </handlers> + </binding> + + <binding id="splitmenu"> + <content> + <xul:hbox anonid="menuitem" flex="1" + class="splitmenu-menuitem" + xbl:inherits="iconic,label,disabled,onclick=oncommand,_moz-menuactive=active"/> + <xul:menu anonid="menu" class="splitmenu-menu" + xbl:inherits="disabled,_moz-menuactive=active" + oncommand="event.stopPropagation();"> + <children includes="menupopup"/> + </xul:menu> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor><![CDATA[ + this._parentMenupopup.addEventListener("DOMMenuItemActive", this, false); + this._parentMenupopup.addEventListener("popuphidden", this, false); + ]]></constructor> + + <destructor><![CDATA[ + this._parentMenupopup.removeEventListener("DOMMenuItemActive", this, false); + this._parentMenupopup.removeEventListener("popuphidden", this, false); + ]]></destructor> + + <field name="menuitem" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "menuitem"); + </field> + <field name="menu" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "menu"); + </field> + + <field name="_menuDelay">600</field> + + <field name="_parentMenupopup"><![CDATA[ + this._getParentMenupopup(this); + ]]></field> + + <method name="_getParentMenupopup"> + <parameter name="aNode"/> + <body><![CDATA[ + let node = aNode.parentNode; + while (node) { + if (node.localName == "menupopup") + break; + node = node.parentNode; + } + return node; + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="event"/> + <body><![CDATA[ + switch (event.type) { + case "DOMMenuItemActive": + if (this.getAttribute("active") == "true" && + event.target != this && + this._getParentMenupopup(event.target) == this._parentMenupopup) + this.removeAttribute("active"); + break; + case "popuphidden": + if (event.target == this._parentMenupopup) + this.removeAttribute("active"); + break; + } + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="mouseover"><![CDATA[ + if (this.getAttribute("active") != "true") { + this.setAttribute("active", "true"); + + let event = document.createEvent("Events"); + event.initEvent("DOMMenuItemActive", true, false); + this.dispatchEvent(event); + + if (this.getAttribute("disabled") != "true") { + let self = this; + setTimeout(function () { + if (self.getAttribute("active") == "true") + self.menu.open = true; + }, this._menuDelay); + } + } + ]]></handler> + + <handler event="popupshowing"><![CDATA[ + if (event.target == this.firstChild && + this._parentMenupopup._currentPopup) + this._parentMenupopup._currentPopup.hidePopup(); + ]]></handler> + + <handler event="click" phase="capturing"><![CDATA[ + if (this.getAttribute("disabled") == "true") { + // Prevent the command from being carried out + event.stopPropagation(); + return; + } + + let node = event.originalTarget; + while (true) { + if (node == this.menuitem) + break; + if (node == this) + return; + node = node.parentNode; + } + + this._parentMenupopup.hidePopup(); + ]]></handler> + </handlers> + </binding> + + <binding id="menuitem-tooltip" extends="chrome://global/content/bindings/menu.xml#menuitem"> + <implementation> + <constructor><![CDATA[ + this.setAttribute("tooltiptext", this.getAttribute("acceltext")); + // TODO: Simplify this to this.setAttribute("acceltext", "") once bug + // 592424 is fixed + document.getAnonymousElementByAttribute(this, "anonid", "accel").firstChild.setAttribute("value", ""); + ]]></constructor> + </implementation> + </binding> + + <binding id="menuitem-iconic-tooltip" extends="chrome://global/content/bindings/menu.xml#menuitem-iconic"> + <implementation> + <constructor><![CDATA[ + this.setAttribute("tooltiptext", this.getAttribute("acceltext")); + // TODO: Simplify this to this.setAttribute("acceltext", "") once bug + // 592424 is fixed + document.getAnonymousElementByAttribute(this, "anonid", "accel").firstChild.setAttribute("value", ""); + ]]></constructor> + </implementation> + </binding> + + <binding id="toolbarbutton-badged" display="xul:button" + 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"/> + <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0"/> + </xul:stack> + <xul:label class="toolbarbutton-text" crop="right" flex="1" + xbl:inherits="value=label,accesskey,crop"/> + </content> + </binding> + +</bindings> diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js new file mode 100644 index 000000000..28fc4f838 --- /dev/null +++ b/browser/base/content/utilityOverlay.js @@ -0,0 +1,889 @@ +# -*- Mode: javascript; 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/. + +// Services = object with smart getters for common XPCOM services +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +Components.utils.import("resource:///modules/RecentWindow.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ShellService", + "resource:///modules/ShellService.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", + "resource://gre/modules/Deprecated.jsm"); + +XPCOMUtils.defineLazyGetter(this, "BROWSER_NEW_TAB_URL", function() { + const PREF = "browser.newtab.url"; + + function getNewTabPageURL() { + if (!Services.prefs.prefHasUserValue(PREF)) { + if (PrivateBrowsingUtils.isWindowPrivate(window) && + !PrivateBrowsingUtils.permanentPrivateBrowsing) { + return "about:privatebrowsing"; + } + } + return Services.prefs.getCharPref(PREF) || "about:blank"; + } + + function update() { + BROWSER_NEW_TAB_URL = getNewTabPageURL(); + } + + Services.prefs.addObserver(PREF, update, false); + + addEventListener("unload", function onUnload() { + removeEventListener("unload", onUnload); + Services.prefs.removeObserver(PREF, update); + }); + + return getNewTabPageURL(); +}); + +var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab"; + +var gBidiUI = false; + +/** + * Determines whether the given url is considered a special URL for new tabs. + */ +function isBlankPageURL(aURL) { + // Only make "about:blank", the logopage, or "about:newtab" be + // a "blank page" to fix focus issues. + // Original code: return aURL == "about:blank" || aURL == BROWSER_NEW_TAB_URL; + return aURL == "about:blank" || aURL == "about:newtab" || aURL == "about:logopage"; +} + +function getBrowserURL() { + return "chrome://browser/content/browser.xul"; +} + +function getBoolPref(pref, defaultValue) { + Deprecated.warning("getBoolPref is deprecated and will be removed in a future release. " + + "You should use Services.pref.getBoolPref (Services.jsm).", + "https://github.com/MoonchildProductions/UXP/issues/989"); + return Services.prefs.getBoolPref(pref, defaultValue); +} + + +function getTopWin(skipPopups) { + // If this is called in a browser window, use that window regardless of + // whether it's the frontmost window, since commands can be executed in + // background windows (bug 626148). + if (top.document.documentElement.getAttribute("windowtype") == "navigator:browser" && + (!skipPopups || top.toolbar.visible)) { + return top; + } + + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + return RecentWindow.getMostRecentBrowserWindow({ private: isPrivate, + allowPopups: !skipPopups }); +} + +function openTopWin(url) { + /* deprecated */ + openUILinkIn(url, "current"); +} + +/* openUILink handles clicks on UI elements that cause URLs to load. + * + * As the third argument, you may pass an object with the same properties as + * accepted by openUILinkIn, plus "ignoreButton" and "ignoreAlt". + */ +function openUILink(url, event, aIgnoreButton, aIgnoreAlt, aAllowThirdPartyFixup, + aPostData, aReferrerURI) { + let params; + + if (aIgnoreButton && typeof aIgnoreButton == "object") { + params = aIgnoreButton; + + // don't forward "ignoreButton" and "ignoreAlt" to openUILinkIn + aIgnoreButton = params.ignoreButton; + aIgnoreAlt = params.ignoreAlt; + delete params.ignoreButton; + delete params.ignoreAlt; + } else { + params = { + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostData, + referrerURI: aReferrerURI, + referrerPolicy: Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT, + initiatingDoc: event ? event.target.ownerDocument : null, + }; + } + + let where = whereToOpenLink(event, aIgnoreButton, aIgnoreAlt); + openUILinkIn(url, where, params); +} + + +/* whereToOpenLink() looks at an event to decide where to open a link. + * + * The event may be a mouse event (click, double-click, middle-click) or keypress event (enter). + * + * On Windows, the modifiers are: + * Ctrl new tab, selected + * Shift new window + * Ctrl+Shift new tab, in background + * Alt save + * + * Middle-clicking is the same as Ctrl+clicking (it opens a new tab). + * + * Exceptions: + * - Alt is ignored for menu items selected using the keyboard so you don't accidentally save stuff. + * (Currently, the Alt isn't sent here at all for menu items, but that will change in bug 126189.) + * - Alt is hard to use in context menus, because pressing Alt closes the menu. + * - Alt can't be used on the bookmarks toolbar because Alt is used for "treat this as something draggable". + * - The button is ignored for the middle-click-paste-URL feature, since it's always a middle-click. + */ +function whereToOpenLink(e, ignoreButton, ignoreAlt) { + // This method must treat a null event like a left click without modifier keys (i.e. + // e = { shiftKey:false, ctrlKey:false, metaKey:false, altKey:false, button:0 }) + // for compatibility purposes. + if (!e) { + return "current"; + } + + var shift = e.shiftKey; + var ctrl = e.ctrlKey; + var meta = e.metaKey; + var alt = e.altKey && !ignoreAlt; + + // ignoreButton allows "middle-click paste" to use function without always opening in a new window. + var middle = !ignoreButton && e.button == 1; + var middleUsesTabs = Services.prefs.getBoolPref("browser.tabs.opentabfor.middleclick", true); + + // Don't do anything special with right-mouse clicks. They're probably clicks on context menu items. + + if (ctrl || (middle && middleUsesTabs)) { + return shift ? "tabshifted" : "tab"; + } + + if (alt && Services.prefs.getBoolPref("browser.altClickSave", false)) { + return "save"; + } + + if (shift || (middle && !middleUsesTabs)) { + return "window"; + } + + return "current"; +} + +/* openUILinkIn opens a URL in a place specified by the parameter |where|. + * + * |where| can be: + * "current" current tab (if there aren't any browser windows, then in a new window instead) + * "tab" new tab (if there aren't any browser windows, then in a new window instead) + * "tabshifted" same as "tab" but in background if default is to select new tabs, and vice versa + * "window" new window + * "save" save to disk (with no filename hint!) + * + * aAllowThirdPartyFixup controls whether third party services such as Google's + * I Feel Lucky are allowed to interpret this URL. This parameter may be + * undefined, which is treated as false. + * + * Instead of aAllowThirdPartyFixup, you may also pass an object with any of + * these properties: + * allowThirdPartyFixup (boolean) + * postData (nsIInputStream) + * referrerURI (nsIURI) + * relatedToCurrent (boolean) + */ +function openUILinkIn(url, where, aAllowThirdPartyFixup, aPostData, aReferrerURI) { + var params; + + if (arguments.length == 3 && typeof arguments[2] == "object") { + params = aAllowThirdPartyFixup; + } else { + params = { + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostData, + referrerURI: aReferrerURI, + referrerPolicy: Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT, + }; + } + + params.fromChrome = true; + + openLinkIn(url, where, params); +} + +/* eslint-disable complexity */ +function openLinkIn(url, where, params) { + if (!where || !url) { + return; + } + const Cc = Components.classes; + const Ci = Components.interfaces; + + var aFromChrome = params.fromChrome; + var aAllowThirdPartyFixup = params.allowThirdPartyFixup; + var aPostData = params.postData; + var aCharset = params.charset; + var aReferrerURI = params.referrerURI; + var aReferrerPolicy = ('referrerPolicy' in params ? + params.referrerPolicy : + Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT); + var aRelatedToCurrent = params.relatedToCurrent; + var aForceAllowDataURI = params.forceAllowDataURI; + var aInBackground = params.inBackground; + var aDisallowInheritPrincipal = params.disallowInheritPrincipal; + var aInitiatingDoc = params.initiatingDoc; + var aIsPrivate = params.private; + var aPrincipal = params.originPrincipal; + var aTriggeringPrincipal = params.triggeringPrincipal; + var aForceAboutBlankViewerInCurrent = params.forceAboutBlankViewerInCurrent; + var sendReferrerURI = true; + + if (where == "save") { + if (!aInitiatingDoc) { + Components.utils.reportError("openUILink/openLinkIn was called with " + + "where == 'save' but without initiatingDoc. See bug 814264."); + return; + } + // TODO(1073187): propagate referrerPolicy. + saveURL(url, null, null, true, null, aReferrerURI, aInitiatingDoc); + return; + } + + var w = getTopWin(); + if ((where == "tab" || where == "tabshifted") && + w && !w.toolbar.visible) { + w = getTopWin(true); + aRelatedToCurrent = false; + } + + // We can only do this after we're sure of what |w| will be the rest of this function. + // Note that if |w| is null we might have no current browser (we'll open a new window). + var aCurrentBrowser = params.currentBrowser || (w && w.gBrowser.selectedBrowser); + + // Teach the principal about the right OA to use, e.g. in case when + // opening a link in a new private window. + // Please note we do not have to do that for SystemPrincipals and we + // can not do it for NullPrincipals since NullPrincipals are only + // identical if they actually are the same object (See Bug: 1346759) + function useOAForPrincipal(principal) { + if (principal && principal.isCodebasePrincipal) { + let attrs = { + privateBrowsingId: aIsPrivate || (w && PrivateBrowsingUtils.isWindowPrivate(w)), + }; + return Services.scriptSecurityManager.createCodebasePrincipal(principal.URI, attrs); + } + return principal; + } + aPrincipal = useOAForPrincipal(aPrincipal); + aTriggeringPrincipal = useOAForPrincipal(aTriggeringPrincipal); + + if (!w || where == "window") { + // This propagates to window.arguments. + // Strip referrer data when opening a new private window, to prevent + // regular browsing data from leaking into it. + if (aIsPrivate) { + sendReferrerURI = false; + } + + var sa = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray); + + var wuri = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + wuri.data = url; + + let charset = null; + if (aCharset) { + charset = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + charset.data = "charset=" + aCharset; + } + + var allowThirdPartyFixupSupports = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + allowThirdPartyFixupSupports.data = aAllowThirdPartyFixup; + + var referrerURISupports = null; + if (aReferrerURI && sendReferrerURI) { + referrerURISupports = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + referrerURISupports.data = aReferrerURI.spec; + } + + var referrerPolicySupports = Cc["@mozilla.org/supports-PRUint32;1"] + .createInstance(Ci.nsISupportsPRUint32); + referrerPolicySupports.data = aReferrerPolicy; + + sa.AppendElement(wuri); + sa.AppendElement(charset); + sa.AppendElement(referrerURISupports); + sa.AppendElement(aPostData); + sa.AppendElement(allowThirdPartyFixupSupports); + sa.AppendElement(referrerPolicySupports); + sa.AppendElement(aPrincipal); + sa.AppendElement(aTriggeringPrincipal); + + let features = "chrome,dialog=no,all"; + if (aIsPrivate) { + features += ",private"; + } + + Services.ww.openWindow(w || window, getBrowserURL(), null, features, sa); + return; + } + + let loadInBackground = where == "current" ? false : aInBackground; + if (loadInBackground == null) { + loadInBackground = aFromChrome ? + false : + Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + } + + let uriObj; + if (where == "current") { + try { + uriObj = Services.io.newURI(url, null, null); + } catch(e) {} + } + + if (where == "current" && w.gBrowser.selectedTab.pinned) { + try { + // nsIURI.host can throw for non-nsStandardURL nsIURIs. + if (!uriObj || !uriObj.schemeIs("javascript") && + w.gBrowser.currentURI.host != uriObj.host) { + where = "tab"; + loadInBackground = false; + } + } catch(err) { + where = "tab"; + loadInBackground = false; + } + } + + // Raise the target window before loading the URI, since loading it may + // result in a new frontmost window (e.g. "javascript:window.open('');"). + w.focus(); + + let browserUsedForLoad = null; + switch (where) { + case "current": + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (aAllowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + if (aDisallowInheritPrincipal) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; + } + if (aForceAllowDataURI) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI; + } + let {URI_INHERITS_SECURITY_CONTEXT} = Ci.nsIProtocolHandler; + if (aForceAboutBlankViewerInCurrent && + (!uriObj || + (Services.io.getProtocolFlags(uriObj.scheme) & URI_INHERITS_SECURITY_CONTEXT))) { + // Unless we know for sure we're not inheriting principals, + // force the about:blank viewer to have the right principal: + w.gBrowser.selectedBrowser.createAboutBlankContentViewer(aPrincipal); + } + + w.gBrowser.loadURIWithFlags(url, { flags: flags, + triggeringPrincipal: aTriggeringPrincipal, + referrerURI: aReferrerURI, + referrerPolicy: aReferrerPolicy, + postData: aPostData, + originPrincipal: aPrincipal }); + browserUsedForLoad = aCurrentBrowser; + break; + case "tabshifted": + loadInBackground = !loadInBackground; + // fall through + case "tab": + let browser = w.gBrowser; + let tabUsedForLoad = browser.loadOneTab(url, { referrerURI: aReferrerURI, + referrerPolicy: aReferrerPolicy, + charset: aCharset, + postData: aPostData, + inBackground: loadInBackground, + allowThirdPartyFixup: aAllowThirdPartyFixup, + relatedToCurrent: aRelatedToCurrent, + originPrincipal: aPrincipal, + triggeringPrincipal: aTriggeringPrincipal }); + browserUsedForLoad = tabUsedForLoad.linkedBrowser; + break; + } + + // Focus the content, but only if the browser used for the load is selected. + if (browserUsedForLoad && + browserUsedForLoad == browserUsedForLoad.getTabBrowser().selectedBrowser) { + browserUsedForLoad.focus(); + } + + if (!loadInBackground && w.isBlankPageURL(url)) { + if (!w.focusAndSelectUrlBar()) { + console.error("Unable to focus and select address bar.") + } + } +} + +// Used as an onclick handler for UI elements with link-like behavior. +// e.g. onclick="checkForMiddleClick(this, event);" +function checkForMiddleClick(node, event) { + // We should be using the disabled property here instead of the attribute, + // but some elements that this function is used with don't support it (e.g. + // menuitem). + if (node.getAttribute("disabled") == "true") { + // Do nothing + return; + } + + if (event.button == 1) { + /* Execute the node's oncommand or command. + * + * XXX: we should use node.oncommand(event) once bug 246720 is fixed. + */ + var target = node.hasAttribute("oncommand") ? + node : + node.ownerDocument.getElementById(node.getAttribute("command")); + var fn = new Function("event", target.getAttribute("oncommand")); + fn.call(target, event); + + // If the middle-click was on part of a menu, close the menu. + // (Menus close automatically with left-click but not with middle-click.) + closeMenus(event.target); + } +} + +// Closes all popups that are ancestors of the node. +function closeMenus(node) +{ + if ("tagName" in node) { + if (node.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" && + (node.tagName == "menupopup" || node.tagName == "popup")) { + node.hidePopup(); + } + + closeMenus(node.parentNode); + } +} + +// Gather all descendant text under given document node. +function gatherTextUnder(root) { + var text = ""; + var node = root.firstChild; + var depth = 1; + while ( node && depth > 0 ) { + // See if this node is text. + if ( node.nodeType == Node.TEXT_NODE ) { + // Add this text to our collection. + text += " " + node.data; + } else if ( node instanceof HTMLImageElement) { + // If it has an alt= attribute, use that. + var altText = node.getAttribute( "alt" ); + if ( altText && altText != "" ) { + text = altText; + break; + } + } + // Find next node to test. + // First, see if this node has children. + if ( node.hasChildNodes() ) { + // Go to first child. + node = node.firstChild; + depth++; + } else { + // No children, try next sibling (or parent next sibling). + while ( depth > 0 && !node.nextSibling ) { + node = node.parentNode; + depth--; + } + if ( node.nextSibling ) { + node = node.nextSibling; + } + } + } + // Strip leading whitespace. + text = text.replace( /^\s+/, "" ); + // Strip trailing whitespace. + text = text.replace( /\s+$/, "" ); + // Compress remaining whitespace. + text = text.replace( /\s+/g, " " ); + return text; +} + +// This function exists for legacy reasons. +function getShellService() { + return ShellService; +} + +function isBidiEnabled() { + // first check the pref. + if (Services.prefs.getBoolPref("bidi.browser.ui", false)) { + return true; + } + + // if the pref isn't set, check for an RTL locale and force the pref to true + // if we find one. + var rv = false; + + try { + var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"] + .getService(Components.interfaces.nsILocaleService); + var systemLocale = localeService.getSystemLocale().getCategory("NSILOCALE_CTYPE").substr(0,3); + + switch (systemLocale) { + case "ar-": + case "he-": + case "fa-": + case "ur-": + case "syr": + rv = true; + Services.prefs.setBoolPref("bidi.browser.ui", true); + } + } catch(e) {} + + return rv; +} + +#ifdef MOZ_UPDATER +/** + * Opens the update manager and checks for updates to the application. + */ +function checkForUpdates() { + var um = Components.classes["@mozilla.org/updates/update-manager;1"] + .getService(Components.interfaces.nsIUpdateManager); + var prompter = Components.classes["@mozilla.org/updates/update-prompt;1"] + .createInstance(Components.interfaces.nsIUpdatePrompt); + + // If there's an update ready to be applied, show the "Update Downloaded" + // UI instead and let the user know they have to restart the application for + // the changes to be applied. + if (um.activeUpdate && ["pending", "pending-elevate", "applied"].includes(um.activeUpdate.state)) { + prompter.showUpdateDownloaded(um.activeUpdate); + } else { + prompter.checkForUpdates(); + } +} +#endif + +/** + * Set up the help menu software update items to show proper status, + * also disabling the items if update is disabled. + */ +function buildHelpMenu() { +#ifdef MOZ_UPDATER + var updates = Components.classes["@mozilla.org/updates/update-service;1"] + .getService(Components.interfaces.nsIApplicationUpdateService); + var um = Components.classes["@mozilla.org/updates/update-manager;1"] + .getService(Components.interfaces.nsIUpdateManager); + + // Disable the UI if the update enabled pref has been locked by the + // administrator or if we cannot update for some other reason. + var checkForUpdates = document.getElementById("checkForUpdates"); + var appMenuCheckForUpdates = document.getElementById("appmenu_checkForUpdates"); + var canCheckForUpdates = updates.canCheckForUpdates; + + checkForUpdates.setAttribute("disabled", !canCheckForUpdates); + + if (appMenuCheckForUpdates) { + appMenuCheckForUpdates.setAttribute("disabled", !canCheckForUpdates); + } + + if (!canCheckForUpdates) { + return; + } + + var strings = document.getElementById("bundle_browser"); + var activeUpdate = um.activeUpdate; + + // If there's an active update, substitute its name into the label + // we show for this item, otherwise display a generic label. + function getStringWithUpdateName(key) { + if (activeUpdate && activeUpdate.name) { + return strings.getFormattedString(key, [activeUpdate.name]); + } + return strings.getString(key + "Fallback"); + } + + // By default, show "Check for Updates..." from updatesItem_default or + // updatesItem_defaultFallback + var key = "default"; + if (activeUpdate) { + switch (activeUpdate.state) { + case "downloading": + // If we're downloading an update at present, show the text: + // "Downloading Thunderbird x.x..." from updatesItem_downloading or + // updatesItem_downloadingFallback, otherwise we're paused, and show + // "Resume Downloading Thunderbird x.x..." from updatesItem_resume or + // updatesItem_resumeFallback + key = updates.isDownloading ? "downloading" : "resume"; + break; + case "pending": + // If we're waiting for the user to restart, show: "Apply Downloaded + // Updates Now..." from updatesItem_pending or + // updatesItem_pendingFallback + key = "pending"; + break; + } + } + + checkForUpdates.label = getStringWithUpdateName("updatesItem_" + key); + + if (appMenuCheckForUpdates) { + appMenuCheckForUpdates.label = getStringWithUpdateName("updatesItem_" + key); + } + + // updatesItem_default.accesskey, updatesItem_downloading.accesskey, + // updatesItem_resume.accesskey or updatesItem_pending.accesskey + checkForUpdates.accessKey = strings.getString("updatesItem_" + key + ".accesskey"); + + if (appMenuCheckForUpdates) { + appMenuCheckForUpdates.accessKey = strings.getString("updatesItem_" + key + ".accesskey"); + } + + if (um.activeUpdate && updates.isDownloading) { + checkForUpdates.setAttribute("loading", "true"); + if (appMenuCheckForUpdates) { + appMenuCheckForUpdates.setAttribute("loading", "true"); + } + } else { + checkForUpdates.removeAttribute("loading"); + if (appMenuCheckForUpdates) { + appMenuCheckForUpdates.removeAttribute("loading"); + } + } +#else + // Some extensions may rely on these being present so only hide the about + // separator when there are no elements besides the check for updates menuitem + // in between the about separator and the updates separator. + var updatesSeparator = document.getElementById("updatesSeparator"); + var aboutSeparator = document.getElementById("aboutSeparator"); + var checkForUpdates = document.getElementById("checkForUpdates"); + if (updatesSeparator.nextSibling === checkForUpdates && + checkForUpdates.nextSibling === aboutSeparator) { + updatesSeparator.hidden = true; + } +#endif +} + + +function openAboutDialog() { + var enumerator = Services.wm.getEnumerator("Browser:About"); + while (enumerator.hasMoreElements()) { + // Only open one about window (Bug 599573) + let win = enumerator.getNext(); + win.focus(); + return; + } + +#ifdef XP_WIN + var features = "chrome,centerscreen,dependent"; +#else + var features = "chrome,centerscreen,dependent,dialog=no"; +#endif + window.openDialog("chrome://browser/content/aboutDialog.xul", "", features); +} + +function openPreferences(paneID, extraArgs) { + var instantApply = Services.prefs.getBoolPref("browser.preferences.instantApply", false); + var features = "chrome,titlebar,toolbar,centerscreen" + (instantApply ? ",dialog=no" : ",modal"); + + var win = Services.wm.getMostRecentWindow("Browser:Preferences"); + if (win) { + win.focus(); + if (paneID) { + var pane = win.document.getElementById(paneID); + win.document.documentElement.showPane(pane); + } + + if (extraArgs && extraArgs["advancedTab"]) { + var advancedPaneTabs = win.document.getElementById("advancedPrefs"); + advancedPaneTabs.selectedTab = win.document.getElementById(extraArgs["advancedTab"]); + } + + return win; + } + + return openDialog("chrome://browser/content/preferences/preferences.xul", + "Preferences", features, paneID, extraArgs); +} + +function openAdvancedPreferences(tabID) { + openPreferences("paneAdvanced", { "advancedTab" : tabID }); +} + +/** + * Opens the release notes page for this version of the application. + */ +function openReleaseNotes() { + var relnotesURL = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Components.interfaces.nsIURLFormatter) + .formatURLPref("app.releaseNotesURL"); + + openUILinkIn(relnotesURL, "tab"); +} + +/** + * Opens the troubleshooting information (about:support) page for this version + * of the application. + */ +function openTroubleshootingPage() { + openUILinkIn("about:support", "tab"); +} + +/** + * Opens the feedback page for this version of the application. + */ +function openFeedbackPage() { + openUILinkIn(Services.prefs.getCharPref("browser.feedback.url"), "tab"); +} + +function isElementVisible(aElement) { + if (!aElement) { + return false; + } + + // If aElement or a direct or indirect parent is hidden or collapsed, + // height, width or both will be 0. + var bo = aElement.boxObject; + return (bo.height > 0 && bo.width > 0); +} + +function makeURLAbsolute(aBase, aUrl) +{ + // Note: makeURI() will throw if aUri is not a valid URI + return makeURI(aUrl, null, makeURI(aBase)).spec; +} + + +/** + * openNewTabWith: opens a new tab with the given URL. + * + * @param aURL + * The URL to open (as a string). + * @param aDocument + * The document from which the URL came, or null. This is used to set the + * referrer header and to do a security check of whether the document is + * allowed to reference the URL. If null, there will be no referrer + * header and no security check. + * @param aPostData + * Form POST data, or null. + * @param aEvent + * The triggering event (for the purpose of determining whether to open + * in the background), or null. + * @param aAllowThirdPartyFixup + * If true, then we allow the URL text to be sent to third party services + * (e.g., Google's I Feel Lucky) for interpretation. This parameter may + * be undefined in which case it is treated as false. + * @param [optional] aReferrer + * If aDocument is null, then this will be used as the referrer. + * There will be no security check. + * @param [optional] aReferrerPolicy + * Referrer policy - Ci.nsIHttpChannel.REFERRER_POLICY_*. + */ +function openNewTabWith(aURL, aDocument, aPostData, aEvent, + aAllowThirdPartyFixup, aReferrer, aReferrerPolicy) { + if (aDocument) { + urlSecurityCheck(aURL, aDocument.nodePrincipal); + } + + // As in openNewWindowWith(), we want to pass the charset of the + // current document over to a new tab. + var originCharset = aDocument && aDocument.characterSet; + if (!originCharset && + document.documentElement.getAttribute("windowtype") == "navigator:browser") { + originCharset = window.content.document.characterSet; + } + + openLinkIn(aURL, aEvent && aEvent.shiftKey ? "tabshifted" : "tab", + { charset: originCharset, + postData: aPostData, + allowThirdPartyFixup: aAllowThirdPartyFixup, + referrerURI: aDocument ? aDocument.documentURIObject : aReferrer, + referrerPolicy: aReferrerPolicy, + }); +} + +function openNewWindowWith(aURL, aDocument, aPostData, aAllowThirdPartyFixup, + aReferrer, aReferrerPolicy) { + if (aDocument) { + urlSecurityCheck(aURL, aDocument.nodePrincipal); + } + + // if and only if the current window is a browser window and it has a + // document with a character set, then extract the current charset menu + // setting from the current document and use it to initialize the new browser + // window... + var originCharset = aDocument && aDocument.characterSet; + if (!originCharset && + document.documentElement.getAttribute("windowtype") == "navigator:browser") { + originCharset = window.content.document.characterSet; + } + + openLinkIn(aURL, "window", + { charset: originCharset, + postData: aPostData, + allowThirdPartyFixup: aAllowThirdPartyFixup, + referrerURI: aDocument ? aDocument.documentURIObject : aReferrer, + referrerPolicy: aReferrerPolicy, + }); +} + +/** + * isValidFeed: checks whether the given data represents a valid feed. + * + * @param aLink + * An object representing a feed with title, href and type. + * @param aPrincipal + * The principal of the document, used for security check. + * @param aIsFeed + * Whether this is already a known feed or not, if true only a security + * check will be performed. + */ +function isValidFeed(aLink, aPrincipal, aIsFeed) { + if (!aLink || !aPrincipal) { + return false; + } + + var type = aLink.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, ""); + if (!aIsFeed) { + aIsFeed = (type == "application/rss+xml" || + type == "application/atom+xml"); + } + + if (aIsFeed) { + try { + urlSecurityCheck(aLink.href, aPrincipal, + Components.interfaces.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + return type || "application/rss+xml"; + } catch(ex) {} + } + + return null; +} + +// aCalledFromModal is optional +function openHelpLink(aHelpTopic, aCalledFromModal) { + var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Components.interfaces.nsIURLFormatter) + .formatURLPref("app.support.baseURL"); + url += aHelpTopic; + + var where = aCalledFromModal ? "window" : "tab"; + openUILinkIn(url, where); +} + +function openPrefsHelp() { + // non-instant apply prefwindows are usually modal, so we can't open in the topmost window, + // since its probably behind the window. + var instantApply = Services.prefs.getBoolPref("browser.preferences.instantApply"); + + var helpTopic = document.getElementsByTagName("prefwindow")[0].currentPane.helpTopic; + openHelpLink(helpTopic, !instantApply); +} + +function trimURL(aURL) { + // This function must not modify the given URL such that calling + // nsIURIFixup::createFixupURI with the result will produce a different URI. + return aURL /* remove single trailing slash for http/https/ftp URLs */ + .replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1") + /* remove http:// unless the host starts with "ftp\d*\." or contains "@" */ + .replace(/^http:\/\/((?!ftp\d*\.)[^\/@]+(?:\/|$))/, "$1"); +} diff --git a/browser/base/content/viewSourceOverlay.xul b/browser/base/content/viewSourceOverlay.xul new file mode 100644 index 000000000..4946d27cc --- /dev/null +++ b/browser/base/content/viewSourceOverlay.xul @@ -0,0 +1,22 @@ +<?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/. --> + +<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?> + +<overlay id="viewSourceOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<window id="viewSource"> + <commandset id="baseMenuCommandSet"/> + <keyset id="baseMenuKeyset"/> + <stringbundleset id="stringbundleset"/> +</window> + +<menubar id="viewSource-main-menubar"> + <menu id="helpMenu"/> +</menubar> + +</overlay> diff --git a/browser/base/content/web-panels.js b/browser/base/content/web-panels.js new file mode 100644 index 000000000..93c2a8c14 --- /dev/null +++ b/browser/base/content/web-panels.js @@ -0,0 +1,97 @@ +/* -*- 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/. */ + +const NS_ERROR_MODULE_NETWORK = 2152398848; +const NS_NET_STATUS_READ_FROM = NS_ERROR_MODULE_NETWORK + 8; +const NS_NET_STATUS_WROTE_TO = NS_ERROR_MODULE_NETWORK + 9; + +function getPanelBrowser() { + return document.getElementById("web-panels-browser"); +} + +var panelProgressListener = { + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + }, + + onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { + if (!aRequest) { + return; + } + + //ignore local/resource:/chrome: files + if (aStatus == NS_NET_STATUS_READ_FROM || aStatus == NS_NET_STATUS_WROTE_TO) { + return; + } + + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + window.parent.document.getElementById('sidebar-throbber').setAttribute("loading", "true"); + } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + window.parent.document.getElementById('sidebar-throbber').removeAttribute("loading"); + } + }, + + onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { + UpdateBackForwardCommands(getPanelBrowser().webNavigation); + }, + + onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) { + }, + + onSecurityChange: function(aWebProgress, aRequest, aState) { + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_NOINTERFACE; + } +}; + +var gLoadFired = false; +function loadWebPanel(aURI) { + var panelBrowser = getPanelBrowser(); + if (gLoadFired) { + panelBrowser.webNavigation + .loadURI(aURI, nsIWebNavigation.LOAD_FLAGS_NONE, + null, null, null); + } + panelBrowser.setAttribute("cachedurl", aURI); +} + +function load() { + var panelBrowser = getPanelBrowser(); + panelBrowser.webProgress.addProgressListener(panelProgressListener, + Ci.nsIWebProgress.NOTIFY_ALL); + var cachedurl = panelBrowser.getAttribute("cachedurl") + if (cachedurl) { + panelBrowser.webNavigation + .loadURI(cachedurl, nsIWebNavigation.LOAD_FLAGS_NONE, null, + null, null); + } + + gLoadFired = true; +} + +function unload() { + getPanelBrowser().webProgress.removeProgressListener(panelProgressListener); +} + +function PanelBrowserStop() { + getPanelBrowser().webNavigation.stop(nsIWebNavigation.STOP_ALL) +} + +function PanelBrowserReload() { + getPanelBrowser().webNavigation + .sessionHistory + .QueryInterface(nsIWebNavigation) + .reload(nsIWebNavigation.LOAD_FLAGS_NONE); +} diff --git a/browser/base/content/web-panels.xul b/browser/base/content/web-panels.xul new file mode 100644 index 000000000..ea1e2eba7 --- /dev/null +++ b/browser/base/content/web-panels.xul @@ -0,0 +1,84 @@ +<?xml version="1.0"?> + +# -*- 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/. + +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +<!DOCTYPE page [ +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> +%browserDTD; +<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd"> +%textcontextDTD; +]> + +<page id="webpanels-window" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="load()" onunload="unload()"> + <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + <script type="application/javascript" src="chrome://browser/content/browser.js"/> + <script type="application/javascript" src="chrome://global/content/inlineSpellCheckUI.js"/> + <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/> + <script type="application/javascript" src="chrome://browser/content/web-panels.js"/> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/> + </stringbundleset> + + <broadcasterset id="mainBroadcasterSet"> + <broadcaster id="isFrameImage"/> + </broadcasterset> + + <commandset id="mainCommandset"> + <command id="Browser:Back" + oncommand="getPanelBrowser().webNavigation.goBack();" + disabled="true"/> + <command id="Browser:Forward" + oncommand="getPanelBrowser().webNavigation.goForward();" + disabled="true"/> + <command id="Browser:Stop" oncommand="PanelBrowserStop();"/> + <command id="Browser:Reload" oncommand="PanelBrowserReload();"/> + <command id="Browser:BackOrBackDuplicate" + oncommand="getPanelBrowser().webNavigation.goBack(event);" + disabled="true"> + <observes element="Browser:Back" attribute="disabled"/> + </command> + <command id="Browser:ForwardOrForwardDuplicate" + oncommand="getPanelBrowser().webNavigation.goForward(event);" + disabled="true"> + <observes element="Browser:Forward" attribute="disabled"/> + </command> + <command id="Browser:ReloadOrDuplicate" + oncommand="PanelBrowserReload(event)" + disabled="true"> + <observes element="Browser:Reload" attribute="disabled"/> + </command> + </commandset> + + <popupset id="mainPopupSet"> + <tooltip id="aHTMLTooltip" page="true"/> + <menupopup id="contentAreaContextMenu" pagemenu="start" + onpopupshowing="if (event.target != this) + return true; + gContextMenu = new nsContextMenu(this, event.shiftKey); + if (gContextMenu.shouldDisplay) + document.popupNode = this.triggerNode; + return gContextMenu.shouldDisplay;" + onpopuphiding="if (event.target != this) + return; + gContextMenu.hiding(); + gContextMenu = null;"> +#include browser-context.inc + </menupopup> + </popupset> + + <commandset id="editMenuCommands"/> + <browser id="web-panels-browser" persist="cachedurl" type="content" flex="1" + context="contentAreaContextMenu" tooltip="aHTMLTooltip" + onclick="window.parent.contentAreaClick(event, true);"/> +</page> diff --git a/browser/base/content/win6BrowserOverlay.xul b/browser/base/content/win6BrowserOverlay.xul new file mode 100644 index 000000000..a69e3f6bd --- /dev/null +++ b/browser/base/content/win6BrowserOverlay.xul @@ -0,0 +1,12 @@ +<?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/. --> + +<overlay id="win6-browser-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <toolbar id="toolbar-menubar" + autohide="true"/> +</overlay> |