Bug 610015: Implement updated Cocoa NPAPI text input spec. Part of this patch written by Steven Michaud. r=josh r=smichaud a=blocking2.0betaN+
authorJosh Aas <joshmoz@gmail.com>
Thu, 20 Jan 2011 20:08:11 -0500
changeset 61078 3d49b553bc4ba88e083bd13fd4a8ea25a5081fd1
parent 61077 a7026bd0bd6b664758e48225fcd78b11b20603e5
child 61079 4d89f48b2abdd9b001728b05f257a5972918da80
push idunknown
push userunknown
push dateunknown
reviewersjosh, smichaud, blocking2
bugs610015
milestone2.0b10pre
Bug 610015: Implement updated Cocoa NPAPI text input spec. Part of this patch written by Steven Michaud. r=josh r=smichaud a=blocking2.0betaN+
dom/plugins/PluginInstanceChild.cpp
modules/plugin/base/src/nsNPAPIPlugin.cpp
widget/src/cocoa/ComplexTextInputPanel.h
widget/src/cocoa/ComplexTextInputPanel.mm
widget/src/cocoa/nsChildView.h
widget/src/cocoa/nsChildView.mm
--- a/dom/plugins/PluginInstanceChild.cpp
+++ b/dom/plugins/PluginInstanceChild.cpp
@@ -404,17 +404,22 @@ PluginInstanceChild::NPN_GetValue(NPNVar
         *((NPBool*)aValue) = true;
         return NPERR_NO_ERROR;
     }
 
     case NPNVsupportsCocoaBool: {
         *((NPBool*)aValue) = true;
         return NPERR_NO_ERROR;
     }
-  
+
+    case NPNVsupportsUpdatedCocoaTextInputBool: {
+      *static_cast<NPBool*>(aValue) = true;
+      return NPERR_NO_ERROR;
+    }
+
 #ifndef NP_NO_QUICKDRAW
     case NPNVsupportsQuickDrawBool: {
         *((NPBool*)aValue) = false;
         return NPERR_NO_ERROR;
     }
 #endif /* NP_NO_QUICKDRAW */
 #endif /* XP_MACOSX */
 
--- a/modules/plugin/base/src/nsNPAPIPlugin.cpp
+++ b/modules/plugin/base/src/nsNPAPIPlugin.cpp
@@ -2229,16 +2229,21 @@ NPError NP_CALLBACK
     return NPERR_NO_ERROR;
   }
 #endif
   case NPNVsupportsCocoaBool: {
     *(NPBool*)result = PR_TRUE;
 
     return NPERR_NO_ERROR;
   }
+
+  case NPNVsupportsUpdatedCocoaTextInputBool: {
+    *(NPBool*)result = true;
+    return NPERR_NO_ERROR;
+  }
 #endif
 
   // we no longer hand out any XPCOM objects, except on WINCE,
   // where it's needed for the ActiveX shunt that makes Flash
   // work until we get an NPAPI plugin there.
 #ifdef WINCE
   case NPNVDOMWindow: {
     nsNPAPIPluginInstance *inst = (nsNPAPIPluginInstance *)npp->ndata;
--- a/widget/src/cocoa/ComplexTextInputPanel.h
+++ b/widget/src/cocoa/ComplexTextInputPanel.h
@@ -34,12 +34,13 @@
   NSTextView *mInputTextView;
 }
 
 + (ComplexTextInputPanel*)sharedComplexTextInputPanel;
 
 - (NSTextInputContext*)inputContext;
 - (BOOL)interpretKeyEvent:(NSEvent*)event string:(NSString**)string;
 - (void)cancelComposition;
+- (BOOL)inComposition;
 
 @end
 
 #endif // ComplexTextInputPanel_h_
--- a/widget/src/cocoa/ComplexTextInputPanel.mm
+++ b/widget/src/cocoa/ComplexTextInputPanel.mm
@@ -119,9 +119,14 @@
 }
 
 - (void)cancelComposition
 {
   [mInputTextView setString:@""];
   [self orderOut:nil];
 }
 
+- (BOOL)inComposition
+{
+  return [mInputTextView hasMarkedText];
+}
+
 @end
