Bug 917322 part.19 Add nsITextInputProcessorCallback r=smaug+xyuan, sr=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Wed, 28 Jan 2015 15:27:33 +0900
changeset 226225 62716b1991452c571272936eab7dde3f7042408a
parent 226224 b97c59579393f6fbf66ceac4141f3c55e47e560e
child 226226 6418c75f250bb251daab83e8d2af447abbe4e89d
push id28187
push usercbook@mozilla.com
push dateWed, 28 Jan 2015 13:20:48 +0000
treeherderautoland@fc21937ca612 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, smaug
bugs917322
milestone38.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 917322 part.19 Add nsITextInputProcessorCallback r=smaug+xyuan, sr=smaug
dom/base/TextInputProcessor.cpp
dom/base/TextInputProcessor.h
dom/base/test/chrome/window_nsITextInputProcessor.xul
dom/inputmethod/forms.js
dom/interfaces/base/moz.build
dom/interfaces/base/nsITextInputProcessor.idl
dom/interfaces/base/nsITextInputProcessorCallback.idl
widget/nsBaseWidget.cpp
--- a/dom/base/TextInputProcessor.cpp
+++ b/dom/base/TextInputProcessor.cpp
@@ -10,16 +10,53 @@
 #include "nsIWidget.h"
 #include "nsPIDOMWindow.h"
 #include "nsPresContext.h"
 
 using namespace mozilla::widget;
 
 namespace mozilla {
 
+/******************************************************************************
+ * TextInputProcessorNotification
+ ******************************************************************************/
+
+class TextInputProcessorNotification MOZ_FINAL :
+        public nsITextInputProcessorNotification
+{
+public:
+  explicit TextInputProcessorNotification(const char* aType)
+    : mType(aType)
+  {
+  }
+
+  NS_DECL_ISUPPORTS
+
+  NS_IMETHOD GetType(nsACString& aType) MOZ_OVERRIDE MOZ_FINAL
+  {
+    aType = mType;
+    return NS_OK;
+  }
+
+protected:
+  ~TextInputProcessorNotification() { }
+
+private:
+  nsAutoCString mType;
+
+  TextInputProcessorNotification() { }
+};
+
+NS_IMPL_ISUPPORTS(TextInputProcessorNotification,
+                  nsITextInputProcessorNotification)
+
+/******************************************************************************
+ * TextInputProcessor
+ ******************************************************************************/
+
 NS_IMPL_ISUPPORTS(TextInputProcessor,
                   nsITextInputProcessor,
                   TextEventDispatcherListener,
                   nsISupportsWeakReference)
 
 TextInputProcessor::TextInputProcessor()
   : mDispatcher(nullptr)
   , mForTests(false)
@@ -27,34 +64,44 @@ TextInputProcessor::TextInputProcessor()
 }
 
 TextInputProcessor::~TextInputProcessor()
 {
 }
 
 NS_IMETHODIMP
 TextInputProcessor::Init(nsIDOMWindow* aWindow,
+                         nsITextInputProcessorCallback* aCallback,
                          bool* aSucceeded)
 {
   MOZ_RELEASE_ASSERT(aSucceeded, "aSucceeded must not be nullptr");
   MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome());
-  return InitInternal(aWindow, false, *aSucceeded);
+  if (NS_WARN_IF(!aCallback)) {
+    *aSucceeded = false;
+    return NS_ERROR_INVALID_ARG;
+  }
+  return InitInternal(aWindow, aCallback, false, *aSucceeded);
 }
 
 NS_IMETHODIMP
 TextInputProcessor::InitForTests(nsIDOMWindow* aWindow,
+                                 nsITextInputProcessorCallback* aCallback,
+                                 uint8_t aOptionalArgc,
                                  bool* aSucceeded)
 {
   MOZ_RELEASE_ASSERT(aSucceeded, "aSucceeded must not be nullptr");
   MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome());
