browser/components/search/content/search.xml
author Boris Zbarsky <bzbarsky@mit.edu>
Mon, 10 Oct 2016 21:07:47 -0400
changeset 317391 67d40adfa5e12f58326a00f27262036edd9386ba
parent 314403 2a6c70abee77fd8b967c8e1c4f77ae36619880ff
child 319323 b9ca193b71b30b2b20294389838ee4e166c6fa5a
permissions -rw-r--r--
Bug 1298243 part 7. Change DataTransfer.types from being a DOMStringList to being a frozen array. r=mystor,gijs

<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->

<!DOCTYPE bindings [
<!ENTITY % searchBarDTD SYSTEM "chrome://browser/locale/searchbar.dtd" >
%searchBarDTD;
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
]>

<bindings id="SearchBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="searchbar">
    <resources>
      <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
      <stylesheet src="chrome://browser/skin/searchbar.css"/>
    </resources>
    <content>
      <xul:stringbundle src="chrome://browser/locale/search.properties"
                        anonid="searchbar-stringbundle"/>
      <!--
      There is a dependency between "maxrows" attribute and
      "SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing
      one of them requires changing the other one.
      -->
      <xul:textbox class="searchbar-textbox"
                   anonid="searchbar-textbox"
                   type="autocomplete"
                   inputtype="search"
                   flex="1"
                   autocompletepopup="PopupSearchAutoComplete"
                   autocompletesearch="search-autocomplete"
                   autocompletesearchparam="searchbar-history"
                   maxrows="10"
                   completeselectedindex="true"
                   minresultsforpopup="0"
                   xbl:inherits="disabled,disableautocomplete,searchengine,src,newlines">
        <!--
        Empty <box> to properly position the icon within the autocomplete
        binding's anonymous children (the autocomplete binding positions <box>
        children differently)
        -->
        <xul:box>
          <xul:hbox class="searchbar-search-button-container">
            <xul:image class="searchbar-search-button"
                       anonid="searchbar-search-button"
                       xbl:inherits="addengines"
                       tooltiptext="&searchEndCap.label;"/>
          </xul:hbox>
        </xul:box>
        <xul:hbox class="search-go-container">
          <xul:image class="search-go-button" hidden="true"
                     anonid="search-go-button"
                     onclick="handleSearchCommand(event);"
                     tooltiptext="&searchEndCap.label;"/>
        </xul:hbox>
      </xul:textbox>
    </content>

    <implementation implements="nsIObserver">
      <constructor><![CDATA[
        if (this.parentNode.parentNode.localName == "toolbarpaletteitem")
          return;
        // Make sure we rebuild the popup in onpopupshowing
        this._needToBuildPopup = true;

        Services.obs.addObserver(this, "browser-search-engine-modified", false);

        this._initialized = true;

        Services.search.init((function search_init_cb(aStatus) {
          // Bail out if the binding's been destroyed
          if (!this._initialized)
            return;

          if (Components.isSuccessCode(aStatus)) {
            // Refresh the display (updating icon, etc)
            this.updateDisplay();
            BrowserSearch.updateOpenSearchBadge();
          } else {
            Components.utils.reportError("Cannot initialize search service, bailing out: " + aStatus);
          }
        }).bind(this));
      ]]></constructor>

      <destructor><![CDATA[
        this.destroy();
      ]]></destructor>

      <method name="destroy">
        <body><![CDATA[
        if (this._initialized) {
          this._initialized = false;

          Services.obs.removeObserver(this, "browser-search-engine-modified");
        }

        // Make sure to break the cycle from _textbox to us. Otherwise we leak
        // the world. But make sure it's actually pointing to us.
        // Also make sure the textbox has ever been constructed, otherwise the
        // _textbox getter will cause the textbox constructor to run, add an
        // observer, and leak the world too.
        if (this._textboxInitialized && this._textbox.mController.input == this)
          this._textbox.mController.input = null;
        ]]></body>
      </method>

      <field name="_ignoreFocus">false</field>
      <field name="_clickClosedPopup">false</field>
      <field name="_stringBundle">document.getAnonymousElementByAttribute(this,
          "anonid", "searchbar-stringbundle");</field>
      <field name="_textboxInitialized">false</field>
      <field name="_textbox">document.getAnonymousElementByAttribute(this,
          "anonid", "searchbar-textbox");</field>
      <field name="_engines">null</field>
      <field name="FormHistory" readonly="true">
        (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
      </field>

      <property name="engines" readonly="true">
        <getter><![CDATA[
          if (!this._engines)
            this._engines = Services.search.getVisibleEngines();
          return this._engines;
        ]]></getter>
      </property>

      <property name="currentEngine">
        <setter><![CDATA[
          Services.search.currentEngine = val;
          return val;
        ]]></setter>
        <getter><![CDATA[
          var currentEngine = Services.search.currentEngine;
          // Return a dummy engine if there is no currentEngine
          return currentEngine || {name: "", uri: null};
        ]]></getter>
      </property>

      <!-- textbox is used by sanitize.js to clear the undo history when
           clearing form information. -->
      <property name="textbox" readonly="true"
                onget="return this._textbox;"/>

      <property name="value" onget="return this._textbox.value;"
                             onset="return this._textbox.value = val;"/>

      <method name="focus">
        <body><![CDATA[
          this._textbox.focus();
        ]]></body>
      </method>

      <method name="select">
        <body><![CDATA[
          this._textbox.select();
        ]]></body>
      </method>

      <method name="observe">
        <parameter name="aEngine"/>
        <parameter name="aTopic"/>
        <parameter name="aVerb"/>
        <body><![CDATA[
          if (aTopic == "browser-search-engine-modified") {
            switch (aVerb) {
            case "engine-removed":
              this.offerNewEngine(aEngine);
              break;
            case "engine-added":
              this.hideNewEngine(aEngine);
              break;
            case "engine-changed":
              // An engine was removed (or hidden) or added, or an icon was
              // changed.  Do nothing special.
            }

            // Make sure the engine list is refetched next time it's needed
            this._engines = null;

            // Update the popup header and update the display after any modification.
            this._textbox.popup.updateHeader();
            this.updateDisplay();
          }
        ]]></body>
      </method>

      <!-- There are two seaprate lists of search engines, whose uses intersect
      in this file.  The search service (nsIBrowserSearchService and
      nsSearchService.js) maintains a list of Engine objects which is used to
      populate the searchbox list of available engines and to perform queries.
      That list is accessed here via this.SearchService, and it's that sort of
      Engine that is passed to this binding's observer as aEngine.

      In addition, browser.js fills two lists of autodetected search engines
      (browser.engines and browser.hiddenEngines) as properties of
      mCurrentBrowser.  Those lists contain unnamed JS objects of the form
      { uri:, title:, icon: }, and that's what the searchbar uses to determine
      whether to show any "Add <EngineName>" menu items in the drop-down.

      The two types of engines are currently related by their identifying
      titles (the Engine object's 'name'), although that may change; see bug
      335102.  -->

      <!-- If the engine that was just removed from the searchbox list was
      autodetected on this page, move it to each browser's active list so it
      will be offered to be added again. -->
      <method name="offerNewEngine">
        <parameter name="aEngine"/>
        <body><![CDATA[
          for (let browser of gBrowser.browsers) {
            if (browser.hiddenEngines) {
              // XXX This will need to be changed when engines are identified by
              // URL rather than title; see bug 335102.
              var removeTitle = aEngine.wrappedJSObject.name;
              for (var i = 0; i < browser.hiddenEngines.length; i++) {
                if (browser.hiddenEngines[i].title == removeTitle) {
                  if (!browser.engines)
                    browser.engines = [];
                  browser.engines.push(browser.hiddenEngines[i]);
                  browser.hiddenEngines.splice(i, 1);
                  break;
                }
              }
            }
          }
          BrowserSearch.updateOpenSearchBadge();
        ]]></body>
      </method>

      <!-- If the engine that was just added to the searchbox list was
      autodetected on this page, move it to each browser's hidden list so it is
      no longer offered to be added. -->
      <method name="hideNewEngine">
        <parameter name="aEngine"/>
        <body><![CDATA[
          for (let browser of gBrowser.browsers) {
            if (browser.engines) {
              // XXX This will need to be changed when engines are identified by
              // URL rather than title; see bug 335102.
              var removeTitle = aEngine.wrappedJSObject.name;
              for (var i = 0; i < browser.engines.length; i++) {
                if (browser.engines[i].title == removeTitle) {
                  if (!browser.hiddenEngines)
                    browser.hiddenEngines = [];
                  browser.hiddenEngines.push(browser.engines[i]);
                  browser.engines.splice(i, 1);
                  break;
                }
              }
            }
          }
          BrowserSearch.updateOpenSearchBadge();
        ]]></body>
      </method>

      <method name="setIcon">
        <parameter name="element"/>
        <parameter name="uri"/>
        <body><![CDATA[
          element.setAttribute("src", uri);
        ]]></body>
      </method>

      <method name="updateDisplay">
        <body><![CDATA[
          var uri = this.currentEngine.iconURI;
          this.setIcon(this, uri ? uri.spec : "");

          var name = this.currentEngine.name;
          var text = this._stringBundle.getFormattedString("searchtip", [name]);

          this._textbox.placeholder = this._stringBundle.getString("searchPlaceholder");
          this._textbox.label = text;
          this._textbox.tooltipText = text;
        ]]></body>
      </method>

      <method name="updateGoButtonVisibility">
        <body><![CDATA[
          document.getAnonymousElementByAttribute(this, "anonid",
                                                  "search-go-button")
                  .hidden = !this._textbox.value;
        ]]></body>
      </method>

      <method name="openSuggestionsPanel">
        <parameter name="aShowOnlySettingsIfEmpty"/>
        <body><![CDATA[
          if (this._textbox.open)
            return;

          this._textbox.showHistoryPopup();

          if (this._textbox.value) {
            // showHistoryPopup does a startSearch("") call, ensure the
            // controller handles the text from the input box instead:
            this._textbox.mController.handleText();
          }
          else if (aShowOnlySettingsIfEmpty) {
            this.setAttribute("showonlysettings", "true");
          }
        ]]></body>
      </method>

      <method name="selectEngine">
        <parameter name="aEvent"/>
        <parameter name="isNextEngine"/>
        <body><![CDATA[
          // Find the new index
          var newIndex = this.engines.indexOf(this.currentEngine);
          newIndex += isNextEngine ? 1 : -1;

          if (newIndex >= 0 && newIndex < this.engines.length) {
            this.currentEngine = this.engines[newIndex];
          }

          aEvent.preventDefault();
          aEvent.stopPropagation();

          this.openSuggestionsPanel();
        ]]></body>
      </method>

      <method name="handleSearchCommand">
        <parameter name="aEvent"/>
        <parameter name="aEngine"/>
        <parameter name="aForceNewTab"/>
        <body><![CDATA[
          var where = "current";
          let params;

          // Open ctrl/cmd clicks on one-off buttons in a new background tab.
          if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
            if (aEvent.button == 2)
              return;
            where = whereToOpenLink(aEvent, false, true);
          }
          else if (aForceNewTab) {
            where = "tab";
            if (Services.prefs.getBoolPref("browser.tabs.loadInBackground"))
              where += "-background";
          }
          else {
            var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
            if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
              where = "tab";
            if ((aEvent instanceof MouseEvent) &&
                (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
              where = "tab";
              params = {
                inBackground: true,
              };
            }
          }

          this.handleSearchCommandWhere(aEvent, aEngine, where, params);
        ]]></body>
      </method>

      <method name="handleSearchCommandWhere">
        <parameter name="aEvent"/>
        <parameter name="aEngine"/>
        <parameter name="aWhere"/>
        <parameter name="aParams"/>
        <body><![CDATA[
          var textBox = this._textbox;
          var textValue = textBox.value;

          let selection = this.telemetrySearchDetails;
          this.doSearch(textValue, aWhere, aEngine, aParams);

          if (!selection || (selection.index == -1)) {
            let recorded = this.textbox.popup.oneOffButtons
                               .maybeRecordTelemetry(aEvent, aWhere, aParams);
            if (!recorded) {
              let source = "unknown";
              let type = "unknown";
              let target = aEvent.originalTarget;
              if (aEvent instanceof KeyboardEvent) {
                type = "key";
              } else if (aEvent instanceof MouseEvent) {
                type = "mouse";
                if (target.classList.contains("search-panel-header") ||
                    target.parentNode.classList.contains("search-panel-header")) {
                  source = "header";
                }
              } else if (aEvent instanceof XULCommandEvent) {
                if (target.getAttribute("anonid") == "paste-and-search") {
                  source = "paste";
                }
              }
              if (!aEngine) {
                aEngine = this.currentEngine;
              }
              BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
                                                          aWhere);
            }
          }

          if (aWhere == "tab" && aParams && aParams.inBackground)
            this.focus();
        ]]></body>
      </method>

      <method name="doSearch">
        <parameter name="aData"/>
        <parameter name="aWhere"/>
        <parameter name="aEngine"/>
        <parameter name="aParams"/>
        <body><![CDATA[
          var textBox = this._textbox;

          // Save the current value in the form history
          if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
            this.FormHistory.update(
              { op : "bump",
                fieldname : textBox.getAttribute("autocompletesearchparam"),
                value : aData },
              { handleError : function(aError) {
                  Components.utils.reportError("Saving search to form history failed: " + aError.message);
              }});
          }

          let engine = aEngine || this.currentEngine;
          var submission = engine.getSubmission(aData, null, "searchbar");
          let telemetrySearchDetails = this.telemetrySearchDetails;
          this.telemetrySearchDetails = null;
          if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
            telemetrySearchDetails = null;
          }
          BrowserSearch.recordSearchInTelemetry(engine, "searchbar", telemetrySearchDetails);
          // null parameter below specifies HTML response for search
          let params = {
            postData: submission.postData,
          };
          if (aParams) {
            for (let key in aParams) {
              params[key] = aParams[key];
            }
          }
          openUILinkIn(submission.uri.spec, aWhere, params);
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="command"><![CDATA[
        const target = event.originalTarget;
        if (target.engine) {
          this.currentEngine = target.engine;
        } else if (target.classList.contains("addengine-item")) {
          // Select the installed engine if the installation succeeds
          var installCallback = {
            onSuccess: engine => this.currentEngine = engine
          }
          Services.search.addEngine(target.getAttribute("uri"), null,
                                    target.getAttribute("src"), false,
                                    installCallback);
        }
        else
          return;

        this.focus();
        this.select();
      ]]></handler>

      <handler event="DOMMouseScroll"
               phase="capturing"
               modifiers="accel"
               action="this.selectEngine(event, (event.detail > 0));"/>

      <handler event="input" action="this.updateGoButtonVisibility();"/>
      <handler event="drop" action="this.updateGoButtonVisibility();"/>

      <handler event="blur">
      <![CDATA[
        // If the input field is still focused then a different window has
        // received focus, ignore the next focus event.
        this._ignoreFocus = (document.activeElement == this._textbox.inputField);
      ]]></handler>

      <handler event="focus">
      <![CDATA[
        // Speculatively connect to the current engine's search URI (and
        // suggest URI, if different) to reduce request latency
        this.currentEngine.speculativeConnect({window: window});

        if (this._ignoreFocus) {
          // This window has been re-focused, don't show the suggestions
          this._ignoreFocus = false;
          return;
        }

        // Don't open the suggestions if there is no text in the textbox.
        if (!this._textbox.value)
          return;

        // Don't open the suggestions if the mouse was used to focus the
        // textbox, that will be taken care of in the click handler.
        if (Services.focus.getLastFocusMethod(window) & Services.focus.FLAG_BYMOUSE)
          return;

        this.openSuggestionsPanel();
      ]]></handler>

      <handler event="mousedown" phase="capturing">
      <![CDATA[
        if (event.originalTarget.getAttribute("anonid") == "searchbar-search-button") {
          this._clickClosedPopup = this._textbox.popup._isHiding;
        }
      ]]></handler>

      <handler event="click" button="0">
      <![CDATA[
        // Ignore clicks on the search go button.
        if (event.originalTarget.getAttribute("anonid") == "search-go-button") {
          return;
        }

        let isIconClick = event.originalTarget.getAttribute("anonid") == "searchbar-search-button";

        // Ignore clicks on the icon if they were made to close the popup
        if (isIconClick && this._clickClosedPopup) {
          return;
        }

        // Open the suggestions whenever clicking on the search icon or if there
        // is text in the textbox.
        if (isIconClick || this._textbox.value) {
          this.openSuggestionsPanel(true);
        }
      ]]></handler>

    </handlers>
  </binding>

  <binding id="searchbar-textbox"
      extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
    <implementation implements="nsIObserver">
      <constructor><![CDATA[
        const kXULNS =
          "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

        if (document.getBindingParent(this).parentNode.parentNode.localName ==
            "toolbarpaletteitem")
          return;

        // Initialize fields
        this._stringBundle = document.getBindingParent(this)._stringBundle;
        this._suggestEnabled =
          Services.prefs.getBoolPref("browser.search.suggest.enabled");

        if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
          this.setAttribute("clickSelectsAll", true);

        // Add items to context menu and attach controller to handle them
        var textBox = document.getAnonymousElementByAttribute(this,
                                              "anonid", "textbox-input-box");
        var cxmenu = document.getAnonymousElementByAttribute(textBox,
                                          "anonid", "input-box-contextmenu");
        var pasteAndSearch;
        cxmenu.addEventListener("popupshowing", function() {
          BrowserSearch.searchBar._textbox.closePopup();
          if (!pasteAndSearch)
            return;
          var controller = document.commandDispatcher.getControllerForCommand("cmd_paste");
          var enabled = controller.isCommandEnabled("cmd_paste");
          if (enabled)
            pasteAndSearch.removeAttribute("disabled");
          else
            pasteAndSearch.setAttribute("disabled", "true");
        }, false);

        var element, label, akey;

        element = document.createElementNS(kXULNS, "menuseparator");
        cxmenu.appendChild(element);

        this.setAttribute("aria-owns", this.popup.id);

        var insertLocation = cxmenu.firstChild;
        while (insertLocation.nextSibling &&
               insertLocation.getAttribute("cmd") != "cmd_paste")
          insertLocation = insertLocation.nextSibling;
        if (insertLocation) {
          element = document.createElementNS(kXULNS, "menuitem");
          label = this._stringBundle.getString("cmd_pasteAndSearch");
          element.setAttribute("label", label);
          element.setAttribute("anonid", "paste-and-search");
          element.setAttribute("oncommand", "BrowserSearch.pasteAndSearch(event)");
          cxmenu.insertBefore(element, insertLocation.nextSibling);
          pasteAndSearch = element;
        }

        element = document.createElementNS(kXULNS, "menuitem");
        label = this._stringBundle.getString("cmd_clearHistory");
        akey = this._stringBundle.getString("cmd_clearHistory_accesskey");
        element.setAttribute("label", label);
        element.setAttribute("accesskey", akey);
        element.setAttribute("cmd", "cmd_clearhistory");
        cxmenu.appendChild(element);

        element = document.createElementNS(kXULNS, "menuitem");
        label = this._stringBundle.getString("cmd_showSuggestions");
        akey = this._stringBundle.getString("cmd_showSuggestions_accesskey");
        element.setAttribute("anonid", "toggle-suggest-item");
        element.setAttribute("label", label);
        element.setAttribute("accesskey", akey);
        element.setAttribute("cmd", "cmd_togglesuggest");
        element.setAttribute("type", "checkbox");
        element.setAttribute("checked", this._suggestEnabled);
        element.setAttribute("autocheck", "false");
        this._suggestMenuItem = element;
        cxmenu.appendChild(element);

        this.addEventListener("keypress", aEvent => {
          if (navigator.platform.startsWith("Mac") && aEvent.keyCode == KeyEvent.VK_F4)
            this.openSearch()
        }, true);

        this.controllers.appendController(this.searchbarController);
        document.getBindingParent(this)._textboxInitialized = true;

        // Add observer for suggest preference
        Services.prefs.addObserver("browser.search.suggest.enabled", this, false);
      ]]></constructor>

      <destructor><![CDATA[
        Services.prefs.removeObserver("browser.search.suggest.enabled", this);

        // Because XBL and the customize toolbar code interacts poorly,
        // there may not be anything to remove here
        try {
          this.controllers.removeController(this.searchbarController);
        } catch (ex) { }
      ]]></destructor>

      <field name="_stringBundle"/>
      <field name="_suggestMenuItem"/>
      <field name="_suggestEnabled"/>

      <!--
        This overrides the searchParam property in autocomplete.xml.  We're
        hijacking this property as a vehicle for delivering the privacy
        information about the window into the guts of nsSearchSuggestions.

        Note that the setter is the same as the parent.  We were not sure whether
        we can override just the getter.  If that proves to be the case, the setter
        can be removed.
      -->
      <property name="searchParam"
                onget="return this.getAttribute('autocompletesearchparam') +
                       (PrivateBrowsingUtils.isWindowPrivate(window) ? '|private' : '');"
                onset="this.setAttribute('autocompletesearchparam', val); return val;"/>

      <!-- This is implemented so that when textbox.value is set directly (e.g.,
           by tests), the one-off query is updated. -->
      <method name="onBeforeValueSet">
        <parameter name="aValue"/>
        <body><![CDATA[
          this.popup.oneOffButtons.query = aValue;
          return aValue;
        ]]></body>
      </method>

      <!--
        This method overrides the autocomplete binding's openPopup (essentially
        duplicating the logic from the autocomplete popup binding's
        openAutocompletePopup method), modifying it so that the popup is aligned with
        the inner textbox, but sized to not extend beyond the search bar border.
      -->
      <method name="openPopup">
        <body><![CDATA[
          var popup = this.popup;
          if (!popup.mPopupOpen) {
            // Initially the panel used for the searchbar (PopupSearchAutoComplete
            // in browser.xul) is hidden to avoid impacting startup / new
            // window performance. The base binding's openPopup would normally
            // call the overriden openAutocompletePopup in urlbarBindings.xml's
            // browser-autocomplete-result-popup binding to unhide the popup,
            // but since we're overriding openPopup we need to unhide the panel
            // ourselves.
            popup.hidden = false;

            // Don't roll up on mouse click in the anchor for the search UI.
            if (popup.id == "PopupSearchAutoComplete") {
              popup.setAttribute("norolluponanchor", "true");
            }

            popup.mInput = this;
            popup.view = this.controller.QueryInterface(Ci.nsITreeView);
            popup.invalidate();

            popup.showCommentColumn = this.showCommentColumn;
            popup.showImageColumn = this.showImageColumn;

            document.popupNode = null;

            const isRTL = getComputedStyle(this, "").direction == "rtl";

            var outerRect = this.getBoundingClientRect();
            var innerRect = this.inputField.getBoundingClientRect();
            let width = isRTL ?
                        innerRect.right - outerRect.left :
                        outerRect.right - innerRect.left;
            popup.setAttribute("width", width > 100 ? width : 100);

            var yOffset = outerRect.bottom - innerRect.bottom;
            popup.openPopup(this.inputField, "after_start", 0, yOffset, false, false);
          }
        ]]></body>
      </method>

      <method name="observe">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body><![CDATA[
          if (aTopic == "nsPref:changed") {
            this._suggestEnabled =
              Services.prefs.getBoolPref("browser.search.suggest.enabled");
            this._suggestMenuItem.setAttribute("checked", this._suggestEnabled);
          }
        ]]></body>
      </method>

      <method name="openSearch">
        <body>
          <![CDATA[
            if (!this.popupOpen) {
              document.getBindingParent(this).openSuggestionsPanel();
              return false;
            }
            return true;
          ]]>
        </body>
      </method>

      <!-- override |onTextEntered| in autocomplete.xml -->
      <method name="onTextEntered">
        <parameter name="aEvent"/>
        <body><![CDATA[
          let engine;
          let oneOff = this.selectedButton;
          if (oneOff) {
            if (!oneOff.engine) {
              oneOff.doCommand();
              return;
            }
            engine = oneOff.engine;
          }
          if (this._selectionDetails &&
              this._selectionDetails.currentIndex != -1) {
            BrowserSearch.searchBar.telemetrySearchDetails = this._selectionDetails;
            this._selectionDetails = null;
          }
          document.getBindingParent(this).handleSearchCommand(aEvent, engine);
        ]]></body>
      </method>

      <property name="selectedButton">
        <getter><![CDATA[
          return this.popup.oneOffButtons.selectedButton;
        ]]></getter>
        <setter><![CDATA[
          return this.popup.oneOffButtons.selectedButton = val;
        ]]></setter>
      </property>

      <method name="handleKeyboardNavigation">
        <parameter name="aEvent"/>
        <body><![CDATA[
          let popup = this.popup;
          if (!popup.popupOpen)
            return;

          // accel + up/down changes the default engine and shouldn't affect
          // the selection on the one-off buttons.
          if (aEvent.getModifierState("Accel"))
            return;

          let suggestions =
            document.getAnonymousElementByAttribute(popup, "anonid", "tree");
          let suggestionsHidden =
            suggestions.getAttribute("collapsed") == "true";
          let numItems = suggestionsHidden ? 0 : this.popup.view.rowCount;
          this.popup.oneOffButtons.handleKeyPress(aEvent, numItems, true);
        ]]></body>
      </method>

      <!-- nsIController -->
      <field name="searchbarController" readonly="true"><![CDATA[({
        _self: this,
        supportsCommand: function(aCommand) {
          return aCommand == "cmd_clearhistory" ||
                 aCommand == "cmd_togglesuggest";
        },

        isCommandEnabled: function(aCommand) {
          return true;
        },

        doCommand: function (aCommand) {
          switch (aCommand) {
            case "cmd_clearhistory":
              var param = this._self.getAttribute("autocompletesearchparam");

              let searchBar = this._self.parentNode;

              BrowserSearch.searchBar.FormHistory.update({ op : "remove", fieldname : param }, null);
              this._self.value = "";
              break;
            case "cmd_togglesuggest":
              // The pref observer will update _suggestEnabled and the menu
              // checkmark.
              Services.prefs.setBoolPref("browser.search.suggest.enabled",
                                         !this._self._suggestEnabled);
              break;
            default:
              // do nothing with unrecognized command
          }
        }
      })]]></field>
    </implementation>

    <handlers>
      <handler event="input"><![CDATA[
        this.popup.removeAttribute("showonlysettings");
      ]]></handler>

      <handler event="keypress" phase="capturing"
               action="return this.handleKeyboardNavigation(event);"/>

      <handler event="keypress" keycode="VK_UP" modifiers="accel"
               phase="capturing"
               action="document.getBindingParent(this).selectEngine(event, false);"/>

      <handler event="keypress" keycode="VK_DOWN" modifiers="accel"
               phase="capturing"
               action="document.getBindingParent(this).selectEngine(event, true);"/>

      <handler event="keypress" keycode="VK_DOWN" modifiers="alt"
               phase="capturing"
               action="return this.openSearch();"/>

      <handler event="keypress" keycode="VK_UP" modifiers="alt"
               phase="capturing"
               action="return this.openSearch();"/>

      <handler event="dragover">
      <![CDATA[
        var types = event.dataTransfer.types;
        if (types.includes("text/plain") || types.includes("text/x-moz-text-internal"))
          event.preventDefault();
      ]]>
      </handler>

      <handler event="drop">
      <![CDATA[
        var dataTransfer = event.dataTransfer;
        var data = dataTransfer.getData("text/plain");
        if (!data)
          data = dataTransfer.getData("text/x-moz-text-internal");
        if (data) {
          event.preventDefault();
          this.value = data;
          document.getBindingParent(this).openSuggestionsPanel();
        }
      ]]>
      </handler>

    </handlers>
  </binding>

  <binding id="browser-search-autocomplete-result-popup" extends="chrome://browser/content/urlbarBindings.xml#browser-autocomplete-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">
      <xul:hbox anonid="searchbar-engine" xbl:inherits="showonlysettings"
                class="search-panel-header search-panel-current-engine">
        <xul:image class="searchbar-engine-image" xbl:inherits="src"/>
        <xul:label anonid="searchbar-engine-name" flex="1" crop="end"
                   role="presentation"/>
      </xul:hbox>
      <xul:tree anonid="tree" flex="1"
                class="autocomplete-tree plain search-panel-tree"
                hidecolumnpicker="true" seltype="single">
        <xul:treecols anonid="treecols">
          <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
        </xul:treecols>
        <xul:treechildren class="autocomplete-treebody"/>
      </xul:tree>
      <xul:vbox anonid="search-one-off-buttons" class="search-one-offs"/>
    </content>
    <implementation>
      <!-- Popup rollup is triggered by native events before the mousedown event
           reaches the DOM. The will be set to true by the popuphiding event and
           false after the mousedown event has been triggered to detect what
           caused rollup. -->
      <field name="_isHiding">false</field>
      <field name="_bundle">null</field>
      <property name="bundle" readonly="true">
        <getter>
          <![CDATA[
            if (!this._bundle) {
              const kBundleURI = "chrome://browser/locale/search.properties";
              this._bundle = Services.strings.createBundle(kBundleURI);
            }
            return this._bundle;
          ]]>
        </getter>
      </property>

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

      <method name="updateHeader">
        <body><![CDATA[
          let currentEngine = Services.search.currentEngine;
          let uri = currentEngine.iconURI;
          if (uri) {
            this.setAttribute("src", uri.spec);
          }
          else {
            // If the default has just been changed to a provider without icon,
            // avoid showing the icon of the previous default provider.
            this.removeAttribute("src");
          }

          let headerText = this.bundle.formatStringFromName("searchHeader",
                                                            [currentEngine.name], 1);
          document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name")
                  .setAttribute("value", headerText);
          document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine")
                  .engine = currentEngine;
        ]]></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[
          let searchbar = document.getElementById("searchbar");
          searchbar.handleSearchCommandWhere(event, engine, where, params);
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="popupshowing"><![CDATA[
        if (!this.oneOffButtons.popup) {
          // The panel width only spans to the textbox size, but we also want it
          // to include the magnifier icon's width.
          let ltr = getComputedStyle(this).direction == "ltr";
          let magnifierWidth = parseInt(getComputedStyle(this)[
                                 ltr ? "marginLeft" : "marginRight"
                               ]) * -1;
          // Ensure the panel is wide enough to fit at least 3 engines.
          let minWidth = Math.max(
            parseInt(this.width) + magnifierWidth,
            this.oneOffButtons.buttonWidth * 3
          );
          this.style.minWidth = minWidth + "px";

          // Set popup after setting the minWidth since it builds the buttons.
          this.oneOffButtons.popup = this;
          this.oneOffButtons.textbox = this.input;
          this.oneOffButtons.telemetryOrigin = "searchbar";
        }

        // First handle deciding if we are showing the reduced version of the
        // popup containing only the preferences button. We do this if the
        // glass icon has been clicked if the text field is empty.
        let searchbar = document.getElementById("searchbar");
        let tree = document.getAnonymousElementByAttribute(this, "anonid",
                                                           "tree")
        if (searchbar.hasAttribute("showonlysettings")) {
          searchbar.removeAttribute("showonlysettings");
          this.setAttribute("showonlysettings", "true");

          // Setting this with an xbl-inherited attribute gets overridden the
          // second time the user clicks the glass icon for some reason...
          tree.collapsed = true;
        }
        else {
          this.removeAttribute("showonlysettings");
          // Uncollapse as long as we have a tree with a view which has >= 1 row.
          // The autocomplete binding itself will take care of uncollapsing later,
          // if we currently have no rows but end up having some in the future
          // when the search string changes
          tree.collapsed = !tree.view || !tree.view.rowCount;
        }

        // Show the current default engine in the top header of the panel.
        this.updateHeader();
      ]]></handler>

      <handler event="popuphiding"><![CDATA[
        this._isHiding = true;
        setTimeout(() => {
          this._isHiding = false;
        }, 0);
      ]]></handler>
    </handlers>

  </binding>

  <!-- Used for additional open search providers in the search panel. -->
  <binding id="addengine-icon" extends="xul:box">
    <content>
      <xul:image class="addengine-icon" xbl:inherits="src"/>
      <xul:image class="addengine-badge"/>
    </content>
  </binding>

  <binding id="search-one-offs">
    <content context="_child">
      <xul:deck anonid="search-panel-one-offs-header"
                selectedIndex="0"
                class="search-panel-header search-panel-current-input">
        <xul:label anonid="searchbar-oneoffheader-search"
                   value="&searchWithHeader.label;"/>
        <xul:hbox anonid="search-panel-searchforwith"
                  class="search-panel-current-input">
          <xul:label anonid="searchbar-oneoffheader-before"
                     value="&searchFor.label;"/>
          <xul:label anonid="searchbar-oneoffheader-searchtext"
                     class="search-panel-input-value"
                     flex="1"
                     crop="end"/>
          <xul:label anonid="searchbar-oneoffheader-after"
                     flex="10000"
                     value="&searchWith.label;"/>
        </xul:hbox>
        <xul:hbox anonid="search-panel-searchonengine"
                  class="search-panel-current-input">
          <xul:label anonid="searchbar-oneoffheader-beforeengine"
                     value="&search.label;"/>
          <xul:label anonid="searchbar-oneoffheader-engine"
                     class="search-panel-input-value"
                     flex="1"
                     crop="end"/>
          <xul:label anonid="searchbar-oneoffheader-afterengine"
                     flex="10000"
                     value="&searchAfter.label;"/>
        </xul:hbox>
      </xul:deck>
      <xul:description anonid="search-panel-one-offs"
                       role="group"
                       class="search-panel-one-offs"
                       xbl:inherits="compact">
        <xul:button anonid="search-settings-compact"
                    oncommand="showSettings();"
                    class="searchbar-engine-one-off-item search-setting-button-compact"
                    tooltiptext="&changeSearchSettings.tooltip;"
                    xbl:inherits="compact"/>
      </xul:description>
      <xul:vbox anonid="add-engines"/>
      <xul:button anonid="search-settings"
                  oncommand="showSettings();"
                  class="search-setting-button search-panel-header"
                  label="&changeSearchSettings.button;"
                  xbl:inherits="compact"/>
      <xul:menupopup anonid="search-one-offs-context-menu">
        <xul:menuitem anonid="search-one-offs-context-open-in-new-tab"
                      label="&searchInNewTab.label;"
                      accesskey="&searchInNewTab.accesskey;"/>
        <xul:menuitem anonid="search-one-offs-context-set-default"
                      label="&searchSetAsDefault.label;"
                      accesskey="&searchSetAsDefault.accesskey;"/>
      </xul:menupopup>
    </content>

    <implementation implements="nsIDOMEventListener">

      <!-- Width in pixels of the one-off buttons.  49px is the min-width of
           each search engine button, adapt this const when changing the css.
           It's actually 48px + 1px of right border. -->
      <property name="buttonWidth" readonly="true" onget="return 49;"/>

      <field name="_popup">null</field>

      <!-- The popup that contains the one-offs.  This is required, so it should
           never be null or undefined, except possibly before the one-offs are
           used. -->
      <property name="popup">
        <getter><![CDATA[
          return this._popup;
        ]]></getter>
        <setter><![CDATA[
          if (this._popup == val) {
            return val;
          }

          let events = [
            "popupshowing",
            "popuphidden",
          ];
          if (this._popup) {
            for (let event of events) {
              this._popup.removeEventListener(event, this);
            }
          }
          if (val) {
            for (let event of events) {
              val.addEventListener(event, this);
            }
          }
          this._popup = val;

          // If the popup is already open, rebuild the one-offs now.  The
          // popup may be opening, so check that the state is not closed
          // instead of checking popupOpen.
          if (val && val.state != "closed") {
            this._rebuild();
          }
          return val;
        ]]></setter>
      </property>

      <field name="_textbox">null</field>

      <!-- The textbox associated with the one-offs.  Set this to a textbox to
           automatically keep the related one-offs UI up to date.  Otherwise you
           can leave it null/undefined, and in that case you should update the
           query property manually. -->
      <property name="textbox">
        <getter><![CDATA[
          return this._textbox;
        ]]></getter>
        <setter><![CDATA[
          if (this._textbox == val) {
            return val;
          }
          if (this._textbox) {
            this._textbox.removeEventListener("input", this);
          }
          if (val) {
            val.addEventListener("input", this);
          }
          return this._textbox = val;
        ]]></setter>
      </property>

      <!-- Set this to a string that identifies your one-offs consumer.  It'll
           be appended to telemetry recorded with maybeRecordTelemetry(). -->
      <field name="telemetryOrigin">""</field>

      <field name="_query">""</field>

      <!-- The query string currently shown in the one-offs.  If the textbox
           property is non-null, then this is automatically updated on
           input. -->
      <property name="query">
        <getter><![CDATA[
          return this._query;
        ]]></getter>
        <setter><![CDATA[
          this._query = val;
          if (this.popup && this.popup.popupOpen) {
            this._updateAfterQueryChanged();
          }
          return val;
        ]]></setter>
      </property>

      <field name="_selectedButton">null</field>

      <!-- The selected one-off, a xul:button, including the add-engine button
           and the search-settings button.  Null if no one-off is selected. -->
      <property name="selectedButton">
        <getter><![CDATA[
          return this._selectedButton;
        ]]></getter>
        <setter><![CDATA[
          this._changeVisuallySelectedButton(val, true);
          return val;
        ]]></setter>
      </property>

      <!-- The index of the selected one-off, including the add-engine button
           and the search-settings button.  -1 if no one-off is selected. -->
      <property name="selectedButtonIndex">
        <getter><![CDATA[
          let buttons = this.getSelectableButtons(true);
          for (let i = 0; i < buttons.length; i++) {
            if (buttons[i] == this._selectedButton) {
              return i;
            }
          }
          return -1;
        ]]></getter>
        <setter><![CDATA[
          let buttons = this.getSelectableButtons(true);
          this.selectedButton = buttons[val];
          return val;
        ]]></setter>
      </property>

      <!-- The visually selected one-off is the same as the selected one-off
           unless a one-off is moused over.  In that case, the visually selected
           one-off is the moused-over one-off, which may be different from the
           selected one-off.  The visually selected one-off is always the one
           that is visually highlighted.  Includes the add-engine button and the
           search-settings button.  A xul:button. -->
      <property name="visuallySelectedButton" readonly="true">
        <getter><![CDATA[
          return this.getSelectableButtons(true).find(button => {
            return button.getAttribute("selected") == "true";
          });
        ]]></getter>
      </property>

      <property name="compact" readonly="true">
        <getter><![CDATA[
          return this.getAttribute("compact") == "true";
        ]]></getter>
      </property>

      <property name="settingsButton" readonly="true">
        <getter><![CDATA[
          let id = this.compact ? "search-settings-compact" : "search-settings";
          return document.getAnonymousElementByAttribute(this, "anonid", id);
        ]]></getter>
      </property>

      <field name="_bundle">null</field>

      <property name="bundle" readonly="true">
        <getter><![CDATA[
          if (!this._bundle) {
            const kBundleURI = "chrome://browser/locale/search.properties";
            this._bundle = Services.strings.createBundle(kBundleURI);
          }
          return this._bundle;
        ]]></getter>
      </property>

      <!-- When a context menu is opened on a one-off button, this is set to the
           engine of that button for use with the context menu actions. -->
      <field name="_contextEngine">null</field>

      <constructor><![CDATA[
        // Prevent popup events from the context menu from reaching the autocomplete
        // binding (or other listeners).
        let menu = document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-menu");
        let listener = aEvent => aEvent.stopPropagation();
        menu.addEventListener("popupshowing", listener);
        menu.addEventListener("popuphiding", listener);
        menu.addEventListener("popupshown", aEvent => {
          this._ignoreMouseEvents = true;
          aEvent.stopPropagation();
        });
        menu.addEventListener("popuphidden", aEvent => {
          this._ignoreMouseEvents = false;
          aEvent.stopPropagation();
        });
      ]]></constructor>

      <!-- This handles events outside the one-off buttons, like on the popup
           and textbox. -->
      <method name="handleEvent">
        <parameter name="event"/>
        <body><![CDATA[
          switch (event.type) {
            case "input":
              // Allow the consumer's input to override its value property with
              // a oneOffSearchQuery property.  That way if the value is not
              // actually what the user typed (e.g., it's autofilled, or it's a
              // mozaction URI), the consumer has some way of providing it.
              this.query = event.target.oneOffSearchQuery || event.target.value;
              break;
            case "popupshowing":
              this._rebuild();
              break;
            case "popuphidden":
              Services.tm.mainThread.dispatch(() => {
                this.selectedButton = null;
                this._contextEngine = null;
              }, Ci.nsIThread.DISPATCH_NORMAL);
              break;
          }
        ]]></body>
      </method>

      <method name="showSettings">
        <body><![CDATA[
          BrowserUITelemetry.countSearchSettingsEvent(this.telemetryOrigin);
          openPreferences("paneSearch");
          // If the preference tab was already selected, the panel doesn't
          // close itself automatically.
          this.popup.hidePopup();
        ]]></body>
      </method>

      <!-- Updates the parts of the UI that show the query string. -->
      <method name="_updateAfterQueryChanged">
        <body><![CDATA[
          let headerSearchText =
            document.getAnonymousElementByAttribute(this, "anonid",
                                                    "searchbar-oneoffheader-searchtext");
          let headerPanel =
            document.getAnonymousElementByAttribute(this, "anonid",
                                                    "search-panel-one-offs-header");
          let list = document.getAnonymousElementByAttribute(this, "anonid",
                                                             "search-panel-one-offs");
          headerSearchText.setAttribute("value", this.query);
          let groupText;
          let isOneOffSelected =
            this.selectedButton &&
            this.selectedButton.classList.contains("searchbar-engine-one-off-item");
          // Typing de-selects the settings or opensearch buttons at the bottom
          // of the search panel, as typing shows the user intends to search.
          if (this.selectedButton && !isOneOffSelected)
            this.selectedButton = null;
          if (this.query) {
            groupText = headerSearchText.previousSibling.value +
                        '"' + headerSearchText.value + '"' +
                        headerSearchText.nextSibling.value;
            if (!isOneOffSelected)
              headerPanel.selectedIndex = 1;
          }
          else {
            let noSearchHeader =
              document.getAnonymousElementByAttribute(this, "anonid",
                                                      "searchbar-oneoffheader-search");
            groupText = noSearchHeader.value;
            if (!isOneOffSelected)
              headerPanel.selectedIndex = 0;
          }
          list.setAttribute("aria-label", groupText);
        ]]></body>
      </method>

      <!-- Builds all the UI. -->
      <method name="_rebuild">
        <body><![CDATA[
          // Update the 'Search for <keywords> with:" header.
          this._updateAfterQueryChanged();

          let list = document.getAnonymousElementByAttribute(this, "anonid",
                                                             "search-panel-one-offs");

          // Handle opensearch items. This needs to be done before building the
          // list of one off providers, as that code will return early if all the
          // alternative engines are hidden.
          let addEngineList =
            document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
          while (addEngineList.firstChild)
            addEngineList.firstChild.remove();

          const kXULNS =
            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

          // Add a button for each engine that the page in the selected browser
          // offers.  But not when the one-offs are compact.  Compact one-offs
          // are shown in the urlbar, and the add-engine buttons span the width
          // of the popup, so if we added all the engines that a site offers, it
          // could effectively break the urlbar popup by offering a ton of
          // engines.  We should probably make a smaller version of the buttons
          // for compact one-offs.
          if (!this.compact) {
            for (let engine of gBrowser.selectedBrowser.engines || []) {
              let button = document.createElementNS(kXULNS, "button");
              let label = this.bundle.formatStringFromName("cmd_addFoundEngine",
                                                           [engine.title], 1);
              button.id = this.telemetryOrigin + "-add-engine-" +
                          engine.title.replace(/ /g, '-');
              button.setAttribute("class", "addengine-item");
              button.setAttribute("label", label);
              button.setAttribute("pack", "start");

              button.setAttribute("crop", "end");
              button.setAttribute("tooltiptext", engine.uri);
              button.setAttribute("uri", engine.uri);
              if (engine.icon) {
                button.setAttribute("image", engine.icon);
              }
              button.setAttribute("title", engine.title);
              addEngineList.appendChild(button);
            }
          }

          let settingsButton =
            document.getAnonymousElementByAttribute(this, "anonid",
                                                    "search-settings-compact");
          // Finally, build the list of one-off buttons.
          while (list.firstChild != settingsButton)
            list.firstChild.remove();
          // Remove the trailing empty text node introduced by the binding's
          // content markup above.
          if (settingsButton.nextSibling)
            settingsButton.nextSibling.remove();

          let Preferences =
            Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
          let pref = Preferences.get("browser.search.hiddenOneOffs");
          let hiddenList = pref ? pref.split(",") : [];

          let currentEngineName = Services.search.currentEngine.name;
          let includeCurrentEngine = this.getAttribute("includecurrentengine");
          let engines = Services.search.getVisibleEngines().filter(e => {
            return (includeCurrentEngine || e.name != currentEngineName) &&
                   !hiddenList.includes(e.name);
          });

          let header = document.getAnonymousElementByAttribute(this, "anonid",
                                                               "search-panel-one-offs-header")
          // header is a xul:deck so collapsed doesn't work on it, see bug 589569.
          header.hidden = list.collapsed = !engines.length;

          if (!engines.length)
            return;

          let panelWidth = parseInt(this.popup.clientWidth);
          // The + 1 is because the last button doesn't have a right border.
          let enginesPerRow = Math.floor((panelWidth + 1) / this.buttonWidth);
          let buttonWidth = Math.floor(panelWidth / enginesPerRow);
          // There will be an emtpy area of:
          //   panelWidth - enginesPerRow * buttonWidth  px
          // at the end of each row.

          // If the <description> tag with the list of search engines doesn't have
          // a fixed height, the panel will be sized incorrectly, causing the bottom
          // of the suggestion <tree> to be hidden.
          let oneOffCount = engines.length;
          if (this.compact)
            ++oneOffCount;
          let rowCount = Math.ceil(oneOffCount / enginesPerRow);
          let height = rowCount * 33; // 32px per row, 1px border.
          list.setAttribute("height", height + "px");

          // Ensure we can refer to the settings buttons by ID:
          let settingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings");
          settingsEl.id = this.telemetryOrigin + "-anon-search-settings";
          let compactSettingsEl = document.getAnonymousElementByAttribute(this, "anonid", "search-settings-compact");
          compactSettingsEl.id = this.telemetryOrigin +
                                 "-anon-search-settings-compact";

          let dummyItems = enginesPerRow - (oneOffCount % enginesPerRow || enginesPerRow);
          for (let i = 0; i < engines.length; ++i) {
            let engine = engines[i];
            let button = document.createElementNS(kXULNS, "button");
            button.id = this._buttonIDForEngine(engine);
            let uri = "chrome://browser/skin/search-engine-placeholder.png";
            if (engine.iconURI) {
              uri = engine.iconURI.spec;
            }
            button.setAttribute("image", uri);
            button.setAttribute("class", "searchbar-engine-one-off-item");
            button.setAttribute("tooltiptext", engine.name);
            button.setAttribute("width", buttonWidth);
            button.engine = engine;

            if ((i + 1) % enginesPerRow == 0)
              button.classList.add("last-of-row");

            if (i + 1 == engines.length)
              button.classList.add("last-engine");

            if (i >= oneOffCount + dummyItems - enginesPerRow)
              button.classList.add("last-row");

            list.insertBefore(button, settingsButton);
          }

          let hasDummyItems = !!dummyItems;
          while (dummyItems) {
            let button = document.createElementNS(kXULNS, "button");
            button.setAttribute("class", "searchbar-engine-one-off-item dummy last-row");
            button.setAttribute("width", buttonWidth);

            if (!--dummyItems)
              button.classList.add("last-of-row");

            list.insertBefore(button, settingsButton);
          }

          if (this.compact) {
            this.settingsButton.setAttribute("width", buttonWidth);
            if (rowCount == 1 && hasDummyItems) {
              // When there's only one row, make the compact settings button
              // hug the right edge of the panel.  It may not due to the panel's
              // width not being an integral factor of the button width.  (See
              // the "There will be an emtpy area" comment above.)  Increase the
              // width of the last dummy item by the remainder.
              let remainder = panelWidth - (enginesPerRow * buttonWidth);
              let width = remainder + buttonWidth;
              let lastDummyItem = this.settingsButton.previousSibling;
              lastDummyItem.setAttribute("width", width);
            }
          }
        ]]></body>
      </method>

      <method name="_buttonIDForEngine">
        <parameter name="engine"/>
        <body><![CDATA[
          return this.telemetryOrigin + "-engine-one-off-item-" +
                 engine.name.replace(/ /g, '-');
        ]]></body>
      </method>

      <method name="_buttonForEngine">
        <parameter name="engine"/>
        <body><![CDATA[
          return document.getElementById(this._buttonIDForEngine(engine));
        ]]></body>
      </method>

      <method name="_changeVisuallySelectedButton">
        <parameter name="val"/>
        <parameter name="aUpdateLogicallySelectedButton"/>
        <body><![CDATA[
          let visuallySelectedButton = this.visuallySelectedButton;
          if (visuallySelectedButton)
            visuallySelectedButton.removeAttribute("selected");

          let header =
            document.getAnonymousElementByAttribute(this, "anonid",
                                                    "search-panel-one-offs-header");
          // Avoid selecting dummy buttons.
          if (val && !val.classList.contains("dummy")) {
            val.setAttribute("selected", "true");
            if (val.classList.contains("searchbar-engine-one-off-item") &&
                val.engine) {
              let headerEngineText =
                document.getAnonymousElementByAttribute(this, "anonid",
                                                        "searchbar-oneoffheader-engine");
              header.selectedIndex = 2;
              headerEngineText.value = val.engine.name;
            }
            else {
              header.selectedIndex = this.query ? 1 : 0;
            }
            if (this.textbox) {
              this.textbox.setAttribute("aria-activedescendant", val.id);
            }
          } else {
            val = null;
            header.selectedIndex = this.query ? 1 : 0;
            if (this.textbox) {
              this.textbox.removeAttribute("aria-activedescendant");
            }
          }

          if (aUpdateLogicallySelectedButton) {
            this._selectedButton = val;
            if (val && !val.engine) {
              // If the button doesn't have an engine, then clear the popup's
              // selection to indicate that pressing Return while the button is
              // selected will do the button's command, not search.
              this.popup.selectedIndex = -1;
            }
            let event = document.createEvent("Events");
            event.initEvent("SelectedOneOffButtonChanged", true, false);
            this.dispatchEvent(event);
          }
        ]]></body>
      </method>

      <method name="getSelectableButtons">
        <parameter name="aIncludeNonEngineButtons"/>
        <body><![CDATA[
          let buttons = [];
          let oneOff = document.getAnonymousElementByAttribute(this, "anonid",
                                                               "search-panel-one-offs");
          for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
            // oneOff may be a text node since the list xul:description contains
            // whitespace and the compact settings button.  See the markup
            // above.  _rebuild removes text nodes, but it may not have been
            // called yet (because e.g. the popup hasn't been opened yet).
            if (oneOff.nodeType == Node.ELEMENT_NODE) {
              if (oneOff.classList.contains("dummy") ||
                  oneOff.classList.contains("search-setting-button-compact"))
                break;
              buttons.push(oneOff);
            }
          }

          if (!aIncludeNonEngineButtons)
            return buttons;

          let addEngine =
            document.getAnonymousElementByAttribute(this, "anonid", "add-engines");
          for (addEngine = addEngine.firstChild; addEngine; addEngine = addEngine.nextSibling)
            buttons.push(addEngine);

          buttons.push(this.settingsButton);
          return buttons;
        ]]></body>
      </method>

      <method name="handleSearchCommand">
        <parameter name="aEvent"/>
        <parameter name="aEngine"/>
        <parameter name="aForceNewTab"/>
        <body><![CDATA[
          let where = "current";
          let params;

          // Open ctrl/cmd clicks on one-off buttons in a new background tab.
          if (aForceNewTab) {
            where = "tab";
            if (Services.prefs.getBoolPref("browser.tabs.loadInBackground")) {
              params = {
                inBackground: true,
              };
            }
          }
          else {
            var newTabPref = Services.prefs.getBoolPref("browser.search.openintab");
            if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
              where = "tab";
            if ((aEvent instanceof MouseEvent) &&
                (aEvent.button == 1 || aEvent.getModifierState("Accel"))) {
              where = "tab";
              params = {
                inBackground: true,
              };
            }
          }

          this.popup.handleOneOffSearch(aEvent, aEngine, where, params);
        ]]></body>
      </method>

      <!--
        Increments or decrements the index of the currently selected one-off.

        @param aForward
               If true, the index is incremented, and if false, the index is
               decremented.
        @param aWrapAround
               This has a couple of effects, depending on whether there is
               currently a selection.
               (1) If true and the last one-off is currently selected,
               incrementing the index will cause the selection to be cleared and
               this method to return true.  Calling advanceSelection again after
               that (again with aForward=true) will select the first one-off.
               Likewise if decrementing the index when the first one-off is
               selected, except in the opposite direction of course.
               (2) If true and there currently is no selection, decrementing the
               index will cause the last one-off to become selected and this
               method to return true.  Only the aForward=false case is affected
               because it is always the case that if aForward=true and there
               currently is no selection, the first one-off becomes selected and
               this method returns true.
        @param aCycleEngines
               If true, only engine buttons are included.
        @return True if the selection can continue to advance after this method
                returns and false if not.
      -->
      <method name="advanceSelection">
        <parameter name="aForward"/>
        <parameter name="aWrapAround"/>
        <parameter name="aCycleEngines"/>
        <body><![CDATA[
          let popup = this.popup;
          let list = document.getAnonymousElementByAttribute(popup, "anonid",
                                                             "search-panel-one-offs");
          let selectedButton = this.selectedButton;
          let buttons = this.getSelectableButtons(aCycleEngines);

          if (selectedButton) {
            // cycle through one-off buttons.
            let index = buttons.indexOf(selectedButton);
            if (aForward)
              ++index;
            else
              --index;

            if (index >= 0 && index < buttons.length)
              this.selectedButton = buttons[index];
            else
              this.selectedButton = null;

            if (this.selectedButton || aWrapAround)
              return true;

            return false;
          }

          // If no selection, select the first button or ...
          if (aForward) {
            this.selectedButton = buttons[0];
            return true;
          }

          if (!aForward && aWrapAround) {
            // the last button.
            this.selectedButton = buttons[buttons.length - 1];
            return true;
          }

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

      <!--
        This handles key presses specific to the one-off buttons like Tab and
        Alt-Up/Down, and Up/Down keys within the buttons.  Since one-off buttons
        are always used in conjunction with a list of some sort (in this.popup),
        it also handles Up/Down keys that cross the boundaries between list
        items and the one-off buttons.

        @param event
               The key event.
        @param numListItems
               The number of items in the list.  The reason that this is a
               parameter at all is that the list may contain items at the end
               that should be ignored, depending on the consumer.  That's true
               for the urlbar for example.
        @param allowEmptySelection
               Pass true if it's OK that neither the list nor the one-off
               buttons contains a selection.  Pass false if either the list or
               the one-off buttons (or both) should always contain a selection.
        @param textboxUserValue
               When the last list item is selected and the user presses Down,
               the first one-off becomes selected and the textbox value is
               restored to the value that the user typed.  Pass that value here.
               However, if you pass true for allowEmptySelection, you don't need
               to pass anything for this parameter.  (Pass undefined or null.)
        @return True if this method handled the keypress and false if not.  If
                false, then you should let the autocomplete controller handle
                the keypress.  The value of event.defaultPrevented will be the
                same as this return value.
      -->
      <method name="handleKeyPress">
        <parameter name="event"/>
        <parameter name="numListItems"/>
        <parameter name="allowEmptySelection"/>
        <parameter name="textboxUserValue"/>
        <body><![CDATA[
          if (!this.popup) {
            return false;
          }

          let stopEvent = false;

          // Tab cycles through the one-offs and moves the focus out at the end.
          // But only if non-Shift modifiers aren't also pressed, to avoid
          // clobbering other shortcuts.
          if (event.keyCode == KeyEvent.DOM_VK_TAB &&
              !event.altKey &&
              !event.ctrlKey &&
              !event.metaKey &&
              this.getAttribute("disabletab") != "true") {
            stopEvent = this.advanceSelection(!event.shiftKey, false, true);
          }

          // Alt + up/down is very similar to (shift +) tab but differs in that
          // it loops through the list, whereas tab will move the focus out.
          else if (event.altKey &&
                   (event.keyCode == KeyEvent.DOM_VK_DOWN ||
                    event.keyCode == KeyEvent.DOM_VK_UP)) {
            stopEvent =
              this.advanceSelection(event.keyCode == KeyEvent.DOM_VK_DOWN,
                                    true, false);
          }

          else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP) {
            if (numListItems > 0) {
              if (this.popup.selectedIndex > 0) {
                // The autocomplete controller should handle this case.
              } else if (this.popup.selectedIndex == 0) {
                if (!allowEmptySelection) {
                  // Wrap around the selection to the last one-off.
                  this.selectedButton = null;
                  this.popup.selectedIndex = -1;
                  // Call advanceSelection after setting selectedIndex so that
                  // screen readers see the newly selected one-off. Both trigger
                  // accessibility events.
                  this.advanceSelection(false, true, true);
                  stopEvent = true;
                }
              } else {
                let firstButtonSelected =
                  this.selectedButton &&
                  this.selectedButton == this.getSelectableButtons(true)[0];
                if (firstButtonSelected) {
                  this.selectedButton = null;
                } else {
                  stopEvent = this.advanceSelection(false, true, true);
                }
              }
            } else {
              stopEvent = this.advanceSelection(false, true, true);
            }
          }

          else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
            if (numListItems > 0) {
              if (this.popup.selectedIndex >= 0 &&
                  this.popup.selectedIndex < numListItems - 1) {
                // The autocomplete controller should handle this case.
              } else if (this.popup.selectedIndex == numListItems - 1) {
                this.selectedButton = null;
                if (!allowEmptySelection) {
                  this.popup.selectedIndex = -1;
                  stopEvent = true;
                }
                if (this.textbox && typeof(textboxUserValue) == "string") {
                  this.textbox.value = textboxUserValue;
                }
                // Call advanceSelection after setting selectedIndex so that
                // screen readers see the newly selected one-off. Both trigger
                // accessibility events.
                this.advanceSelection(true, true, true);
              } else {
                let buttons = this.getSelectableButtons(true);
                let lastButtonSelected =
                  this.selectedButton &&
                  this.selectedButton == buttons[buttons.length - 1];
                if (lastButtonSelected) {
                  this.selectedButton = null;
                  stopEvent = allowEmptySelection;
                } else if (this.selectedButton) {
                  stopEvent = this.advanceSelection(true, true, true);
                } else {
                  // The autocomplete controller should handle this case.
                }
              }
            } else {
              stopEvent = this.advanceSelection(true, true, true);
            }
          }

          if (stopEvent) {
            event.preventDefault();
            event.stopPropagation();
            return true;
          }
          return false;
        ]]></body>
      </method>

      <!--
        If the given event is related to the one-offs, this method records
        one-off telemetry for it.  this.telemetryOrigin will be appended to the
        computed source, so make sure you set that first.

        @param aEvent
               An event, like a click on a one-off button.
        @param aOpenUILinkWhere
               The "where" passed to openUILink.
        @param aOpenUILinkParams
               The "params" passed to openUILink.
        @return True if telemetry was recorded and false if not.
      -->
      <method name="maybeRecordTelemetry">
        <parameter name="aEvent"/>
        <parameter name="aOpenUILinkWhere"/>
        <parameter name="aOpenUILinkParams"/>
        <body><![CDATA[
          if (!aEvent) {
            return false;
          }

          let source = null;
          let type = "unknown";
          let engine = null;
          let target = aEvent.originalTarget;

          if (aEvent instanceof KeyboardEvent) {
            type = "key";
            if (this.selectedButton) {
              source = "oneoff";
              engine = this.selectedButton.engine;
            }
          } else if (aEvent instanceof MouseEvent) {
            type = "mouse";
            if (target.classList.contains("searchbar-engine-one-off-item")) {
              source = "oneoff";
              engine = target.engine;
            }
          } else if ((aEvent instanceof XULCommandEvent) &&
                     target.getAttribute("anonid") ==
                       "search-one-offs-context-open-in-new-tab") {
            source = "oneoff-context";
            engine = this._contextEngine;
          }

          if (!source) {
            return false;
          }

          if (this.telemetryOrigin) {
            source += "-" + this.telemetryOrigin;
          }

          let tabBackground = aOpenUILinkWhere == "tab" &&
                              aOpenUILinkParams &&
                              aOpenUILinkParams.inBackground;
          let where = tabBackground ? "tab-background" : aOpenUILinkWhere;
          BrowserSearch.recordOneoffSearchInTelemetry(engine, source, type,
                                                      where);
          return true;
        ]]></body>
      </method>

    </implementation>

    <handlers>

      <handler event="mousedown"><![CDATA[
        // Required to receive click events from the buttons on Linux.
        event.preventDefault();
      ]]></handler>

      <handler event="mousemove"><![CDATA[
        let target = event.originalTarget;
        if (target.localName != "button")
          return;

        // Ignore mouse events when the context menu is open.
         if (this._ignoreMouseEvents)
           return;

        if ((target.classList.contains("searchbar-engine-one-off-item") &&
             !target.classList.contains("dummy")) ||
            target.classList.contains("addengine-item") ||
            target.classList.contains("search-setting-button")) {
          this._changeVisuallySelectedButton(target);
        }
      ]]></handler>

      <handler event="mouseout"><![CDATA[
        let target = event.originalTarget;
        if (target.localName != "button") {
          return;
        }

        // Don't deselect the current button if the context menu is open.
        if (this._ignoreMouseEvents)
          return;

        // Unfortunately this will fire before mouseover hits another item.
        // If this button is selected, we replace that selection only if
        // we're not moving to a different one-off item:
        if (target.getAttribute("selected") == "true" &&
            (!event.relatedTarget ||
             !event.relatedTarget.classList.contains("searchbar-engine-one-off-item") ||
             event.relatedTarget.classList.contains("dummy"))) {
          this._changeVisuallySelectedButton(this.selectedButton);
        }
      ]]></handler>

      <handler event="click"><![CDATA[
        if (event.button == 2)
          return; // ignore right clicks.

        let button = event.originalTarget;
        let engine = button.engine || button.parentNode.engine;

        if (!engine)
          return;

        // For some reason, if the context menu had been opened prior to the
        // click, the suggestions popup won't be closed after loading the search
        // in the current tab - so we hide it manually. Some focusing magic
        // that happens when a search is loaded ensures that the popup is opened
        // again if it needs to be, so we don't need to worry about which cases
        // require manual hiding.
        this.popup.hidePopup();

        // Select the clicked button so that consumers can easily tell which
        // button was acted on.
        this.selectedButton = button;
        this.handleSearchCommand(event, engine);
      ]]></handler>

      <handler event="command"><![CDATA[
        let target = event.originalTarget;
        if (target.classList.contains("addengine-item")) {
          // On success, hide the panel and tell event listeners to reshow it to
          // show the new engine.
          let installCallback = {
            onSuccess: engine => {
              this._rebuild();
            },
            onError: function(errorCode) {
              if (errorCode != Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE) {
                // Download error is shown by the search service
                return;
              }
              const kSearchBundleURI = "chrome://global/locale/search/search.properties";
              let searchBundle = Services.strings.createBundle(kSearchBundleURI);
              let brandBundle = document.getElementById("bundle_brand");
              let brandName = brandBundle.getString("brandShortName");
              let title = searchBundle.GetStringFromName("error_invalid_engine_title");
              let text = searchBundle.formatStringFromName("error_duplicate_engine_msg",
                                                           [brandName, target.getAttribute("uri")], 2);
              Services.prompt.QueryInterface(Ci.nsIPromptFactory);
              let prompt = Services.prompt.getPrompt(gBrowser.contentWindow, Ci.nsIPrompt);
              prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
              prompt.setPropertyAsBool("allowTabModal", true);
              prompt.alert(title, text);
            }
          }
          Services.search.addEngine(target.getAttribute("uri"), null,
                                    target.getAttribute("image"), false,
                                    installCallback);
        }
        let anonid = target.getAttribute("anonid");
        if (anonid == "search-one-offs-context-open-in-new-tab") {
          // Select the context-clicked button so that consumers can easily
          // tell which button was acted on.
          this.selectedButton = this._buttonForEngine(this._contextEngine);
          this.handleSearchCommand(event, this._contextEngine, true);
        }
        if (anonid == "search-one-offs-context-set-default") {
          let currentEngine = Services.search.currentEngine;

          // Make the target button of the context menu reflect the current
          // search engine first. Doing this as opposed to rebuilding all the
          // one-off buttons avoids flicker.
          let button = this._buttonForEngine(this._contextEngine);
          button.id = this._buttonIDForEngine(currentEngine);
          let uri = "chrome://browser/skin/search-engine-placeholder.png";
          if (currentEngine.iconURI)
            uri = currentEngine.iconURI.spec;
          button.setAttribute("image", uri);
          button.setAttribute("tooltiptext", currentEngine.name);
          button.engine = currentEngine;

          Services.search.currentEngine = this._contextEngine;
        }
      ]]></handler>

      <handler event="contextmenu"><![CDATA[
        let target = event.originalTarget;
        // Prevent the context menu from appearing except on the one off buttons.
        if (!target.classList.contains("searchbar-engine-one-off-item") ||
            target.classList.contains("dummy")) {
          event.preventDefault();
          return;
        }
        document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-set-default")
                .setAttribute("disabled", target.engine == Services.search.currentEngine);

        this._contextEngine = target.engine;
      ]]></handler>
    </handlers>

  </binding>

</bindings>