--- a/widget/src/cocoa/nsChildView.h
+++ b/widget/src/cocoa/nsChildView.h
@@ -169,19 +169,23 @@ extern "C" long TSMProcessRawKeyEvent(Ev
   // when handling |draggingUpdated:| messages.
   nsIDragService* mDragService;
 
 #ifndef NP_NO_CARBON
   // For use with plugins, so that we can support IME in them.  We can't use
   // Cocoa TSM documents (those created and managed by the NSTSMInputContext
   // class) -- for some reason TSMProcessRawKeyEvent() doesn't work with them.
   TSMDocumentID mPluginTSMDoc;
+  BOOL mPluginTSMInComposition;
 #endif
   BOOL mPluginComplexTextInputRequested;
 
+  // When this is YES the next key up event (keyUp:) will be ignored.
+  BOOL mIgnoreNextKeyUpEvent;
+
   NSOpenGLContext *mGLContext;
 
   // Simple gestures support
   //
   // mGestureState is used to detect when Cocoa has called both
   // magnifyWithEvent and rotateWithEvent within the same
   // beginGestureWithEvent and endGestureWithEvent sequence. We
   // discard the spurious gesture event so as not to confuse Gecko.
--- a/widget/src/cocoa/nsChildView.mm
+++ b/widget/src/cocoa/nsChildView.mm
@@ -177,16 +177,20 @@ PRUint32 nsChildView::sLastInputEventCou
 
 - (void)setIsPluginView:(BOOL)aIsPlugin;
 - (BOOL)isPluginView;
 - (void)setPluginEventModel:(NPEventModel)eventModel;
 - (void)setPluginDrawingModel:(NPDrawingModel)drawingModel;
 - (NPEventModel)pluginEventModel;
 - (NPDrawingModel)pluginDrawingModel;
 
+#ifndef NP_NO_CARBON
+- (void)setPluginTSMInComposition:(BOOL)inComposition;
+#endif
+
 - (BOOL)isRectObscuredBySubview:(NSRect)inRect;
 
 - (void)processPendingRedraws;
 
 - (void)maybeInitContextMenuTracking;
 
 + (NSEvent*)makeNewCocoaEventWithType:(NSEventType)type fromEvent:(NSEvent*)theEvent;
 
@@ -536,16 +540,25 @@ nsresult nsChildView::Create(nsIWidget *
   // we need to provide an autorelease pool to avoid leaking cocoa objects
   // (see bug 559075).
   nsAutoreleasePool localPool;
 
   // See NSView (MethodSwizzling) below.
   if (!gChildViewMethodsSwizzled) {
     nsToolkit::SwizzleMethods([NSView class], @selector(mouseDownCanMoveWindow),
                               @selector(nsChildView_NSView_mouseDownCanMoveWindow));
+#ifndef NP_NO_CARBON
+    Class IMKInputSessionClass = ::NSClassFromString(@"IMKInputSession");
+    nsToolkit::SwizzleMethods(IMKInputSessionClass, @selector(handleEvent:),
+                              @selector(nsChildView_IMKInputSession_handleEvent:));
+    nsToolkit::SwizzleMethods(IMKInputSessionClass, @selector(commitComposition),
+                              @selector(nsChildView_IMKInputSession_commitComposition));
+    nsToolkit::SwizzleMethods(IMKInputSessionClass, @selector(finishSession),
+                              @selector(nsChildView_IMKInputSession_finishSession));
+#endif
     gChildViewMethodsSwizzled = PR_TRUE;
   }
 
   mBounds = aRect;
 
   BaseCreate(aParent, aRect, aHandleEventFunction, 
              aContext, aAppShell, aToolkit, aInitData);
 
@@ -2221,19 +2234,22 @@ NSEvent* gLastDragMouseDownEvent = nil;
     mMarkedRange.length = 0;
 
     mLastMouseDownEvent = nil;
     mClickThroughMouseDownEvent = nil;
     mDragService = nsnull;
 
 #ifndef NP_NO_CARBON
     mPluginTSMDoc = nil;
+    mPluginTSMInComposition = NO;
 #endif
     mPluginComplexTextInputRequested = NO;
 
+    mIgnoreNextKeyUpEvent = NO;
+
     mGestureState = eGestureState_None;
     mCumulativeMagnification = 0.0;
     mCumulativeRotation = 0.0;
 
     [self setFocusRingType:NSFocusRingTypeNone];
   }
   
   // register for things we'll take from other applications
@@ -2500,26 +2516,33 @@ NSEvent* gLastDragMouseDownEvent = nil;
   mPluginEventModel = eventModel;
 }
 
 - (void)setPluginDrawingModel:(NPDrawingModel)drawingModel
 {
   mPluginDrawingModel = drawingModel;
 }
 
