Bug 705057 part.1 Ensure a set of composition events is fired on same content r=smaug

This commit is contained in:
Masayuki Nakano 2012-09-26 14:47:45 +09:00
Родитель d3f82bacd5
Коммит 8acd6ae7f5
14 изменённых файлов: 479 добавлений и 58 удалений

Просмотреть файл

@ -61,6 +61,7 @@ CPPSRCS = \
nsDOMTouchEvent.cpp \
nsDOMCompositionEvent.cpp \
DOMWheelEvent.cpp \
TextComposition.cpp \
$(NULL)
ifdef MOZ_B2G_RIL

Просмотреть файл

@ -0,0 +1,136 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=80: */
/* 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 "TextComposition.h"
#include "nsContentUtils.h"
#include "nsEventDispatcher.h"
#include "nsGUIEvent.h"
#include "nsIPresShell.h"
#include "nsIWidget.h"
#include "nsPresContext.h"
namespace mozilla {
/******************************************************************************
* TextComposition
******************************************************************************/
TextComposition::TextComposition(nsPresContext* aPresContext,
nsINode* aNode,
nsGUIEvent* aEvent) :
mPresContext(aPresContext), mNode(aNode),
// temporarily, we should assume that one native IME context is per native
// widget.
mNativeContext(aEvent->widget)
{
}
TextComposition::TextComposition(const TextComposition& aOther)
{
mNativeContext = aOther.mNativeContext;
mPresContext = aOther.mPresContext;
mNode = aOther.mNode;
}
bool
TextComposition::MatchesNativeContext(nsIWidget* aWidget) const
{
// temporarily, we should assume that one native IME context is per one
// native widget.
return mNativeContext == static_cast<void*>(aWidget);
}
bool
TextComposition::MatchesEventTarget(nsPresContext* aPresContext,
nsINode* aNode) const
{
return mPresContext == aPresContext && mNode == aNode;
}
void
TextComposition::DispatchEvent(nsGUIEvent* aEvent,
nsEventStatus* aStatus,
nsDispatchingCallback* aCallBack)
{
nsEventDispatcher::Dispatch(mNode, mPresContext,
aEvent, nullptr, aStatus, aCallBack);
}
/******************************************************************************
* TextCompositionArray
******************************************************************************/
TextCompositionArray::index_type
TextCompositionArray::IndexOf(nsIWidget* aWidget)
{
for (index_type i = Length(); i > 0; --i) {
if (ElementAt(i - 1).MatchesNativeContext(aWidget)) {
return i - 1;
}
}
return NoIndex;
}
TextCompositionArray::index_type
TextCompositionArray::IndexOf(nsPresContext* aPresContext)
{
for (index_type i = Length(); i > 0; --i) {
if (ElementAt(i - 1).GetPresContext() == aPresContext) {
return i - 1;
}
}
return NoIndex;
}
TextCompositionArray::index_type
TextCompositionArray::IndexOf(nsPresContext* aPresContext,
nsINode* aNode)
{
index_type index = IndexOf(aPresContext);
if (index == NoIndex) {
return NoIndex;
}
nsINode* node = ElementAt(index).GetEventTargetNode();
return node == aNode ? index : NoIndex;
}
TextComposition*
TextCompositionArray::GetCompositionFor(nsIWidget* aWidget)
{
index_type i = IndexOf(aWidget);
return i != NoIndex ? &ElementAt(i) : nullptr;
}
TextComposition*
TextCompositionArray::GetCompositionFor(nsPresContext* aPresContext)
{
index_type i = IndexOf(aPresContext);
return i != NoIndex ? &ElementAt(i) : nullptr;
}
TextComposition*
TextCompositionArray::GetCompositionFor(nsPresContext* aPresContext,
nsINode* aNode)
{
index_type i = IndexOf(aPresContext, aNode);
return i != NoIndex ? &ElementAt(i) : nullptr;
}
TextComposition*
TextCompositionArray::GetCompositionInContent(nsPresContext* aPresContext,
nsIContent* aContent)
{
// There should be only one composition per content object.
for (index_type i = Length(); i > 0; --i) {
nsINode* node = ElementAt(i - 1).GetEventTargetNode();
if (node && nsContentUtils::ContentIsDescendantOf(node, aContent)) {
return &ElementAt(i - 1);
}
}
return nullptr;
}
} // namespace mozilla

