Bug 1296638 - Make Form Autocomplete popup use a richlistbox instead of a tree. r?MattN draft
authorMike Conley <mconley@mozilla.com>
Fri, 19 Aug 2016 17:15:56 -0400
changeset 425611 0465a8a9d61c48c202e70d469b5cb4d43472b5b5
parent 425590 65c262bfdda15d4050d008effa98413f21eb1ffc
child 425612 97e8bdba9da883ee13b46105c10c6bc3e01e26b5
push id32463
push usermconley@mozilla.com
push dateSat, 15 Oct 2016 02:16:58 +0000
reviewersMattN
bugs1296638
milestone52.0a1
Bug 1296638 - Make Form Autocomplete popup use a richlistbox instead of a tree. r?MattN MozReview-Commit-ID: 6ybBqAwxlmb
browser/base/content/browser.css
browser/base/content/browser.xul
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/shared/autocomplete.inc.css
browser/themes/windows/browser.css
toolkit/components/satchel/AutoCompletePopup.jsm
toolkit/content/browser-content.js
toolkit/content/widgets/autocomplete.xml
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -470,18 +470,22 @@ toolbar:not(#TabsToolbar) > #personal-bo
 #PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-type-icon {
   list-style-image: none;
 }
 
 #urlbar:not([actiontype="switchtab"]) > #urlbar-display-box {
   display: none;
 }
 
-#PopupAutoComplete {
-  -moz-binding: url("chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup");
+#PopupAutoComplete > richlistbox > richlistitem > .ac-type-icon,
+#PopupAutoComplete > richlistbox > richlistitem > .ac-site-icon,
+#PopupAutoComplete > richlistbox > richlistitem > .ac-tags,
+#PopupAutoComplete > richlistbox > richlistitem > .ac-separator,
+#PopupAutoComplete > richlistbox > richlistitem > .ac-url {
+  display: none;
 }
 
 #PopupSearchAutoComplete {
   -moz-binding: url("chrome://browser/content/search/search.xml#browser-search-autocomplete-result-popup");
 }
 
 /* Overlay a badge on top of the icon of additional open search providers
    in the search panel. */
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -136,17 +136,17 @@
     <menupopup id="backForwardMenu"
                onpopupshowing="return FillHistoryMenu(event.target);"
                oncommand="gotoHistoryIndex(event); event.stopPropagation();"
                onclick="checkForMiddleClick(this, event);"/>
     <tooltip id="aHTMLTooltip" page="true"/>
     <tooltip id="remoteBrowserTooltip"/>
 
     <!-- for search and content formfill/pw manager -->
-    <panel type="autocomplete" id="PopupAutoComplete" noautofocus="true" hidden="true"/>
+    <panel type="autocomplete-richlistbox" id="PopupAutoComplete" noautofocus="true" hidden="true"/>
 
     <!-- for search with one-off buttons -->
     <panel type="autocomplete" id="PopupSearchAutoComplete" noautofocus="true" hidden="true"/>
 
     <!-- for url bar autocomplete -->
     <panel type="autocomplete-richlistbox"
            id="PopupAutoCompleteRichResult"
            noautofocus="true"
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -909,16 +909,20 @@ notification[value="translation"] button
 notification[value="translation"] button:not([type="menu"]) > .button-box {
   margin-inline-end: 3ch;
 }
 
 notification[value="translation"] menulist > .menulist-dropmarker {
   display: block;
 }
 
+/* AutoComplete */
+
+%include ../shared/autocomplete.inc.css
+
 #treecolAutoCompleteImage {
   max-width : 36px;
 }
 
 .autocomplete-richlistbox {
   padding: 4px;
 }
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1745,16 +1745,18 @@ toolbar .toolbarbutton-1 > .toolbarbutto
   margin-bottom: 0;
   color: GrayText;
 }
 
 %include ../shared/urlbarSearchSuggestionsNotification.inc.css
 
 /* ----- AUTOCOMPLETE ----- */
 
