browser/metro/base/content/bindings/urlbar.xml
author Matt Brubeck <mbrubeck@mozilla.com>
Fri, 26 Jul 2013 14:34:03 -0700
changeset 152503 bf035bca7f6529bdc298449646fcf6f09549f3fe
parent 150842 38e84638eb6da79a54966a8065b07258f264e6f0
child 154724 7e0356305566d1c67f71d440e80241c7884e1593
permissions -rw-r--r--
Bug 896097 - Hide Metro overlay buttons during autocomplete [r=sfoster]

<?xml version="1.0" encoding="Windows-1252" ?>
<!-- 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 % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
]>

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

  <binding id="urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
    <implementation implements="nsIObserver">
      <constructor>
        <![CDATA[
          this._mayFormat = Services.prefs.getBoolPref("browser.urlbar.formatting.enabled");
          this._mayTrimURLs = Services.prefs.getBoolPref("browser.urlbar.trimURLs");
          this._maySelectAll = Services.prefs.getBoolPref("browser.urlbar.doubleClickSelectsAll");

          Services.prefs.addObserver("browser.urlbar.formatting.enabled", this, false);
          Services.prefs.addObserver("browser.urlbar.trimURLs", this, false);
          Services.prefs.addObserver("browser.urlbar.doubleClickSelectsAll", this, false);

          this.inputField.controllers.insertControllerAt(0, this._copyCutValueController);

          this.minResultsForPopup = 0;
          this.popup._input = this;
        ]]>
      </constructor>

      <destructor>
        <![CDATA[
          Services.prefs.removeObserver("browser.urlbar.formatting.enabled", this);
          Services.prefs.removeObserver("browser.urlbar.trimURLs", this);
          Services.prefs.removeObserver("browser.urlbar.doubleClickSelectsAll", this);
        ]]>
      </destructor>

      <field name="_mayFormat"/>
      <field name="_mayTrimURLs"/>
      <field name="_maySelectAll"/>
      <field name="_lastKnownGoodURL"/>

      <method name="openPopup">
        <body>
          <![CDATA[
            this.popup.openAutocompletePopup(this, null);
          ]]>
        </body>
      </method>

      <method name="closePopup">
        <body>
          <![CDATA[
            this.popup.closePopup(this, null);
          ]]>
        </body>
      </method>

      <!-- URI Display: Domain Highlighting -->

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

            let controller = this.editor.selectionController;
            let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
            selection.removeAllRanges();
          ]]>
        </body>
      </method>

      <method name="formatValue">
        <body>
          <![CDATA[
            if (!this._mayFormat || this.isEditing)
              return;

            let controller = this.editor.selectionController;
            let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
            selection.removeAllRanges();

            let textNode = this.editor.rootElement.firstChild;
            let value = textNode.textContent;

            let protocol = value.match(/^[a-z\d.+\-]+:(?=[^\d])/);
            if (protocol &&
                ["http:", "https:", "ftp:"].indexOf(protocol[0]) == -1)
              return;
            let matchedURL = value.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
            if (!matchedURL)
              return;

            let [, preDomain, domain] = matchedURL;
            let baseDomain = domain;
            let subDomain = "";
            // getBaseDomainFromHost doesn't recognize IPv6 literals in brackets as IPs (bug 667159)
            if (domain[0] != "[") {
              try {
                baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
                if (!domain.endsWith(baseDomain)) {
                  // getBaseDomainFromHost converts its resultant to ACE.
                  let IDNService = Cc["@mozilla.org/network/idn-service;1"]
                                   .getService(Ci.nsIIDNService);
                  baseDomain = IDNService.convertACEtoUTF8(baseDomain);
                }
              } catch (e) {}
            }
            if (baseDomain != domain) {
              subDomain = domain.slice(0, -baseDomain.length);
            }

            let rangeLength = preDomain.length + subDomain.length;
            if (rangeLength) {
              let range = document.createRange();
              range.setStart(textNode, 0);
              range.setEnd(textNode, rangeLength);
              selection.addRange(range);
            }

            let startRest = preDomain.length + domain.length;
            if (startRest < value.length) {
              let range = document.createRange();
              range.setStart(textNode, startRest);
              range.setEnd(textNode, value.length);
              selection.addRange(range);
            }
          ]]>
        </body>
      </method>

      <!-- URI Display: Scheme and Trailing Slash Triming -->

      <method name="_trimURL">
        <parameter name="aURL"/>
        <body>
          <![CDATA[
            // This function must not modify the given URL such that calling
            // nsIURIFixup::createFixupURI with the rfdesult will produce a different URI.
            return aURL /* remove single trailing slash for http/https/ftp URLs */
               .replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1")
                /* remove http:// unless the host starts with "ftp\d*\." or contains "@" */
               .replace(/^http:\/\/((?!ftp\d*\.)[^\/@]+(?:\/|$))/, "$1");
          ]]>
        </body>
      </method>

      <method name="_getSelectedValueForClipboard">
        <body>
          <![CDATA[
            // Grab the actual input field's value, not our value, which could include moz-action:
            let inputVal = this.inputField.value;
            let selectedVal = inputVal.substring(this.selectionStart, this.electionEnd);

            // If the selection doesn't start at the beginning or doesn't span the full domain or
            // the URL bar is modified, nothing else to do here.
            if (this.selectionStart > 0 || this.valueIsTyped)
              return selectedVal;

            // The selection doesn't span the full domain if it doesn't contain a slash and is
            // followed by some character other than a slash.
            if (!selectedVal.contains("/")) {
              let remainder = inputVal.replace(selectedVal, "");
              if (remainder != "" && remainder[0] != "/")
                return selectedVal;
            }

            let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);

            let uri;
            try {
              uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_USE_UTF8);
            } catch (e) {}
            if (!uri)
              return selectedVal;

            // Only copy exposable URIs
            try {
              uri = uriFixup.createExposableURI(uri);
            } catch (ex) {}

            // If the entire URL is selected, just use the actual loaded URI.
            if (inputVal == selectedVal) {
              // ... but only if  isn't a javascript: or data: URI, since those
              // are hard to read when encoded
              if (!uri.schemeIs("javascript") && !uri.schemeIs("data")) {
                // Parentheses are known to confuse third-party applications (bug 458565).
                selectedVal = uri.spec.replace(/[()]/g, function (c) escape(c));
              }

              return selectedVal;
            }

            // Just the beginning of the URL is selected, check for a trimmed value
            let spec = uri.spec;
            let trimmedSpec = this._trimURL(spec);
            if (spec != trimmedSpec) {
              // Prepend the portion that trimURL removed from the beginning.
              // This assumes trimURL 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="_copyCutValueController">
        <![CDATA[
          ({
            urlbar: this,
            doCommand: function(aCommand) {
              let urlbar = this.urlbar;
              let 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;
              }

              Cc["@mozilla.org/widget/clipboardhelper;1"]
                .getService(Ci.nsIClipboardHelper)
                .copyString(val, document);
            },

            supportsCommand: function(aCommand) {
              switch (aCommand) {
                case "cmd_copy":
                case "cmd_cut":
                  return true;
              }
              return false;
            },

            isCommandEnabled: function(aCommand) {
              let urlbar = this.urlbar;
              return this.supportsCommand(aCommand) &&
                     (aCommand != "cmd_cut" || !urlbar.readOnly) &&
                     urlbar.selectionStart < urlbar.selectionEnd;
            },

            onEvent: function(aEventName) {}
          })
        ]]>
      </field>

      <method name="trimValue">
        <parameter name="aURL"/>
        <body>
          <![CDATA[
            return (this._mayTrimURLs) ? this._trimURL(aURL) : aURL;
          ]]>
        </body>
      </method>

      <!-- URI Editing -->

      <property name="isEditing" readonly="true">
        <getter>
          <![CDATA[
            return Elements.urlbarState.getAttribute("mode") == "edit";
          ]]>
        </getter>
      </property>

      <method name="beginEditing">
        <parameter name="aShouldDismiss"/>
        <body>
          <![CDATA[
            if (this.isEditing)
              return;

            Elements.urlbarState.setAttribute("mode", "edit");
            this._lastKnownGoodURL = this.value;

            if (!this.focused)
              this.focus();

            this._clearFormatting();
            this.select();

            if (aShouldDismiss)
              ContextUI.dismissTabs();
          ]]>
        </body>
      </method>

      <method name="endEditing">
        <parameter name="aShouldRevert"/>
        <body>
          <![CDATA[
            if (!this.isEditing)
              return;

            Elements.urlbarState.setAttribute("mode", "view");
            this.closePopup();
            this.formatValue();

            if (this.focused)
              this.blur();

            if (aShouldRevert)
              this.value = this._lastKnownGoodURL;
          ]]>
        </body>
      </method>

      <!-- URI Submission -->

      <method name="_canonizeURL">
        <parameter name="aURL"/>
        <parameter name="aTriggeringEvent"/>
        <body>
          <![CDATA[
            if (!aURL)
              return "";

            // Only add the suffix when the URL bar value isn't already "URL-like",
            // and only if we get a keyboard event, to match user expectations.
            if (/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(aURL)) {
              let accel = aTriggeringEvent.ctrlKey;
              let shift = aTriggeringEvent.shiftKey;
              let suffix = "";

              switch (true) {
                case (accel && shift):
                  suffix = ".org/";
                  break;
                case (shift):
                  suffix = ".net/";
                  break;
                case (accel):
                  try {
                    suffix = gPrefService.getCharPref("browser.fixup.alternate.suffix");
                    if (suffix.charAt(suffix.length - 1) != "/")
                      suffix += "/";
                  } catch(e) {
                    suffix = ".com/";
                  }
                  break;
              }

              if (suffix) {
                // trim leading/trailing spaces (bug 233205)
                aURL = aURL.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 = aURL.indexOf("/");
                if (firstSlash >= 0) {
                  aURL = aURL.substring(0, firstSlash) + suffix + aURL.substring(firstSlash + 1);
                } else {
                  aURL = aURL + suffix;
                }
                aURL = "http://www." + aURL;
              }
            }

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

      <method name="submitURL">
        <parameter name="aEvent"/>
        <body>
          <![CDATA[
            // If the address was typed in by a user, tidy it up
            if (aEvent instanceof KeyEvent)
              this.value = this._canonizeURL(this.value, aEvent);

            this.endEditing();
            BrowserUI.goToURI(this.value);

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

      <method name="submitSearch">
        <parameter name="anEngineName"/>
        <body>
          <![CDATA[
            this.endEditing();
            BrowserUI.doOpenSearch(anEngineName);

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

      <!-- nsIObserver -->

      <method name="observe">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body>
          <![CDATA[
            if (aTopic != "nsPref:changed")
              return;

            switch (aData) {
              case "browser.urlbar.formatting.enabled":
                this._mayFormat = Services.prefs.getBoolPref(aData);
                break;
              case "browser.urlbar.trimURLs":
                this._mayTrimURLs = Services.prefs.getBoolPref(aData);
                break;
              case "browser.urlbar.doubleClickSelectsAll":
                this._maySelectAll = Services.prefs.getBoolPref(aData);
                break;
            }
          ]]>
        </body>
      </method>
    </implementation>

    <handlers>
      <!-- Entering editing mode -->

      <handler event="input" phase="capturing">
        <![CDATA[
          // Ensures that paste-and-go actually brings the URL bar into editing mode
          // and displays the half-height autocomplete popup.
          this.beginEditing();
          this.openPopup();
        ]]>
      </handler>

      <handler event="click" phase="capturing">
        <![CDATA[
          this.beginEditing(true);
        ]]>
      </handler>

      <!-- Editing mode behaviors -->

      <handler event="dblclick" phase="capturing">
        <![CDATA[
            if (this._maySelectAll) this.select();
        ]]>
      </handler>

      <handler event="contextmenu" phase="capturing">
        <![CDATA[
          let box = this.inputField.parentNode;
          box.showContextMenu(this, event, true);
        ]]>
      </handler>

      <!-- Leaving editing mode -->

      <handler event="blur" phase="capturing">
        <![CDATA[
          this.endEditing();
        ]]>
      </handler>

      <handler event="keypress" phase="capturing" keycode="VK_RETURN">
        <![CDATA[
          if (this.popup.submitSelected())
            return;

          if (this.submitURL(event))
            return;
        ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="urlbar-autocomplete">
    <content orient="horizontal">
      <xul:vbox id="results-vbox" flex="1">
        <xul:label class="meta-section-title" value="&autocompleteResultsHeader.label;"/>
        <richgrid id="results-richgrid" rows="3" deferlayout="true" anonid="results" seltype="single" flex="1"/>
      </xul:vbox>

      <xul:vbox id="searches-vbox" flex="1">
        <xul:label class="meta-section-title" value="&autocompleteSearchesHeader.label;"/>
        <richgrid id="searches-richgrid" rows="3" deferlayout="true" anonid="searches" seltype="single" flex="1"/>
      </xul:vbox>
    </content>

    <implementation implements="nsIAutoCompletePopup, nsIObserver">
      <constructor>
        <![CDATA[
          this.hidden = true;

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

          this._results.controller = this;
          this._searches.controller = this;
        ]]>
      </constructor>

      <destructor>
        <![CDATA[
          Services.obs.removeObserver(this, "browser-search-engine-modified");
        ]]>
      </destructor>

      <!-- nsIAutocompleteInput -->

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

      <property name="input" readonly="true" onget="return this._input;"/>
      <property name="matchCount" readonly="true" onget="return this.input.controller.matchCount;"/>
      <property name="popupOpen" readonly="true" onget="return !this.hidden"/>
      <property name="overrideValue" readonly="true" onget="return null;"/>

      <property name="selectedItem">
        <getter>
          <![CDATA[
            return this._isGridBound(this._results) ? this._results.selectedItem : null;
          ]]>
        </getter>
        <setter>
          <![CDATA[
            return this._isGridBound(this._results) ? this._results.selectedItem : null;
          ]]>
        </setter>
      </property>

      <property name="selectedIndex">
        <getter>
          <![CDATA[
            return this._isGridBound(this._results) ? this._results.selectedIndex : -1;
          ]]>
        </getter>
        <setter>
          <![CDATA[
            return this._isGridBound(this._results) ? this._results.selectedIndex : -1;
          ]]>
        </setter>
      </property>

      <method name="openAutocompletePopup">
        <parameter name="aInput"/>
        <parameter name="aElement"/>
        <body>
          <![CDATA[
            if (this.popupOpen)
              return;

            ContextUI.dismissContextAppbar();

            this._input = aInput;
            this._grid = this._results;

            this.clearSelection();
            this.invalidate();

            this._results.arrangeItemsNow();
            this._searches.arrangeItemsNow();

            this.hidden = false;
            Elements.urlbarState.setAttribute("autocomplete", "true");
          ]]>
        </body>
      </method>

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

            this.input.controller.stopSearch();
            this.hidden = true;
            Elements.urlbarState.removeAttribute("autocomplete");
          ]]>
        </body>
      </method>

      <!-- Updating grid content -->

      <field name="_grid">null</field>
      <field name="_results" readonly="true">document.getAnonymousElementByAttribute(this, 'anonid', 'results');</field>
      <field name="_searches" readonly="true">document.getAnonymousElementByAttribute(this, 'anonid', 'searches');</field>

      <property name="_otherGrid" readonly="true">
        <getter>
          <![CDATA[
            if (this._grid == null)
              return null;

            return (this._grid == this._results) ? this._searches : this._results;
          ]]>
        </getter>
      </property>

      <method name="_isGridBound">
        <parameter name="aGrid"/>
        <body>
          <![CDATA[
            return aGrid && aGrid.isBound;
          ]]>
        </body>
      </method>

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

            this.updateResults();
            this.updateSearchEngineSubtitles();
          ]]>
        </body>
      </method>

      <!-- Updating grid content: results -->

      <method name="updateResults">
        <body>
          <![CDATA[
            if (!this._isGridBound(this._results))
              return;

            if (!this.input)
              return;

            let controller = this.input.controller;
            let lastMatch = this.matchCount - 1;
            let iterCount = Math.max(this._results.itemCount, this.matchCount);

            // Swap out existing items for new search hit results
            for (let i = 0; i < iterCount; i++) {
              if (i > lastMatch) {
                let lastItem = this._results.itemCount - 1;
                this._results.removeItemAt(lastItem, true);
                continue;
              }

              let value = controller.getValueAt(i);
              let label = controller.getCommentAt(i) || value;
              let iconURI = controller.getImageAt(i);

              let item = this._results.getItemAtIndex(i);
              if (item == null) {
                item = this._results.appendItem(label, value, true);
                item.setAttribute("autocomplete", "true");
              } else {
                item.setAttribute("label", label);
                item.setAttribute("value", value);
              }

              item.setAttribute("iconURI", iconURI);
            }

            this._results.arrangeItems();
          ]]>
        </body>
      </method>

      <!-- Updating grid content: search engines -->

      <field name="_engines">[]</field>

      <method name="_initSearchEngines">
        <body>
          <![CDATA[
            Services.search.init(this.updateSearchEngineGrid.bind(this));
          ]]>
        </body>
      </method>

      <method name="updateSearchEngineGrid">
        <body>
          <![CDATA[
            if (!this._isGridBound(this._searches))
              return;

            this._engines = Services.search.getVisibleEngines();

            while (this._searches.itemCount > 0)
              this._searches.removeItemAt(0, true);

            this._engines.forEach(function (anEngine) {
              let item = this._searches.appendItem(anEngine.name, anEngine.name, true);
              item.setAttribute("autocomplete", "true");
              item.setAttribute("search", "true");

              let iconURI = anEngine.iconURI ? anEngine.iconURI.spec : "";
              item.setAttribute("iconURI", iconURI);
            }.bind(this));

            this._searches.arrangeItems();
          ]]>
        </body>
      </method>

      <method name="updateSearchEngineSubtitles">
        <body>
          <![CDATA[
            if (!this._isGridBound(this._searches))
              return;

            let searchString = this.input.controller.searchString;
            let label = Strings.browser.formatStringFromName("opensearch.search", [searchString], 1);

            for (let i = 0, len = this._searches.itemCount; i < len; i++) {
              let item = this._searches.getItemAtIndex(i);
              item.setAttribute("label", label);
              item.refresh && item.refresh();
            }
          ]]>
        </body>
      </method>

      <!-- Selecting results -->

      <method name="selectBy">
        <parameter name="aReverse"/>
        <parameter name="aPage"/>
        <body>
          <![CDATA[
            if (!this._isGridBound(this._results) ||
                !this._isGridBound(this._searches))
              return;

            // Move between grids if we're at the edge of one.
            if ((this._grid.isSelectionAtEnd && !aReverse) ||
                (this._grid.isSelectionAtStart && aReverse)) {
              let index = !aReverse ? 0 : this._otherGrid.itemCount - 1;
              this._otherGrid.selectedIndex = index;
            } else {
              this._grid.offsetSelection(aReverse ? -1 : 1);
            }
          ]]>
        </body>
      </method>

      <method name="clearSelection">
        <body>
          <![CDATA[
            if (this._isGridBound(this._results))
              this._results.clearSelection();

            if (this._isGridBound(this._searches))
              this._searches.clearSelection();
          ]]>
        </body>
      </method>

      <!-- Submitting selected results -->

      <method name="submitSelected">
        <body>
          <![CDATA[
            if (this._isGridBound(this._results) &&
                this._results.selectedIndex >= 0) {
              let url = this.input.controller.getValueAt(this._results.selectedIndex);
              this.input.value = url;
              return this.input.submitURL();
            }

            if (this._isGridBound(this._searches) &&
                this._searches.selectedIndex >= 0) {
              let engine = this._engines[this._searches.selectedIndex];
              return this.input.submitSearch(engine.name);
            }

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

      <method name="handleItemClick">
        <parameter name="aItem"/>
        <parameter name="aEvent"/>
        <body>
          <![CDATA[
            this.submitSelected();
          ]]>
        </body>
      </method>

      <!-- nsIObserver -->

      <method name="observe">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body>
          <![CDATA[
            switch (aTopic) {
              case "browser-search-engine-modified":
                this.updateSearchEngineGrid();
                break;
            }
          ]]>
        </body>
      </method>
    </implementation>

    <handlers>
      <handler event="contentgenerated">
        <![CDATA[
          let grid = event.originalTarget;

          if (grid == this._searches)
            this._initSearchEngines();

          if (grid == this._results)
            this.updateResults();
        ]]>
      </handler>

      <handler event="select">
        <![CDATA[
          let grid = event.originalTarget;

          // If a selection was made on a different grid,
          // remove selection from the current grid.
          if (grid != this._grid) {
            this._grid.clearSelection();
            this._grid = this._otherGrid;
          }
        ]]>
      </handler>
    </handlers>
  </binding>
</bindings>