-- (NPEventModel)pluginEventModel;
+- (NPEventModel)pluginEventModel
 {
   return mPluginEventModel;
 }
 
-- (NPDrawingModel)pluginDrawingModel;
+- (NPDrawingModel)pluginDrawingModel
 {
   return mPluginDrawingModel;
 }
 
+#ifndef NP_NO_CARBON
+- (void)setPluginTSMInComposition:(BOOL)inComposition
+{
+  mPluginTSMInComposition = inComposition;
+}
+#endif
+
 - (void)sendFocusEvent:(PRUint32)eventType
 {
   if (!mGeckoChild)
     return;
 
   nsEventStatus status = nsEventStatus_eIgnore;
   nsGUIEvent focusGuiEvent(PR_TRUE, eventType, mGeckoChild);
   focusGuiEvent.time = PR_IntervalNow();
@@ -5342,82 +5365,140 @@ static const char* ToEscapedString(NSStr
 // Cocoa will call this after the menu system returns "NO" for "performKeyEquivalent:".
 // We want all they key events we can get so just return YES. In particular, this fixes
 // ctrl-tab - we don't get a "keyDown:" call for that without this.
 - (BOOL)_wantsKeyDownForEvent:(NSEvent*)event
 {
   return YES;
 }
 
+- (BOOL)inCocoaPluginComposition
+{
+#ifdef NP_NO_CARBON
+  return [[ComplexTextInputPanel sharedComplexTextInputPanel] inComposition];
+#else
+  return mPluginTSMInComposition;
+#endif
+}
+
+- (void)sendCocoaNPAPITextEvent:(NSString*)string
+{
+  NPCocoaEvent cocoaTextEvent;
+  InitNPCocoaEvent(&cocoaTextEvent);
+  cocoaTextEvent.type = NPCocoaEventTextInput;
+  cocoaTextEvent.data.text.text = (NPNSString*)string;
+  
+  nsGUIEvent pluginTextEvent(PR_TRUE, NS_NON_RETARGETED_PLUGIN_EVENT, mGeckoChild);
+  pluginTextEvent.time = PR_IntervalNow();
+  pluginTextEvent.pluginEvent = (void*)&cocoaTextEvent;
+  mGeckoChild->DispatchWindowEvent(pluginTextEvent);
+}
+
 - (void)keyDown:(NSEvent*)theEvent
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   if (mGeckoChild && mIsPluginView) {
-    // If a plugin has the focus, we need to use an alternate method for
-    // handling NSKeyDown and NSKeyUp events (otherwise Carbon-based IME won't
-    // work in plugins like the Flash plugin).  The same strategy is used by the
-    // WebKit.  See PluginKeyEventsHandler() and [ChildView processPluginKeyEvent:]
-    // for more info.
+#ifdef NP_NO_CARBON
     if (mPluginEventModel == NPEventModelCocoa) {
-      // Reset complex text input request.
+      ComplexTextInputPanel* ctiPanel = [ComplexTextInputPanel sharedComplexTextInputPanel];
+
+      // If a composition is in progress then simply let the input panel continue it.
+      if ([self inCocoaPluginComposition]) {
+        // Don't send key up events for key downs associated with compositions.
+        mIgnoreNextKeyUpEvent = YES;
+
+        NSString* textString = nil;
+        [ctiPanel interpretKeyEvent:theEvent string:&textString];
+        if (textString) {
+          [self sendCocoaNPAPITextEvent:textString];
+        }
+
+        return;
+      }
+
+      // Reset complex text input request flag.
       mPluginComplexTextInputRequested = NO;
-      
-      // Send key down event.
+
+      // Send key down event to the plugin.
       nsGUIEvent pluginEvent(PR_TRUE, NS_NON_RETARGETED_PLUGIN_EVENT, mGeckoChild);
       NPCocoaEvent cocoaEvent;
       ConvertCocoaKeyEventToNPCocoaEvent(theEvent, cocoaEvent);
       pluginEvent.pluginEvent = &cocoaEvent;
       mGeckoChild->DispatchWindowEvent(pluginEvent);
-      if (!mGeckoChild)
+      if (!mGeckoChild) {
         return;
-
-      if (!mPluginComplexTextInputRequested) {
-#ifdef NP_NO_CARBON
-        [[ComplexTextInputPanel sharedComplexTextInputPanel] cancelComposition];
-#else
-        if (mPluginTSMDoc) {
-          ::FixTSMDocument(mPluginTSMDoc);
+      }
+
+      // Start complex text composition if requested.
+      if (mPluginComplexTextInputRequested) {
+        // Don't send key up events for key downs associated with compositions.
+        mIgnoreNextKeyUpEvent = YES;
+
+        NSString* textString = nil;
+        [ctiPanel interpretKeyEvent:theEvent string:&textString];
+        if (textString) {
+          [self sendCocoaNPAPITextEvent:textString];
         }
-#endif
+
         return;
       }
 
-#ifdef NP_NO_CARBON
-      ComplexTextInputPanel* ctInputPanel = [ComplexTextInputPanel sharedComplexTextInputPanel];
-      NSString* textString = nil;
-      [ctInputPanel interpretKeyEvent:theEvent string:&textString];
-      if (textString) {
-        NPCocoaEvent cocoaTextEvent;
-        InitNPCocoaEvent(&cocoaTextEvent);
-        cocoaTextEvent.type = NPCocoaEventTextInput;
-        cocoaTextEvent.data.text.text = (NPNSString*)textString;
-        
-        nsGUIEvent pluginTextEvent(PR_TRUE, NS_NON_RETARGETED_PLUGIN_EVENT, mGeckoChild);
-        pluginTextEvent.time = PR_IntervalNow();
-        pluginTextEvent.pluginEvent = (void*)&cocoaTextEvent;
-        mGeckoChild->DispatchWindowEvent(pluginTextEvent);
-      }
+      // Nothing else to do for Cocoa NPAPI plugins.
       return;
+    }
 #endif
-    }
 
 #ifndef NP_NO_CARBON
+    BOOL wasInComposition = NO;
+    if (mPluginEventModel == NPEventModelCocoa) {
+      if ([self inCocoaPluginComposition]) {
+        wasInComposition = YES;
+
+        // Don't send key up events for key downs associated with compositions.
+        mIgnoreNextKeyUpEvent = YES;
+      }
+      else {
+        // Reset complex text input request flag.
+        mPluginComplexTextInputRequested = NO;
+
+        // Send key down event to the plugin.
+        nsGUIEvent pluginEvent(PR_TRUE, NS_NON_RETARGETED_PLUGIN_EVENT, mGeckoChild);
+        NPCocoaEvent cocoaEvent;
+        ConvertCocoaKeyEventToNPCocoaEvent(theEvent, cocoaEvent);
+        pluginEvent.pluginEvent = &cocoaEvent;
+        mGeckoChild->DispatchWindowEvent(pluginEvent);
+        if (!mGeckoChild) {
+          return;
+        }
+
+        // Only continue if plugin wants complex text input.
+        if (mPluginComplexTextInputRequested) {
+          // Don't send key up events for key downs associated with compositions.
+          mIgnoreNextKeyUpEvent = YES;
+        }
+        else {
+          return;
+        }
+      }
+    }
+
     // This will take care of all Carbon plugin events and also send Cocoa plugin
     // text events when NSInputContext is not available (ifndef NP_NO_CARBON).
     [self activatePluginTSMDoc];
     // We use the active TSM document to pass a pointer to ourselves (the
     // currently focused ChildView) to PluginKeyEventsHandler().  Because this
     // pointer is weak, we should retain and release ourselves around the call
     // to TSMProcessRawKeyEvent().
     nsAutoRetainCocoaObject kungFuDeathGrip(self);
     ::TSMSetDocumentProperty(mPluginTSMDoc, kFocusedChildViewTSMDocPropertyTag,
                              sizeof(ChildView *), &self);
     ::TSMProcessRawKeyEvent([theEvent _eventRef]);
     ::TSMRemoveDocumentProperty(mPluginTSMDoc, kFocusedChildViewTSMDocPropertyTag);
+
     return;
 #endif
   }
 
   PRBool handled = [self processKeyDownEvent:theEvent];
   
   // We always allow keyboard events to propagate to keyDown: but if they are not
   // handled we give special Application menu items a chance to act.
@@ -5441,20 +5522,30 @@ static const char* ToEscapedString(NSStr
           [theEvent keyCode],
           [theEvent modifierFlags],
           ToEscapedString([theEvent characters], str1),
           ToEscapedString([theEvent charactersIgnoringModifiers], str2)));
 
   if (!mGeckoChild)
     return;
 
+  if (mIgnoreNextKeyUpEvent) {
+    mIgnoreNextKeyUpEvent = NO;
+    return;
+  }
+
   nsAutoRetainCocoaObject kungFuDeathGrip(self);
 
   if (mIsPluginView) {
     if (mPluginEventModel == NPEventModelCocoa) {
+      // Don't send key up events to Cocoa plugins during composition.
+      if ([self inCocoaPluginComposition]) {
+        return;
+      }
+
       nsKeyEvent keyUpEvent(PR_TRUE, NS_KEY_UP, nsnull);
       [self convertCocoaKeyEvent:theEvent toGeckoEvent:&keyUpEvent];
       NPCocoaEvent pluginEvent;
       ConvertCocoaKeyEventToNPCocoaEvent(theEvent, pluginEvent);
       keyUpEvent.pluginEvent = &pluginEvent;
       mGeckoChild->DispatchWindowEvent(keyUpEvent);
     }
 #ifndef NP_NO_CARBON
@@ -6590,16 +6681,82 @@ void NS_InstallPluginKeyEventsHandler()
 void NS_RemovePluginKeyEventsHandler()
 {
   if (!gPluginKeyEventsHandler)
     return;
   ::RemoveEventHandler(gPluginKeyEventsHandler);
   gPluginKeyEventsHandler = NULL;
 }
 
+// IMKInputSession is an undocumented class in the HIToolbox framework.  It's
+// present on both Leopard and SnowLeopard, and is used at a low level to
+// process IME input regardless of which high-level API is used (Text Services
+// Manager or Cocoa).  It works the same way in both 32-bit and 64-bit code.
+@interface NSObject (IMKInputSessionMethodSwizzling)
+- (BOOL)nsChildView_IMKInputSession_handleEvent:(EventRef)theEvent;
+- (void)nsChildView_IMKInputSession_commitComposition;
+- (void)nsChildView_IMKInputSession_finishSession;
+@end
+
+@implementation NSObject (IMKInputSessionMethodSwizzling)
+
+- (BOOL)nsChildView_IMKInputSession_handleEvent:(EventRef)theEvent
+{
+  [self retain];
+  BOOL retval = [self nsChildView_IMKInputSession_handleEvent:theEvent];
+  NSUInteger retainCount = [self retainCount];
+  [self release];
+  // Return without doing anything if we've been deleted.
+  if (retainCount == 1) {
+    return retval;
+  }
+
+  NSWindow *mainWindow = [NSApp mainWindow];
+  NSResponder *firstResponder = [mainWindow firstResponder];
+  if (![firstResponder isKindOfClass:[ChildView class]]) {
+    return retval;
+  }
+
+  // 'charactersEntered' is the length (in bytes) of currently "marked text"
+  // -- text that's been entered in IME but not yet committed.  If it's
+  // non-zero we're composing text in an IME session; if it's zero we're
+  // not in an IME session.
+  NSInteger charactersEntered = 0;
+  object_getInstanceVariable(self, "charactersEntered", (void **) &charactersEntered);
+  [(ChildView*)firstResponder setPluginTSMInComposition:(charactersEntered != 0)];
+
+  return retval;
+}
+
+// This method is called whenever IME input is committed as a result of an
+// "abnormal" termination -- for example when changing the keyboard focus from
+// one input field to another.
+- (void)nsChildView_IMKInputSession_commitComposition
+{
+  NSWindow *mainWindow = [NSApp mainWindow];
+  NSResponder *firstResponder = [mainWindow firstResponder];
+  if ([firstResponder isKindOfClass:[ChildView class]]) {
+    [(ChildView*)firstResponder setPluginTSMInComposition:NO];
+  }
+  [self nsChildView_IMKInputSession_commitComposition];
+}
+
+// This method is called just before we're deallocated.
+- (void)nsChildView_IMKInputSession_finishSession
+{
+  NSWindow *mainWindow = [NSApp mainWindow];
+  NSResponder *firstResponder = [mainWindow firstResponder];
+  if ([firstResponder isKindOfClass:[ChildView class]]) {
+    [(ChildView*)firstResponder setPluginTSMInComposition:NO];
+  }
+  [self nsChildView_IMKInputSession_finishSession];
+}
+
+@end
+
 #endif // NP_NO_CARBON
 
 @interface NSView (MethodSwizzling)
 - (BOOL)nsChildView_NSView_mouseDownCanMoveWindow;
 @end
 
 @implementation NSView (MethodSwizzling)