+%include ../shared/autocomplete.inc.css
+
 #treecolAutoCompleteImage {
   max-width: 36px;
 }
 
 .autocomplete-richlistbox {
   padding: 4px;
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/autocomplete.inc.css
@@ -0,0 +1,20 @@
+%if 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+%endif
+
+#PopupAutoComplete > richlistbox > richlistitem {
+  height: 20px;
+  min-height: 20px;
+  border-radius: 0;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem > .ac-title {
+  font: icon;
+  margin-inline-start: 6px;
+}
+
+#PopupAutoComplete > richlistbox {
+  padding: 0;
+}
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1527,16 +1527,18 @@ html|*.urlbar-input:-moz-lwtheme::placeh
 }
 
 /* page proxy icon */
 
 %include ../shared/identity-block/identity-block.inc.css
 
 /* autocomplete */
 
+%include ../shared/autocomplete.inc.css
+
 #treecolAutoCompleteImage {
   max-width: 36px;
 }
 
 .autocomplete-richlistbox {
   padding: 4px;
 }
 
--- a/toolkit/components/satchel/AutoCompletePopup.jsm
+++ b/toolkit/components/satchel/AutoCompletePopup.jsm
@@ -8,67 +8,78 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 this.EXPORTED_SYMBOLS = [ "AutoCompletePopup" ];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
-// nsITreeView implementation that feeds the autocomplete popup
-// with the search data.
-var AutoCompleteTreeView = {
+// AutoCompleteResultView is an abstraction around a list of results
+// we got back up from browser-content.js. It implements enough of
+// nsIAutoCompleteController and nsIAutoCompleteInput to make the
+// richlistbox popup work.
+var AutoCompleteResultView = {
   // nsISupports
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView,
-                                         Ci.nsIAutoCompleteController]),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteController,
+                                         Ci.nsIAutoCompleteInput]),
 
   // Private variables
-  treeBox: null,
   results: [],
 
-  // nsITreeView
-  selection: null,
-
-  get rowCount()                     { return this.results.length; },
-  setTree: function(treeBox)         { this.treeBox = treeBox; },
-  getCellText: function(idx, column) { return this.results[idx].value },
-  isContainer: function(idx)         { return false; },
-  getCellValue: function(idx, column) { return false },
-  isContainerOpen: function(idx)     { return false; },
-  isContainerEmpty: function(idx)    { return false; },
-  isSeparator: function(idx)         { return false; },
-  isSorted: function()               { return false; },
-  isEditable: function(idx, column)  { return false; },
-  canDrop: function(idx, orientation, dt) { return false; },
-  getLevel: function(idx)            { return 0; },
-  getParentIndex: function(idx)      { return -1; },
-  hasNextSibling: function(idx, after) { return idx < this.results.length - 1 },
-  toggleOpenState: function(idx)     { },
-  getCellProperties: function(idx, column) { return this.results[idx].style || ""; },
-  getRowProperties: function(idx)    { return ""; },
-  getImageSrc: function(idx, column) { return null; },
-  getProgressMode : function(idx, column) { },
-  cycleHeader: function(column) { },
-  cycleCell: function(idx, column) { },
-  selectionChanged: function() { },
-  performAction: function(action) { },
-  performActionOnCell: function(action, index, column) { },
-  getColumnProperties: function(column) { return ""; },
-
   // nsIAutoCompleteController
   get matchCount() {
-    return this.rowCount;
+    return this.results.length;
+  },
+
+  getValueAt(index) {
+    return this.results[index].value;
+  },
+
+  getLabelAt(index) {
+    // Unused by richlist autocomplete - see getCommentAt.
+    return "";
+  },
+
+  getCommentAt(index) {
+    // The richlist autocomplete popup uses comment for its main
+    // display of an item, which is why we're returning the label
+    // here instead.
+    return this.results[index].label;
+  },
+
+  getStyleAt(index) {
+    return this.results[index].style;
+  },
+
+  getImageAt(index) {
+    return this.results[index].image;
   },
 
   handleEnter: function(aIsPopupSelection) {
     AutoCompletePopup.handleEnter(aIsPopupSelection);
   },
 
   stopSearch: function() {},
 
