Bug 1137557 - Part 2: Remove sendKeyEvent from forms.js. r=masayuki, r=smaug (to make the webidl hook happy)
authorTim Chien <timdream@gmail.com>
Fri, 17 Jul 2015 04:41:00 -0400
changeset 259090 e89f50c10fc263b6eda1672d81305b46232435d2
parent 259089 1d47c8de65633109bd821654e873ad8abd418899
child 259091 478dd607957309378617041c9dad3ad0f8402e99
push id29268
push userryanvm@gmail.com
push dateTue, 25 Aug 2015 00:37:23 +0000
treeherdermozilla-central@08015770c9d6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmasayuki, smaug
bugs1137557
milestone43.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 1137557 - Part 2: Remove sendKeyEvent from forms.js. r=masayuki, r=smaug (to make the webidl hook happy)
dom/inputmethod/MozKeyboard.js
dom/inputmethod/forms.js
dom/webidl/InputMethod.webidl
--- a/dom/inputmethod/MozKeyboard.js
+++ b/dom/inputmethod/MozKeyboard.js
@@ -735,23 +735,25 @@ MozInputContext.prototype = {
   },
 
   deleteSurroundingText: function ic_deleteSurrText(offset, length) {
     return this.replaceSurroundingText(null, offset, length);
   },
 
   sendKey: function ic_sendKey(keyCode, charCode, modifiers, repeat) {
     let self = this;
+
+    // XXX: modifiers are ignored in this API method.
+
     return this._sendPromise(function(resolverId) {
       cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SendKey', {
         contextId: self._contextId,
         requestId: resolverId,
         keyCode: keyCode,
         charCode: charCode,
-        modifiers: modifiers,
         repeat: repeat
       });
     });
   },
 
   setComposition: function ic_setComposition(text, cursor, clauses) {
     let self = this;
     return this._sendPromise(function(resolverId) {
--- a/dom/inputmethod/forms.js
+++ b/dom/inputmethod/forms.js
@@ -13,20 +13,60 @@ 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);
-});
+/*
+ * A WeakMap to map window to objects keeping it's TextInputProcessor instance.
+ */
+let WindowMap = {
+  // WeakMap of <window, object> pairs.
+  _map: null,
+
+  /*
+   * Set the object associated to the window and return it.
+   */
+  _getObjForWin: function(win) {
+    if (!this._map) {
+      this._map = new WeakMap();
+    }
+    if (this._map.has(win)) {
+      return this._map.get(win);
+    } else {
+      let obj = {
+        tip: null
+      };
+      this._map.set(win, obj);
+
+      return obj;
+    }
+  },
+
+  getTextInputProcessor: function(win) {
+    if (!win) {
+      return;
+    }
+    let obj = this._getObjForWin(win);
+    let tip = obj.tip
+
+    if (!tip) {
+      tip = obj.tip = Cc["@mozilla.org/text-input-processor;1"]
+        .createInstance(Ci.nsITextInputProcessor);
+    }
+
+    if (!tip.beginInputTransaction(win, textInputProcessorCallback)) {
+      tip = obj.tip = null;
+    }
+    return tip;
+  }
+};
 
 const RESIZE_SCROLL_DELAY = 20;
 // In content editable node, when there are hidden elements such as <br>, it
 // may need more than one (usually less than 3 times) move/extend operations
 // to change the selection range. If we cannot change the selection range
 // with more than 20 opertations, we are likely being blocked and cannot change
 // the selection range any more.
 const MAX_BLOCKED_COUNT = 20;
@@ -36,16 +76,161 @@ let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlE
 let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement;
 let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
 let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
 let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
 let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
 let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
 let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
 
