browser/base/content/urlbarBindings.xml
author Henri Kemppainen <mozilla@guu.fi>
Mon, 04 Sep 2017 18:28:17 +0300
changeset 378880 76834322c5a8ccd7ab9cc63f79abe34b8dba2ba0
parent 378102 b7a2fb849cfa6e12d5e43b8af3cee6eca97854d5
child 378884 fcb0997350fdf472f6ec3546e7767e11f4784cc1
permissions -rw-r--r--
Bug 1385374 - Disable tab scrolling when urlbar popup only displays a notification. r=mak

<?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/.
-->

<!-- eslint-env mozilla/browser-window -->

<!DOCTYPE bindings [
<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
%notificationDTD;
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
%brandDTD;
]>

<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://global/content/bindings/autocomplete.xml#autocomplete">

    <content sizetopopup="pref">
      <xul:hbox anonid="textbox-container"
                class="autocomplete-textbox-container urlbar-textbox-container"
                flex="1" xbl:inherits="focused">
        <children includes="image|deck|stack|box">
          <xul:image class="autocomplete-icon" allowevents="true"/>
        </children>
        <xul:hbox anonid="textbox-input-box"
                  class="textbox-input-box urlbar-input-box"
                  flex="1" xbl:inherits="tooltiptext=inputtooltiptext">
          <children/>
          <html:input anonid="input"
                      class="autocomplete-textbox urlbar-input textbox-input uri-element-right-align"
                      allowevents="true"
                      inputmode="url"
                      xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,focused,textoverflow"/>
          <xul:image anonid="urlbar-go-button"
                     class="urlbar-go-button urlbar-icon"
                     onclick="gURLBar.handleCommand(event);"
                     tooltiptext="&goEndCap.tooltip;"
                     xbl:inherits="pageproxystate,parentfocused=focused"/>
        </xul:hbox>
        <xul:dropmarker anonid="historydropmarker"
                        class="autocomplete-history-dropmarker urlbar-history-dropmarker urlbar-icon"
                        tooltiptext="&urlbar.openHistoryPopup.tooltip;"
                        allowevents="true"
                        xbl:inherits="open,enablehistory,parentfocused=focused"/>
        <children includes="hbox"/>
      </xul:hbox>
      <xul:popupset anonid="popupset"
                    class="autocomplete-result-popupset"/>
      <children includes="toolbarbutton"/>
    </content>

    <implementation implements="nsIObserver, nsIDOMEventListener">
      <field name="ExtensionSearchHandler" readonly="true">
        (Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
      </field>

      <constructor><![CDATA[
        this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
                                .getService(Components.interfaces.nsIPrefService)
                                .getBranch("browser.urlbar.");
        this._prefs.addObserver("", this);

        this._defaultPrefs = Components.classes["@mozilla.org/preferences-service;1"]
                                       .getService(Components.interfaces.nsIPrefService)
                                       .getDefaultBranch("browser.urlbar.");

        Services.prefs.addObserver("browser.search.openintab", this);
        this.browserSearchOpenInTab = Services.prefs.getBoolPref("browser.search.openintab");
        Services.prefs.addObserver("browser.search.suggest.enabled", this);
        this.browserSearchSuggestEnabled = Services.prefs.getBoolPref("browser.search.suggest.enabled");

        this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll");
        this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll");
        this.completeDefaultIndex = this._prefs.getBoolPref("autoFill");
        this.speculativeConnectEnabled = this._prefs.getBoolPref("speculativeConnect.enabled");
        this.urlbarSearchSuggestEnabled = this._prefs.getBoolPref("suggest.searches");
        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("paste", this);
        this.inputField.addEventListener("mousedown", this);
        this.inputField.addEventListener("mousemove", this);
        this.inputField.addEventListener("mouseout", this);
        this.inputField.addEventListener("overflow", this);
        this.inputField.addEventListener("underflow", this);

        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");
        });

        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);
        }

        this.popup.addEventListener("popupshowing", () => {
          this._enableOrDisableOneOffSearches();
        }, {capturing: true, once: true});

        // The autocomplete controller uses heuristic on some internal caches
        // to handle cases like backspace, autofill or repeated searches.
        // Ensure to clear those internal caches when switching tabs.
        gBrowser.tabContainer.addEventListener("TabSelect", this);
      ]]></constructor>

      <destructor><![CDATA[
        this._prefs.removeObserver("", this);
        this._prefs = null;
        Services.prefs.removeObserver("browser.search.openintab", this);
        Services.prefs.removeObserver("browser.search.suggest.enabled", this);
        this.inputField.controllers.removeController(this._copyCutController);
        this.inputField.removeEventListener("paste", this);
        this.inputField.removeEventListener("mousedown", this);
        this.inputField.removeEventListener("mousemove", this);
        this.inputField.removeEventListener("mouseout", this);
        this.inputField.removeEventListener("overflow", this);
        this.inputField.removeEventListener("underflow", this);

        if (this._deferredKeyEventTimeout) {
          clearTimeout(this._deferredKeyEventTimeout);
          this._deferredKeyEventTimeout = null;
        }

        // Null out the one-offs' popup and textbox so that it cleans up its
        // internal state for both.  Most importantly, it removes the event
        // listeners that it added to both.
        this.popup.oneOffSearchButtons.popup = null;
        this.popup.oneOffSearchButtons.textbox = null;
      ]]></destructor>

      <field name="goButton">
        document.getAnonymousElementByAttribute(this, "anonid", "urlbar-go-button");
      </field>

      <field name="_value">""</field>
      <field name="gotResultForCurrentQuery">false</field>

      <!--
        This is set around HandleHenter so it can be used in handleCommand.
        It is also used to track whether we must handle a delayed handleEnter,
        by checking if it has been cleared.
      -->
      <field name="handleEnterInstance">null</field>

      <!--
        For performance reasons we want to limit the size of the text runs we
        build and show to the user.
      -->
      <field name="textRunsMaxLen">255</field>

      <!--
        Since we never want scrollbars, we always use the maxResults value.
      -->
      <property name="maxRows"
                onget="return this.popup.maxResults;"/>

      <!--
        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[
          return { value: this._value };
        ]]></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) {
            switch (action.type) {
              case "switchtab": // Fall through.
              case "remotetab": // Fall through.
              case "visiturl": {
                returnValue = action.params.displayUrl;
                break;
              }
              case "keyword": // Fall through.
              case "searchengine": {
                returnValue = action.params.input;
                break;
              }
              case "extension": {
                returnValue = action.params.content;
                break;
              }
            }
          } else {
            let originalUrl = ReaderMode.getOriginalUrlObjectForDisplay(aValue);
            if (originalUrl) {
              returnValue = originalUrl.displaySpec;
            }
          }

          // Set the actiontype only if the user is not overriding actions.
          if (action && this._pressedNoActionKeys.size == 0) {
            this.setAttribute("actiontype", action.type);
          } else {
            this.removeAttribute("actiontype");
          }
          return returnValue;
        ]]></body>
      </method>

      <method name="onKeyPress">
        <parameter name="aEvent"/>
        <body><![CDATA[
          switch (aEvent.keyCode) {
            case KeyEvent.DOM_VK_LEFT:
            case KeyEvent.DOM_VK_RIGHT:
            case KeyEvent.DOM_VK_HOME:
              // Reset the selected index so that nsAutoCompleteController
              // simply closes the popup without trying to fill anything.
              this.popup.selectedIndex = -1;
              break;
          }
          if (!this.popup.disableKeyNavigation) {
            if (this._keyCodesToDefer.has(aEvent.keyCode) &&
                this._shouldDeferKeyEvent(aEvent)) {
              this._deferKeyEvent(aEvent, "onKeyPress");
              return false;
            }
            if (this.popup.popupOpen && this.popup.handleKeyPress(aEvent)) {
              return true;
            }
          }
          return this.handleKeyPress(aEvent);
        ]]></body>
      </method>

      <!--
        Search results arrive asynchronously, which means that keypresses may
        arrive before results do and therefore not have the effect the user
        intends.  That's especially likely to happen with the down arrow and
        enter keys due to the one-off search buttons: if the user very quickly
        pastes something in the input, presses the down arrow key, and then hits
        enter, they are probably expecting to visit the first result.  But if
        there are no results, then pressing down and enter will trigger the
        first one-off button.  To prevent that undesirable behavior, certain
        keys are buffered and deferred until more results arrive, at which time
        they're replayed.

        @param event
               The key event that should maybe be deferred.  You can pass null
               or undefined if you don't have one to see whether the next key
               event in general should be deferred.
        @return True if the event should be deferred, false if not.
       -->
      <method name="_shouldDeferKeyEvent">
        <parameter name="event"/>
        <body><![CDATA[
          let waitedLongEnough =
            this._searchStartDate + this._deferredKeyEventTimeoutMs < Date.now();
          if (waitedLongEnough && !this._deferredKeyEventTimeout) {
            return false;
          }
          if (event && event.keyCode == KeyEvent.DOM_VK_TAB && !this.popupOpen) {
            // In this case, the popup is closed and the user pressed the Tab
            // key.  The focus should move out of the urlbar immediately.
            return false;
          }
          if (!this.gotResultForCurrentQuery || !this.popupOpen) {
            return true;
          }
          let maxResultsRemaining =
            this.popup.maxResults - this.popup._matchCount;
          let lastResultSelected =
            this.popup.selectedIndex + 1 == this.popup._matchCount;
          return maxResultsRemaining > 0 && lastResultSelected;
        ]]></body>
      </method>

      <!--
        Adds a key event to the deferred event queue.

        @param event
               The key event to defer.
        @param methodName
               The name of the method on `this` to call.  It's expected to take
               a single argument, the event.
      -->
      <method name="_deferKeyEvent">
        <parameter name="event"/>
        <parameter name="methodName"/>
        <body><![CDATA[
          // Somehow event.defaultPrevented ends up true for deferred events.
          // autocomplete ignores defaultPrevented events, which means it would
          // ignore replayed deferred events if we didn't tell it to bypass
          // defaultPrevented.  That's the purpose of this expando.  If we could
          // figure out what's setting defaultPrevented and prevent it, then we
          // could get rid of this.
          event.urlbarDeferred = true;

          this._deferredKeyEventQueue.push({
            methodName,
            event,
            searchString: this.mController.searchString,
          });

          if (!this._deferredKeyEventTimeout) {
            this._deferredKeyEventTimeout = setTimeout(() => {
              this._deferredKeyEventTimeout = null;
              this.maybeReplayDeferredKeyEvents();
            }, this._deferredKeyEventTimeoutMs);
          }
        ]]></body>
      </method>

      <!-- The enter key is always deferred, so it's not included here. -->
      <field name="_keyCodesToDefer">new Set([
        Ci.nsIDOMKeyEvent.DOM_VK_DOWN,
        Ci.nsIDOMKeyEvent.DOM_VK_TAB,
      ])</field>
      <field name="_deferredKeyEventQueue">[]</field>
      <field name="_deferredKeyEventTimeout">null</field>
      <field name="_deferredKeyEventTimeoutMs">200</field>
      <field name="_searchStartDate">0</field>

      <method name="maybeReplayDeferredKeyEvents">
        <body><![CDATA[
          if (!this._deferredKeyEventQueue.length ||
              this._shouldDeferKeyEvent()) {
            return;
          }
          if (this._deferredKeyEventTimeout) {
            clearTimeout(this._deferredKeyEventTimeout);
            this._deferredKeyEventTimeout = null;
          }
          let instance = this._deferredKeyEventQueue.shift();
          // Safety check: handle only if the search string didn't change.
          if (this.mController.searchString == instance.searchString) {
            this[instance.methodName](instance.event);
          }
          setTimeout(() => {
            this.maybeReplayDeferredKeyEvents();
          });
        ]]></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.editor)
            return;

          let controller = this.editor.selectionController;
          let strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT);
          strikeOut.removeAllRanges();

          let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
          selection.removeAllRanges();

          if (this.focused)
            return;

          let textNode = this.editor.rootElement.firstChild;
          let value = textNode.textContent;
          if (!value)
            return;

          // Get the URL from the fixup service:
          let flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
                      Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
          let uriInfo;
          try {
            uriInfo = Services.uriFixup.getFixupURIInfo(value, flags);
          } catch (ex) {}
          // Ignore if we couldn't make a URI out of this, the URI resulted in a search,
          // or the URI has a non-http(s)/ftp protocol.
          if (!uriInfo ||
              !uriInfo.fixedURI ||
              uriInfo.keywordProviderName ||
              ["http", "https", "ftp"].indexOf(uriInfo.fixedURI.scheme) == -1) {
            return;
          }

          // If we trimmed off the http scheme, ensure we stick it back on before
          // trying to figure out what domain we're accessing, so we don't get
          // confused by user:pass@host http URLs. We later use
          // trimmedLength to ensure we don't count the length of a trimmed protocol
          // when determining which parts of the URL to highlight as "preDomain".
          let trimmedLength = 0;
          if (uriInfo.fixedURI.scheme == "http" && !value.startsWith("http://")) {
            value = "http://" + value;
            trimmedLength = "http://".length;
          }

          let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(\S+?)(?::\d+)?\s*(?:[\/#?]|$)/);
          if (!matchedURL)
            return;

          // Strike out the "https" part if mixed active content is loaded.
          if (this.getAttribute("pageproxystate") == "valid" &&
              value.startsWith("https:") &&
              gBrowser.securityUI.state &
                Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) {
            let range = document.createRange();
            range.setStart(textNode, 0);
            range.setEnd(textNode, 5);
            strikeOut.addRange(range);
          }

          let [, preDomain, domain] = matchedURL;
          let baseDomain = domain;
          let subDomain = "";
          try {
            baseDomain = Services.eTLD.getBaseDomainFromHost(uriInfo.fixedURI.host);
            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 - trimmedLength;
          if (rangeLength) {
            let range = document.createRange();
            range.setStart(textNode, 0);
            range.setEnd(textNode, rangeLength);
            selection.addRange(range);
          }

          let startRest = preDomain.length + domain.length - trimmedLength;
          if (startRest < value.length - trimmedLength) {
            let range = document.createRange();
            range.setStart(textNode, startRest);
            range.setEnd(textNode, value.length - trimmedLength);
            selection.addRange(range);
          }
        ]]></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>

      <!--
        This is ultimately called by the autocomplete controller as the result
        of handleEnter when the Return key is pressed in the textbox.  Since
        onPopupClick also calls handleEnter, this is also called as a result in
        that case.

        @param event
               The event that triggered the command.
        @param openUILinkWhere
               Optional.  The "where" to pass to openUILinkIn.  This method
               computes the appropriate "where" given the event, but you can
               use this to override it.
        @param openUILinkParams
               Optional.  The parameters to pass to openUILinkIn.  As with
               "where", this method computes the appropriate parameters, but
               any parameters you supply here will override those.
      -->
      <method name="handleCommand">
        <parameter name="event"/>
        <parameter name="openUILinkWhere"/>
        <parameter name="openUILinkParams"/>
        <body><![CDATA[
          let isMouseEvent = event instanceof MouseEvent;
          if (isMouseEvent && event.button == 2) {
            // Do nothing for right clicks.
            return;
          }

          BrowserUsageTelemetry.recordUrlbarSelectedResultMethod(event);

          // Determine whether to use the selected one-off search button.  In
          // one-off search buttons parlance, "selected" means that the button
          // has been navigated to via the keyboard.  So we want to use it if
          // the triggering event is not a mouse click -- i.e., it's a Return
          // key -- or if the one-off was mouse-clicked.
          let selectedOneOff = this.popup.oneOffSearchButtons.selectedButton;
          if (selectedOneOff &&
              isMouseEvent &&
              event.originalTarget != selectedOneOff) {
            selectedOneOff = null;
          }

          // Do the command of the selected one-off if it's not an engine.
          if (selectedOneOff && !selectedOneOff.engine) {
            selectedOneOff.doCommand();
            return;
          }

          let where = openUILinkWhere;
          if (!where) {
            if (isMouseEvent) {
              where = whereToOpenLink(event, false, false);
            } else if (this.browserSearchOpenInTab) {
              where = "tab";
            } else {
              // If the current tab is empty, ignore Alt+Enter (reuse this tab)
              let altEnter = !isMouseEvent &&
                             event &&
                             event.altKey &&
                             !isTabEmpty(gBrowser.selectedTab);
              where = altEnter ? "tab" : "current";
            }
          }

          let url = this.value;
          if (!url) {
            return;
          }

          let mayInheritPrincipal = false;
          let postData = null;
          let browser = gBrowser.selectedBrowser;
          let action = this._parseActionUrl(url);

          if (selectedOneOff && selectedOneOff.engine) {
            // If there's a selected one-off button then load a search using
            // the one-off's engine.
            [url, postData] =
              this._parseAndRecordSearchEngineLoad(selectedOneOff.engine,
                                                   this.oneOffSearchQuery,
                                                   event, where,
                                                   openUILinkParams);
          } else if (action) {
            switch (action.type) {
              case "visiturl":
                // Unifiedcomplete uses fixupURI to tell if something is a visit
                // or a search, and passes out the fixedURI as the url param.
                // By using that uri we would end up passing a different string
                // to the docshell that may run a different not-found heuristic.
                // For example, "mozilla/run" would be fixed by unifiedcomplete
                // to "http://mozilla/run". The docshell, once it can't resolve
                // mozilla, would note the string has a scheme, and try to load
                // http://mozilla.com/run instead of searching "mozilla/run".
                // So, if we have the original input at hand, we pass it through
                // and let the docshell handle it.
                if (action.params.input) {
                  url = action.params.input;
                  break;
                }
                url = action.params.url;
                break;
              case "remotetab":
                url = action.params.url;
                break;
              case "keyword":
                if (action.params.postData) {
                  postData = getPostDataStream(action.params.postData);
                }
                mayInheritPrincipal = true;
                url = action.params.url;
                break;
              case "switchtab":
                url = action.params.url;
                if (this.hasAttribute("actiontype")) {
                  this.handleRevert();
                  let prevTab = gBrowser.selectedTab;
                  if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) {
                    gBrowser.removeTab(prevTab);
                  }
                  return;
                }
                break;
              case "searchengine":
                if (selectedOneOff && selectedOneOff.engine) {
                  // Replace the engine with the selected one-off engine.
                  action.params.engineName = selectedOneOff.engine.name;
                }
                const actionDetails = {
                  isSuggestion: !!action.params.searchSuggestion,
                  isAlias: !!action.params.alias
                };
                [url, postData] = this._parseAndRecordSearchEngineLoad(
                  action.params.engineName,
                  action.params.searchSuggestion || action.params.searchQuery,
                  event,
                  where,
                  openUILinkParams,
                  actionDetails
                );
                break;
              case "extension":
                this.handleRevert();
                // Give the extension control of handling the command.
                let searchString = action.params.content;
                let keyword = action.params.keyword;
                this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where);
                return;
            }
          } else {
            // This is a fallback for add-ons and old testing code that directly
            // set value and try to confirm it. UnifiedComplete should always
            // resolve to a valid url.
            try {
              new URL(url);
            } catch (ex) {
              let lastLocationChange = browser.lastLocationChange;
              getShortcutOrURIAndPostData(url).then(data => {
                if (where != "current" ||
                    browser.lastLocationChange == lastLocationChange) {
                  this._loadURL(data.url, browser, data.postData, where,
                                openUILinkParams, data.mayInheritPrincipal);
                }
              });
              return;
            }
          }

          this._loadURL(url, browser, postData, where, openUILinkParams,
                        mayInheritPrincipal);
        ]]></body>
      </method>

      <property name="oneOffSearchQuery">
        <getter><![CDATA[
          // If the user has selected a search suggestion, chances are they
          // want to use the one off search engine to search for that suggestion,
          // not the string that they manually entered into the location bar.
          let action = this._parseActionUrl(this.value);
          if (action && action.type == "searchengine") {
            return action.params.input;
          }
          // this.textValue may be an autofilled string.  Search only with the
          // portion that the user typed, if any, by preferring the autocomplete
          // controller's searchString (including handleEnterInstance.searchString).
          return this.handleEnterSearchString ||
                 this.mController.searchString ||
                 this.textValue;
        ]]></getter>
      </property>

      <method name="_loadURL">
        <parameter name="url"/>
        <parameter name="browser"/>
        <parameter name="postData"/>
        <parameter name="openUILinkWhere"/>
        <parameter name="openUILinkParams"/>
        <parameter name="mayInheritPrincipal"/>
        <body><![CDATA[
          this.value = url;
          browser.userTypedValue = url;
          if (gInitialPages.includes(url)) {
            browser.initialPageLoadedFromURLBar = 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);
          }

          let params = {
            postData,
            allowThirdPartyFixup: true,
          };
          if (openUILinkWhere == "current") {
            params.targetBrowser = browser;
            params.indicateErrorPageLoad = true;
            params.allowPinnedTabHostChange = true;
            params.disallowInheritPrincipal = !mayInheritPrincipal;
            params.allowPopups = url.startsWith("javascript:");
          } else {
            params.initiatingDoc = document;
          }

          if (openUILinkParams) {
            for (let key in openUILinkParams) {
              params[key] = openUILinkParams[key];
            }
          }

          // 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.
          browser.focus();

          if (openUILinkWhere != "current") {
            this.handleRevert();
          }

          try {
            openUILinkIn(url, openUILinkWhere, params);
          } catch (ex) {
            // This load can throw an exception in certain cases, which means
            // we'll want to replace the URL with the loaded URL:
            if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
              this.handleRevert();
            }
          }

          if (openUILinkWhere == "current") {
            // Ensure the start of the URL is visible for usability reasons.
            this.selectionStart = this.selectionEnd = 0;
          }
        ]]></body>
      </method>

      <method name="_parseAndRecordSearchEngineLoad">
        <parameter name="engineOrEngineName"/>
        <parameter name="query"/>
        <parameter name="event"/>
        <parameter name="openUILinkWhere"/>
        <parameter name="openUILinkParams"/>
        <parameter name="searchActionDetails"/>
        <body><![CDATA[
          let engine =
            typeof(engineOrEngineName) == "string" ?
              Services.search.getEngineByName(engineOrEngineName) :
              engineOrEngineName;
          let isOneOff = this.popup.oneOffSearchButtons
              .maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
          // Infer the type of the event which triggered the search.
          let eventType = "unknown";
          if (event instanceof KeyboardEvent) {
            eventType = "key";
          } else if (event instanceof MouseEvent) {
            eventType = "mouse";
          }
          // Augment the search action details object.
          let details = searchActionDetails || {};
          details.isOneOff = isOneOff;
          details.type = eventType;

          BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details);
          let submission = engine.getSubmission(query, null, "keyword");
          return [submission.uri.spec, submission.postData];
        ]]></body>
      </method>

      <method name="maybeCanonizeURL">
        <parameter name="aTriggeringEvent"/>
        <parameter name="aUrl"/>
        <body><![CDATA[
          // 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(aUrl) ||
              !(aTriggeringEvent instanceof KeyEvent)) {
            return;
          }

          let url = aUrl;
          let accel = AppConstants.platform == "macosx" ?
                      aTriggeringEvent.metaKey :
                      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)
            return;

          // 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;
          }

          this.popup.overrideValue = "http://www." + url;
        ]]></body>
      </method>

      <method name="_initURLTooltip">
        <body><![CDATA[
          if (this.focused || !this.hasAttribute("textoverflow"))
            return;
          this.inputField.setAttribute("tooltiptext", this.value);
        ]]></body>
      </method>

      <method name="_hideURLTooltip">
        <body><![CDATA[
          this.inputField.removeAttribute("tooltiptext");
        ]]></body>
      </method>

      <!-- Returns:
           null if there's a security issue and we should do nothing.
           a URL object if there is one that we're OK with loading,
           a text value otherwise.
           -->
      <method name="_getDroppableItem">
        <parameter name="aEvent"/>
        <body><![CDATA[
          let links;
          try {
            links = browserDragAndDrop.dropLinks(aEvent);
          } catch (ex) {
            // this is possibly a security exception, in which case we should return
            // null. Always return null because we can't *know* what exception is
            // being returned.
            return null;
          }
          // 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) {
            aEvent.preventDefault();
            let url = links[0].url;
            let strippedURL = stripUnsafeProtocolOnPaste(url);
            if (strippedURL != url) {
              aEvent.stopImmediatePropagation();
              return null;
            }
            let urlObj;
            try {
              // If this throws, urlSecurityCheck would also throw, as that's what it
              // does with things that don't pass the IO service's newURI constructor
              // without fixup. It's conceivable we may want to relax this check in
              // the future (so e.g. www.foo.com gets fixed up), but not right now.
              urlObj = new URL(url);
              // If we succeed, try to pass security checks. If this works, return the
              // URL object. If the *security checks* fail, return null.
              try {
                urlSecurityCheck(url,
                                 gBrowser.contentPrincipal,
                                 Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
                return urlObj;
              } catch (ex) {
                return null;
              }
            } catch (ex) {
              // We couldn't make a URL out of this. Continue on, and return text below.
            }
          }
          return aEvent.dataTransfer.getData("text/unicode");
        ]]></body>
      </method>

      <method name="onDragOver">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (!this._getDroppableItem(aEvent)) {
            aEvent.dataTransfer.dropEffect = "none";
          }
        ]]></body>
      </method>

      <method name="onDrop">
        <parameter name="aEvent"/>
        <body><![CDATA[
          let droppedItem = this._getDroppableItem(aEvent);
          if (droppedItem) {
            this.value = droppedItem instanceof URL ? droppedItem.href : droppedItem;
            SetPageProxyState("invalid");
            this.focus();
            if (droppedItem instanceof URL) {
              this.handleCommand();
              // Force not showing the dropped URI immediately.
              gBrowser.userTypedValue = null;
              URLBarSetURI();
            }
          }
        ]]></body>
      </method>

      <method name="makeURIReadable">
        <parameter name="aURI"/>
        <body>
          <![CDATA[
            // Avoid copying 'about:reader?url=', and always provide the original URI:
            // Reader mode ensures we call createExposableURI itself.
            let readerStrippedURI = ReaderMode.getOriginalUrlObjectForDisplay(aURI.displaySpec);
            if (readerStrippedURI) {
              aURI = readerStrippedURI;
            } else {
              // Only copy exposable URIs
              try {
                aURI = Services.uriFixup.createExposableURI(aURI);
              } catch (ex) {}
            }
            return aURI;
          ]]>
        </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;
          let selection = this.editor.selection;
          var selectedVal = selection.toString();

          // Handle multiple-range selection as a string for simplicity.
          if (selection.rangeCount > 1) {
             return selectedVal;
          }

          // If the selection doesn't start at the beginning or doesn't span the full domain or
          // the URL bar is modified or there is no text at all, nothing else to do here.
          if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "")
            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;
          }

          uri = this.makeURIReadable(uri);

          // 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.displaySpec;
          }

          // Just the beginning of the URL is selected, or we want a decoded
          // url. First check for a trimmed value.
          let spec = uri.displaySpec;
          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(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;

              let event = document.createEvent("UIEvents");
              event.initUIEvent("input", true, false, window, 0);
              urlbar.dispatchEvent(event);

              SetPageProxyState("invalid");
            }

            Cc["@mozilla.org/widget/clipboardhelper;1"]
              .getService(Ci.nsIClipboardHelper)
              .copyString(val);
          },
          supportsCommand(aCommand) {
            switch (aCommand) {
              case "cmd_copy":
              case "cmd_cut":
                return true;
            }
            return false;
          },
          isCommandEnabled(aCommand) {
            return this.supportsCommand(aCommand) &&
                   (aCommand != "cmd_cut" || !this.urlbar.readOnly) &&
                   this.urlbar.selectionStart < this.urlbar.selectionEnd;
          },
          onEvent(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 "speculativeConnect.enabled":
                this.speculativeConnectEnabled = this._prefs.getBoolPref(aData);
                break;
              case "browser.search.openintab":
                this.browserSearchOpenInTab = Services.prefs.getBoolPref(aData);
                break;
              case "browser.search.suggest.enabled":
                this.browserSearchSuggestEnabled = Services.prefs.getBoolPref(aData);
                break;
              case "suggest.searches":
                this.urlbarSearchSuggestEnabled = this._prefs.getBoolPref(aData);
              case "userMadeSearchSuggestionsChoice":
                // Mirror the value for future use, see the comment in the
                // binding's constructor.
                this._prefs.setBoolPref("searchSuggestionsChoice",
                  this.urlbarSearchSuggestEnabled);
                // Clear the cached value to allow changing conditions in tests.
                delete this._whichSearchSuggestionsNotification;
                break;
              case "trimURLs":
                this._mayTrimURLs = this._prefs.getBoolPref(aData);
                break;
              case "oneOffSearches":
                this._enableOrDisableOneOffSearches();
                break;
              case "maxRichResults":
                this.popup.maxResults = this._prefs.getIntPref(aData);
            }
          }
        ]]></body>
      </method>

      <method name="_enableOrDisableOneOffSearches">
        <body><![CDATA[
          let enable = this._prefs.getBoolPref("oneOffSearches");
          this.popup.enableOneOffSearches(enable);
        ]]></body>
      </method>

      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          switch (aEvent.type) {
            case "paste":
              let originalPasteData = aEvent.clipboardData.getData("text/plain");
              if (!originalPasteData) {
                return;
              }

              let oldValue = this.inputField.value;
              let oldStart = oldValue.substring(0, this.inputField.selectionStart);
              // If there is already non-whitespace content in the URL bar
              // preceding the pasted content, it's not necessary to check
              // protocols used by the pasted content:
              if (oldStart.trim()) {
                return;
              }
              let oldEnd = oldValue.substring(this.inputField.selectionEnd);

              let pasteData = stripUnsafeProtocolOnPaste(originalPasteData);
              if (originalPasteData != pasteData) {
                // Unfortunately we're not allowed to set the bits being pasted
                // so cancel this event:
                aEvent.preventDefault();
                aEvent.stopImmediatePropagation();

                this.inputField.value = oldStart + pasteData + oldEnd;
                // Fix up cursor/selection:
                let newCursorPos = oldStart.length + pasteData.length;
                this.inputField.selectionStart = newCursorPos;
                this.inputField.selectionEnd = newCursorPos;
              }
              break;
            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":
              if (!this.value) {
                // We initially get a spurious overflow event from the
                // anonymous div containing the placeholder text; bail out.
                break;
              }
              this.setAttribute("textoverflow", "true");
              break;
            case "underflow":
              this.removeAttribute("textoverflow");
              this._hideURLTooltip();
              break;
            case "TabSelect":
              this.controller.resetInternalState();
              break;
          }
        ]]></body>
      </method>

      <!--
        onBeforeTextValueSet is called by the base-binding's .textValue getter.
        It should return the value that the getter should use.
      -->
      <method name="onBeforeTextValueGet">
        <body><![CDATA[
          return { value: this.inputField.value };
        ]]></body>
      </method>

      <!--
        onBeforeTextValueSet is called by the base-binding's .textValue setter.
        It should return the value that the setter should use.
      -->
      <method name="onBeforeTextValueSet">
        <parameter name="aValue"/>
        <body><![CDATA[
          let val = aValue;
          let uri;
          try {
            uri = makeURI(val);
          } catch (ex) {}

          if (uri) {
            // Do not touch moz-action URIs at all.  They depend on being
            // properly encoded and decoded and will break if decoded
            // unexpectedly.
            if (!this._parseActionUrl(val)) {
              val = losslessDecodeURI(uri);
            }
          }

          return val;
        ]]></body>
      </method>

      <method name="_parseActionUrl">
        <parameter name="aUrl"/>
        <body><![CDATA[
          const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
          if (!MOZ_ACTION_REGEX.test(aUrl))
            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_REGEX);

          let action = {
            type,
          };

          action.params = JSON.parse(params);
          for (let key in action.params) {
            action.params[key] = decodeURIComponent(action.params[key]);
          }

          if ("url" in action.params) {
            let uri;
            try {
              uri = makeURI(action.params.url);
              action.params.displayUrl = losslessDecodeURI(uri);
            } catch (e) {
              action.params.displayUrl = action.params.url;
            }
          }

          return action;
        ]]></body>
      </method>

      <property name="_noActionKeys" readonly="true">
        <getter><![CDATA[
          if (!this.__noActionKeys) {
            this.__noActionKeys = new Set([
              KeyEvent.DOM_VK_ALT,
              KeyEvent.DOM_VK_SHIFT,
            ]);
            let modifier = AppConstants.platform == "macosx" ?
                           KeyEvent.DOM_VK_META :
                           KeyEvent.DOM_VK_CONTROL;
            this.__noActionKeys.add(modifier);
          }
          return this.__noActionKeys;
        ]]></getter>
      </property>

      <field name="_pressedNoActionKeys"><![CDATA[
        new Set()
      ]]></field>

      <method name="_clearNoActions">
        <parameter name="aURL"/>
        <body><![CDATA[
          this._pressedNoActionKeys.clear();
          this.popup.removeAttribute("noactions");
          let action = this._parseActionUrl(this._value);
          if (action)
            this.setAttribute("actiontype", action.type);
        ]]></body>
      </method>

      <method name="onInput">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (!this.mIgnoreInput && this.mController.input == this) {
            this._value = this.inputField.value;
            gBrowser.userTypedValue = this.value;
            this.valueIsTyped = true;
            if (this.inputField.value) {
              this.setAttribute("usertyping", "true");
            } else {
              this.removeAttribute("usertyping");
            }
            // Only wait for a result when we are sure to get one.  In some
            // cases, like when pasting the same exact text, we may not fire
            // a new search and we won't get a result.
            if (this.mController.handleText()) {
              this.gotResultForCurrentQuery = false;
              this._searchStartDate = Date.now();
              this._deferredKeyEventQueue = [];
              if (this._deferredKeyEventTimeout) {
                clearTimeout(this._deferredKeyEventTimeout);
                this._deferredKeyEventTimeout = null;
              }
            }
          }
          this.resetActionType();
        ]]></body>
      </method>

      <method name="handleEnter">
        <parameter name="event"/>
        <body><![CDATA[
          // We need to ensure we're using a selected autocomplete result.
          // A result should automatically be selected by default,
          // however autocomplete is async and therefore we may not
          // have a result set relating to the current input yet. If that
          // happens, we need to mark that when the first result does get added,
          // it needs to be handled as if enter was pressed with that first
          // result selected.
          // If anything other than the default (first) result is selected, then
          // it must have been manually selected by the human. We let this
          // explicit choice be used, even if it may be related to a previous
          // input.
          // However, if the default result is automatically selected, we
          // ensure that it corresponds to the current input.

          // Store the current search string so it can be used in handleCommand,
          // which will be called as a result of mController.handleEnter().
          this.handleEnterSearchString = this.mController.searchString;

          if (!this._deferredKeyEventQueue.length &&
              (this.popup.selectedIndex != 0 || this.gotResultForCurrentQuery)) {
            let canonizeValue = this.value;
            if (event.shiftKey || (AppConstants.platform === "macosx" ?
                                   event.metaKey :
                                   event.ctrlKey)) {
              let action = this._parseActionUrl(canonizeValue);
              if (action && "searchSuggestion" in action.params) {
                canonizeValue = action.params.searchSuggestion;
              } else if (this.popup.selectedIndex === 0 &&
                         this.mController.getStyleAt(0).includes("autofill")) {
                canonizeValue = this.handleEnterSearchString;
              }
            }
            this.maybeCanonizeURL(event, canonizeValue);
            let handled = this.mController.handleEnter(false, event);
            this.handleEnterSearchString = null;
            this.popup.overrideValue = null;
            return handled;
          }

          // Defer the event until the first non-heuristic result comes in.
          this._deferKeyEvent(event, "handleEnter");
          return false;
        ]]></body>
      </method>

      <method name="handleDelete">
        <body><![CDATA[
          // If the heuristic result is selected, then the autocomplete
          // controller's handleDelete implementation will remove it, which is
          // not what we want.  So in that case, call handleText so it acts as
          // a backspace on the text value instead of removing the result.
          if (this.popup.selectedIndex == 0 &&
              this.popup._isFirstResultHeuristic) {
            this.mController.handleText();
            return false;
          }
          return this.mController.handleDelete();
        ]]></body>
      </method>

      <property name="_userMadeSearchSuggestionsChoice" readonly="true">
        <getter><![CDATA[
          return this._prefs.getBoolPref("userMadeSearchSuggestionsChoice") ||
                 this._defaultPrefs.getBoolPref("suggest.searches") != this._prefs.getBoolPref("suggest.searches");
        ]]></getter>
      </property>

      <property name="whichSearchSuggestionsNotification" readonly="true">
        <getter><![CDATA[
          // Once we return "none" once, we'll always return "none".
          // If available, use the cached value, rather than running all of the
          // checks again at every locationbar focus.
          if (this._whichSearchSuggestionsNotification) {
            return this._whichSearchSuggestionsNotification;
          }

          if (this.browserSearchSuggestEnabled && !this.inPrivateContext &&
              // In any case, if the user made a choice we should not nag him.
              !this._userMadeSearchSuggestionsChoice) {
            let enabledByDefault = this._defaultPrefs.getBoolPref("suggest.searches");
            if (!enabledByDefault &&
                this._prefs.getIntPref("daysBeforeHidingSuggestionsPrompt")) {
              return "opt-in";
            }
            if (enabledByDefault &&
                // Has not been switched off.
                this.urlbarSearchSuggestEnabled &&
                this._prefs.getIntPref("timesBeforeHidingSuggestionsHint")) {
              return "opt-out";
            }
          }
          return this._whichSearchSuggestionsNotification = "none";
        ]]></getter>
      </property>

      <method name="updateSearchSuggestionsNotificationImpressions">
        <parameter name="whichNotification"/>
        <body><![CDATA[
          if (whichNotification == "none") {
            throw new Error("Unexpected notification type");
          }

          let useDays = whichNotification == "opt-in";
          let prefName = useDays ? "daysBeforeHidingSuggestionsPrompt"
                                 : "timesBeforeHidingSuggestionsHint";
          let remaining = this._prefs.getIntPref(prefName);
          if (remaining <= 0)
            return;

          let now = new Date();
          let date = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate();

          let previousDate = this._prefs.getIntPref("lastSuggestionsPromptDate");
          if (!useDays || previousDate != date) {
            this._prefs.setIntPref(prefName, remaining - 1);
          }
          this._prefs.setIntPref("lastSuggestionsPromptDate", date);
        ]]></body>
      </method>

    </implementation>

    <handlers>
      <handler event="keydown"><![CDATA[
        if (this._noActionKeys.has(event.keyCode) &&
            this.popup.selectedIndex >= 0 &&
            !this._pressedNoActionKeys.has(event.keyCode)) {
          if (this._pressedNoActionKeys.size == 0) {
            this.popup.setAttribute("noactions", "true");
            this.removeAttribute("actiontype");
          }
          this._pressedNoActionKeys.add(event.keyCode);
        }
      ]]></handler>

      <handler event="keyup"><![CDATA[
        if (this._noActionKeys.has(event.keyCode) &&
            this._pressedNoActionKeys.has(event.keyCode)) {
          this._pressedNoActionKeys.delete(event.keyCode);
          if (this._pressedNoActionKeys.size == 0)
            this._clearNoActions();
        }
      ]]></handler>

      <handler event="focus"><![CDATA[
        if (event.originalTarget == this.inputField) {
          this._hideURLTooltip();
          this.formatValue();
          if (this.getAttribute("pageproxystate") != "valid") {
            UpdatePopupNotificationsVisibility();
          }

          // We show the opt-out notification on every kind of focus to the urlbar
          // included opening a new tab, but we want to enforce at least one
          // notification when the user focuses it with the mouse.
          let whichNotification = this.whichSearchSuggestionsNotification;
          if (whichNotification == "opt-out" &&
              this._showSearchSuggestionNotificationOnMouseFocus === undefined) {
            this._showSearchSuggestionNotificationOnMouseFocus = true;
          }

          // Check whether the focus change came from a user mouse action.
          let focusMethod = Services.focus.getLastFocusMethod(window);
          let mouseFocused = !!(focusMethod & Services.focus.FLAG_BYMOUSE);
          if (this._showSearchSuggestionNotificationOnMouseFocus &&
              mouseFocused) {
            // Force showing the opt-out notification.
            this._whichSearchSuggestionsNotification = whichNotification = "opt-out";
          }

          if (whichNotification == "opt-out") {
            try {
              this.popup.openAutocompletePopup(this, this);
            } finally {
              if (mouseFocused) {
                delete this._whichSearchSuggestionsNotification;
                this._showSearchSuggestionNotificationOnMouseFocus = false;
              }
            }
          }
        }
      ]]></handler>

      <handler event="blur"><![CDATA[
        if (event.originalTarget == this.inputField) {
          this._clearNoActions();
          this.formatValue();
          if (this.getAttribute("pageproxystate") != "valid") {
            UpdatePopupNotificationsVisibility();
          }
        }
        if (this.ExtensionSearchHandler.hasActiveInputSession()) {
          this.ExtensionSearchHandler.handleInputCancelled();
        }
        if (this._deferredKeyEventTimeout) {
          clearTimeout(this._deferredKeyEventTimeout);
          this._deferredKeyEventTimeout = null;
        }
        this._deferredKeyEventQueue = [];
      ]]></handler>

      <handler event="dragstart" phase="capturing"><![CDATA[
        // Drag only if the gesture starts from the input field.
        if (this.inputField != event.originalTarget &&
            !(this.inputField.compareDocumentPosition(event.originalTarget) &
              Node.DOCUMENT_POSITION_CONTAINED_BY))
          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 = gBrowser.selectedBrowser.currentURI.displaySpec;
        var title = gBrowser.selectedBrowser.contentTitle || 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="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;

        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);
      ]]></handler>
    </handlers>

  </binding>

  <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">

    <resources>
      <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
      <stylesheet src="chrome://browser/skin/searchbar.css"/>
    </resources>

    <content ignorekeys="true" level="top" consumeoutsideclicks="never"
             aria-owns="richlistbox">
      <xul:deck anonid="search-suggestions-notification"
                align="center"
                role="alert"
                selectedIndex="0">
        <!-- OPT-IN -->
        <xul:hbox flex="1" align="center" anonid="search-suggestions-opt-in">
          <xul:description flex="1" id="search-suggestions-question">
            &urlbar.searchSuggestionsNotification.question;
            <!-- Several things here are to make the label accessibile via an
                accesskey so that a11y doesn't suck: the accesskey, using an
                onclick handler instead of an href attribute, the control
                attribute, and having the control attribute refer to a valid ID
                that is the label itself. -->
            <xul:label id="search-suggestions-learn-more"
                      class="text-link"
                      role="link"
                      value="&urlbar.searchSuggestionsNotification.learnMore;"
                      accesskey="&urlbar.searchSuggestionsNotification.learnMore.accesskey;"
                      onclick="document.getBindingParent(this).openSearchSuggestionsNotificationLearnMoreURL();"
                      control="search-suggestions-learn-more"/>
          </xul:description>
          <xul:button anonid="search-suggestions-notification-disable"
                      label="&urlbar.searchSuggestionsNotification.disable;"
                      accesskey="&urlbar.searchSuggestionsNotification.disable.accesskey;"
                      onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/>
          <xul:button anonid="search-suggestions-notification-enable"
                      label="&urlbar.searchSuggestionsNotification.enable;"
                      accesskey="&urlbar.searchSuggestionsNotification.enable.accesskey;"
                      onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/>
        </xul:hbox>
        <!-- OPT-OUT -->
        <xul:hbox flex="1" align="center" anonid="search-suggestions-opt-out">
          <xul:image class="ac-site-icon" type="searchengine"/>
          <xul:hbox anonid="search-suggestions-hint-typing">
            <xul:description class="ac-title-text">&brandShortName;</xul:description>
          </xul:hbox>
          <xul:hbox anonid="search-suggestions-hint-box" flex="1">
            <xul:description id="search-suggestions-hint">
              <html:span class="prefix">&#x1f4a1; &urlbar.searchSuggestionsNotification.hintPrefix;</html:span>
              <html:span>&urlbar.searchSuggestionsNotification.hint;</html:span>
            </xul:description>
          </xul:hbox>
          <xul:label id="search-suggestions-change-settings"
                     class="text-link"
                     role="link"
