Bug 1189618 [WIP] - Restyle password manager autocomplete popup. r?MattN draft
authorMike Conley <mconley@mozilla.com>
Fri, 15 Jul 2016 15:45:47 -0400 (2016-07-15)
changeset 389593 d9600233d40479dfb01936b8924711830c4140ba
parent 389143 be79f9b6ecc4a46ccff6c0d65d797d1b8bf17325
child 525808 f7dacca1268620128dfcd3a9575f807c7793addf
push id23468
push usermconley@mozilla.com
push dateTue, 19 Jul 2016 18:35:48 +0000 (2016-07-19)
reviewersMattN
bugs1189618
milestone50.0a1
Bug 1189618 [WIP] - Restyle password manager autocomplete popup. r?MattN MozReview-Commit-ID: 5mtzxBKwuud
browser/base/content/browser-doctype.inc
browser/base/content/browser.xul
browser/base/content/urlbarBindings.xml
browser/themes/osx/browser.css
layout/xul/tree/nsTreeBodyFrame.cpp
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/satchel/AutoCompleteE10S.jsm
toolkit/components/satchel/nsFormFillController.cpp
toolkit/components/satchel/nsIFormFillController.idl
toolkit/content/browser-child.js
--- a/browser/base/content/browser-doctype.inc
+++ b/browser/base/content/browser-doctype.inc
@@ -16,10 +16,12 @@
 #ifdef MOZ_SAFE_BROWSING
 <!ENTITY % safebrowsingDTD SYSTEM "chrome://browser/locale/safebrowsing/phishing-afterload-warning-message.dtd">
 %safebrowsingDTD;
 #endif
 <!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd">
 %aboutHomeDTD;
 <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
 %syncBrandDTD;
+<!ENTITY % securityPrefsDTD SYSTEM "chrome://browser/locale/preferences/security.dtd">
+%securityPrefsDTD;
 ]>
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -136,17 +136,22 @@
     <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" id="PopupAutoComplete" noautofocus="true" hidden="true">
+      <footer id="LoginAutoCompleteFooter">
+        <button class="plain" label="&savedLogins.label;"
+                oncommand="LoginHelper.openPasswordManager(window);"/>
+      </footer>
+    </panel>
 
     <!-- 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/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1055,30 +1055,55 @@ file, You can obtain one at http://mozil
           .copyStringToClipboard(val, Ci.nsIClipboard.kSelectionClipboard);
       ]]></handler>
     </handlers>
 
   </binding>
 
   <!-- Note: this binding is applied to the autocomplete popup used in web page content and extended in search.xml for the searchbar. -->
   <binding id="browser-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-result-popup">
+    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
+      <xul:tree anonid="tree" class="autocomplete-tree plain" hidecolumnpicker="true" flex="1" 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="footer" flex="1">
+        <children includes="footer"/>
+      </xul:vbox>
+    </content>
+
     <implementation>
       <field name="AppConstants" readonly="true">
         (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
       </field>
 
+      <field name="footer" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "footer");
+      </field>
+
       <method name="openAutocompletePopup">
         <parameter name="aInput"/>
         <parameter name="aElement"/>
         <body>
           <![CDATA[
           // initially the panel is hidden
           // to avoid impacting startup / new window performance
           aInput.popup.hidden = false;
 
+          let controller =
+            Components.classes["@mozilla.org/satchel/form-fill-controller;1"]
+                      .getService(Components.interfaces.nsIFormFillController);
+
+          // If this popup is being populated by the login manager,
+          // then we show a footer to the user to give them some
+          // options for logging in.
+          this.footer.hidden = !controller.isLoginManagerField(aElement);
+
           // this method is defined on the base binding
           this._openAutocompletePopup(aInput, aElement);
         ]]></body>
       </method>
 
       <method name="onPopupClick">
         <parameter name="aEvent"/>
         <body><![CDATA[
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1829,16 +1829,51 @@ html|span.ac-emphasize-text-url {
   color: GrayText;
   font-size: smaller;
 }
 
 .autocomplete-treebody::-moz-tree-cell(suggesthint) {
   border-top: 1px solid GrayText;
 }
 
+.autocomplete-treebody::-moz-tree-image(login) {
+  padding-inline-start: 8px;
+  padding-inline-end: 8px;
+  list-style-image: url(chrome://browser/skin/permissions.svg#login-detailed);
+}
+
+.autocomplete-treebody::-moz-tree-row(login) {
+  height: 28px; /* <-- this totally doesn't work, unless I remove
+                 * the login selector from -moz-tree-row, but then
+                 * all autocomplete results can sized this way. :(
+                 */
+}
+
+#LoginAutoCompleteFooter {
+  /** Copied from Downloads Panel... we might want to share this. **/
+  background: #e5e5e5;
+  border-top: 1px solid hsla(0,0%,0%,.1);
+  box-shadow: 0 -1px hsla(0,0%,100%,.5) inset, 0 1px 1px hsla(0,0%,0%,.03) inset;
+}
+
+#LoginAutoCompleteFooter > button {
+  -moz-appearance: none;
+  -moz-box-flex: 1;
+  border-top: 1px solid #ccc;
+  font-size: 10px;
+  font-weight: normal;
+  background-color: rgb(245, 245, 245);
+  margin: 0;
+  padding: 3px 6px;
+  color: #666;
+}
+
+#LoginAutoCompleteFooter > button > .button-box {
+  margin: 0.5em;
+}
 
 /* ----- COMBINED GO/RELOAD/STOP BUTTON IN LOCATION BAR ----- */
 
 #urlbar-go-button,
 #urlbar-reload-button,
 #urlbar-stop-button {
   margin: 0;
   list-style-image: url("chrome://browser/skin/reload-stop-go.png");