+function guessKeyNameFromKeyCode(KeyboardEvent, aKeyCode) {
+  switch (aKeyCode) {
+    case KeyboardEvent.DOM_VK_CANCEL:
+      return "Cancel";
+    case KeyboardEvent.DOM_VK_HELP:
+      return "Help";
+    case KeyboardEvent.DOM_VK_BACK_SPACE:
+      return "Backspace";
+    case KeyboardEvent.DOM_VK_TAB:
+      return "Tab";
+    case KeyboardEvent.DOM_VK_CLEAR:
+      return "Clear";
+    case KeyboardEvent.DOM_VK_RETURN:
+      return "Enter";
+    case KeyboardEvent.DOM_VK_SHIFT:
+      return "Shift";
+    case KeyboardEvent.DOM_VK_CONTROL:
+      return "Control";
+    case KeyboardEvent.DOM_VK_ALT:
+      return "Alt";
+    case KeyboardEvent.DOM_VK_PAUSE:
+      return "Pause";
+    case KeyboardEvent.DOM_VK_EISU:
+      return "Eisu";
+    case KeyboardEvent.DOM_VK_ESCAPE:
+      return "Escape";
+    case KeyboardEvent.DOM_VK_CONVERT:
+      return "Convert";
+    case KeyboardEvent.DOM_VK_NONCONVERT:
+      return "NonConvert";
+    case KeyboardEvent.DOM_VK_ACCEPT:
+      return "Accept";
+    case KeyboardEvent.DOM_VK_MODECHANGE:
+      return "ModeChange";
+    case KeyboardEvent.DOM_VK_PAGE_UP:
+      return "PageUp";
+    case KeyboardEvent.DOM_VK_PAGE_DOWN:
+      return "PageDown";
+    case KeyboardEvent.DOM_VK_END:
+      return "End";
+    case KeyboardEvent.DOM_VK_HOME:
+      return "Home";
+    case KeyboardEvent.DOM_VK_LEFT:
+      return "ArrowLeft";
+    case KeyboardEvent.DOM_VK_UP:
+      return "ArrowUp";
+    case KeyboardEvent.DOM_VK_RIGHT:
+      return "ArrowRight";
+    case KeyboardEvent.DOM_VK_DOWN:
+      return "ArrowDown";
+    case KeyboardEvent.DOM_VK_SELECT:
+      return "Select";
+    case KeyboardEvent.DOM_VK_PRINT:
+      return "Print";
+    case KeyboardEvent.DOM_VK_EXECUTE:
+      return "Execute";
+    case KeyboardEvent.DOM_VK_PRINTSCREEN:
+      return "PrintScreen";
+    case KeyboardEvent.DOM_VK_INSERT:
+      return "Insert";
+    case KeyboardEvent.DOM_VK_DELETE:
+      return "Delete";
+    case KeyboardEvent.DOM_VK_WIN:
+      return "OS";
+    case KeyboardEvent.DOM_VK_CONTEXT_MENU:
+      return "ContextMenu";
+    case KeyboardEvent.DOM_VK_SLEEP:
+      return "Standby";
+    case KeyboardEvent.DOM_VK_F1:
+      return "F1";
+    case KeyboardEvent.DOM_VK_F2:
+      return "F2";
+    case KeyboardEvent.DOM_VK_F3:
+      return "F3";
+    case KeyboardEvent.DOM_VK_F4:
+      return "F4";
+    case KeyboardEvent.DOM_VK_F5:
+      return "F5";
+    case KeyboardEvent.DOM_VK_F6:
+      return "F6";
+    case KeyboardEvent.DOM_VK_F7:
+      return "F7";
+    case KeyboardEvent.DOM_VK_F8:
+      return "F8";
+    case KeyboardEvent.DOM_VK_F9:
+      return "F9";
+    case KeyboardEvent.DOM_VK_F10:
+      return "F10";
+    case KeyboardEvent.DOM_VK_F11:
+      return "F11";
+    case KeyboardEvent.DOM_VK_F12:
+      return "F12";
+    case KeyboardEvent.DOM_VK_F13:
+      return "F13";
+    case KeyboardEvent.DOM_VK_F14:
+      return "F14";
+    case KeyboardEvent.DOM_VK_F15:
+      return "F15";
+    case KeyboardEvent.DOM_VK_F16:
+      return "F16";
+    case KeyboardEvent.DOM_VK_F17:
+      return "F17";
+    case KeyboardEvent.DOM_VK_F18:
+      return "F18";
+    case KeyboardEvent.DOM_VK_F19:
+      return "F19";
+    case KeyboardEvent.DOM_VK_F20:
+      return "F20";
+    case KeyboardEvent.DOM_VK_F21:
+      return "F21";
+    case KeyboardEvent.DOM_VK_F22:
+      return "F22";
+    case KeyboardEvent.DOM_VK_F23:
+      return "F23";
+    case KeyboardEvent.DOM_VK_F24:
+      return "F24";
+    case KeyboardEvent.DOM_VK_NUM_LOCK:
+      return "NumLock";
+    case KeyboardEvent.DOM_VK_SCROLL_LOCK:
+      return "ScrollLock";
+    case KeyboardEvent.DOM_VK_VOLUME_MUTE:
+      return "VolumeMute";
+    case KeyboardEvent.DOM_VK_VOLUME_DOWN:
+      return "VolumeDown";
+    case KeyboardEvent.DOM_VK_VOLUME_UP:
+      return "VolumeUp";
+    case KeyboardEvent.DOM_VK_META:
+      return "Meta";
+    case KeyboardEvent.DOM_VK_ALTGR:
+      return "AltGraph";
+    case KeyboardEvent.DOM_VK_ATTN:
+      return "Attn";
+    case KeyboardEvent.DOM_VK_CRSEL:
+      return "CrSel";
+    case KeyboardEvent.DOM_VK_EXSEL:
+      return "ExSel";
+    case KeyboardEvent.DOM_VK_EREOF:
+      return "EraseEof";
+    case KeyboardEvent.DOM_VK_PLAY:
+      return "Play";
+    default:
+      return "Unidentified";
+  }
+}
+
 let FormVisibility = {
   /**
    * Searches upwards in the DOM for an element that has been scrolled.
    *
    * @param {HTMLElement} node element to start search at.
    * @return {Window|HTMLElement|Null} null when none are found window/element otherwise.
    */
   findScrolled: function fv_findScrolled(node) {
@@ -179,16 +364,51 @@ let FormVisibility = {
       (parent !== parent.parent) &&
       (parent = parent.parent)
     );
 
     return visible;
   }
 };
 
