summaryrefslogtreecommitdiff
path: root/browser/base/content
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content')
-rw-r--r--browser/base/content/aboutDialog.css74
-rw-r--r--browser/base/content/aboutDialog.js55
-rw-r--r--browser/base/content/aboutDialog.xul77
-rw-r--r--browser/base/content/autocomplete.css17
-rw-r--r--browser/base/content/autocomplete.xml2112
-rw-r--r--browser/base/content/autorecovery.js58
-rw-r--r--browser/base/content/autorecovery.xul12
-rw-r--r--browser/base/content/baseMenuOverlay.xul78
-rw-r--r--browser/base/content/browser-addons.js562
-rw-r--r--browser/base/content/browser-appmenu.inc381
-rw-r--r--browser/base/content/browser-charsetmenu.inc62
-rw-r--r--browser/base/content/browser-context.inc390
-rw-r--r--browser/base/content/browser-devtools-theme.js91
-rw-r--r--browser/base/content/browser-doctype.inc19
-rw-r--r--browser/base/content/browser-feeds.js236
-rw-r--r--browser/base/content/browser-fullScreen.js454
-rw-r--r--browser/base/content/browser-fullZoom.js539
-rw-r--r--browser/base/content/browser-gestureSupport.js1189
-rw-r--r--browser/base/content/browser-menubar.inc504
-rw-r--r--browser/base/content/browser-menudragging.js351
-rw-r--r--browser/base/content/browser-menudragging.xul13
-rw-r--r--browser/base/content/browser-places.js1352
-rw-r--r--browser/base/content/browser-plugins.js789
-rw-r--r--browser/base/content/browser-sets.inc325
-rw-r--r--browser/base/content/browser-syncui.js491
-rw-r--r--browser/base/content/browser-tabPreviews.js1139
-rw-r--r--browser/base/content/browser-tabPreviews.xml74
-rw-r--r--browser/base/content/browser-thumbnails.js211
-rw-r--r--browser/base/content/browser-title.css204
-rw-r--r--browser/base/content/browser-uacompat.js45
-rw-r--r--browser/base/content/browser.css753
-rw-r--r--browser/base/content/browser.js7383
-rw-r--r--browser/base/content/browser.xul973
-rw-r--r--browser/base/content/content.js178
-rw-r--r--browser/base/content/global-devtools-theme-scripts.inc6
-rw-r--r--browser/base/content/global-scripts.inc13
-rw-r--r--browser/base/content/hiddenWindow.xul6
-rw-r--r--browser/base/content/highlighter.css105
-rw-r--r--browser/base/content/nsContextMenu.js1602
-rw-r--r--browser/base/content/openLocation.js148
-rw-r--r--browser/base/content/openLocation.xul57
-rw-r--r--browser/base/content/overrides/app-license.html6
-rw-r--r--browser/base/content/padlock.css227
-rw-r--r--browser/base/content/padlock.js282
-rw-r--r--browser/base/content/padlock.xul63
-rw-r--r--browser/base/content/padlock_classic_broken.pngbin0 -> 726 bytes
-rw-r--r--browser/base/content/padlock_classic_ev.pngbin0 -> 566 bytes
-rw-r--r--browser/base/content/padlock_classic_https.pngbin0 -> 589 bytes
-rw-r--r--browser/base/content/padlock_classic_low.pngbin0 -> 682 bytes
-rw-r--r--browser/base/content/padlock_classic_mixed.pngbin0 -> 324 bytes
-rw-r--r--browser/base/content/padlock_mod_broken.pngbin0 -> 728 bytes
-rw-r--r--browser/base/content/padlock_mod_ev.pngbin0 -> 290 bytes
-rw-r--r--browser/base/content/padlock_mod_https.pngbin0 -> 338 bytes
-rw-r--r--browser/base/content/padlock_mod_low.pngbin0 -> 386 bytes
-rw-r--r--browser/base/content/padlock_mod_mixed.pngbin0 -> 318 bytes
-rw-r--r--browser/base/content/palemoon.xhtml66
-rw-r--r--browser/base/content/popup-notifications.inc82
-rw-r--r--browser/base/content/safeMode.css8
-rw-r--r--browser/base/content/safeMode.js129
-rw-r--r--browser/base/content/safeMode.xul55
-rw-r--r--browser/base/content/sanitize.js517
-rw-r--r--browser/base/content/sanitize.xul190
-rw-r--r--browser/base/content/sanitizeDialog.css23
-rw-r--r--browser/base/content/sanitizeDialog.js886
-rw-r--r--browser/base/content/tabbrowser.css77
-rw-r--r--browser/base/content/tabbrowser.xml5340
-rw-r--r--browser/base/content/test/general/audio.oggbin0 -> 47411 bytes
-rw-r--r--browser/base/content/urlbarBindings.xml1798
-rw-r--r--browser/base/content/utilityOverlay.js889
-rw-r--r--browser/base/content/viewSourceOverlay.xul22
-rw-r--r--browser/base/content/web-panels.js97
-rw-r--r--browser/base/content/web-panels.xul84
-rw-r--r--browser/base/content/win6BrowserOverlay.xul12
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="&copyButton.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="&copyCmd.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="&copyEmailCmd.label;"
+ accesskey="&copyEmailCmd.accesskey;"
+ oncommand="gContextMenu.copyEmail();"/>
+ <menuitem id="context-copylink"
+ label="&copyLinkCmd.label;"
+ accesskey="&copyLinkCmd.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="&copyImageContentsCmd.label;"
+ accesskey="&copyImageContentsCmd.accesskey;"
+ oncommand="goDoCommand('cmd_copyImage');"/>
+#endif
+ <menuitem id="context-copyimage"
+ label="&copyImageCmd.label;"
+ accesskey="&copyImageCmd.accesskey;"
+ oncommand="gContextMenu.copyMediaLocation();"/>
+ <menuitem id="context-copyvideourl"
+ label="&copyVideoURLCmd.label;"
+ accesskey="&copyVideoURLCmd.accesskey;"
+ oncommand="gContextMenu.copyMediaLocation();"/>
+ <menuitem id="context-copyaudiourl"
+ label="&copyAudioURLCmd.label;"
+ accesskey="&copyAudioURLCmd.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="&copyCmd.label;"
+ accesskey="&copyCmd.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 &amp;&amp;
+#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="&copyCmd.label;"
+ key="key_copy"
+ accesskey="&copyCmd.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="&copyCmd.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="&copyCmd.label;"
+ command="cmd_copy"
+ tooltiptext="&copyButton.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
new file mode 100644
index 000000000..437036fe8
--- /dev/null
+++ b/browser/base/content/padlock_classic_broken.png
Binary files differ
diff --git a/browser/base/content/padlock_classic_ev.png b/browser/base/content/padlock_classic_ev.png
new file mode 100644
index 000000000..b3f80c0da
--- /dev/null
+++ b/browser/base/content/padlock_classic_ev.png
Binary files differ
diff --git a/browser/base/content/padlock_classic_https.png b/browser/base/content/padlock_classic_https.png
new file mode 100644
index 000000000..86026c04f
--- /dev/null
+++ b/browser/base/content/padlock_classic_https.png
Binary files differ
diff --git a/browser/base/content/padlock_classic_low.png b/browser/base/content/padlock_classic_low.png
new file mode 100644
index 000000000..652ad091f
--- /dev/null
+++ b/browser/base/content/padlock_classic_low.png
Binary files differ
diff --git a/browser/base/content/padlock_classic_mixed.png b/browser/base/content/padlock_classic_mixed.png
new file mode 100644
index 000000000..a8be5d4fe
--- /dev/null
+++ b/browser/base/content/padlock_classic_mixed.png
Binary files differ
diff --git a/browser/base/content/padlock_mod_broken.png b/browser/base/content/padlock_mod_broken.png
new file mode 100644
index 000000000..33a6c0645
--- /dev/null
+++ b/browser/base/content/padlock_mod_broken.png
Binary files differ
diff --git a/browser/base/content/padlock_mod_ev.png b/browser/base/content/padlock_mod_ev.png
new file mode 100644
index 000000000..3dfdcbde5
--- /dev/null
+++ b/browser/base/content/padlock_mod_ev.png
Binary files differ
diff --git a/browser/base/content/padlock_mod_https.png b/browser/base/content/padlock_mod_https.png
new file mode 100644
index 000000000..d494b42b0
--- /dev/null
+++ b/browser/base/content/padlock_mod_https.png
Binary files differ
diff --git a/browser/base/content/padlock_mod_low.png b/browser/base/content/padlock_mod_low.png
new file mode 100644
index 000000000..29179efaf
--- /dev/null
+++ b/browser/base/content/padlock_mod_low.png
Binary files differ
diff --git a/browser/base/content/padlock_mod_mixed.png b/browser/base/content/padlock_mod_mixed.png
new file mode 100644
index 000000000..137c338b4
--- /dev/null
+++ b/browser/base/content/padlock_mod_mixed.png
Binary files differ
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
new file mode 100644
index 000000000..7e6ef77ec
--- /dev/null
+++ b/browser/base/content/test/general/audio.ogg
Binary files differ
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>