+  searchString: "",
+
+  // nsIAutoCompleteInput
+  get controller() {
+    return this;
+  },
+
+  get popup() {
+    return null;
+  },
+
+  _focus() {
+    AutoCompletePopup.requestFocus();
+  },
+
   // Internal JS-only API
   clearResults: function() {
     this.results = [];
   },
 
   setResults: function(results) {
     this.results = results;
   },
@@ -101,17 +112,22 @@ this.AutoCompletePopup = {
   handleEvent: function(evt) {
     switch (evt.type) {
       case "popupshowing": {
         this.sendMessageToBrowser("FormAutoComplete:PopupOpened");
         break;
       }
 
       case "popuphidden": {
+        AutoCompleteResultView.clearResults();
         this.sendMessageToBrowser("FormAutoComplete:PopupClosed");
+        // adjustHeight clears the height from the popup so that
+        // we don't have a big shrink effect if we closed with a
+        // large list, and then open on a small one.
+        this.openedPopup.adjustHeight();
         this.openedPopup = null;
         this.weakBrowser = null;
         evt.target.removeEventListener("popuphidden", this);
         evt.target.removeEventListener("popupshowing", this);
         break;
       }
     }
   },
@@ -139,76 +155,74 @@ this.AutoCompletePopup = {
 
     this.weakBrowser = Cu.getWeakReference(browser);
     this.openedPopup = browser.autoCompletePopup;
     this.openedPopup.hidden = false;
     // don't allow the popup to become overly narrow
     this.openedPopup.setAttribute("width", Math.max(100, rect.width));
     this.openedPopup.style.direction = dir;
 
-    AutoCompleteTreeView.setResults(results);
-    this.openedPopup.view = AutoCompleteTreeView;
+    AutoCompleteResultView.setResults(results);
+    this.openedPopup.view = AutoCompleteResultView;
     this.openedPopup.selectedIndex = -1;
-    this.openedPopup.invalidate();
 
     if (results.length) {
       // Reset fields that were set from the last time the search popup was open
-      this.openedPopup.mInput = null;
+      this.openedPopup.mInput = AutoCompleteResultView;
       this.openedPopup.showCommentColumn = false;
       this.openedPopup.showImageColumn = false;
       this.openedPopup.addEventListener("popuphidden", this);
       this.openedPopup.addEventListener("popupshowing", this);
       this.openedPopup.openPopupAtScreenRect("after_start", rect.left, rect.top,
                                              rect.width, rect.height, false,
                                              false);
+      this.openedPopup.invalidate();
     } else {
       this.closePopup();
     }
   },
 
   invalidate(results) {
     if (!this.openedPopup) {
       return;
     }
 
     if (!results.length) {
       this.closePopup();
     } else {
-      AutoCompleteTreeView.setResults(results);
-      // We need to re-set the view in order for the
-      // tree to know the view has changed.
-      this.openedPopup.view = AutoCompleteTreeView;
+      AutoCompleteResultView.setResults(results);
       this.openedPopup.invalidate();
     }
   },
 
   closePopup() {
     if (this.openedPopup) {
       // Note that hidePopup() closes the popup immediately,
       // so popuphiding or popuphidden events will be fired
       // and handled during this call.
       this.openedPopup.hidePopup();
     }
-    AutoCompleteTreeView.clearResults();
   },
 
   removeLogin(login) {
     Services.logins.removeLogin(login);
   },
 
   receiveMessage: function(message) {
     if (!message.target.autoCompletePopup) {
       // Returning false to pacify ESLint, but this return value is
       // ignored by the messaging infrastructure.
       return false;
     }
 
     switch (message.name) {
       case "FormAutoComplete:SelectBy": {
-        this.openedPopup.selectBy(message.data.reverse, message.data.page);
+        if (this.openedPopup) {
+          this.openedPopup.selectBy(message.data.reverse, message.data.page);
+        }
         break;
       }
 
       case "FormAutoComplete:GetSelectedIndex": {
         if (this.openedPopup) {
           return this.openedPopup.selectedIndex;
         }
         // If the popup was closed, then the selection
@@ -242,17 +256,17 @@ this.AutoCompletePopup = {
         break;
       }
 
       case "FormAutoComplete:Disconnect": {
         // The controller stopped controlling the current input, so clear
         // any cached data.  This is necessary cause otherwise we'd clear data
         // only when starting a new search, but the next input could not support
         // autocomplete and it would end up inheriting the existing data.
-        AutoCompleteTreeView.clearResults();
+        AutoCompleteResultView.clearResults();
         break;
       }
     }
     // Returning false to pacify ESLint, but this return value is
     // ignored by the messaging infrastructure.
     return false;
   },
 
@@ -281,10 +295,20 @@ this.AutoCompletePopup = {
   sendMessageToBrowser(msgName, data) {
     let browser = this.weakBrowser ? this.weakBrowser.get()
                                    : null;
     if (browser) {
       browser.messageManager.sendAsyncMessage(msgName, data);
     }
   },
 
-  stopSearch: function() {}
+  stopSearch: function() {},
+
+  /**
+   * Sends a message to the browser requesting that the input
+   * that the AutoCompletePopup is open for be focused.
+   */
+  requestFocus: function() {
+    if (this.openedPopup) {
+      this.sendMessageToBrowser("FormAutoComplete:Focus");
+    }
+  },
 }
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -1396,18 +1396,19 @@ addEventListener("MozApplicationManifest
 
 let AutoCompletePopup = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup]),
 
   _connected: false,
 
   MESSAGES: [
     "FormAutoComplete:HandleEnter",
+    "FormAutoComplete:PopupClosed",
     "FormAutoComplete:PopupOpened",
-    "FormAutoComplete:PopupClosed",
+    "FormAutoComplete:RequestFocus",
   ],
 
   init: function() {
     addEventListener("unload", this);
     addEventListener("DOMContentLoaded", this);
 
     for (let messageName of this.MESSAGES) {
       addMessageListener(messageName, this);
@@ -1474,16 +1475,23 @@ let AutoCompletePopup = {
         this._popupOpen = false;
         break;
       }
 
       case "FormAutoComplete:PopupOpened": {
         this._popupOpen = true;
         break;
       }
+
+      case "FormAutoComplete:RequestFocus": {
+        if (this._input) {
+          this._input.focus();
+        }
+        break;
+      }
     }
   },
 
   get input () { return this._input; },
   get overrideValue () { return null; },
   set selectedIndex (index) {
     sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index });
   },
@@ -1556,17 +1564,17 @@ let AutoCompletePopup = {
       result.label = controller.getLabelAt(i);
       result.comment = controller.getCommentAt(i);
       result.style = controller.getStyleAt(i);
       result.image = controller.getImageAt(i);
       results.push(result);
     }
 
     return results;
-  }
+  },
 }
 
 AutoCompletePopup.init();
 
 /**
  * DateTimePickerListener is the communication channel between the input box
  * (content) for date/time input types and its picker (chrome).
  */
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1099,16 +1099,20 @@ extends="chrome://global/content/binding
       </method>
 
       <method name="_openAutocompletePopup">
         <parameter name="aInput"/>
         <parameter name="aElement"/>
         <body>
           <![CDATA[
           if (!this.mPopupOpen) {
+            // It's possible that the panel is hidden initially
+            // to avoid impacting startup / new window performance
+            aInput.popup.hidden = false;
+
             this.mInput = aInput;
             // clear any previous selection, see bugs 400671 and 488357
             this.selectedIndex = -1;
 
             var width = aElement.getBoundingClientRect().width;
             this.setAttribute("width", width > 100 ? width : 100);
             // invalidate() depends on the width attribute
             this._invalidate();