Просмотреть файл

@ -0,0 +1,103 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=80: */
/* 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/. */
#ifndef mozilla_TextComposition_h
#define mozilla_TextComposition_h
#include "nsCOMPtr.h"
#include "nsEvent.h"
#include "nsINode.h"
#include "nsTArray.h"
#include "mozilla/Attributes.h"
class nsCompositionEvent;
class nsDispatchingCallback;
class nsIMEStateManager;
class nsIWidget;
class nsPresContext;
namespace mozilla {
/**
* TextComposition represents a text composition. This class stores the
* composition event target and its presContext. At dispatching the event via
* this class, the instances use the stored event target.
*/
class TextComposition MOZ_FINAL
{
friend class ::nsIMEStateManager;
public:
TextComposition(nsPresContext* aPresContext,
nsINode* aNode,
nsGUIEvent* aEvent);
TextComposition(const TextComposition& aOther);
~TextComposition()
{
// WARNING: mPresContext may be destroying, so, be careful if you touch it.
}
nsPresContext* GetPresContext() const { return mPresContext; }
nsINode* GetEventTargetNode() const { return mNode; }
bool MatchesNativeContext(nsIWidget* aWidget) const;
bool MatchesEventTarget(nsPresContext* aPresContext,
nsINode* aNode) const;
private:
// This class holds nsPresContext weak. This instance shouldn't block
// destroying it. When the presContext is being destroyed, it's notified to
// nsIMEStateManager::OnDestroyPresContext(), and then, it destroy
// this instance.
nsPresContext* mPresContext;
nsCOMPtr<nsINode> mNode;
// mNativeContext stores a opaque pointer. This works as the "ID" for this
// composition. Don't access the instance, it may not be available.
void* mNativeContext;
// Hide the default constructor
TextComposition() {}
/**
* DispatchEvent() dispatches the aEvent to the mContent synchronously.
* The caller must ensure that it's safe to dispatch the event.
*/
void DispatchEvent(nsGUIEvent* aEvent,
nsEventStatus* aStatus,
nsDispatchingCallback* aCallBack);
};
/**
* TextCompositionArray manages the instances of TextComposition class.
* Managing with array is enough because only one composition is typically
* there. Even if user switches native IME context, it's very rare that
* second or more composition is started.
* It's assumed that this is used by nsIMEStateManager for storing all active
* compositions in the process. If the instance is it, each TextComposition
* in the array can be destroyed by calling some methods of itself.
*/
class TextCompositionArray MOZ_FINAL : public nsAutoTArray<TextComposition, 2>
{
public:
index_type IndexOf(nsIWidget* aWidget);
index_type IndexOf(nsPresContext* aPresContext);
index_type IndexOf(nsPresContext* aPresContext, nsINode* aNode);
TextComposition* GetCompositionFor(nsIWidget* aWidget);
TextComposition* GetCompositionFor(nsPresContext* aPresContext);
TextComposition* GetCompositionFor(nsPresContext* aPresContext,
nsINode* aNode);
TextComposition* GetCompositionInContent(nsPresContext* aPresContext,
nsIContent* aContent);
};
} // namespace mozilla
#endif // #ifndef mozilla_TextComposition_h

Просмотреть файл

@ -4886,6 +4886,8 @@ nsEventStateManager::ContentRemoved(nsIDocument* aDocument, nsIContent* aContent
element->LeaveLink(element->GetPresContext());
}
nsIMEStateManager::OnRemoveContent(mPresContext, aContent);
// inform the focus manager that the content is being removed. If this
// content is focused, the focus will be removed without firing events.
nsFocusManager* fm = nsFocusManager::GetFocusManager();

Просмотреть файл

