Bug 897061 - [e10s] Implement support for form autocomplete. r=markh
authorFelipe Gomes <felipc@gmail.com>
Fri, 06 Dec 2013 22:02:05 -0200
changeset 159323 10edfe4c320892086a88fadca317dfd59e091ace
parent 159322 d5d23d937ffc67fb639c36247dae7bbb514d64f0
child 159324 86307d61bcb31e28dab1f71e6bbcf8cb2f429827
push id25784
push usercbook@mozilla.com
push dateSat, 07 Dec 2013 11:44:20 +0000
treeherdermozilla-central@add90fdc2a74 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs897061
milestone28.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 897061 - [e10s] Implement support for form autocomplete. r=markh
toolkit/components/satchel/AutoCompleteE10S.jsm
toolkit/components/satchel/FormHistoryStartup.js
toolkit/components/satchel/moz.build
toolkit/components/satchel/nsFormAutoComplete.js
toolkit/content/browser-child.js
toolkit/content/widgets/remote-browser.xml
new file mode 100644
--- /dev/null
+++ b/toolkit/components/satchel/AutoCompleteE10S.jsm
@@ -0,0 +1,157 @@
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AutoCompleteE10S" ];
+
+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.
+let AutoCompleteE10SView = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView,
+                                         Ci.nsIAutoCompleteController]),
+  treeBox: null,
+  selection: null,
+  treeData: [],
+
+  get rowCount()                     { return this.treeData.length; },
+  setTree: function(treeBox)         { this.treeBox = treeBox; },
+  getCellText: function(idx, column) { return this.treeData[idx] },
+  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.treeData.length - 1 },
+  toggleOpenState: function(idx)     { },
+  getCellProperties: function(idx, column) { return ""; },
+  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 ""; },
+
+  get matchCount() this.rowCount,
+
+
+  clearResults: function() {
+    this.treeData = [];
+  },
+
+  addResult: function(result) {
+    this.treeData.push(result);
+  },
+
+  handleEnter: function(aIsPopupSelection) {
+    AutoCompleteE10S.handleEnter(aIsPopupSelection);
+  },
+
+  stopSearch: function(){}
+};
+
+this.AutoCompleteE10S = {
+  init: function() {
+    let messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
+                         getService(Ci.nsIMessageListenerManager);
+    messageManager.addMessageListener("FormAutoComplete:SelectBy", this);
+    messageManager.addMessageListener("FormAutoComplete:GetSelectedIndex", this);
+    messageManager.addMessageListener("FormAutoComplete:ClosePopup", this);
+  },
+
+  search: function(message) {
+    let browserWindow = message.target.ownerDocument.defaultView;
+    this.browser = browserWindow.gBrowser.selectedBrowser;
+    this.popup = this.browser.autoCompletePopup;
+    this.popup.hidden = false;
+    this.popup.setAttribute("width", message.data.width);
+
+    this.x = message.data.left;
+    this.y = message.data.top + message.data.height;
+
+    let formAutoComplete = Cc["@mozilla.org/satchel/form-autocomplete;1"]
+                             .getService(Ci.nsIFormAutoComplete);
+
+    formAutoComplete.autoCompleteSearchAsync(message.data.inputName,
+                                             message.data.untrimmedSearchString,
+                                             null,
+                                             null,
+                                             this.onSearchComplete.bind(this));
+  },
+
+  onSearchComplete: function(results) {
+    AutoCompleteE10SView.clearResults();
+
+    let resultsArray = [];
+    let count = results.matchCount;
+    for (let i = 0; i < count; i++) {
+      let result = results.getValueAt(i);
+      resultsArray.push(result);
+      AutoCompleteE10SView.addResult(result);
+    }
+
+    this.popup.view = AutoCompleteE10SView;
+
+    this.browser.messageManager.sendAsyncMessage(
+      "FormAutoComplete:AutoCompleteSearchAsyncResult",
+      {results: resultsArray}
+    );
+
+    this.popup.selectedIndex = -1;
+    this.popup.invalidate();
+
+    if (count > 0) {
+      this.popup.openPopup(this.browser, "overlap", this.x, this.y, true, true);
+      // Bug 947503 - This openPopup call is not triggering the "popupshowing"
+      // event, which autocomplete.xml uses to track the openness of the popup
+      // by setting its mPopupOpen flag. This flag needs to be properly set
+      // for closePopup to work. For now, we set it manually.
+      this.popup.mPopupOpen = true;
+    } else {
+      this.popup.closePopup();
+    }
+  },
+
+  receiveMessage: function(message) {
+    switch (message.name) {
+      case "FormAutoComplete:SelectBy":
+        this.popup.selectBy(message.data.reverse, message.data.page);
+        break;
+
+      case "FormAutoComplete:GetSelectedIndex":
+        return this.popup.selectedIndex;
+
+      case "FormAutoComplete:ClosePopup":
+        this.popup.closePopup();
+        break;
+    }
+  },
+
+  handleEnter: function(aIsPopupSelection) {
+    this.browser.messageManager.sendAsyncMessage(
+      "FormAutoComplete:HandleEnter",
+      { selectedIndex: this.popup.selectedIndex,
+        IsPopupSelection: aIsPopupSelection }
+    );
+  },
+
+  stopSearch: function() {}
+}
+
+this.AutoCompleteE10S.init();
--- a/toolkit/components/satchel/FormHistoryStartup.js
+++ b/toolkit/components/satchel/FormHistoryStartup.js
@@ -6,16 +6,19 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AutoCompleteE10S",
+                                  "resource://gre/modules/AutoCompleteE10S.jsm");
+
 function FormHistoryStartup() { }
 
 FormHistoryStartup.prototype = {
   classID: Components.ID("{3A0012EB-007F-4BB8-AA81-A07385F77A25}"),
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference,
@@ -54,25 +57,36 @@ FormHistoryStartup.prototype = {
     // triggers needed service cleanup and db shutdown
     Services.obs.addObserver(this, "profile-before-change", true);
     Services.obs.addObserver(this, "formhistory-expire-now", true);
 
     let messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
                          getService(Ci.nsIMessageListenerManager);
     messageManager.loadFrameScript("chrome://satchel/content/formSubmitListener.js", true);
     messageManager.addMessageListener("FormHistory:FormSubmitEntries", this);
+    messageManager.addMessageListener("FormHistory:AutoCompleteSearchAsync", this);
   },
 
   receiveMessage: function(message) {
-    let entries = message.json;
-    let changes = entries.map(function(entry) {
-      return {
-        op : "bump",
-        fieldname : entry.name,
-        value : entry.value,
+    switch (message.name) {
+      case "FormHistory:FormSubmitEntries": {
+        let entries = message.data;
+        let changes = entries.map(function(entry) {
+          return {
+            op : "bump",
+            fieldname : entry.name,
+            value : entry.value,
+          }
+        });
+
+        FormHistory.update(changes);
+        break;
       }
-    });
 
-    FormHistory.update(changes);
+      case "FormHistory:AutoCompleteSearchAsync": {
+        AutoCompleteE10S.search(message);
+        break;
+      }
+    }
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormHistoryStartup]);
--- a/toolkit/components/satchel/moz.build
+++ b/toolkit/components/satchel/moz.build
@@ -30,16 +30,17 @@ EXTRA_COMPONENTS += [
     'satchel.manifest',
 ]
 
 EXTRA_PP_COMPONENTS += [
     'nsFormHistory.js',
 ]
 
 EXTRA_JS_MODULES += [
+    'AutoCompleteE10S.jsm',
     'nsFormAutoCompleteResult.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'FormHistory.jsm',
 ]
 
 FINAL_LIBRARY = 'xul'
--- a/toolkit/components/satchel/nsFormAutoComplete.js
+++ b/toolkit/components/satchel/nsFormAutoComplete.js
@@ -14,16 +14,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Deprecated.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 
 function FormAutoComplete() {
     this.init();
 }
 
+/**
+ * FormAutoComplete
+ *
+ * Implements the nsIFormAutoComplete interface in the main process.
+ */
 FormAutoComplete.prototype = {
     classID          : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
     QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
 
     _prefBranch         : null,
     _debug              : true, // mirrors browser.formfill.debug
     _enabled            : true, // mirrors browser.formfill.enable preference
     _agedWeight         : 2,
@@ -299,18 +304,110 @@ FormAutoComplete.prototype = {
         boundaryCalc += this._prefixWeight *
                         (entry.textLowerCase.
                          indexOf(aSearchString) == 0);
         entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
     }
 
 }; // end of FormAutoComplete implementation
 
+/**
+ * FormAutoCompleteChild
+ *
+ * Implements the nsIFormAutoComplete interface in a child content process,
+ * and forwards the auto-complete requests to the parent process which
+ * also implements a nsIFormAutoComplete interface and has
+ * direct access to the FormHistory database.
+ */
+function FormAutoCompleteChild() {
+  this.init();
+}
 
+FormAutoCompleteChild.prototype = {
+    classID          : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
+    QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
 
+    _debug: false,
+    _enabled: true,
+
+    /*
+     * init
+     *
+     * Initializes the content-process side of the FormAutoComplete component,
+     * and add a listener for the message that the parent process sends when
+     * a result is produced.
+     */
+    init: function() {
+      this._debug    = Services.prefs.getBoolPref("browser.formfill.debug");
+      this._enabled  = Services.prefs.getBoolPref("browser.formfill.enable");
+      this.log("init");
+    },
+
+    /*
+     * log
+     *
+     * Internal function for logging debug messages
+     */
+    log : function (message) {
+      if (!this._debug)
+        return;
+      dump("FormAutoCompleteChild: " + message + "\n");
+    },
+
+    autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
+      // This function is deprecated
+    },
+
+    autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
+      this.log("autoCompleteSearchAsync");
+
+      this._pendingListener = aListener;
+
+      let rect = aField.getBoundingClientRect();
+
+      let topLevelDocshell = aField.ownerDocument.defaultView
+                                   .QueryInterface(Ci.nsIInterfaceRequestor)
+                                   .getInterface(Ci.nsIDocShell)
+                                   .sameTypeRootTreeItem
+                                   .QueryInterface(Ci.nsIDocShell);
+
+      let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIContentFrameMessageManager);
+
+      mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
+        inputName: aInputName,
+        untrimmedSearchString: aUntrimmedSearchString,
+        left: rect.left,
+        top: rect.top,
+        width: rect.width,
+        height: rect.height
+      });
+
+      mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult",
+        function searchFinished(message) {
+          mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
+          let result = new FormAutoCompleteResult(
+            null,
+            [{text: res} for (res of message.data.results)],
+            null,
+            null
+          );
+          if (aListener) {
+            aListener.onSearchCompletion(result);
+          }
+        }
+      );
+
+      this.log("autoCompleteSearchAsync message was sent");
+    },
+
+    stopAutoCompleteSearch : function () {
+       this.log("stopAutoCompleteSearch");
+    },
+}; // end of FormAutoCompleteChild implementation
 
 // nsIAutoCompleteResult implementation
 function FormAutoCompleteResult (formHistory, entries, fieldName, searchString) {
     this.formHistory = formHistory;
     this.entries = entries;
     this.fieldName = fieldName;
     this.searchString = searchString;
 }