#ifdef XP_WIN
                     value="&urlbar.searchSuggestionsNotification.changeSettingsWin;"
                     accesskey="&urlbar.searchSuggestionsNotification.changeSettingsWin.accesskey;"
#else
                     value="&urlbar.searchSuggestionsNotification.changeSettingsUnix;"
                     accesskey="&urlbar.searchSuggestionsNotification.changeSettingsUnix.accesskey;"
#endif
                     onclick="openPreferences('paneSearch');"
                     control="search-suggestions-change-settings"/>
        </xul:hbox>
      </xul:deck>
      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
                       flex="1"/>
      <xul:hbox anonid="footer">
        <children/>
        <xul:vbox anonid="one-off-search-buttons"
                  class="search-one-offs"
                  compact="true"
                  includecurrentengine="true"
                  disabletab="true"
                  flex="1"/>
      </xul:hbox>
    </content>

    <implementation>
      <field name="DOMWindowUtils">
        window.QueryInterface(Ci.nsIInterfaceRequestor)
              .getInterface(Ci.nsIDOMWindowUtils);
      </field>

      <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>

      <field name="searchSuggestionsNotification" readonly="true">
        document.getAnonymousElementByAttribute(
          this, "anonid", "search-suggestions-notification"
        );
      </field>

      <field name="footer" readonly="true">
        document.getAnonymousElementByAttribute(this, "anonid", "footer");
      </field>

      <field name="oneOffSearchButtons" readonly="true">
        document.getAnonymousElementByAttribute(this, "anonid",
                                                "one-off-search-buttons");
      </field>

      <field name="_oneOffSearchesEnabled">false</field>

      <field name="_overrideValue">null</field>
      <property name="overrideValue"
                onget="return this._overrideValue;"
                onset="this._overrideValue = val; return val;"/>

      <method name="onPopupClick">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.button == 2) {
            // Ignore right-clicks.
            return;
          }
          // Otherwise "call super" -- do what autocomplete-base-popup does.
          let controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
          controller.handleEnter(true, aEvent);
        ]]></body>
      </method>

      <method name="enableOneOffSearches">
        <parameter name="enable"/>
        <body><![CDATA[
          this._oneOffSearchesEnabled = enable;
          if (enable) {
            this.oneOffSearchButtons.telemetryOrigin = "urlbar";
            this.oneOffSearchButtons.style.display = "-moz-box";
            // Set .textbox first, since the popup setter will cause
            // a _rebuild call that uses it.
            this.oneOffSearchButtons.textbox = this.input;
            this.oneOffSearchButtons.popup = this;
          } else {
            this.oneOffSearchButtons.telemetryOrigin = null;
            this.oneOffSearchButtons.style.display = "none";
            this.oneOffSearchButtons.textbox = null;
            this.oneOffSearchButtons.popup = null;
          }
        ]]></body>
      </method>

      <method name="openSearchSuggestionsNotificationLearnMoreURL">
        <body><![CDATA[
        let url = Services.urlFormatter.formatURL(
          Services.prefs.getCharPref("app.support.baseURL") + "suggestions"
        );
        openUILinkIn(url, "tab");
        ]]></body>
      </method>

      <method name="dismissSearchSuggestionsNotification">
        <parameter name="enableSuggestions"/>
        <body><![CDATA[
          // Make sure the urlbar is focused.  It won't be, for example, if the
          // user used an accesskey to make an opt-in choice.  mIgnoreFocus
          // prevents the text from being selected.
          this.input.mIgnoreFocus = true;
          this.input.focus();
          this.input.mIgnoreFocus = false;

          Services.prefs.setBoolPref(
            "browser.urlbar.suggest.searches", enableSuggestions
          );
          Services.prefs.setBoolPref(
            "browser.urlbar.userMadeSearchSuggestionsChoice", true
          );
          // Hide the notification.
          this.searchSuggestionsNotificationWasDismissed(
            Services.prefs.getBoolPref("browser.urlbar.suggest.searches")
          );
        ]]></body>
      </method>

      <!-- Override this so that navigating between items results in an item
           always being selected. -->
      <method name="getNextIndex">
        <parameter name="reverse"/>
        <parameter name="amount"/>
        <parameter name="index"/>
        <parameter name="maxRow"/>
        <body><![CDATA[
          if (maxRow < 0)
            return -1;

          let newIndex = index + (reverse ? -1 : 1) * amount;

          // We only want to wrap if navigation is in any direction by one item,
          // otherwise we clamp to one end of the list.
          // ie, hitting page-down will only cause is to wrap if we're already
          // at one end of the list.

          // Allow the selection to be removed if the first result is not a
          // heuristic result.
          if (!this._isFirstResultHeuristic) {
            if (reverse && index == -1 || newIndex > maxRow && index != maxRow)
              newIndex = maxRow;
            else if (!reverse && index == -1 || newIndex < 0 && index != 0)
              newIndex = 0;

            if (newIndex < 0 && index == 0 || newIndex > maxRow && index == maxRow)
              newIndex = -1;

            return newIndex;
          }

          // Otherwise do not allow the selection to be removed.
          if (newIndex < 0) {
            newIndex = index > 0 ? 0 : maxRow;
          } else if (newIndex > maxRow) {
            newIndex = index < maxRow ? maxRow : 0;
          }
          return newIndex;
        ]]></body>
      </method>

      <property name="_isFirstResultHeuristic" readonly="true">
        <getter>
          <![CDATA[
            // The popup usually has a special "heuristic" first result (added
            // by UnifiedComplete.js) that is automatically selected when the
            // popup opens.
            return this.input.mController.matchCount > 0 &&
                   this.input.mController
                             .getStyleAt(0)
                             .split(/\s+/).indexOf("heuristic") > 0;
          ]]>
        </getter>
      </property>

      <property name="maxResults">
        <getter>
          <![CDATA[
            if (!this._maxResults) {
              this._maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
            }
            return this._maxResults;
          ]]>
        </getter>
        <setter>
          <![CDATA[
            return this._maxResults = parseInt(val);
          ]]>
        </setter>
      </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._openAutocompletePopup(aInput, aElement);
          ]]>
        </body>
      </method>

      <method name="_openAutocompletePopup">
        <parameter name="aInput"/>
        <parameter name="aElement"/>
        <body><![CDATA[
          if (this.mPopupOpen) {
            return;
          }

          this.mInput = aInput;
          aInput.controller.setInitiallySelectedIndex(this._isFirstResultHeuristic ? 0 : -1);
          this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView);
          this._invalidate();

          var documentRect = window.document.documentElement.getBoundingClientRect();
          var width = documentRect.right - documentRect.left;
          this.setAttribute("width", width);

          // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840
          var popupDirection = aElement.ownerGlobal.getComputedStyle(aElement).direction;
          this.style.direction = popupDirection;

          // Make the popup's starting margin negative so that the leading edge
          // of the popup aligns with the window border.
          let elementRect = aElement.getBoundingClientRect();
          if (popupDirection == "rtl") {
            let offset = elementRect.right - documentRect.right
            this.style.marginRight = offset + "px";
          } else {
            let offset = documentRect.left - elementRect.left;
            this.style.marginLeft = offset + "px";
          }

          // Keep the popup items' site icons aligned with the urlbar's identity
          // icon if it's not too far from the edge of the window.  We define
          // "too far" as "more than 30% of the window's width AND more than
          // 250px"
          let boundToCheck = popupDirection == "rtl" ? "right" : "left";
          let inputRect = this.DOMWindowUtils.getBoundsWithoutFlushing(aInput);
          let startOffset = Math.abs(inputRect[boundToCheck] - documentRect[boundToCheck]);
          let alignSiteIcons = startOffset / width <= 0.3 || startOffset <= 250;
          if (alignSiteIcons) {
            let identityRect =
              document.getElementById("identity-icon").getBoundingClientRect();
            this.siteIconStart = popupDirection == "rtl" ? identityRect.right
                                                         : identityRect.left;
          } else {
            // Reset the alignment so that the site icons are positioned
            // according to whatever's in the CSS.
            this.siteIconStart = undefined;
          }

          try {
            let whichNotification = aInput.whichSearchSuggestionsNotification;
            if (whichNotification != "none") {
              // Update the impressions count on real popupshown, since there's
              // no guarantee openPopup will be respected by the platform.
              // Though, we must ensure the handled event is the expected one.
              let impressionId = this._searchSuggestionsImpressionId = {};
              this.addEventListener("popupshown", () => {
                if (this._searchSuggestionsImpressionId == impressionId)
                  aInput.updateSearchSuggestionsNotificationImpressions(whichNotification);
              }, {once: true});
              this._showSearchSuggestionsNotification(whichNotification, popupDirection);
            } else if (this.classList.contains("showSearchSuggestionsNotification")) {
              this._hideSearchSuggestionsNotification();
            }
          } catch (ex) {
            // Not critical for the urlbar functionality, just report the error.
            Components.utils.reportError(ex);
          }

          // Position the popup below the navbar.  To get the y-coordinate,
          // which is an offset from the bottom of the input, subtract the
          // bottom of the navbar from the buttom of the input.
          let yOffset =
            this.DOMWindowUtils.getBoundsWithoutFlushing(document.getElementById("nav-bar")).bottom -
            this.DOMWindowUtils.getBoundsWithoutFlushing(aInput).bottom;
          this.openPopup(aElement, "after_start", 0, yOffset, false, false);
        ]]></body>
      </method>

      <method name="_showSearchSuggestionsNotification">
        <parameter name="whichNotification"/>
        <parameter name="popupDirection"/>
        <body>
          <![CDATA[
          let deckIndex = 0;
          if (whichNotification == "opt-out") {
            deckIndex = 1;

            if (this.siteIconStart) {
              let rect = this.DOMWindowUtils.getBoundsWithoutFlushing(window.document.documentElement);
              let padding = popupDirection == "rtl" ? rect.right - this.siteIconStart
                                                    : this.siteIconStart;
              this.searchSuggestionsNotification.style.paddingInlineStart = padding + "px";
            } else {
              this.searchSuggestionsNotification.style.removeProperty("padding-inline-start");
            }

            // We want to animate the opt-out hint only once.
            if (!this._firstSearchSuggestionsNotification) {
              this._firstSearchSuggestionsNotification = true;
              this.searchSuggestionsNotification.setAttribute("animate", "true");
            }
          }
          this.searchSuggestionsNotification.setAttribute("selectedIndex", deckIndex);

          let ariaDescElt = whichNotification == "opt-in" ?
            "search-suggestions-question" : "search-suggestions-hint";

          this.searchSuggestionsNotification.setAttribute("aria-describedby", ariaDescElt);

          // With the notification shown, the listbox's height can sometimes be
          // too small when it's flexed, as it normally is.  Also, it can start
          // out slightly scrolled down.  Both problems appear together, most
          // often when the popup is very narrow and the notification's text
          // must wrap.  Work around them by removing the flex.
          //
          // But without flexing the listbox, the listbox's height animation
          // sometimes fails to complete, leaving the popup too tall.  Work
          // around that problem by disabling the listbox animation.
          this.richlistbox.flex = 0;
          this.setAttribute("dontanimate", "true");

          this.classList.add("showSearchSuggestionsNotification");
          // Don't show the one-off buttons if we are showing onboarding and
          // there's no result, since it would be ugly and pointless.
          this.footer.collapsed = this._matchCount == 0;
          this.input.tabScrolling = this._matchCount != 0;

          // This event allows accessibility APIs to see the notification.
          if (!this.popupOpen) {
            let event = document.createEvent("Events");
            event.initEvent("AlertActive", true, true);
            this.searchSuggestionsNotification.dispatchEvent(event);
          }
          ]]>
        </body>
      </method>

      <method name="searchSuggestionsNotificationWasDismissed">
        <parameter name="enableSuggestions"/>
        <body>
          <![CDATA[
          if (!this.popupOpen) {
            this._hideSearchSuggestionsNotification();
            return;
          }
          this._hideSearchSuggestionsNotificationWithAnimation().then(() => {
            if (enableSuggestions && this.input.textValue) {
              // Start a new search so that suggestions appear immediately.
              this.input.controller.startSearch(this.input.textValue);
            }
          });
          ]]>
        </body>
      </method>

      <method name="_hideSearchSuggestionsNotification">
        <body>
          <![CDATA[
          this.classList.remove("showSearchSuggestionsNotification");
          this.richlistbox.flex = 1;
          this.removeAttribute("dontanimate");
          this.searchSuggestionsNotification.removeAttribute("animate");
          if (this._matchCount) {
            // Update popup height.
            this._invalidate();
          } else {
            this.closePopup();
          }
          ]]>
        </body>
      </method>

      <method name="_hideSearchSuggestionsNotificationWithAnimation">
        <body>
          <![CDATA[
          return new Promise(resolve => {
            let notificationHeight = this.searchSuggestionsNotification
                                         .getBoundingClientRect()
                                         .height;
            this.searchSuggestionsNotification.style.marginTop =
              "-" + notificationHeight + "px";

            let popupHeightPx =
              (this.getBoundingClientRect().height - notificationHeight) + "px";
            this.style.height = popupHeightPx;

            let onTransitionEnd = () => {
              this.removeEventListener("transitionend", onTransitionEnd, true);
              this.searchSuggestionsNotification.style.marginTop = "0px";
              this.style.removeProperty("height");
              this._hideSearchSuggestionsNotification();
              resolve();
            };
            this.addEventListener("transitionend", onTransitionEnd, true);
          });
          ]]>
        </body>
      </method>

      <method name="_selectedOneOffChanged">
        <body><![CDATA[
          // Update all searchengine result items to use the newly selected
          // engine.
          for (let item of this.richlistbox.childNodes) {
            if (item.collapsed) {
              break;
            }
            let url = item.getAttribute("url");
            if (url) {
              let action = item._parseActionUrl(url);
              if (action && action.type == "searchengine") {
                item._adjustAcItem();
              }
            }
          }
        ]]></body>
      </method>

      <!-- This handles keypress changes to the selection among the one-off
           search buttons and between the one-offs and the listbox.  It returns
           true if the keypress was consumed and false if not. -->
      <method name="handleKeyPress">
        <parameter name="aEvent"/>
        <body><![CDATA[
          this.oneOffSearchButtons.handleKeyPress(aEvent, this._matchCount,
                                                  !this._isFirstResultHeuristic,
                                                  gBrowser.userTypedValue);
          return aEvent.defaultPrevented && !aEvent.urlbarDeferred;
        ]]></body>
      </method>

      <!-- This is called when a one-off is clicked and when "search in new tab"
           is selected from a one-off context menu. -->
      <method name="handleOneOffSearch">
        <parameter name="event"/>
        <parameter name="engine"/>
        <parameter name="where"/>
        <parameter name="params"/>
        <body><![CDATA[
          this.input.handleCommand(event, where, params);
        ]]></body>
      </method>

      <!-- Result listitems call this to determine which search engine they
           should show in their labels and include in their url attributes. -->
      <property name="overrideSearchEngineName" readonly="true">
        <getter><![CDATA[
          let button = this.oneOffSearchButtons.selectedButton;
          return button && button.engine && button.engine.name;
        ]]></getter>
      </property>

      <method name="createResultLabel">
        <parameter name="item"/>
        <parameter name="proposedLabel"/>
        <body>
          <![CDATA[
            let parts = [proposedLabel];

            let action = this.mInput._parseActionUrl(item.getAttribute("url"));
            if (action) {
              switch (action.type) {
              case "searchengine":
                parts = [
                  action.params.searchSuggestion || action.params.searchQuery,
                  action.params.engineName,
                ];
                break;
              case "switchtab":
              case "remotetab":
                parts = [
                  item.getAttribute("title"),
                  item.getAttribute("displayurl"),
                ];
                break;
              }
            }

            let types = item.getAttribute("type").split(/\s+/);
            let type = types.find(t => t != "action" && t != "heuristic");
            try {
              // Some types intentionally do not map to strings, which is not
              // an error.
              parts.push(this._bundle.GetStringFromName(type + "ResultLabel"));
            } catch (e) {}

            return parts.filter(str => str).join(" ");
          ]]>
        </body>
      </method>

      <method name="maybeSetupSpeculativeConnect">
        <parameter name="aUriString"/>
        <body><![CDATA[
          try {
            let uri = makeURI(aUriString);
            Services.io.speculativeConnect2(uri, gBrowser.contentPrincipal, null);
          } catch (ex) {
            // Can't setup speculative connection for this uri string for some
            // reason, just ignore it.
          }
        ]]></body>
      </method>

      <method name="onResultsAdded">
        <body>
          <![CDATA[
            // If nothing is selected yet, select the first result if it is a
            // pre-selected "heuristic" result.  (See UnifiedComplete.js.)
            if (this.selectedIndex == -1 && this._isFirstResultHeuristic) {
              // Don't fire DOMMenuItemActive so that screen readers still see
              // the input as being focused.
              this.richlistbox.suppressMenuItemEvent = true;
              this.input.controller.setInitiallySelectedIndex(0);
              this.richlistbox.suppressMenuItemEvent = false;
            }
            // If this is the first time we get the result from the current
            // search and we are not in the private context, we can speculatively
            // connect to the intended site as a performance optimization.
            if (!this.input.gotResultForCurrentQuery &&
                this.input.speculativeConnectEnabled &&
                !this.input.inPrivateContext &&
                this.input.mController.matchCount > 0) {
              let firstStyle = this.input.mController.getStyleAt(0);
              if (firstStyle.includes("autofill")) {
                let uri = this.input.mController.getFinalCompleteValueAt(0);
                // "http" will be stripped out, but other scheme won't.
                if (!uri.includes("://")) {
                  uri = "http://" + uri;
                }
                this.maybeSetupSpeculativeConnect(uri);
              } else if (firstStyle.includes("searchengine") &&
                         this.input.browserSearchSuggestEnabled &&
                         this.input.urlbarSearchSuggestEnabled) {
                // Preconnect to the current search engine only if the search
                // suggestions are enabled.
                let engine = Services.search.currentEngine;
                engine.speculativeConnect({window,
                                           originAttributes: gBrowser.contentPrincipal.originAttributes});
              }
            }

            // When a result is present the footer should always be visible.
            this.footer.collapsed = false;
            this.input.tabScrolling = true;

            this.input.gotResultForCurrentQuery = true;
            this.input.maybeReplayDeferredKeyEvents();
          ]]>
        </body>
      </method>

      <method name="_onSearchBegin">
        <body><![CDATA[
          // Set the selected index to 0 (heuristic) until a result comes back
          // and we can evaluate it better.
          //
          // This is required to properly manage delayed handleEnter:
          // 1. if a search starts we set selectedIndex to 0 here, and it will
          //    be updated by onResultsAdded. Since selectedIndex is 0,
          //    handleEnter will delay the action if a result didn't arrive yet.
          // 2. if a search doesn't start (for example if autocomplete is
          //    disabled), this won't be called, and the selectedIndex will be
          //    the default -1 value. Then handleEnter will know it should not
          //    delay the action, cause a result wont't ever arrive.
          this.input.controller.setInitiallySelectedIndex(0);
        ]]></body>
      </method>

      <field name="_addonIframe">null</field>
      <field name="_addonIframeOwner">null</field>
      <field name="_addonIframeOverriddenFunctionsByName">{}</field>

      <!-- These methods must be overridden and properly handled by the API
           runtime so that it doesn't break the popup.  If any of these methods
           is not overridden, then initAddonIframe should throw. -->
      <field name="_addonIframeOverrideFunctionNames">[
        "_invalidate",
      ]</field>

      <field name="_addonIframeHiddenAnonids">[
        "search-suggestions-notification",
        "richlistbox",
        "one-off-search-buttons",
      ]</field>
      <field name="_addonIframeHiddenDisplaysByAnonid">{}</field>

      <method name="initAddonIframe">
        <parameter name="owner"/>
        <parameter name="overrides"/>
        <body><![CDATA[
          if (this._addonIframeOwner) {
            // Another add-on has already requested the iframe.  Return null to
            // signal to the calling add-on that it should not take over the
            // popup.  First add-on wins for now.
            return null;
          }
          // Make sure all overrides are provided before doing anything.
          for (let name of this._addonIframeOverrideFunctionNames) {
            if (typeof(overrides[name]) != "function") {
              throw new Error(
                "Override for method '" + name + "' must be given"
              );
            }
          }
          // OK, insert the iframe.
          this._addonIframeOwner = owner;
          this._addonIframe = this._makeAddonIframe();
          this._addonIframeOverriddenFunctionsByName = {};
          for (let name of this._addonIframeOverrideFunctionNames) {
            this._addonIframeOverriddenFunctionsByName[name] = this[name];
            this[name] = overrides[name];
          }
          return this._addonIframe;
        ]]></body>
      </method>

      <method name="destroyAddonIframe">
        <parameter name="owner"/>
        <body><![CDATA[
          if (this._addonIframeOwner != owner) {
            throw new Error("You're not the iframe owner");
          }
          this._addonIframeOwner = null;
          this._addonIframe.remove();
          this._addonIframe = null;
          for (let anonid of this._addonIframeHiddenAnonids) {
            let child = document.getAnonymousElementByAttribute(
              this, "anonid", anonid
            );
            child.style.display =
              this._addonIframeHiddenDisplaysByAnonid[anonid];
          }
          for (let name in this._addonIframeOverriddenFunctionsByName) {
            this[name] = this._addonIframeOverriddenFunctionsByName[name];
          }
          this._addonIframeOverriddenFunctionsByName = {};
        ]]></body>
      </method>

      <method name="_makeAddonIframe">
        <body><![CDATA[
          this._addonIframeHiddenDisplaysByAnonid = {};
          for (let anonid of this._addonIframeHiddenAnonids) {
            let child = document.getAnonymousElementByAttribute(
              this, "anonid", anonid
            );
            this._addonIframeHiddenDisplaysByAnonid[anonid] =
              child.style.display;
            child.style.display = "none";
          }
          let XUL_NS =
            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
          let iframe = document.createElementNS(XUL_NS, "iframe");
          iframe.setAttribute("type", "content");
          iframe.setAttribute("flex", "1");
          iframe.style.transition = "height 100ms";
          this.appendChild(iframe);
          return iframe;
        ]]></body>
      </method>

    </implementation>
    <handlers>

      <handler event="SelectedOneOffButtonChanged"><![CDATA[
        this._selectedOneOffChanged();
      ]]></handler>

      <handler event="mousedown"><![CDATA[
        // Required to make the xul:label.text-link elements in the search
        // suggestions notification work correctly when clicked on Linux.
        // This is copied from the mousedown handler in
        // browser-search-autocomplete-result-popup, which apparently had a
        // similar problem.
        event.preventDefault();

        if (!this.input.speculativeConnectEnabled) {
          return;
        }
        if (event.button == 2) {
          // Ignore right-clicks.
          return;
        }
        // Ensure the user is clicking on an url instead of other buttons
        // on the popup.
        let elt = event.originalTarget;
        while (elt && elt.localName != "richlistitem" && elt != this) {
          elt = elt.parentNode;
        }
        if (!elt || elt.localName != "richlistitem") {
          return;
        }
        // The user might click on a ghost entry which was removed because of
        // the coming new results.
        if (this.input.controller.matchCount <= this.selectedIndex) {
          return;
        }

        let url = this.input.controller.getFinalCompleteValueAt(this.selectedIndex);

        // Whitelist the cases that we want to speculative connect, and ignore
        // other moz-action uris or fancy protocols.
        // Note that it's likely we've speculatively connected to the first
        // url because it is a heuristic "autofill" result (see bug 1348275).
        // "moz-action:searchengine" is also the same case. (see bug 1355443)
        // So we won't duplicate the effort here.
        if (url.startsWith("http") && this.selectedIndex > 0) {
          this.maybeSetupSpeculativeConnect(url);
        } else if (url.startsWith("moz-action:remotetab")) {
          // URL is in the format moz-action:ACTION,PARAMS
          // Where PARAMS is a JSON encoded object.
          const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
          if (!MOZ_ACTION_REGEX.test(url))
            return;

          let params = JSON.parse(url.match(MOZ_ACTION_REGEX)[2]);
          if (params.url) {
            this.maybeSetupSpeculativeConnect(decodeURIComponent(params.url));
          }
        }

      ]]></handler>

    </handlers>
  </binding>

  <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
    <implementation>
      <constructor><![CDATA[
        if (!this.notification)
          return;

        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.getElementById("addon-progress-notification-progressmeter");
      </field>
      <field name="progresstext" readonly="true">
        document.getElementById("addon-progress-notification-progresstext");
      </field>
      <property name="DownloadUtils" readonly="true">
        <getter><![CDATA[
          let module = {};
          Components.utils.import("resource://gre/modules/DownloadUtils.jsm", module);
          Object.defineProperty(this, "DownloadUtils", {
            configurable: true,
            enumerable: true,
            writable: true,
            value: module.DownloadUtils
          });
          return module.DownloadUtils;
        ]]></getter>
      </property>

      <method name="destroy">
        <body><![CDATA[
          if (!this.notification)
            return;

          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.setAttribute("mode", "undetermined");
          } else {
            this.progressmeter.setAttribute("mode", "determined");
            this.progressmeter.setAttribute("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 algorithm is the same used by the downloads code.
          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.setAttribute("value", status);
          this.progresstext.setAttribute("tooltiptext", status);
        ]]></body>
      </method>

      <method name="cancel">
        <body><![CDATA[
          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);

          PopupNotifications.remove(this.notification);
        ]]></body>
      </method>

      <method name="updateProgress">
        <body><![CDATA[
          if (!this.notification)
            return;

          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();
            if (Services.prefs.getBoolPref("xpinstall.customConfirmationUI", false)) {
              this.progressmeter.setAttribute("mode", "undetermined");
              let status = gNavigatorBundle.getString("addonDownloadVerifying");
              this.progresstext.setAttribute("value", status);
              this.progresstext.setAttribute("tooltiptext", status);
            } else {
              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;

        let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow);
        let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin);

        if (isWindowPrivate) {
          // TODO: temporary compromise of hiding some privacy leaks, remove once bug 892487 is fixed
          let allowalways = document.getAnonymousElementByAttribute(this, "anonid", "allowalways");
          let block = document.getAnonymousElementByAttribute(this, "anonid", "block");
          let allownow = document.getAnonymousElementByAttribute(this, "anonid", "allownow");

          allowalways.hidden = curState !== "allowalways";
          block.hidden       = curState !== "block";
          allownow.hidden    = curState === "allowalways";
        }

        if (url || linkHandler) {
          link.value = linkString;
          if (url) {
            link.href = url;
          }
          if (linkHandler) {
            link.addEventListener("click", linkHandler);
          }
        } 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" style="width: &pluginNotification.width;;">
      <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 popup-notification-closebutton tabbable close-icon"
                             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"
                      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";
        let sortedActions = [];
        for (let action of this.notification.options.pluginData.values()) {
          sortedActions.push(action);
        }
        sortedActions.sort((a, b) => a.pluginName.localeCompare(b.pluginName));

        for (let action of sortedActions) {
          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._items.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.options.principal.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._items[0].action;
          var prePath = action.pluginPermissionPrePath;
          let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow);
          let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin);

          let label, linkLabel, 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"));
            }

            // TODO: temporary compromise, remove this once bug 892487 is fixed
            if (isWindowPrivate) {
              this._buttonContainer.hidden = true;
            }
          } 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 = "pluginActivate2.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"));
            }

            // TODO: temporary compromise, remove this once bug 892487 is fixed
            if (isWindowPrivate) {
              button1.default = true;
              this._secondaryButton.hidden = true;
            }
          }
          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 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._items[0].action,
            "allownow");
          this._cancel();
        ]]></body>
      </method>
      <method name="_singleBlock">
        <body><![CDATA[
          gPluginHandler._updatePluginPermission(this.notification,
            this._items[0].action,
            "block");
            this._cancel();
        ]]></body>
      </method>
      <method name="_singleActivateAlways">
        <body><![CDATA[
          gPluginHandler._updatePluginPermission(this.notification,
            this._items[0].action,
            "allowalways");
          this._cancel();
        ]]></body>
      </method>
      <method name="_singleContinue">
        <body><![CDATA[
          gPluginHandler._updatePluginPermission(this.notification,
            this._items[0].action,
            "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_RETURN" group="system" action="this._accept(event);"/>
    </handlers>
  </binding>

  <!-- This binding is only retained for add-ons compatibility -->
  <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>
</bindings>