--- a/layout/xul/tree/nsTreeBodyFrame.cpp
+++ b/layout/xul/tree/nsTreeBodyFrame.cpp
@@ -2421,16 +2421,17 @@ nsTreeBodyFrame::GetImageSourceRect(nsSt
   return r;
 }
 
 int32_t nsTreeBodyFrame::GetRowHeight()
 {
   // Look up the correct height.  It is equal to the specified height
   // + the specified margins.
   mScratchArray.Clear();
+
   nsStyleContext* rowContext = GetPseudoStyleContext(nsCSSAnonBoxes::moztreerow);
   if (rowContext) {
     const nsStylePosition* myPosition = rowContext->StylePosition();
 
     nscoord minHeight = 0;
     if (myPosition->mMinHeight.GetUnit() == eStyleUnit_Coord)
       minHeight = myPosition->mMinHeight.GetCoordValue();
 
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -1242,17 +1242,17 @@ UserAutoCompleteResult.prototype = {
     return this.getValueAt(index);
   },
 
   getCommentAt(index) {
     return "";
   },
 
   getStyleAt(index) {
-    return "";
+    return "login";
   },
 
   getImageAt(index) {
     return "";
   },
 
   getFinalCompleteValueAt(index) {
     return this.getValueAt(index);
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -269,17 +269,17 @@ var LoginManagerParent = {
 
     // XXX In the E10S case, we're responsible for showing our own
     // autocomplete popup here because the autocomplete protocol hasn't
     // been e10s-ized yet. In the non-e10s case, our caller is responsible
     // for showing the autocomplete popup (via the regular
     // nsAutoCompleteController).
     if (remote) {
       result = new UserAutoCompleteResult(searchString, matchingLogins);
-      AutoCompleteE10S.showPopupWithResults(target.ownerDocument.defaultView, rect, result);
+      AutoCompleteE10S.showPopupWithResults(target.ownerDocument.defaultView, rect, result, true);
     }
 
     // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
     // doesn't support structured cloning.
     var jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
     target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
       requestId: requestId,
       logins: jsLogins,
--- a/toolkit/components/satchel/AutoCompleteE10S.jsm
+++ b/toolkit/components/satchel/AutoCompleteE10S.jsm
@@ -40,17 +40,17 @@ var AutoCompleteE10SView = {
   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.treeData.length - 1 },
   toggleOpenState: function(idx)     { },
   getCellProperties: function(idx, column) { return this.properties[idx] || ""; },
-  getRowProperties: function(idx)    { return ""; },
+  getRowProperties: function(idx)    { return this.properties[idx] || ""; },
   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 ""; },
@@ -85,21 +85,22 @@ this.AutoCompleteE10S = {
     messageManager.addMessageListener("FormAutoComplete:SelectBy", this);
     messageManager.addMessageListener("FormAutoComplete:GetSelectedIndex", this);
     messageManager.addMessageListener("FormAutoComplete:MaybeOpenPopup", this);
     messageManager.addMessageListener("FormAutoComplete:ClosePopup", this);
     messageManager.addMessageListener("FormAutoComplete:Disconnect", this);
     messageManager.addMessageListener("FormAutoComplete:RemoveEntry", this);
   },
 
-  _initPopup: function(browserWindow, rect, direction) {
+  _initPopup: function(browserWindow, rect, forLogin, direction) {
     this._popupCache = { browserWindow, rect, direction };
 
     this.browser = browserWindow.gBrowser.selectedBrowser;
     this.popup = this.browser.autoCompletePopup;
+    this.popup.footer.hidden = !forLogin;
     this.popup.hidden = false;
     // don't allow the popup to become overly narrow
     this.popup.setAttribute("width", Math.max(100, rect.width));
     this.popup.style.direction = direction;
 
     this.x = rect.left;
     this.y = rect.top;
     this.width = rect.width;
@@ -137,18 +138,18 @@ this.AutoCompleteE10S = {
 
     this._resultCache = results;
     return resultsArray;
   },
 
   // This function is used by the login manager, which uses a single message
   // to fill in the autocomplete results. See
   // "RemoteLogins:autoCompleteLogins".
-  showPopupWithResults: function(browserWindow, rect, results) {
-    this._initPopup(browserWindow, rect);
+  showPopupWithResults: function(browserWindow, rect, results, forLogin=false) {
+    this._initPopup(browserWindow, rect, forLogin);
     this._showPopup(results);
   },
 
   removeLogin(login) {
     Services.logins.removeLogin(login);
 
     // It's possible to race and have the deleted login no longer be in our
     // resultCache's logins, so we remove it from the database above and only
@@ -178,17 +179,17 @@ this.AutoCompleteE10S = {
   // This function is called in response to AutoComplete requests from the
   // child (received via the message manager, see
   // "FormHistory:AutoCompleteSearchAsync").
   search: function(message) {
     let browserWindow = message.target.ownerDocument.defaultView;
     let rect = message.data;
     let direction = message.data.direction;
 
-    this._initPopup(browserWindow, rect, direction);
+    this._initPopup(browserWindow, rect, false, direction);
 
     // NB: We use .wrappedJSObject here in order to pass our mock DOM object
     // without being rejected by XPConnect (which attempts to enforce that DOM
     // objects are implemented in C++.
     let formAutoComplete = Cc["@mozilla.org/satchel/form-autocomplete;1"]
                              .getService(Ci.nsIFormAutoComplete).wrappedJSObject;
 
     let values, labels;
@@ -243,23 +244,25 @@ this.AutoCompleteE10S = {
 
       case "FormAutoComplete:RemoveEntry":
         this.removeEntry(message.data.index);
         break;
 
       case "FormAutoComplete:MaybeOpenPopup":
         if (AutoCompleteE10SView.treeData.length > 0 &&
             !this.popup.popupOpen) {
+          let { forLogin } = msg.data;
           // This happens when one of the arrow keys is pressed after a search
           // has already been completed. nsAutoCompleteController tries to
           // re-use its own cache of the results without re-doing the search.
           // Detect that and show the popup here.
           this.showPopupWithResults(this._popupCache.browserWindow,
                                     this._popupCache.rect,
-                                    this._resultCache);
+                                    this._resultCache,
+                                    forLogin);
         }
         break;
 
       case "FormAutoComplete:ClosePopup":
         this.popup.closePopup();
         break;
 
       case "FormAutoComplete:Disconnect":
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -273,16 +273,23 @@ nsFormFillController::MarkAsLoginManager
   node->AddMutationObserverUnlessExists(this);
 
   if (!mLoginManager)
     mLoginManager = do_GetService("@mozilla.org/login-manager;1");
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsFormFillController::IsLoginManagerField(nsIDOMHTMLInputElement *aInput,
+                                          bool* aResult)
+{
+  *aResult = mPwmgrInputs.Get(mFocusedInputNode);
+  return NS_OK;
+}
 
 ////////////////////////////////////////////////////////////////////////
 //// nsIAutoCompleteInput
 
 NS_IMETHODIMP
 nsFormFillController::GetPopup(nsIAutoCompletePopup **aPopup)
 {
   *aPopup = mFocusedPopup;
--- a/toolkit/components/satchel/nsIFormFillController.idl
+++ b/toolkit/components/satchel/nsIFormFillController.idl
@@ -38,9 +38,17 @@ interface nsIFormFillController : nsISup
   /*
    * Mark the specified <input> element as being managed by password manager.
    * Autocomplete requests will be handed off to the password manager, and will
    * not be stored in form history.
    *
    * @param aInput - The HTML <input> element to tag
    */
   void markAsLoginManagerField(in nsIDOMHTMLInputElement aInput);
+
+  /*
+   * See if the specified <input> element has been marked as being managed
+   * by the password manager via markAsLoginManagerField.
+   *
+   * @param aInput - the HTML <input> element to check
+   */
+  bool isLoginManagerField(in nsIDOMHTMLInputElement aInput);
 };
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -618,20 +618,25 @@ var AutoCompletePopup = {
     return sendSyncMessage("FormAutoComplete:GetSelectedIndex", {});
   },
   get popupOpen () {
     return this._popupOpen;
   },
 
   openAutocompletePopup: function (input, element) {
     if (!this._popupOpen) {
+      let controller = Cc["@mozilla.org/satchel/form-fill-controller;1"]
+                         .getService(Ci.nsIFormFillController);
+      // If we're populating the autocomplete from the login manager,
+      // tell the parent so that it can specially style the popup.
+      let forLogin = controller.isLoginManagerField(element);
       // The search itself normally opens the popup itself, but in some cases,
       // nsAutoCompleteController tries to use cached results so notify our
       // popup to reuse the last results.
-      sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", {});
+      sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", { forLogin });
     }
     this._input = input;
     this._popupOpen = true;
   },
 
   closePopup: function () {
     this._popupOpen = false;
     sendAsyncMessage("FormAutoComplete:ClosePopup", {});