@@ -385,10 +482,20 @@ FormAutoCompleteResult.prototype = {
         if (removeFromDB) {
           this.formHistory.update({ op: "remove",
                                     fieldname: this.fieldName,
                                     value: removedEntry.text });
         }
     }
 };
 
-let component = [FormAutoComplete];
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
+
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT &&
+    Services.prefs.prefHasUserValue("browser.tabs.remote") &&
+    Services.prefs.getBoolPref("browser.tabs.remote")) {
+  // Register the stub FormAutoComplete module in the child which will
+  // forward messages to the parent through the process message manager.
+  let component = [FormAutoCompleteChild];
+  this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
+} else {
+  let component = [FormAutoComplete];
+  this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
+}
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -4,16 +4,17 @@
 
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import("resource://gre/modules/RemoteAddonsChild.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
 
 let SyncHandler = {
   init: function() {
     sendAsyncMessage("SetSyncHandler", {}, {handler: this});
   },
 
   getFocusedElementAndWindow: function() {
     let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
@@ -242,8 +243,71 @@ addEventListener("ImageContentLoaded", f
   }
 }, false);
 
 RemoteAddonsChild.init(this);
 
 addMessageListener("History:UseGlobalHistory", function (aMessage) {
   docShell.useGlobalHistory = aMessage.data.enabled;
 });
+
+let AutoCompletePopup = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup]),
+
+  init: function() {
+    // Hook up the form fill autocomplete controller.
+    let controller = Cc["@mozilla.org/satchel/form-fill-controller;1"]
+                       .getService(Ci.nsIFormFillController);
+
+    controller.attachToBrowser(docShell, this.QueryInterface(Ci.nsIAutoCompletePopup));
+
+    this._input = null;
+    this._popupOpen = false;
+
+    addMessageListener("FormAutoComplete:HandleEnter", message => {
+      this.selectedIndex = message.data.selectedIndex;
+
+      let controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+                  getService(Components.interfaces.nsIAutoCompleteController);
+      controller.handleEnter(message.data.isPopupSelection);
+    });
+  },
+
+  get input () { return this._input; },
+  get overrideValue () { return null; },
+  set selectedIndex (index) { },
+  get selectedIndex () {
+    // selectedIndex getter must be synchronous because we need the
+    // correct value when the controller is in controller::HandleEnter.
+    // We can't easily just let the parent inform us the new value every
+    // time it changes because not every action that can change the
+    // selectedIndex is trivial to catch (e.g. moving the mouse over the
+    // list).
+    return sendSyncMessage("FormAutoComplete:GetSelectedIndex", {});
+  },
+  get popupOpen () {
+    return this._popupOpen;
+  },
+
+  openAutocompletePopup: function (input, element) {
+    this._input = input;
+    this._popupOpen = true;
+  },
+
+  closePopup: function () {
+    this._popupOpen = false;
+    sendAsyncMessage("FormAutoComplete:ClosePopup", {});
+  },
+
+  invalidate: function () {
+  },
+
+  selectBy: function(reverse, page) {
+    this._index = sendSyncMessage("FormAutoComplete:SelectBy", {
+      reverse: reverse,
+      page: page
+    });
+  }
+}
+
+addMessageListener("FormAutoComplete:InitPopup", function (aMessage) {
+  setTimeout(function() AutoCompletePopup.init(), 0);
+});
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -114,16 +114,20 @@
                 readonly="true"/>
 
       <field name="_imageDocument">null</field>
 
       <property name="imageDocument"
                 onget="return this._imageDocument"
                 readonly="true"/>
 