@ -34,7 +34,10 @@
#include "nsIForm.h"
#include "nsHTMLFormElement.h"
#include "mozilla/Attributes.h"
#include "nsEventDispatcher.h"
#include "TextComposition.h"
using namespace mozilla;
using namespace mozilla::widget;
/******************************************************************/
@ -47,11 +50,33 @@ bool nsIMEStateManager::sInstalledMenuKeyboardListener = false;
bool nsIMEStateManager::sInSecureInputMode = false;
nsTextStateManager* nsIMEStateManager::sTextStateObserver = nullptr;
TextCompositionArray* nsIMEStateManager::sTextCompositions = nullptr;
void
nsIMEStateManager::Shutdown()
{
MOZ_ASSERT(!sTextCompositions || !sTextCompositions->Length());
delete sTextCompositions;
sTextCompositions = nullptr;
}
nsresult
nsIMEStateManager::OnDestroyPresContext(nsPresContext* aPresContext)
{
NS_ENSURE_ARG_POINTER(aPresContext);
// First, if there is a composition in the aPresContext, clean up it.
if (sTextCompositions) {
TextCompositionArray::index_type i =
sTextCompositions->IndexOf(aPresContext);
if (i != TextCompositionArray::NoIndex) {
// there should be only one composition per presContext object.
sTextCompositions->RemoveElementAt(i);
MOZ_ASSERT(sTextCompositions->IndexOf(aPresContext) ==
TextCompositionArray::NoIndex);
}
}
if (aPresContext != sPresContext)
return NS_OK;
nsCOMPtr<nsIWidget> widget = GetWidget(sPresContext);
@ -72,17 +97,35 @@ nsIMEStateManager::OnRemoveContent(nsPresContext* aPresContext,
nsIContent* aContent)
{
NS_ENSURE_ARG_POINTER(aPresContext);
// First, if there is a composition in the aContent, clean up it.
if (sTextCompositions) {
TextComposition* compositionInContent =
sTextCompositions->GetCompositionInContent(aPresContext, aContent);
if (compositionInContent) {
// Try resetting the native IME state. Be aware, typically, this method
// is called during the content being removed. Then, the native
// composition events which are caused by following APIs are ignored due
// to unsafe to run script (in PresShell::HandleEvent()).
nsCOMPtr<nsIWidget> widget = aPresContext->GetNearestWidget();
if (widget) {
nsresult rv = widget->CancelIMEComposition();
if (NS_FAILED(rv)) {
widget->ResetInputState();
}
// WARNING: the |compositionInContent| may have been destroyed.
}
}
}
if (!sPresContext || !sContent ||
aPresContext != sPresContext ||
aContent != sContent)
!nsContentUtils::ContentIsDescendantOf(sContent, aContent)) {
return NS_OK;
}
// Current IME transaction should commit
nsCOMPtr<nsIWidget> widget = GetWidget(sPresContext);
if (widget) {
nsresult rv = widget->CancelIMEComposition();
if (NS_FAILED(rv))
widget->ResetInputState();
IMEState newState = GetNewIMEState(sPresContext, nullptr);
InputContextAction action(InputContextAction::CAUSE_UNKNOWN,
InputContextAction::LOST_FOCUS);
@ -384,6 +427,61 @@ nsIMEStateManager::GetWidget(nsPresContext* aPresContext)
return widget;
}
void
nsIMEStateManager::EnsureTextCompositionArray()
{
if (sTextCompositions) {
return;
}
sTextCompositions = new TextCompositionArray();
}
void
nsIMEStateManager::DispatchCompositionEvent(nsINode* aEventTargetNode,
nsPresContext* aPresContext,
nsEvent* aEvent,
nsEventStatus* aStatus,
nsDispatchingCallback* aCallBack)
{
MOZ_ASSERT(aEvent->eventStructType == NS_COMPOSITION_EVENT ||
aEvent->eventStructType == NS_TEXT_EVENT);
if (!NS_IS_TRUSTED_EVENT(aEvent) ||
(aEvent->flags & NS_EVENT_FLAG_STOP_DISPATCH) != 0) {
return;
}
EnsureTextCompositionArray();
nsGUIEvent* GUIEvent = static_cast<nsGUIEvent*>(aEvent);
TextComposition* composition =
sTextCompositions->GetCompositionFor(GUIEvent->widget);
if (!composition) {
MOZ_ASSERT(GUIEvent->message == NS_COMPOSITION_START);
TextComposition newComposition(aPresContext, aEventTargetNode, GUIEvent);
composition = sTextCompositions->AppendElement(newComposition);
}
#ifdef DEBUG
else {
MOZ_ASSERT(GUIEvent->message != NS_COMPOSITION_START);
}
#endif // #ifdef DEBUG
// Dispatch the event on composing target.
composition->DispatchEvent(GUIEvent, aStatus, aCallBack);
// WARNING: the |composition| might have been destroyed already.
// Remove the ended composition from the array.
if (aEvent->message == NS_COMPOSITION_END) {
TextCompositionArray::index_type i =
sTextCompositions->IndexOf(GUIEvent->widget);
if (i != TextCompositionArray::NoIndex) {
sTextCompositions->RemoveElementAt(i);
}
}
}
// nsTextStateManager notifies widget of any text and selection changes
// in the currently focused editor

Просмотреть файл

@ -9,13 +9,19 @@
#include "nscore.h"
#include "nsIWidget.h"
class nsDispatchingCallback;
class nsIContent;
class nsIDOMMouseEvent;
class nsINode;
class nsPIDOMWindow;
class nsPresContext;
class nsTextStateManager;
class nsISelection;
namespace mozilla {
class TextCompositionArray;
} // namespace mozilla
/*
* IME state manager
*/
@ -28,6 +34,8 @@ protected:
typedef mozilla::widget::InputContextAction InputContextAction;
public:
static void Shutdown();
static nsresult OnDestroyPresContext(nsPresContext* aPresContext);
static nsresult OnRemoveContent(nsPresContext* aPresContext,
nsIContent* aContent);
@ -75,6 +83,19 @@ public:
nsIContent* aContent,
nsIDOMMouseEvent* aMouseEvent);
/**
* All DOM composition events and DOM text events must be dispatched via
* DispatchCompositionEvent() for storing the composition target
* and ensuring a set of composition events must be fired the stored target.
* If the stored composition event target is destroying, this removes the
* stored composition automatically.
*/
static void DispatchCompositionEvent(nsINode* aEventTargetNode,
nsPresContext* aPresContext,
nsEvent* aEvent,
nsEventStatus* aStatus,
nsDispatchingCallback* aCallBack);
protected:
static nsresult OnChangeFocusInternal(nsPresContext* aPresContext,
nsIContent* aContent,
@ -87,6 +108,7 @@ protected:
nsIContent* aContent);
static nsIWidget* GetWidget(nsPresContext* aPresContext);
static void EnsureTextCompositionArray();
static nsIContent* sContent;
static nsPresContext* sPresContext;
@ -94,6 +116,12 @@ protected:
static bool sInSecureInputMode;
static nsTextStateManager* sTextStateObserver;
// All active compositions in the process are stored by this array.
// When you get an item of this array and use it, please be careful.
// The instances in this array can be destroyed automatically if you do
// something to cause committing or canceling the composition.
static mozilla::TextCompositionArray* sTextCompositions;
};
#endif // nsIMEStateManager_h__