+// This object implements nsITextInputProcessorCallback
+let textInputProcessorCallback = {
+  onNotify: function(aTextInputProcessor, aNotification) {
+    try {
+      switch (aNotification.type) {
+        case "request-to-commit":
+          // TODO: Send a notification through asyncMessage to the keyboard here.
+          aTextInputProcessor.commitComposition();
+
+          break;
+        case "request-to-cancel":
+          // TODO: Send a notification through asyncMessage to the keyboard here.
+          aTextInputProcessor.cancelComposition();
+
+          break;
+
+        case "notify-detached":
+          // TODO: Send a notification through asyncMessage to the keyboard here.
+          break;
+
+        // TODO: Manage _focusedElement for text input from here instead.
+        //       (except for <select> which will be need to handled elsewhere)
+        case "notify-focus":
+          break;
+
+        case "notify-blur":
+          break;
+      }
+    } catch (e) {
+      return false;
+    }
+    return true;
+  }
+};
+
 let FormAssistant = {
   init: function fa_init() {
     addEventListener("focus", this, true, false);
     addEventListener("blur", this, true, false);
     addEventListener("resize", this, true, false);
     // We should not blur the fucus if the submit event is cancelled,
     // therefore we are binding our event listener in the bubbling phase here.
     addEventListener("submit", this, false, false);
@@ -497,44 +717,97 @@ let FormAssistant = {
         event.initEvent('input', true, false);
         target.dispatchEvent(event);
         break;
       }
 
       case "Forms:Input:SendKey":
         CompositionManager.endComposition('');
 
-        let flags = domWindowUtils.KEY_FLAG_NOT_SYNTHESIZED_FOR_TESTS;
-        this._editing = true;
-        let doKeypress = domWindowUtils.sendKeyEvent('keydown', json.keyCode,
-                                                     json.charCode, json.modifiers, flags);
-        if (doKeypress) {
-          domWindowUtils.sendKeyEvent('keypress', json.keyCode,
-                                      json.charCode, json.modifiers, flags);
-        }
+        let win = target.ownerDocument.defaultView;
+        let tip = WindowMap.getTextInputProcessor(win);
+        if (!tip) {
+          if (json.requestId) {
+            sendAsyncMessage("Forms:SendKey:Result:Error", {
+              requestId: json.requestId,
+              error: "Unable to start input transaction."
+            });
+          }
 
-        if(!json.repeat) {
-          domWindowUtils.sendKeyEvent('keyup', json.keyCode,
-                                      json.charCode, json.modifiers, flags);
+          break;
         }
 
-        this._editing = false;
+        // The naive way to figure out if the key to dispatch is printable.
+        let printable = !!json.charCode;
+
+        let keyboardEventDict = {
+          // For printable keys, the value should be the actual character.
+          // For non-printable keys, it should be a value in the D3E spec.
+          // Here we make some educated guess for it.
+          key: printable ?
+            String.fromCharCode(json.charCode) :
+            guessKeyNameFromKeyCode(win.KeyboardEvent, json.keyCode),
+          // We don't have any information to tell the virtual key the
+          // user have interacted with.
+          code: "",
+          // We violate the spec here and ask TextInputProcessor not to infer
+          // this value from value of key nor code so we could keep the original
+          // behavior.
+          keyCode: json.keyCode,
+          // We do not have the information to infer location of the virtual key
+          // either (and we would need TextInputProcessor not to compute it).
+          location: 0,
+          // This indicates the key is triggered for repeats.
+          repeat: json.repeat
+        };
+
+        let keyboardEvent = new win.KeyboardEvent("", keyboardEventDict);
+        let flags = tip.KEY_KEEP_KEY_LOCATION_STANDARD;
+        if (!printable) {
+          flags |= tip.KEY_NON_PRINTABLE_KEY;
+        }
+        if (!json.keyCode) {
+          flags |= tip.KEY_KEEP_KEYCODE_ZERO;
+        }
 
-        if (json.requestId && doKeypress) {
-          sendAsyncMessage("Forms:SendKey:Result:OK", {
-            requestId: json.requestId,
-            selectioninfo: this.getSelectionInfo()
-          });
+        let keydownDefaultPrevented;
+        try {
+          let consumedFlags = tip.keydown(keyboardEvent, flags);
+          keydownDefaultPrevented =
+            !!(tip.KEYDOWN_IS_CONSUMED & consumedFlags);
+          if (!json.repeat) {
+            tip.keyup(keyboardEvent, flags);
+          }
+        } catch (e) {
+          dump("forms.js:" + e.toString() + "\n");
+
+          if (json.requestId) {
+            sendAsyncMessage("Forms:SendKey:Result:Error", {
+              requestId: json.requestId,
+              error: "Unable to type into destoryed input."
+            });
+          }
+
+          break;
         }
-        else if (json.requestId && !doKeypress) {
-          sendAsyncMessage("Forms:SendKey:Result:Error", {
-            requestId: json.requestId,
-            error: "Keydown event got canceled"
-          });
+
+        if (json.requestId) {
+          if (keydownDefaultPrevented) {
+            sendAsyncMessage("Forms:SendKey:Result:Error", {
+              requestId: json.requestId,
+              error: "Key event(s) was cancelled."
+            });
+          } else {
+            sendAsyncMessage("Forms:SendKey:Result:OK", {
+              requestId: json.requestId,
+              selectioninfo: this.getSelectionInfo()
+            });
+          }
         }
+
         break;
 
       case "Forms:Select:Choice":
         let options = target.options;
         let valueChanged = false;
         if ("index" in json) {
           if (options.selectedIndex != json.index) {
             options.selectedIndex = json.index;
@@ -1165,56 +1438,28 @@ function replaceSurroundingText(element,
     // Insert the text to be replaced with.
     editor.insertText(text);
   }
   return true;
 }
 
 let CompositionManager =  {
   _isStarted: false,
-  _textInputProcessor: null,
+  _tip: null,
   _clauseAttrMap: {
     'raw-input':
       Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE,
     'selected-raw-text':
       Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE,
     'converted-text':
       Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE,
     'selected-converted-text':
       Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE
   },
 
-  _callback: function cm_callback(aTIP, aNotification)
-  {
-    try {
-      switch (aNotification.type) {
-        case "request-to-commit":
-          aTIP.commitComposition();
-          break;
-        case "request-to-cancel":
-          aTIP.cancelComposition();
-          break;
-      }
-    } catch (e) {
-      return false;
-    }
-    return true;
-  },
-
-  _prepareTextInputProcessor: function cm_prepareTextInputProcessor(aWindow)
-  {
-    if (!this._textInputProcessor) {
-      this._textInputProcessor =
-        Cc["@mozilla.org/text-input-processor;1"].
-          createInstance(Ci.nsITextInputProcessor);
-    }
-    return this._textInputProcessor.beginInputTransaction(aWindow,
-                                                          this._callback);
-  },
-
   setComposition: function cm_setComposition(element, text, cursor, clauses) {
     // Check parameters.
     if (!element) {
       return;
     }
     let len = text.length;
     if (cursor > len) {
       cursor = len;
@@ -1243,43 +1488,53 @@ let CompositionManager =  {
         clauseLens[clauseLens.length - 1] += remainingLength;
       }
     } else {
       clauseLens.push(len);
       clauseAttrs.push(Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE);
     }
 
     let win = element.ownerDocument.defaultView;
-    if (!this._prepareTextInputProcessor(win)) {
+    let tip = WindowMap.getTextInputProcessor(win);
+    if (!tip) {
       return;
     }
     // Update the composing text.
-    this._textInputProcessor.setPendingCompositionString(text);
+    tip.setPendingCompositionString(text);
     for (var i = 0; i < clauseLens.length; i++) {
       if (!clauseLens[i]) {
         continue;
       }
-      this._textInputProcessor.appendClauseToPendingComposition(clauseLens[i],
-                                                                clauseAttrs[i]);
+      tip.appendClauseToPendingComposition(clauseLens[i], clauseAttrs[i]);
     }
     if (cursor >= 0) {
-      this._textInputProcessor.setCaretInPendingComposition(cursor);
+      tip.setCaretInPendingComposition(cursor);
     }
-    this._isStarted = this._textInputProcessor.flushPendingComposition();
+    this._isStarted = tip.flushPendingComposition();
+    if (this._isStarted) {
+      this._tip = tip;
+    }
   },
 
   endComposition: function cm_endComposition(text) {
     if (!this._isStarted) {
       return;
     }
-    this._textInputProcessor.commitCompositionWith(text ? text : "");
+    let tip = this._tip;
+    if (!tip) {
+      return;
+    }
+
+    tip.commitCompositionWith(text ? text : "");
     this._isStarted = false;
+    this._tip = null;
   },
 
   // Composition ends due to external actions.
   onCompositionEnd: function cm_onCompositionEnd() {
     if (!this._isStarted) {
       return;
     }
 
     this._isStarted = false;
+    this._tip = null;
   }
 };
--- a/dom/webidl/InputMethod.webidl
+++ b/dom/webidl/InputMethod.webidl
@@ -188,17 +188,17 @@ interface MozInputContext: EventTarget {
      * A dict is provided in the detail property of the event containing the new values, and
      * an "ownAction" property to denote the event is the result of our own mutation to
      * the input field.
      */
     attribute EventHandler onsurroundingtextchange;
 
     /*
       * send a character with its key events.
-      * @param modifiers see http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIDOMWindowUtils.idl#206
+      * @param modifiers this paramater is no longer honored.
       * @param repeat indicates whether a key would be sent repeatedly.
       * @return true if succeeds. Otherwise false if the input context becomes void.
       * Alternative: sendKey(KeyboardEvent event), but we will likely
       * waste memory for creating the KeyboardEvent object.
       * Note that, if you want to send a key n times repeatedly, make sure set
       * parameter repeat to true and invoke sendKey n-1 times, and then set
       * repeat to false in the last invoke.
       */