+      <property name="autoCompletePopup"
+                onget="return document.getElementById(this.getAttribute('autocompletepopup'))"
+                readonly="true"/>
+
       <constructor>
         <![CDATA[
           let jsm = "resource://gre/modules/RemoteWebNavigation.jsm";
           let RemoteWebNavigation = Cu.import(jsm, {}).RemoteWebNavigation;
           this._remoteWebNavigation = new RemoteWebNavigation(this);
 
           this.messageManager.addMessageListener("DOMTitleChanged", this);
           this.messageManager.addMessageListener("ImageDocumentLoaded", this);
@@ -143,16 +147,20 @@
 
           jsm = "resource://gre/modules/RemoteAddonsParent.jsm";
           let RemoteAddonsParent = Components.utils.import(jsm, {}).RemoteAddonsParent;
           RemoteAddonsParent.init();
 
           if (!this.hasAttribute("disableglobalhistory")) {
             this.messageManager.sendAsyncMessage("History:UseGlobalHistory", {enabled:true});
           }
+
+          if (this.autoCompletePopup) {
+            this.messageManager.sendAsyncMessage("FormAutoComplete:InitPopup");
+          }
         ]]>
       </constructor>
 
       <method name="receiveMessage">
         <parameter name="aMessage"/>
         <body><![CDATA[
           let data = aMessage.data;
           switch (aMessage.name) {