Bug 802073 - Receive input event twice from input tag type:time and type:date r=vingtetun a=blocking-basecamp
authorTim Taubert <ttaubert@mozilla.com>
Wed, 07 Nov 2012 12:53:24 +0100
changeset 112533 871a4aa1f78b4f4d9af85261d0f94e8c6d9bcbdd
parent 112532 3a113808c1600be42d13b07eac7aed88031d504e
child 112534 0fb51f488b03bbf1d5d6bc5e534523880189e4e6
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersvingtetun, blocking-basecamp
bugs802073
milestone19.0a1
Bug 802073 - Receive input event twice from input tag type:time and type:date r=vingtetun a=blocking-basecamp
b2g/chrome/content/forms.js
b2g/chrome/content/shell.js
b2g/components/Keyboard.jsm
b2g/components/Makefile.in
b2g/components/MozKeyboard.js
--- a/b2g/chrome/content/forms.js
+++ b/b2g/chrome/content/forms.js
@@ -13,35 +13,45 @@ let Cc = Components.classes;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 XPCOMUtils.defineLazyServiceGetter(Services, "fm",
                                    "@mozilla.org/focus-manager;1",
                                    "nsIFocusManager");
 
+XPCOMUtils.defineLazyGetter(this, "domWindowUtils", function () {
+  return content.QueryInterface(Ci.nsIInterfaceRequestor)
+                .getInterface(Ci.nsIDOMWindowUtils);
+});
+
+const FOCUS_CHANGE_DELAY = 20;
+
 let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
 let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
 let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
 let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
 let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
 
 let FormAssistant = {
   init: function fa_init() {
     addEventListener("focus", this, true, false);
     addEventListener("blur", this, true, false);
-    addEventListener("keypress", this, true, false);
     addEventListener("resize", this, true, false);
     addMessageListener("Forms:Select:Choice", this);
     addMessageListener("Forms:Input:Value", this);
     addMessageListener("Forms:Select:Blur", this);
     Services.obs.addObserver(this, "ime-enabled-state-changed", false);
     Services.obs.addObserver(this, "xpcom-shutdown", false);
   },
 
+  ignoredInputTypes: new Set([
+    'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image'
+  ]),
+
   isKeyboardOpened: false,
   selectionStart: 0,
   selectionEnd: 0,
 
   _focusedElement: null,
 
   get focusedElement() {
     if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
@@ -75,53 +85,34 @@ let FormAssistant = {
   },
 
   handleEvent: function fa_handleEvent(evt) {
     let focusedElement = this.focusedElement;
     let target = evt.target;
 
     switch (evt.type) {
       case "focus":
-        if (this.isKeyboardOpened)
+        if (this.isIMEDisabled())
           return;
 
-        let ignore = {
-          button: true,
-          file: true,
-          checkbox: true,
-          radio: true,
-          reset: true,
-          submit: true,
-          image: true
-        };
-    
-        if (target instanceof HTMLSelectElement) { 
-          content.setTimeout(function showIMEForSelect() {
-            sendAsyncMessage("Forms:Input", getJSON(target));
-          });
-          this.setFocusedElement(target);
-        } else if (target instanceof HTMLOptionElement &&
-                   target.parentNode instanceof HTMLSelectElement) {
-          target = target.parentNode;
-          content.setTimeout(function showIMEForSelect() {
-            sendAsyncMessage("Forms:Input", getJSON(target));
-          });
-          this.setFocusedElement(target);
-        } else if ((target instanceof HTMLInputElement && !ignore[target.type]) ||
-                    target instanceof HTMLTextAreaElement) {
-          this.isKeyboardOpened = this.tryShowIme(target);
-          this.setFocusedElement(target);
+        if (target && this.isFocusableElement(target)) {
+          if (this.blurTimeout) {
+            this.blurTimeout = content.clearTimeout(this.blurTimeout);
+            this.handleIMEStateDisabled();
+          }
+          this.handleIMEStateEnabled(target);
         }
         break;
 
       case "blur":
         if (this.focusedElement) {
-          sendAsyncMessage("Forms:Input", { "type": "blur" });
-          this.setFocusedElement(null);
-          this.isKeyboardOpened = false;
+          this.blurTimeout = content.setTimeout(function () {
+            this.blurTimeout = null;
+            this.handleIMEStateDisabled();
+          }.bind(this), FOCUS_CHANGE_DELAY);
         }
         break;
 
       case 'mousedown':
         // We only listen for this event on the currently focused element.
         // When the mouse goes down, note the cursor/selection position
         this.selectionStart = this.focusedElement.selectionStart;
         this.selectionEnd = this.focusedElement.selectionEnd;
@@ -141,27 +132,16 @@ let FormAssistant = {
       case "resize":
         if (!this.isKeyboardOpened)
           return;
 
         if (this.focusedElement) {
           this.focusedElement.scrollIntoView(false);
         }
         break;
-
-      case "keypress":
-        if (evt.keyCode != evt.DOM_VK_ESCAPE || !this.isKeyboardOpened)
-          return;
-
-        sendAsyncMessage("Forms:Input", { "type": "blur" });
-        this.isKeyboardOpened = false;
-
-        evt.preventDefault();
-        evt.stopPropagation();
-        break;
     }
   },
 
   receiveMessage: function fa_receiveMessage(msg) {
     let target = this.focusedElement;
     if (!target) {
       return;
     }
@@ -208,47 +188,82 @@ let FormAssistant = {
         break;
       }
     }
   },
 
   observe: function fa_observe(subject, topic, data) {
     switch (topic) {
       case "ime-enabled-state-changed":
-        let isOpen = this.isKeyboardOpened;
         let shouldOpen = parseInt(data);
-        if (shouldOpen && !isOpen) {
-          let target = Services.fm.focusedElement;
+        let target = Services.fm.focusedElement;
+        if (!target)
+          return;
 
-          if (!target || !this.tryShowIme(target)) {
-            this.setFocusedElement(null);
-            return;
-          } else {
-            this.setFocusedElement(target);
-          }
-        } else if (!shouldOpen && isOpen) {
-          sendAsyncMessage("Forms:Input", { "type": "blur" });
+        if (shouldOpen) {
+          if (!this.focusedElement && this.isFocusableElement(target))
+            this.handleIMEStateEnabled(target);
+        } else if (this._focusedElement == target) {
+          this.handleIMEStateDisabled();
         }
-        this.isKeyboardOpened = shouldOpen;
         break;
 
       case "xpcom-shutdown":
         Services.obs.removeObserver(this, "ime-enabled-state-changed", false);
         Services.obs.removeObserver(this, "xpcom-shutdown");
         removeMessageListener("Forms:Select:Choice", this);
         removeMessageListener("Forms:Input:Value", this);
         break;
     }
   },
 
-  tryShowIme: function(element) {
-    if (!element) {
+  isIMEDisabled: function fa_isIMEDisabled() {
+    let disabled = false;
+    try {
+      disabled = domWindowUtils.IMEStatus == domWindowUtils.IME_STATUS_DISABLED;
+    } catch (e) {}
+
+    return disabled;
+  },
+
+  handleIMEStateEnabled: function fa_handleIMEStateEnabled(target) {
+    if (this.isKeyboardOpened)
       return;
-    }
+
+    if (target instanceof HTMLOptionElement)
+      target = target.parentNode;
+
+    let kbOpened = this.tryShowIme(target);
+    if (target instanceof HTMLInputElement ||
+        target instanceof HTMLTextAreaElement)
+      this.isKeyboardOpened = kbOpened;
+
+    this.setFocusedElement(target);
+  },
 
+  handleIMEStateDisabled: function fa_handleIMEStateDisabled() {
+    sendAsyncMessage("Forms:Input", { "type": "blur" });
+    this.isKeyboardOpened = false;
+    this.setFocusedElement(null);
+  },
+
+  isFocusableElement: function fa_isFocusableElement(element) {
+    if (element instanceof HTMLSelectElement ||
+        element instanceof HTMLTextAreaElement)
+      return true;
+
+    if (element instanceof HTMLOptionElement &&
+        element.parentNode instanceof HTMLSelectElement)
+      return true;
+
+    return (element instanceof HTMLInputElement &&
+            !this.ignoredInputTypes.has(element.type));
+  },
+
+  tryShowIme: function(element) {
     // FIXME/bug 729623: work around apparent bug in the IME manager
     // in gecko.
     let readonly = element.getAttribute("readonly");
     if (readonly) {
       return false;
     }
 
     sendAsyncMessage("Forms:Input", getJSON(element));
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -12,16 +12,17 @@ Cu.import('resource://gre/modules/DOMFMR
 Cu.import('resource://gre/modules/AlarmService.jsm');
 Cu.import('resource://gre/modules/ActivitiesService.jsm');
 Cu.import('resource://gre/modules/PermissionPromptHelper.jsm');
 Cu.import('resource://gre/modules/ObjectWrapper.jsm');
 Cu.import('resource://gre/modules/accessibility/AccessFu.jsm');
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import('resource://gre/modules/UserAgentOverrides.jsm');
+Cu.import('resource://gre/modules/Keyboard.jsm');
 #ifdef MOZ_B2G_RIL
 Cu.import('resource://gre/modules/NetworkStatsService.jsm');
 #endif
 
 XPCOMUtils.defineLazyServiceGetter(Services, 'env',
                                    '@mozilla.org/process/environment;1',
                                    'nsIEnvironment');
 
new file mode 100644
--- /dev/null
+++ b/b2g/components/Keyboard.jsm
@@ -0,0 +1,101 @@
+/* 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';
+
+this.EXPORTED_SYMBOLS = ['Keyboard'];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const kFormsFrameScript = 'chrome://browser/content/forms.js';
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+  "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
+
+let Keyboard = {
+  _messageManager: null,
+  _messageNames: [
+    'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions'
+  ],
+
+  get messageManager() {
+    if (this._messageManager && !Cu.isDeadWrapper(this._messageManager))
+      return this._messageManager;
+
+    throw Error('no message manager set');
+  },
+
+  set messageManager(mm) {
+    this._messageManager = mm;
+  },
+
+  init: function keyboardInit() {
+    Services.obs.addObserver(this, 'in-process-browser-frame-shown', false);
+    Services.obs.addObserver(this, 'remote-browser-frame-shown', false);
+
+    for (let name of this._messageNames)
+      ppmm.addMessageListener('Keyboard:' + name, this);
+  },
+
+  observe: function keyboardObserve(subject, topic, data) {
+    let frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
+    let mm = frameLoader.messageManager;
+    mm.addMessageListener('Forms:Input', this);
+
+    try {
+      mm.loadFrameScript(kFormsFrameScript, true);
+    } catch (e) {
+      dump('Error loading ' + kFormsFrameScript + ' as frame script: ' + e + '\n');
+    }
+  },
+
+  receiveMessage: function keyboardReceiveMessage(msg) {
+    switch (msg.name) {
+      case 'Forms:Input':
+        this.handleFormsInput(msg);
+        break;
+      case 'Keyboard:SetValue':
+        this.setValue(msg);
+        break;
+      case 'Keyboard:RemoveFocus':
+        this.removeFocus();
+        break;
+      case 'Keyboard:SetSelectedOption':
+        this.setSelectedOption(msg);
+        break;
+      case 'Keyboard:SetSelectedOptions':
+        this.setSelectedOption(msg);
+        break;
+    }
+  },
+
+  handleFormsInput: function keyboardHandleFormsInput(msg) {
+    this.messageManager = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
+                             .frameLoader.messageManager;
+
+    ppmm.broadcastAsyncMessage('Keyboard:FocusChange', msg.data);
+  },
+
+  setSelectedOption: function keyboardSetSelectedOption(msg) {
+    this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
+  },
+
+  setSelectedOptions: function keyboardSetSelectedOptions(msg) {
+    this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data);
+  },
+
+  setValue: function keyboardSetValue(msg) {
+    this.messageManager.sendAsyncMessage('Forms:Input:Value', msg.data);
+  },
+
+  removeFocus: function keyboardRemoveFocus() {
+    this.messageManager.sendAsyncMessage('Forms:Select:Blur', {});
+  }
+};
+
+Keyboard.init();
--- a/b2g/components/Makefile.in
+++ b/b2g/components/Makefile.in
@@ -29,16 +29,17 @@ EXTRA_PP_COMPONENTS = \
         PaymentGlue.js \
         SmsProtocolHandler.js \
         TelProtocolHandler.js \
         YoutubeProtocolHandler.js \
         RecoveryService.js \
         $(NULL)
 
 EXTRA_JS_MODULES = \
+	Keyboard.jsm \
 	TelURIParser.jsm \
 	$(NULL)
 
 TEST_DIRS = \
 	test \
 	$(NULL)
 
 ifdef MOZ_UPDATER
--- a/b2g/components/MozKeyboard.js
+++ b/b2g/components/MozKeyboard.js
@@ -1,23 +1,24 @@
 /* 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;
-const kFormsFrameScript = "chrome://browser/content/forms.js";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/ObjectWrapper.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+  "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender");
+
 // -----------------------------------------------------------------------
 // MozKeyboard
 // -----------------------------------------------------------------------
 
 function MozKeyboard() { } 
 
 MozKeyboard.prototype = {
   classID: Components.ID("{397a7fdf-2254-47be-b74e-76625a1a66d5}"),
@@ -31,110 +32,86 @@ MozKeyboard.prototype = {
     "contractID": "@mozilla.org/b2g-keyboard;1",
     "interfaces": [Ci.nsIB2GKeyboard],
     "flags": Ci.nsIClassInfo.DOM_OBJECT,
     "classDescription": "B2G Virtual Keyboard"
   }),
 
   init: function mozKeyboardInit(win) {
     Services.obs.addObserver(this, "inner-window-destroyed", false);
-    Services.obs.addObserver(this, 'in-process-browser-frame-shown', false);
-    Services.obs.addObserver(this, 'remote-browser-frame-shown', false);
+    cpmm.addMessageListener('Keyboard:FocusChange', this);
 
     this._window = win;
     this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                      .getInterface(Ci.nsIDOMWindowUtils);
     this.innerWindowID = this._utils.currentInnerWindowID;
-
     this._focusHandler = null;
   },
 
   uninit: function mozKeyboardUninit() {
     Services.obs.removeObserver(this, "inner-window-destroyed");
-    this._messageManager = null;
+    cpmm.removeMessageListener('Keyboard:FocusChange', 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) {
       this._utils.sendKeyEvent(type, keyCode, charCode, null);
     }).bind(this));
   },
 
   setSelectedOption: function mozKeyboardSetSelectedOption(index) {
-    this._messageManager.sendAsyncMessage("Forms:Select:Choice", {
-      "index": index
+    cpmm.sendAsyncMessage('Keyboard:SetSelectedOption', {
+      'index': index
     });
   },
 
   setValue: function mozKeyboardSetValue(value) {
-    this._messageManager.sendAsyncMessage("Forms:Input:Value", {
-      "value": value
+    cpmm.sendAsyncMessage('Keyboard:SetValue', {
+      'value': value
     });
   },
 
   setSelectedOptions: function mozKeyboardSetSelectedOptions(indexes) {
-    this._messageManager.sendAsyncMessage("Forms:Select:Choice", {
-      "indexes": indexes || []
+    cpmm.sendAsyncMessage('Keyboard:SetSelectedOptions', {
+      'indexes': indexes
     });
   },
 
   removeFocus: function mozKeyboardRemoveFocus() {
-    this._messageManager.sendAsyncMessage("Forms:Select:Blur", {});
+    cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {});
   },
 
   set onfocuschange(val) {
     this._focusHandler = val;
   },
 
   get onfocuschange() {
     return this._focusHandler;
   },
 
-  handleMessage: function mozKeyboardHandleMessage(msg) {
+  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",
                                            ObjectWrapper.wrap(detail, this._window));
     handler.handleEvent(evt);
   },
 
   observe: function mozKeyboardObserve(subject, topic, data) {
-    switch (topic) {
-    case "inner-window-destroyed": {
-      let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-      if (wId == this.innerWindowID) {
-        this.uninit();
-      }
-      break;
-    }
-    case 'remote-browser-frame-shown':
-    case 'in-process-browser-frame-shown': {
-      let frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
-      let mm = frameLoader.messageManager;
-      mm.addMessageListener("Forms:Input", (function receiveMessage(msg) {
-        // Need to save mm here so later the message can be sent back to the
-        // correct app in the methods called by the value selector.
-        this._messageManager = mm;
-        this.handleMessage(msg);
-      }).bind(this));
-      try {
-        mm.loadFrameScript(kFormsFrameScript, true);
-      } catch (e) {
-        dump('Error loading ' + kFormsFrameScript + ' as frame script: ' + e + '\n');
-      }
-      break;
-    }
-    }
+    let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+    if (wId == this.innerWindowID)
+      this.uninit();
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozKeyboard]);