Просмотреть файл

@ -812,13 +812,6 @@ nsFocusManager::ContentRemoved(nsIDocument* aDocument, nsIContent* aContent)
bool shouldShowFocusRing = window->ShouldShowFocusRing();
window->SetFocusedNode(nullptr);
nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
if (docShell) {
nsCOMPtr<nsIPresShell> presShell;
docShell->GetPresShell(getter_AddRefs(presShell));
nsIMEStateManager::OnRemoveContent(presShell->GetPresContext(), content);
}
// if this window is currently focused, clear the global focused
// element as well, but don't fire any events.
if (window == mFocusedWindow) {

Просмотреть файл

@ -1170,6 +1170,15 @@ nsPresContext::GetToplevelContentDocumentPresContext()
}
}
nsIWidget*
nsPresContext::GetNearestWidget(nsPoint* aOffset)
{
NS_ENSURE_TRUE(mShell, nullptr);
nsIFrame* frame = mShell->GetRootFrame();
NS_ENSURE_TRUE(frame, nullptr);
return frame->GetView()->GetNearestWidget(aOffset);
}
// We may want to replace this with something faster, maybe caching the root prescontext
nsRootPresContext*
nsPresContext::GetRootPresContext()

Просмотреть файл

@ -190,6 +190,16 @@ public:
*/
nsPresContext* GetToplevelContentDocumentPresContext();
/**
* Returns the nearest widget for the root frame of this.
*
* @param aOffset If non-null the offset from the origin of the root
* frame's view to the widget's origin (usually positive)
* expressed in appunits of this will be returned in
* aOffset.
*/
nsIWidget* GetNearestWidget(nsPoint* aOffset = nullptr);
/**
* Return the presentation context for the root of the view manager
* hierarchy that contains this presentation context, or nullptr if it can't

Просмотреть файл

@ -73,6 +73,7 @@
#include "nsViewsCID.h"
#include "nsFrameManager.h"
#include "nsEventStateManager.h"
#include "nsIMEStateManager.h"
#include "nsXPCOM.h"
#include "nsISupportsPrimitives.h"
#include "nsILayoutHistoryState.h"
@ -6435,23 +6436,33 @@ PresShell::HandleEventInternal(nsEvent* aEvent, nsEventStatus* aStatus)
nsPresShellEventCB eventCB(this);
if (aEvent->eventStructType == NS_TOUCH_EVENT) {
DispatchTouchEvent(aEvent, aStatus, &eventCB, touchIsNew);
}
else if (mCurrentEventContent) {
nsEventDispatcher::Dispatch(mCurrentEventContent, mPresContext,
aEvent, nullptr, aStatus, &eventCB);
}
else {
nsCOMPtr<nsIContent> targetContent;
if (mCurrentEventFrame) {
rv = mCurrentEventFrame->GetContentForEvent(aEvent,
getter_AddRefs(targetContent));
} else {
nsCOMPtr<nsINode> eventTarget = mCurrentEventContent.get();
nsPresShellEventCB* eventCBPtr = &eventCB;
if (!eventTarget) {
nsCOMPtr<nsIContent> targetContent;
if (mCurrentEventFrame) {
rv = mCurrentEventFrame->
GetContentForEvent(aEvent, getter_AddRefs(targetContent));
}
if (NS_SUCCEEDED(rv) && targetContent) {
eventTarget = do_QueryInterface(targetContent);
} else if (mDocument) {
eventTarget = do_QueryInterface(mDocument);
// If we don't have any content, the callback wouldn't probably
// do nothing.
eventCBPtr = nullptr;
}
}
if (NS_SUCCEEDED(rv) && targetContent) {
nsEventDispatcher::Dispatch(targetContent, mPresContext, aEvent,
nullptr, aStatus, &eventCB);
} else if (mDocument) {
nsEventDispatcher::Dispatch(mDocument, mPresContext, aEvent,
nullptr, aStatus, nullptr);
if (eventTarget) {
if (aEvent->eventStructType == NS_COMPOSITION_EVENT ||
aEvent->eventStructType == NS_TEXT_EVENT) {
nsIMEStateManager::DispatchCompositionEvent(eventTarget,
mPresContext, aEvent, aStatus, eventCBPtr);
} else {
nsEventDispatcher::Dispatch(eventTarget, mPresContext,
aEvent, nullptr, aStatus, eventCBPtr);
}
}
}
}

Просмотреть файл

@ -13,18 +13,7 @@
iframe.focus();
doc.body.focus();
win.getSelection().collapse(doc.body, 0);
synthesizeText(
{ "composition":
{
"string": "Here's some text.",
"clauses":
[
{ "length": 17, "attr": COMPOSITION_ATTR_RAWINPUT }
]
},
"caret": { "start": 2, "length": 0 }
}
)
sendString("Here's some text.");
synthesizeKey("VK_RETURN", {});
document.documentElement.removeAttribute("class");
}

Просмотреть файл

@ -103,6 +103,7 @@
#include "nsCookieService.h"
#include "nsApplicationCacheService.h"
#include "mozilla/dom/time/DateCacheCleaner.h"
#include "nsIMEStateManager.h"
extern void NS_ShutdownChainItemPool();
@ -284,6 +285,7 @@ nsLayoutStatics::Shutdown()
txMozillaXSLTProcessor::Shutdown();
nsDOMAttribute::Shutdown();
nsEventListenerManager::Shutdown();
nsIMEStateManager::Shutdown();
nsComputedDOMStyle::Shutdown();
nsCSSParser::Shutdown();
nsCSSRuleProcessor::Shutdown();

Просмотреть файл

@ -86,20 +86,47 @@ function sendMouseEvent(aEvent, aTarget, aWindow) {
* chars (sends the right charcode, and sends a shift key for uppercase chars).
* No other modifiers are handled at this point.
*
* For now this method only works for English letters (lower and upper case)
* and the digits 0-9.
* For now this method only works for ASCII characters and emulates the shift
* key state on US keyboard layout.
*/
function sendChar(aChar, aWindow) {
// DOM event charcodes match ASCII (JS charcodes) for a-zA-Z0-9.
var hasShift = (aChar == aChar.toUpperCase());
var hasShift;
// Emulate US keyboard layout for the shiftKey state.
switch (aChar) {
case "!":
case "@":
case "#":
case "$":
case "%":
case "^":
case "&":
case "*":
case "(":
case ")":
case "_":
case "+":
case "{":
case "}":
case ":":
case "\"":
case "|":
case "<":
case ">":
case "?":
hasShift = true;
break;
default:
hasShift = (aChar == aChar.toUpperCase());
break;
}
synthesizeKey(aChar, { shiftKey: hasShift }, aWindow);
}
/**
* Send the string aStr to the focused element.
*
* For now this method only works for English letters (lower and upper case)
* and the digits 0-9.
* For now this method only works for ASCII characters and emulates the shift
* key state on US keyboard layout.
*/
function sendString(aStr, aWindow) {
for (var i = 0; i < aStr.length; ++i) {

Просмотреть файл

@ -115,6 +115,7 @@ function starttest() {
$("testKeyEvent").value = "";
/* test synthesizeComposition */
$("textBoxB").focus();
check = false;
window.addEventListener("compositionstart", function() { check = true; }, false);
synthesizeComposition({ type: "compositionstart" });
@ -122,30 +123,41 @@ function starttest() {
check = false;
window.addEventListener("compositionupdate", function() { check = true; }, false);
synthesizeComposition({ type: "compositionupdate" });
synthesizeComposition({ type: "compositionupdate", data: "a" });
is(check, true, 'synthesizeComposition() should dispatch compositionupdate');
check = false;
window.addEventListener("compositionend", function() { check = true; }, false);
synthesizeComposition({ type: "compositionend" });
is(check, true, 'synthesizeComposition() should dispatch compositionend');
check = false;
$("textBoxB").focus();
const nsIDOMWindowUtils = SpecialPowers.Ci.nsIDOMWindowUtils;
window.addEventListener("text", function() { check = true; }, false);
synthesizeText(
{ "composition":
{ "string": "a",
"clauses":
[
{ "length": 1, "attr": nsIDOMWindowUtils.COMPOSITION_ATTR_RAWINPUT }
{ "length": 1, "attr": COMPOSITION_ATTR_RAWINPUT }
]
},
"caret": { "start": 1, "length": 0 }
}
);
is(check, false, "synthesizeText shouldn't start or end composition");
is($("textBoxB").value, "a", "synthesizeText should send text");
is(check, true, "synthesizeText should dispatch text event");
synthesizeText(
{ "composition":
{ "string": "a",
"clauses":
[
{ "length": 0, "attr": 0 }
]
},
"caret": { "start": 1, "length": 0 }
}
);
check = false;
window.addEventListener("compositionend", function() { check = true; }, false);
synthesizeComposition({ type: "compositionend", data: "a" });
is(check, true, 'synthesizeComposition() should dispatch compositionend');
var querySelectedText = synthesizeQuerySelectedText();
ok(querySelectedText, "query selected text event result is null");
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); //get permission to check members of querySelectedText