-  return InitInternal(aWindow, true, *aSucceeded);
+  nsITextInputProcessorCallback* callback =
+    aOptionalArgc >= 1 ? aCallback : nullptr;
+  return InitInternal(aWindow, callback, true, *aSucceeded);
 }
 
 nsresult
 TextInputProcessor::InitInternal(nsIDOMWindow* aWindow,
+                                 nsITextInputProcessorCallback* aCallback,
                                  bool aForTests,
                                  bool& aSucceeded)
 {
   aSucceeded = false;
   if (NS_WARN_IF(!aWindow)) {
     return NS_ERROR_INVALID_ARG;
   }
   nsCOMPtr<nsPIDOMWindow> pWindow(do_QueryInterface(aWindow));
@@ -79,17 +126,18 @@ TextInputProcessor::InitInternal(nsIDOMW
   }
 
   nsRefPtr<TextEventDispatcher> dispatcher = widget->GetTextEventDispatcher();
   MOZ_RELEASE_ASSERT(dispatcher, "TextEventDispatcher must not be null");
 
   // If the instance was initialized and is being initialized for same
   // dispatcher and same purpose, we don't need to initialize the dispatcher
   // again.
-  if (mDispatcher && dispatcher == mDispatcher && aForTests == mForTests) {
+  if (mDispatcher && dispatcher == mDispatcher && aCallback == mCallback &&
+      aForTests == mForTests) {
     aSucceeded = true;
     return NS_OK;
   }
 
   // If this instance is composing, don't allow to initialize again.
   if (mDispatcher && mDispatcher->IsComposing()) {
     return NS_ERROR_ALREADY_INITIALIZED;
   }
@@ -111,26 +159,36 @@ TextInputProcessor::InitInternal(nsIDOMW
     rv = dispatcher->Init(this);
   }
 
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   mDispatcher = dispatcher;
+  mCallback = aCallback;
   mForTests = aForTests;
   aSucceeded = true;
   return NS_OK;
 }
 
 void
 TextInputProcessor::UnlinkFromTextEventDispatcher()
 {
   mDispatcher = nullptr;
   mForTests = false;
+  if (mCallback) {
+    nsCOMPtr<nsITextInputProcessorCallback> callback(mCallback);
+    mCallback = nullptr;
+
+    nsRefPtr<TextInputProcessorNotification> notification =
+      new TextInputProcessorNotification("notify-detached");
+    bool result = false;
+    callback->OnNotify(this, notification, &result);
+  }
 }
 
 nsresult
 TextInputProcessor::IsValidStateForComposition()
 {
   if (NS_WARN_IF(!mDispatcher)) {
     return NS_ERROR_NOT_INITIALIZED;
   }
@@ -289,16 +347,51 @@ TextInputProcessor::NotifyIME(TextEventD
                               const IMENotification& aNotification)
 {
   // If This is called while this is being initialized, ignore the call.
   if (!mDispatcher) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   MOZ_ASSERT(aTextEventDispatcher == mDispatcher,
              "Wrong TextEventDispatcher notifies this");
+  NS_ASSERTION(mForTests || mCallback,
+               "mCallback can be null only when IME is initialized for tests");
+  if (mCallback) {
+    nsRefPtr<TextInputProcessorNotification> notification;
+    switch (aNotification.mMessage) {
+      case REQUEST_TO_COMMIT_COMPOSITION: {
+        NS_ASSERTION(aTextEventDispatcher->IsComposing(),
+                     "Why is this requested without composition?");
+        notification = new TextInputProcessorNotification("request-to-commit");
+        break;
+      }
+      case REQUEST_TO_CANCEL_COMPOSITION: {
+        NS_ASSERTION(aTextEventDispatcher->IsComposing(),
+                     "Why is this requested without composition?");
+        notification = new TextInputProcessorNotification("request-to-cancel");
+        break;
+      }
+      case NOTIFY_IME_OF_FOCUS:
+        notification = new TextInputProcessorNotification("notify-focus");
+        break;
+      case NOTIFY_IME_OF_BLUR:
+        notification = new TextInputProcessorNotification("notify-blur");
+        break;
+      default:
+        return NS_ERROR_NOT_IMPLEMENTED;
+    }
+    MOZ_RELEASE_ASSERT(notification);
+    bool result = false;
+    nsresult rv = mCallback->OnNotify(this, notification, &result);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+    return result ? NS_OK : NS_ERROR_FAILURE;
+  }
+
   switch (aNotification.mMessage) {
     case REQUEST_TO_COMMIT_COMPOSITION: {
       NS_ASSERTION(aTextEventDispatcher->IsComposing(),
                    "Why is this requested without composition?");
       CommitCompositionInternal();
       return NS_OK;
     }
     case REQUEST_TO_CANCEL_COMPOSITION: {
--- a/dom/base/TextInputProcessor.h
+++ b/dom/base/TextInputProcessor.h
@@ -3,16 +3,17 @@
  * 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/. */
 
 #ifndef mozilla_dom_textinputprocessor_h_
 #define mozilla_dom_textinputprocessor_h_
 
 #include "mozilla/TextEventDispatcherListener.h"
 #include "nsITextInputProcessor.h"
+#include "nsITextInputProcessorCallback.h"
 
 namespace mozilla {
 
 namespace widget{
 class TextEventDispatcher;
 } // namespace widget
 
 class TextInputProcessor MOZ_FINAL : public nsITextInputProcessor
@@ -32,23 +33,25 @@ public:
                        const IMENotification& aNotification) MOZ_OVERRIDE;
   NS_IMETHOD_(void)
     OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) MOZ_OVERRIDE;
 
 private:
   ~TextInputProcessor();
 
   nsresult InitInternal(nsIDOMWindow* aWindow,
+                        nsITextInputProcessorCallback* aCallback,
                         bool aForTests,
                         bool& aSucceeded);
   nsresult CommitCompositionInternal(const nsAString* aCommitString = nullptr,
                                      bool* aSucceeded = nullptr);
   nsresult CancelCompositionInternal();
   nsresult IsValidStateForComposition();
   void UnlinkFromTextEventDispatcher();
 
   TextEventDispatcher* mDispatcher; // [Weak]
+  nsCOMPtr<nsITextInputProcessorCallback> mCallback;
   bool mForTests;
 };
 
 } // namespace mozilla
 
 #endif // #ifndef mozilla_dom_textinputprocessor_h_
--- a/dom/base/test/chrome/window_nsITextInputProcessor.xul
+++ b/dom/base/test/chrome/window_nsITextInputProcessor.xul
@@ -1,16 +1,18 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
                  type="text/css"?>
 <window title="Testing nsITextInputProcessor behavior"
   xmlns:html="http://www.w3.org/1999/xhtml"
   xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   onunload="onunload();">
+<script type="application/javascript"
+        src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
 <body  xmlns="http://www.w3.org/1999/xhtml">
 <p id="display">
 <input id="input" type="text"/><br/>
 <iframe id="iframe" width="300" height="150"
         src="data:text/html,&lt;textarea id='textarea' cols='20' rows='4'&gt;&lt;/textarea&gt;"></iframe><br/>
 </p>
 <div id="content" style="display: none">
   
@@ -72,27 +74,40 @@ function createTIP()
 }
 
 function runInitMethodTests()
 {
   var description = "runInitMethodTest: ";
   input.value = "";
   input.focus();
 
+  var simpleCallback = function (aTIP, aNotification)
+  {
+    switch (aNotification.type) {
+      case "request-to-commit":
+        aTIP.commitComposition();
+        break;
+      case "request-to-cancel":
+        aTIP.cancelComposition();
+        break;
+    }
+    return true;
+  };
+
   var TIP1 = createTIP();
   var TIP2 = createTIP();
   isnot(TIP1, TIP2,
         description + "TIP instances should be different");
 
   // init() and initForTests() can take ownership if there is no composition.
-  ok(TIP1.init(window),
+  ok(TIP1.init(window, simpleCallback),
      description + "TIP1.init(window) should succeed because there is no composition");
   ok(TIP1.initForTests(window),
      description + "TIP1.initForTests(window) should succeed because there is no composition");
-  ok(TIP2.init(window),
+  ok(TIP2.init(window, simpleCallback),
      description + "TIP2.init(window) should succeed because there is no composition");
   ok(TIP2.initForTests(window),
      description + "TIP2.initForTests(window) should succeed because there is no composition");
 
   // Start composition with TIP1, then, other TIPs cannot take ownership during a composition.
   ok(TIP1.initForTests(window),
      description + "TIP1.initForTests() should succeed because there is no composition");
   var composingStr = "foo";
@@ -100,17 +115,17 @@ function runInitMethodTests()
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   ok(TIP1.flushPendingComposition(),
      description + "TIP1.flushPendingComposition() should return true becuase it should be valid composition");
   is(input.value, composingStr,
      description + "The input element should have composing string");
 
   // Composing nsITextInputProcessor instance shouldn't allow initialize it again.
   try {
-    TIP1.init(window);
+    TIP1.init(window, simpleCallback);
     ok(false,
        "TIP1.init(window) should cause throwing an exception because it's composing with different purpose");
   } catch (e) {
     ok(e.message.contains("NS_ERROR_ALREADY_INITIALIZED"),
        description + "TIP1.init(window) should cause throwing an exception including NS_ERROR_ALREADY_INITIALIZED because it's composing for tests");
   }
   try {
     TIP1.initForTests(otherWindow);
@@ -119,36 +134,36 @@ function runInitMethodTests()
   } catch (e) {
     ok(e.message.contains("NS_ERROR_ALREADY_INITIALIZED"),
        description + "TIP1.init(otherWindow) should cause throwing an exception including NS_ERROR_ALREADY_INITIALIZED because it's composing on this window");
   }
   ok(TIP1.initForTests(window),
      description + "TIP1.initForTests(window) should succeed because TextEventDispatcher was initialized with same purpose");
   ok(TIP1.initForTests(childWindow),
      description + "TIP1.initForTests(childWindow) should succeed because TextEventDispatcher was initialized with same purpose and is shared by window and childWindow");
-  ok(!TIP2.init(window),
+  ok(!TIP2.init(window, simpleCallback),
      description + "TIP2.init(window) should not succeed because there is composition synthesized by TIP1");
   ok(!TIP2.initForTests(window),
      description + "TIP2.initForTests(window) should not succeed because there is composition synthesized by TIP1");
-  ok(!TIP2.init(childWindow),
+  ok(!TIP2.init(childWindow, simpleCallback),
      description + "TIP2.init(childWindow) should not succeed because there is composition synthesized by TIP1");
   ok(!TIP2.initForTests(childWindow),
      description + "TIP2.initForTests(childWindow) should not succeed because there is composition synthesized by TIP1");
-  ok(TIP2.init(otherWindow),
+  ok(TIP2.init(otherWindow, simpleCallback),
      description + "TIP2.init(otherWindow) should succeed because there is composition synthesized by TIP1 but it's in other window");
   ok(TIP2.initForTests(otherWindow),
      description + "TIP2.initForTests(otherWindow) should succeed because there is composition synthesized by TIP1 but it's in other window");
 
   // Let's confirm that the composing string is NOT committed by above tests.
   ok(TIP1.commitComposition(),
      description + "TIP1.commitString() should succeed because there should be composing string");
   is(input.value, composingStr,
      description + "TIP1.commitString() without specifying commit string should be committed with the last composing string");
 
-  ok(TIP1.init(window),
+  ok(TIP1.init(window, simpleCallback),
      description + "TIP1.init() should succeed because there is no composition #2");
   ok(TIP1.initForTests(window),
      description + "TIP1.initForTests() should succeed because there is no composition #2");
   ok(TIP2.initForTests(window),
      description + "TIP2.initForTests() should succeed because the composition was already committed #2");
 
   // Let's check if startComposition() throws an exception after ownership is strolen.
   input.value = "";
@@ -198,16 +213,46 @@ function runInitMethodTests()
        description + "TIP1.commitComposition(\"bar\") should cause throwing an exception because TIP2 took the ownership");
   } catch (e) {
     ok(e.message.contains("NS_ERROR_NOT_INITIALIZED"),
        description + "TIP1.commitComposition(\"bar\") should cause throwing an exception including NS_ERROR_NOT_INITIALIZED");
   } finally {
     is(input.value, "",
        description + "The input element should not have commit string");
   }
+
+  // aCallback of nsITextInputProcessor.init() must not be omitted.
+  try {
+    TIP1.init(window);
+    ok(false,
+       description + "TIP1.init(window) should be failed since aCallback is omitted");
+  } catch (e) {
+    ok(e.message.contains("Not enough arguments"),
+       description + "TIP1.init(window) should cause throwing an exception including \"Not enough arguments\" since aCallback is omitted");
+  }
+
+  // aCallback of nsITextInputProcessor.init() must not be undefined.
+  try {
+    TIP1.init(window, undefined);
+    ok(false,
+       description + "TIP1.init(window, undefined) should be failed since aCallback is undefined");
+  } catch (e) {
+    ok(e.message.contains("NS_ERROR_ILLEGAL_VALUE"),
+       description + "TIP1.init(window, undefined) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE since aCallback is undefined");
+  }
+
+  // aCallback of nsITextInputProcessor.init() must not be null.
+  try {
+    TIP1.init(window, null);
+    ok(false,
+       description + "TIP1.init(window, null) should be failed since aCallback is null");
+  } catch (e) {
+    ok(e.message.contains("NS_ERROR_ILLEGAL_VALUE"),
+       description + "TIP1.init(window, null) should cause throwing an exception including NS_ERROR_ILLEGAL_VALUE since aCallback is null");
+  }
 }
 
 function runCompositionTests()
 {
   var description = "runCompositionTests(): ";
 
   var TIP = createTIP();
   ok(TIP.initForTests(window),
@@ -848,24 +893,111 @@ function runUnloadTests2(aNextTest)
   TIP.flushPendingComposition();
   is(textareaInFrame.value, "foo",
      description + "the textarea in the iframe should have composition string");
 
   // Load different web page on the frame.
   iframe.src = "data:text/html,<body>dummy page</body>";
 }
 
+function runCallbackTests(aForTests)
+{
+  var description = "runCallbackTests(aForTests=" + aForTests + "): ";
+
+  input.value = "";
+  input.focus();
+  input.blur();
+
+  var TIP = createTIP();
+  var notifications = [];
+  function callback(aTIP, aNotification)
+  {
+    switch (aNotification.type) {
+      case "request-to-commit":
+        aTIP.commitComposition();
+        break;
+      case "request-to-cancel":
+        aTIP.cancelComposition();
+        break;
+    }
+    if (aTIP == TIP) {
+      notifications.push(aNotification);
+    }
+    return true;
+  }
+
+  function dumpUnexpectedNotifications(aExpectedCount)
+  {
+    if (notifications.length <= aExpectedCount) {
+      return;
+    }
+    for (var i = aExpectedCount; i < notifications.length; i++) {
+      ok(false,
+         description + "Unexpected notification: " + notifications[i].type);
+    }
+  }
+
+  if (aForTests) {
+    TIP.initForTests(window, callback);
+  } else {
+    TIP.init(window, callback);
+  }
+
+  notifications = [];
+  input.focus();
+  is(notifications.length, 1,
+     description + "input.focus() should cause a notification");
+  is(notifications[0].type, "notify-focus",
+     description + "input.focus() should cause \"notify-focus\"");
+  dumpUnexpectedNotifications(1);
+
+  notifications = [];
+  input.blur();
+  is(notifications.length, 1,
+     description + "input.blur() should cause a notification");
+  is(notifications[0].type, "notify-blur",
+     description + "input.blur() should cause \"notify-focus\"");
+  dumpUnexpectedNotifications(1);
+
+  input.focus();
+  TIP.setPendingCompositionString("foo");
+  TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE);
+  TIP.flushPendingComposition();
+  notifications = [];
+  synthesizeMouseAtCenter(input, {});
+  is(notifications.length, 1,
+     description + "synthesizeMouseAtCenter(input, {}) during composition should cause a notification");
+  is(notifications[0].type, "request-to-commit",
+     description + "synthesizeMouseAtCenter(input, {}) during composition should cause \"request-to-commit\"");
+  dumpUnexpectedNotifications(1);
+
+  notifications = [];
+  var TIP2 = createTIP();
+  if (aForTests) {
+    TIP2.initForTests(window, callback);
+  } else {
+    TIP2.init(window, callback);
+  }
+  is(notifications.length, 1,
+     description + "Initializing another TIP should cause a notification");
+  is(notifications[0].type, "notify-detached",
+     description + "Initializing another TIP should cause \"notify-detached\"");
+  dumpUnexpectedNotifications(1);
+}
+
 function runTests()
 {
   textareaInFrame = iframe.contentDocument.getElementById("textarea");
 
   runInitMethodTests();
   runCompositionTests();
   runErrorTests();
   runCommitCompositionTests();
+  runCallbackTests(false);
+  runCallbackTests(true);
   runUnloadTests1(function () {
     runUnloadTests2(function () {
       finish();
     });
   });
 }
 
 ]]>
--- a/dom/inputmethod/forms.js
+++ b/dom/inputmethod/forms.js
@@ -1214,24 +1214,41 @@ let CompositionManager =  {
     '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.init(aWindow);
+    return this._textInputProcessor.init(aWindow, this._callback);
   },
 
   setComposition: function cm_setComposition(element, text, cursor, clauses) {
     // Check parameters.
     if (!element) {
       return;
     }
     let len = text.length;
--- a/dom/interfaces/base/moz.build
+++ b/dom/interfaces/base/moz.build
@@ -31,12 +31,13 @@ XPIDL_SOURCES += [
     'nsIIdleObserver.idl',
     'nsIQueryContentEventResult.idl',
     'nsIRemoteBrowser.idl',
     'nsIServiceWorkerManager.idl',
     'nsIStructuredCloneContainer.idl',
     'nsITabChild.idl',
     'nsITabParent.idl',
     'nsITextInputProcessor.idl',
+    'nsITextInputProcessorCallback.idl',
 ]
 
 XPIDL_MODULE = 'dom_base'
 
--- a/dom/interfaces/base/nsITextInputProcessor.idl
+++ b/dom/interfaces/base/nsITextInputProcessor.idl
@@ -1,30 +1,34 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "nsISupports.idl"
 
 interface nsIDOMWindow;
+interface nsITextInputProcessorCallback;
 
 /**
  * An nsITextInputProcessor instance is associated with a top level widget which
  * handles native IME.  It's associated by calling init() or initForTests().
  * While an instance has composition, nobody can steal the rights to make
  * composition on the top level widget.  In other words, if another instance is
  * composing on a top level widget, either init() or initForTests() returns
  * false (i.e., not throws an exception).
  *
+ * NOTE: See nsITextInputProcessorCallback.idl for examples of |callback| in
+ *       following examples,
+ *
  * Example #1 JS-IME can start composition like this:
  *
  *   var TIP = Components.classes["@mozilla.org/text-input-processor;1"].
  *               createInstance(Components.interfaces.nsITextInputProcessor);
- *   if (!TIP.init(window)) {
+ *   if (!TIP.init(window, callback)) {
  *     return; // You failed to get the rights to make composition
  *   }
  *   // Set new composition string first
  *   TIP.setPendingCompositionString("some-words-are-inputted");
  *   // Set clause information.
  *   TIP.appendClauseToPendingComposition(23, TIP.ATTR_RAW_CLAUSE);
  *   // Set caret position, this is optional.
  *   TIP.setCaretInPendingComposition(23);
@@ -75,58 +79,66 @@ interface nsIDOMWindow;
  *   TIP.appendClauseToPendingComposition(27, TIP.ATTR_RAW_CLAUSE);
  *   TIP.flushPendingComposition();
  *   // This is useful when user doesn't want to commit the composition.
  *   // FYI: This is same as TIP.commitComposition("") for now.
  *   TIP.cancelComposition();
  *
  * Example #6 JS-IME can insert text only with commitComposition():
  *
- *   if (!TIP.init(window)) {
+ *   if (!TIP.init(window, callback)) {
  *     return; // You failed to get the rights to make composition
  *   }
  *   TIP.commitComposition("Some words");
  *
  * Example #7 JS-IME can start composition explicitly:
  *
- *   if (!TIP.init(window)) {
+ *   if (!TIP.init(window, callback)) {
  *     return; // You failed to get the rights to make composition
  *   }
  *   // If JS-IME don't want to show composing string in the focused editor,
  *   // JS-IME can dispatch only compositionstart event with this.
  *   if (!TIP.startComposition()) {
  *     // Failed to start composition.
  *     return;
  *   }
  *   // And when user selects a result from UI of JS-IME, commit with it.
  *   TIP.commitComposition("selected-words");
  */
 
-[scriptable, builtinclass, uuid(c9daacc8-17a1-4979-85fa-918c9c0ccb30)]
+[scriptable, builtinclass, uuid(8c20753c-8339-4e9c-86c5-ae30f1b456c3)]
 interface nsITextInputProcessor : nsISupports
 {
   /**
    * When you create an instance, you must call init() first except when you
    * created the instance for automated tests.
    *
    * @param aWindow         A DOM window.  The instance will look for a top
    *                        level widget from this.
+   * @param aCallback       Callback interface which handles requests to
+   *                        IME and notifications to IME.  This must not be
+   *                        null.
    * @return                If somebody uses internal text input service for a
    *                        composition, this returns false.  Otherwise, returns
    *                        true.  I.e., only your TIP can create composition
    *                        when this returns true.  If this returns false,
    *                        your TIP should wait next chance.
    */
-  boolean init(in nsIDOMWindow aWindow);
+  boolean init(in nsIDOMWindow aWindow,
+               in nsITextInputProcessorCallback aCallback);
 
   /**
    * When you create an instance for automated test, you must call
    * initForTest(), first.  See init() for more detail of this.
+   * Note that aCallback can be null.  If it's null, nsITextInputProcessor
+   * implementation will handle them automatically.
    */
-  boolean initForTests(in nsIDOMWindow aWindow);
+  [optional_argc] boolean
+    initForTests(in nsIDOMWindow aWindow,
+                 [optional] in nsITextInputProcessorCallback aCallback);
 
   /**
    * startComposition() dispatches compositionstart event explicitly.
    * IME does NOT need to call this typically since compositionstart event
    * is automatically dispatched by sendPendingComposition() if
    * compositionstart event hasn't been dispatched yet.  If this is called
    * when compositionstart has already been dispatched, this throws an
    * exception.
new file mode 100644
--- /dev/null
+++ b/dom/interfaces/base/nsITextInputProcessorCallback.idl
@@ -0,0 +1,102 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsITextInputProcessor;
+
+/**
+ * nsITextInputProcessorNotification stores the type of notification to IME and
+ * its detail.  See each explanation of attribute for the detail.
+ */
+
+[scriptable, builtinclass, uuid(c0ce1add-82bb-45ab-b99a-42cfba7fd5d7)]
+interface nsITextInputProcessorNotification : nsISupports
+{
+  /**
+   * type attribute represents what's notified or requested.  Value must be
+   * one of following values:
+   *
+   * "request-to-commit"  (required to be handled)
+   *   This is requested when Gecko believes that active composition should be
+   *   committed.  nsITextInputProcessorCallback::onNotify() has to handle this
+   *   notification.
+   *
+   * "request-to-cancel" (required to be handled)
+   *   This is requested when Gecko believes that active composition should be
+   *   canceled.  I.e., composition should be committed with empty string.
+   *   nsITextInputProcessorCallback::onNotify() has to handle this
+   *   notification.
+   *
+   * "notify-detached" (optional)
+   *   This is notified when the callback is detached from
+   *   nsITextInputProcessor.
+   *
+   * "notify-focus" (optional)
+   *   This is notified when an editable editor gets focus and Gecko starts
+   *   to observe changes in the content. E.g., selection changes.
+   *   IME shouldn't change DOM tree, focus nor something when this is notified.
+   *
+   * "notify-blur" (optional)
+   *   This is notified when an editable editor loses focus and Gecko stops
+   *   observing the changes in the content.
+   */
+  readonly attribute ACString type;
+};
+
+/**
+ * nsITextInputProcessorCallback is a callback interface for JS to implement
+ * IME.  IME implemented by JS can implement onNotify() function and must send
+ * it to nsITextInputProcessor at initializing.  Then, onNotify() will be
+ * called with nsITextInputProcessorNotification instance.
+ * The reason why onNotify() uses string simply is that if we will support
+ * other notifications such as text changes and selection changes, we need to
+ * notify IME of some other information.  Then, only changing
+ * nsITextInputProcessorNotification interface is better for compatibility.
+ */
+
+[scriptable, function, uuid(23d5f242-adb5-46f1-8766-90d1bf0383df)]
+interface nsITextInputProcessorCallback : nsISupports
+{
+  /**
+   * When Gecko notifies IME of something or requests something to IME,
+   * this is called.
+   *
+   * @param aTextInputProcessor Reference to the nsITextInputProcessor service
+   *                            which is the original receiver of the request
+   *                            or notification.
+   * @param aNotification       Stores type of notifications and additional
+   *                            information.
+   * @return                    Return true if it succeeded or does nothing.
+   *                            Otherwise, return false.
+   *
+   * Example #1 The simplest implementation of nsITextInputProcessorCallback is:
+   *
+   *   function simpleCallback(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;
+   *   }
+   *
+   *   var TIP = Components.classes["@mozilla.org/text-input-processor;1"].
+   *               createInstance(Components.interfaces.nsITextInputProcessor);
+   *   if (!TIP.init(window, simpleCallback)) {
+   *     return;
+   *   }
+   */
+  boolean onNotify(in nsITextInputProcessor aTextInputProcessor,
+                   in nsITextInputProcessorNotification aNotification);
+};
--- a/widget/nsBaseWidget.cpp
+++ b/widget/nsBaseWidget.cpp
@@ -1594,18 +1594,29 @@ nsBaseWidget::NotifyIME(const IMENotific
     case REQUEST_TO_CANCEL_COMPOSITION:
       // Currently, if native IME handler doesn't use TextEventDispatcher,
       // the request may be notified to mTextEventDispatcher or native IME
       // directly.  Therefore, if mTextEventDispatcher has a composition,
       // the request should be handled by the mTextEventDispatcher.
       if (mTextEventDispatcher && mTextEventDispatcher->IsComposing()) {
         return mTextEventDispatcher->NotifyIME(aIMENotification);
       }
-      // Otherwise, call NotifyIMEInternal() for native IME handlers.
+      // Otherwise, it should be handled by native IME.
+      return NotifyIMEInternal(aIMENotification);
+    case NOTIFY_IME_OF_FOCUS:
+    case NOTIFY_IME_OF_BLUR:
+      // If the notification is a notification which is supported by
+      // nsITextInputProcessorCallback, we should notify the
+      // TextEventDispatcher, first.  After that, notify native IME too.
+      if (mTextEventDispatcher) {
+        mTextEventDispatcher->NotifyIME(aIMENotification);
+      }
+      return NotifyIMEInternal(aIMENotification);
     default:
+      // Otherwise, notify only native IME for now.
       return NotifyIMEInternal(aIMENotification);
   }
 }
 
 NS_IMETHODIMP_(nsIWidget::TextEventDispatcher*)
 nsBaseWidget::GetTextEventDispatcher()
 {
   if (!mTextEventDispatcher) {