Bug 757496 - Support mozKeyboard.setSelectedOption[s]. r=fabrice
authorVivien Nicolas <21@vingtetun.org>
Wed, 06 Jun 2012 14:19:33 +0200
changeset 95955 2e509c91883aaed7cb11a9b0c2ae29cfd61f0a70
parent 95954 abecec00a124a7d079a5124abf5e0ed8d48b4096
child 95956 fc203943d6bc0b89d688ceb24dc02a84fe691d3a
push id10318
push uservnicolas@mozilla.com
push dateWed, 06 Jun 2012 12:26:57 +0000
treeherdermozilla-inbound@ecf7c8f799eb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfabrice
bugs757496
milestone16.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 757496 - Support mozKeyboard.setSelectedOption[s]. r=fabrice
b2g/chrome/content/forms.js
b2g/chrome/content/shell.js
b2g/chrome/jar.mn
b2g/components/MozKeyboard.js
b2g/components/b2g.idl
new file mode 100644
--- /dev/null
+++ b/b2g/chrome/content/forms.js
@@ -0,0 +1,157 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* 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/. */
+
+
+let Ci = Components.interfaces;
+let Cc = Components.classes;
+let Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+dump("###################################### forms.js loaded\n");
+
+let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
+let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
+let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
+
+let FormAssistant = {
+  init: function fa_init() {
+    addEventListener("focus", this, true, false);
+    addEventListener("click", this, true, false);
+    addEventListener("blur", this, true, false);
+    addMessageListener("Forms:Select:Choice", this);
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+  },
+
+  currentTarget: null,
+  handleEvent: function fa_handleEvent(evt) {
+    switch (evt.type) {
+      case "click":
+      case "focus": {
+        content.setTimeout(function(self) {
+          let target = evt.target;
+          if (target instanceof HTMLSelectElement) { 
+            sendAsyncMessage("Forms:Input", self._getJSON(target));
+            self.currentTarget = target;
+          } else if (target instanceof HTMLOptionElement &&
+                     target.parentNode instanceof HTMLSelectElement) {
+            target = target.parentNode;
+            sendAsyncMessage("Forms:Input", self._getJSON(target));
+            self.currentTarget = target;
+          }
+        }, 0, this);
+        break;
+      }
+
+      case "blur": {
+        let target = this.currentTarget;
+        if (!target)
+          return;
+
+        this.currentTarget = null;
+
+        sendAsyncMessage("Forms:Input", { "type": "blur" });
+
+        let e = target.ownerDocument.createEvent("Events");
+        e.initEvent("change", true, true, target.ownerDocument.defaultView,
+                    0, false, false, false, false, null);
+
+        content.setTimeout(function() {
+          target.dispatchEvent(evt);
+        }, 0);
+
+        break;
+      }
+    }
+  },
+
+  receiveMessage: function fa_receiveMessage(msg) {
+    let json = msg.json;
+    switch (msg.name) {
+      case "Forms:Select:Choice":
+        if (!this.currentTarget)
+          return;
+
+        let options = this.currentTarget.options;
+        if ("index" in json) {
+          options.item(json.index).selected = true;
+        } else if ("indexes" in json) {
+          for (let i = 0; i < options.length; i++) {
+            options.item(i).selected = (json.indexes.indexOf(i) != -1);
+          }
+        }
+        break;
+    }
+  },
+
+  observe: function fa_observe(subject, topic, data) {
+    Services.obs.removeObserver(this, "xpcom-shutdown");
+    removeMessageListener("Forms:Select:Choice", this);
+  },
+
+  _getJSON: function fa_getJSON(element) {
+    let choices = getListForElement(element);
+    return {
+      type: element.type.toLowerCase(),
+      choices: choices
+    };
+  }
+};
+
+FormAssistant.init();
+
+function getListForElement(element) {
+  if (!(element instanceof HTMLSelectElement))
+    return null;
+
+  let optionIndex = 0;
+  let result = {
+    "multiple": element.multiple,
+    "choices": []
+  };
+
+  // Build up a flat JSON array of the choices.
+  // In HTML, it's possible for select element choices to be under a
+  // group header (but not recursively). We distinguish between headers
+  // and entries using the boolean "list.group".
+  let children = element.children;
+  for (let i = 0; i < children.length; i++) {
+    let child = children[i];
+
+    if (child instanceof HTMLOptGroupElement) {
+      result.choices.push({
+        "group": true,
+        "text": child.label || child.firstChild.data,
+        "disabled": child.disabled
+      });
+
+      let subchildren = child.children;
+      for (let j = 0; j < subchildren.length; j++) {
+        let subchild = subchildren[j];
+        result.choices.push({
+          "group": false,
+          "inGroup": true,
+          "text": subchild.text,
+          "disabled": child.disabled || subchild.disabled,
+          "selected": subchild.selected,
+          "optionIndex": optionIndex++
+        });
+      }
+    } else if (child instanceof HTMLOptionElement) {
+      result.choices.push({
+        "group": false,
+        "inGroup": false,
+        "text": child.text,
+        "disabled": child.disabled,
+        "selected": child.selected,
+        "optionIndex": optionIndex++
+      });
+    }
+  }
+
+  return result;
+};
+
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -118,21 +118,21 @@ var shell = {
     let domains = "";
     try {
       domains = Services.prefs.getCharPref('b2g.privileged.domains');
     } catch(e) {}
 
     addPermissions(domains.split(","));
 
     // Load webapi.js as a frame script
-    let frameScriptUrl = 'chrome://browser/content/webapi.js';
+    let webapiUrl = 'chrome://browser/content/webapi.js';
     try {
-      messageManager.loadFrameScript(frameScriptUrl, true);
+      messageManager.loadFrameScript(webapiUrl, true);
     } catch (e) {
-      dump('Error loading ' + frameScriptUrl + ' as a frame script: ' + e + '\n');
+      dump('Error loading ' + webapiUrl + ' as a frame script: ' + e + '\n');
     }
 
     CustomEventManager.init();
 
     WebappsHelper.init();
 
     let browser = this.contentBrowser;
     browser.homePage = homeURL;
@@ -401,27 +401,30 @@ var CustomEventManager = {
   init: function custevt_init() {
     window.addEventListener("ContentStart", (function(evt) {
       content.addEventListener("mozContentEvent", this, false, true);
     }).bind(this), false);
   },
 
   handleEvent: function custevt_handleEvent(evt) {
     let detail = evt.detail;
-    dump("XXX FIXME : Got a mozContentEvent: " + detail.type);
+    dump('XXX FIXME : Got a mozContentEvent: ' + detail.type);
 
     switch(detail.type) {
-      case "desktop-notification-click":
-      case "desktop-notification-close":
+      case 'desktop-notification-click':
+      case 'desktop-notification-close':
         AlertsHelper.handleEvent(detail);
         break;
-      case "webapps-install-granted":
-      case "webapps-install-denied":
+      case 'webapps-install-granted':
+      case 'webapps-install-denied':
         WebappsHelper.handleEvent(detail);
         break;
+      case 'select-choicechange':
+        FormsHelper.handleEvent(detail);
+        break;
     }
   }
 }
 
 var AlertsHelper = {
   _listeners: {},
   _count: 0,
 
--- a/b2g/chrome/jar.mn
+++ b/b2g/chrome/jar.mn
@@ -4,16 +4,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 chrome.jar:
 % content branding %content/branding/
 % content browser %content/
 
   content/dbg-browser-actors.js         (content/dbg-browser-actors.js)
+  content/forms.js                      (content/forms.js)
   content/settings.js                   (content/settings.js)
 * content/shell.xul                     (content/shell.xul)
 * content/shell.js                      (content/shell.js)
 #ifndef ANDROID
   content/screen.js                     (content/screen.js)
 #endif
   content/webapi.js                     (content/webapi.js)
   content/content.css                   (content/content.css)
--- a/b2g/components/MozKeyboard.js
+++ b/b2g/components/MozKeyboard.js
@@ -1,52 +1,112 @@
 /* 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';
+"use strict";
 
+const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
+const messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
+                         .getService(Ci.nsIChromeFrameMessageManager);
+
+
 // -----------------------------------------------------------------------
 // MozKeyboard
 // -----------------------------------------------------------------------
 
 function MozKeyboard() { } 
 
 MozKeyboard.prototype = {
   classID: Components.ID("{397a7fdf-2254-47be-b74e-76625a1a66d5}"),
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIB2GKeyboard, Ci.nsIDOMGlobalPropertyInitializer, Ci.nsIObserver]),
-  classInfo: XPCOMUtils.generateCI({classID: Components.ID("{397a7fdf-2254-47be-b74e-76625a1a66d5}"),
-                                    contractID: "@mozilla.org/b2g-keyboard;1",
-                                    interfaces: [Ci.nsIB2GKeyboard],
-                                    flags: Ci.nsIClassInfo.DOM_OBJECT,
-                                    classDescription: "B2G Virtual Keyboard"}),
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIB2GKeyboard, Ci.nsIDOMGlobalPropertyInitializer, Ci.nsIObserver
+  ]),
+
+  classInfo: XPCOMUtils.generateCI({
+    "classID": Components.ID("{397a7fdf-2254-47be-b74e-76625a1a66d5}"),
+    "contractID": "@mozilla.org/b2g-keyboard;1",
+    "interfaces": [Ci.nsIB2GKeyboard],
+    "flags": Ci.nsIClassInfo.DOM_OBJECT,
+    "classDescription": "B2G Virtual Keyboard"
+  }),
 
-  init: function(aWindow) {
-    this._utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+  init: function mozKeyboardInit(win) {
+    messageManager.loadFrameScript("chrome://browser/content/forms.js", true);
+    messageManager.addMessageListener("Forms:Input", this);
+
     Services.obs.addObserver(this, "inner-window-destroyed", false);
+
+    this._window = win;
+    this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                     .getInterface(Ci.nsIDOMWindowUtils);
     this.innerWindowID = this._utils.currentInnerWindowID;
+
+    this._focusHandler = null;
   },
 
-  observe: function(aSubject, aTopic, aData) {
-    if (aTopic == "inner-window-destroyed") {
-      let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
-      if (wId == this.innerWindowID) {
-        Services.obs.removeObserver(this, "inner-window-destroyed");
-        this._utils = null;
-      }
-    }
+  uninit: function mozKeyboardUninit() {
+    Services.obs.removeObserver(this, "inner-window-destroyed");
+    messageManager.removeMessageListener("Forms:Input", this);
+    this._window = null;
+    this._utils = null;
+    this._focusHandler = null;
   },
 
   sendKey: function mozKeyboardSendKey(keyCode, charCode) {
     charCode = (charCode == undefined) ? keyCode : charCode;
-    ['keydown', 'keypress', 'keyup'].forEach((function sendKey(type) {
+    ["keydown", "keypress", "keyup"].forEach((function sendKey(type) {
       this._utils.sendKeyEvent(type, keyCode, charCode, null);
     }).bind(this));
+  },
+
+  setSelectedOption: function mozKeyboardSetSelectedOption(index) {
+    messageManager.sendAsyncMessage("Forms:Select:Choice", {
+      "index": index
+    });
+  },
+
+  setSelectedOptions: function mozKeyboardSetSelectedOptions(indexes) {
+    messageManager.sendAsyncMessage("Forms:Select:Choice", {
+      "indexes": indexes || []
+    });
+  },
+
+  set onfocuschange(val) {
+    this._focusHandler = val;
+  },
+
+  get onfocuschange() {
+    return this._focusHandler;
+  },
+
+  receiveMessage: function mozKeyboardReceiveMessage(msg) {
+    let handler = this._focusHandler;
+    if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
+      return;
+
+    let detail = {
+      "detail": msg.json
+    };
+
+    let evt = new this._window.CustomEvent("focuschanged", detail);
+    handler.handleEvent(evt);
+  },
+
+  observe: function mozKeyboardObserve(subject, topic, data) {
+    if (topic == "inner-window-destroyed") {
+      let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+      if (wId == this.innerWindowID) {
+        this.uninit();
+      }
+    }
   }
 };
 
 const NSGetFactory = XPCOMUtils.generateNSGetFactory([MozKeyboard]);
+
--- a/b2g/components/b2g.idl
+++ b/b2g/components/b2g.idl
@@ -2,17 +2,33 @@
  * 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/. */
 
 #include "domstubs.idl"
 
 [scriptable, uuid(3615a616-571d-4194-bf54-ccf546067b14)]
 interface nsIB2GCameraContent : nsISupports
 {
-    /* temporary solution, waiting for getUserMedia */
-    DOMString getCameraURI([optional] in jsval options);
+  /* temporary solution, waiting for getUserMedia */
+  DOMString getCameraURI([optional] in jsval options);
 };
 
-[scriptable, uuid(80ad05f8-e5f6-4a36-b25d-5d5a969b365d)]
+[scriptable, uuid(53990d7a-ab2a-11e1-8543-7767e4cbcbff)]
 interface nsIB2GKeyboard : nsISupports
 {
-    void sendKey(in long aKeyCode, in long aCharCode);
+  void sendKey(in long keyCode, in long charCode);
+
+  // Select the <select> option specified by index.
+  // If this method is called on a <select> that support multiple
+  // selection, then the option specified by index will be added to
+  // the selection.
+  // If this method is called for a select that does not support multiple
+  // selection the previous element will be unselected.
+  void setSelectedOption(in jsval index);
+
+  // Select the <select> options specified by indexes. All other options
+  // will be deselected.
+  // If this method is called for a <select> that does not support multiple
+  // selection, then the last index specified in indexes will be selected.
+  void setSelectedOptions(in jsval indexes);
+
+  attribute nsIDOMEventListener onfocuschange;
 };