author | Masayuki Nakano <masayuki@d-toybox.com> |
Thu, 19 Feb 2015 15:50:20 +0900 | |
changeset 258625 | bcf39cf754249c23b5204fd091ba73475ba9bd31 |
parent 258624 | 0ea20218201750faf0c76202e58fb8c18a2dd628 |
child 258626 | faa6c7dc1dd121f9c6634275e718fa21a178e57d |
push id | 721 |
push user | jlund@mozilla.com |
push date | Tue, 21 Apr 2015 23:03:33 +0000 |
treeherder | mozilla-release@d27c9211ebb3 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | smaug, smaug |
bugs | 1119609 |
milestone | 38.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
|
--- a/dom/base/TextInputProcessor.cpp +++ b/dom/base/TextInputProcessor.cpp @@ -73,16 +73,31 @@ TextInputProcessor::~TextInputProcessor( if (NS_SUCCEEDED(IsValidStateForComposition())) { nsRefPtr<TextEventDispatcher> kungFuDeathGrip(mDispatcher); nsEventStatus status = nsEventStatus_eIgnore; mDispatcher->CommitComposition(status, &EmptyString()); } } } +bool +TextInputProcessor::IsComposing() const +{ + return mDispatcher && mDispatcher->IsComposing(); +} + +NS_IMETHODIMP +TextInputProcessor::GetHasComposition(bool* aHasComposition) +{ + MOZ_RELEASE_ASSERT(aHasComposition, "aHasComposition must not be nullptr"); + MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome()); + *aHasComposition = IsComposing(); + return NS_OK; +} + NS_IMETHODIMP TextInputProcessor::BeginInputTransaction( nsIDOMWindow* aWindow, nsITextInputProcessorCallback* aCallback, bool* aSucceeded) { MOZ_RELEASE_ASSERT(aSucceeded, "aSucceeded must not be nullptr"); MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome()); @@ -210,34 +225,168 @@ TextInputProcessor::IsValidStateForCompo nsresult rv = mDispatcher->GetState(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } +bool +TextInputProcessor::IsValidEventTypeForComposition( + const WidgetKeyboardEvent& aKeyboardEvent) const +{ + // The key event type of composition methods must be "" or "keydown". + if (aKeyboardEvent.message == NS_KEY_DOWN) { + return true; + } + if (aKeyboardEvent.message == NS_USER_DEFINED_EVENT && + aKeyboardEvent.userType && + nsDependentAtomString(aKeyboardEvent.userType).EqualsLiteral("on")) { + return true; + } + return false; +} + +TextInputProcessor::EventDispatcherResult +TextInputProcessor::MaybeDispatchKeydownForComposition( + const WidgetKeyboardEvent* aKeyboardEvent, + uint32_t aKeyFlags) +{ + EventDispatcherResult result; + + result.mResult = IsValidStateForComposition(); + if (NS_WARN_IF(NS_FAILED(result.mResult))) { + result.mCanContinue = false; + return result; + } + + if (!aKeyboardEvent) { + return result; + } + + // Modifier keys are not allowed because managing modifier state in this + // method makes this messy. + if (NS_WARN_IF(aKeyboardEvent->IsModifierKeyEvent())) { + result.mResult = NS_ERROR_INVALID_ARG; + result.mCanContinue = false; + return result; + } + + result.mResult = KeydownInternal(*aKeyboardEvent, aKeyFlags, false, + result.mDoDefault); + if (NS_WARN_IF(NS_FAILED(result.mResult))) { + result.mCanContinue = false; + return result; + } + + result.mCanContinue = NS_SUCCEEDED(IsValidStateForComposition()); + return result; +} + +TextInputProcessor::EventDispatcherResult +TextInputProcessor::MaybeDispatchKeyupForComposition( + const WidgetKeyboardEvent* aKeyboardEvent, + uint32_t aKeyFlags) +{ + EventDispatcherResult result; + + if (!aKeyboardEvent) { + return result; + } + + // If the message is NS_KEY_DOWN, the caller doesn't want TIP to dispatch + // keyup event. + if (aKeyboardEvent->message == NS_KEY_DOWN) { + return result; + } + + // If the widget has been destroyed, we can do nothing here. + result.mResult = IsValidStateForComposition(); + if (NS_FAILED(result.mResult)) { + result.mCanContinue = false; + return result; + } + + result.mResult = KeyupInternal(*aKeyboardEvent, aKeyFlags, result.mDoDefault); + if (NS_WARN_IF(NS_FAILED(result.mResult))) { + result.mCanContinue = false; + return result; + } + + result.mCanContinue = NS_SUCCEEDED(IsValidStateForComposition()); + return result; +} + +nsresult +TextInputProcessor::PrepareKeyboardEventForComposition( + nsIDOMKeyEvent* aDOMKeyEvent, + uint32_t& aKeyFlags, + uint8_t aOptionalArgc, + WidgetKeyboardEvent*& aKeyboardEvent) +{ + aKeyboardEvent = nullptr; + + aKeyboardEvent = + aOptionalArgc && aDOMKeyEvent ? + aDOMKeyEvent->GetInternalNSEvent()->AsKeyboardEvent() : nullptr; + if (!aKeyboardEvent || aOptionalArgc < 2) { + aKeyFlags = 0; + } + + if (!aKeyboardEvent) { + return NS_OK; + } + + if (NS_WARN_IF(!IsValidEventTypeForComposition(*aKeyboardEvent))) { + return NS_ERROR_INVALID_ARG; + } + + return NS_OK; +} + NS_IMETHODIMP -TextInputProcessor::StartComposition(bool* aSucceeded) +TextInputProcessor::StartComposition(nsIDOMKeyEvent* aDOMKeyEvent, + uint32_t aKeyFlags, + uint8_t aOptionalArgc, + bool* aSucceeded) { MOZ_RELEASE_ASSERT(aSucceeded, "aSucceeded must not be nullptr"); MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome()); *aSucceeded = false; + nsRefPtr<TextEventDispatcher> kungfuDeathGrip(mDispatcher); - nsresult rv = IsValidStateForComposition(); + + WidgetKeyboardEvent* keyboardEvent; + nsresult rv = + PrepareKeyboardEventForComposition(aDOMKeyEvent, aKeyFlags, aOptionalArgc, + keyboardEvent); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - nsEventStatus status = nsEventStatus_eIgnore; - rv = mDispatcher->StartComposition(status); + + EventDispatcherResult dispatcherResult = + MaybeDispatchKeydownForComposition(keyboardEvent, aKeyFlags); + if (NS_WARN_IF(NS_FAILED(dispatcherResult.mResult)) || + !dispatcherResult.mCanContinue) { + return dispatcherResult.mResult; + } + + if (dispatcherResult.mDoDefault) { + nsEventStatus status = nsEventStatus_eIgnore; + rv = mDispatcher->StartComposition(status); + *aSucceeded = status != nsEventStatus_eConsumeNoDefault && + mDispatcher && mDispatcher->IsComposing(); + } + + MaybeDispatchKeyupForComposition(keyboardEvent, aKeyFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - *aSucceeded = status != nsEventStatus_eConsumeNoDefault && - mDispatcher && mDispatcher->IsComposing(); return NS_OK; } NS_IMETHODIMP TextInputProcessor::SetPendingCompositionString(const nsAString& aString) { MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome()); nsRefPtr<TextEventDispatcher> kungfuDeathGrip(mDispatcher); @@ -278,87 +427,176 @@ TextInputProcessor::SetCaretInPendingCom nsresult rv = IsValidStateForComposition(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return mDispatcher->SetCaretInPendingComposition(aOffset, 0); } NS_IMETHODIMP -TextInputProcessor::FlushPendingComposition(bool* aSucceeded) +TextInputProcessor::FlushPendingComposition(nsIDOMKeyEvent* aDOMKeyEvent, + uint32_t aKeyFlags, + uint8_t aOptionalArgc, + bool* aSucceeded) { MOZ_RELEASE_ASSERT(aSucceeded, "aSucceeded must not be nullptr"); MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome()); + + // Even if this doesn't flush pending composition actually, we need to reset + // pending composition for starting next composition with new user input. + AutoPendingCompositionResetter resetter(this); + *aSucceeded = false; nsRefPtr<TextEventDispatcher> kungfuDeathGrip(mDispatcher); - nsresult rv = IsValidStateForComposition(); + bool wasComposing = IsComposing(); + + WidgetKeyboardEvent* keyboardEvent; + nsresult rv = + PrepareKeyboardEventForComposition(aDOMKeyEvent, aKeyFlags, aOptionalArgc, + keyboardEvent); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - nsEventStatus status = nsEventStatus_eIgnore; - rv = mDispatcher->FlushPendingComposition(status); + + EventDispatcherResult dispatcherResult = + MaybeDispatchKeydownForComposition(keyboardEvent, aKeyFlags); + if (NS_WARN_IF(NS_FAILED(dispatcherResult.mResult)) || + !dispatcherResult.mCanContinue) { + return dispatcherResult.mResult; + } + + // Even if the preceding keydown event was consumed, if the composition + // was already started, we shouldn't prevent the change of composition. + if (dispatcherResult.mDoDefault || wasComposing) { + // Preceding keydown event may cause destroying the widget. + if (NS_FAILED(IsValidStateForComposition())) { + return NS_OK; + } + nsEventStatus status = nsEventStatus_eIgnore; + rv = mDispatcher->FlushPendingComposition(status); + *aSucceeded = status != nsEventStatus_eConsumeNoDefault; + } + + MaybeDispatchKeyupForComposition(keyboardEvent, aKeyFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - *aSucceeded = status != nsEventStatus_eConsumeNoDefault; - return rv; + return NS_OK; } NS_IMETHODIMP -TextInputProcessor::CommitComposition(const nsAString& aCommitString, +TextInputProcessor::CommitComposition(nsIDOMKeyEvent* aDOMKeyEvent, + uint32_t aKeyFlags, + const nsAString& aCommitString, uint8_t aOptionalArgc, bool* aSucceeded) { MOZ_RELEASE_ASSERT(aSucceeded, "aSucceeded must not be nullptr"); MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome()); + + WidgetKeyboardEvent* keyboardEvent; + nsresult rv = + PrepareKeyboardEventForComposition(aDOMKeyEvent, aKeyFlags, aOptionalArgc, + keyboardEvent); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + const nsAString* commitString = - aOptionalArgc >= 1 ? &aCommitString : nullptr; - return CommitCompositionInternal(commitString, aSucceeded); + aOptionalArgc < 3 ? nullptr: &aCommitString; + + return CommitCompositionInternal(keyboardEvent, aKeyFlags, + commitString, aSucceeded); } nsresult -TextInputProcessor::CommitCompositionInternal(const nsAString* aCommitString, - bool* aSucceeded) +TextInputProcessor::CommitCompositionInternal( + const WidgetKeyboardEvent* aKeyboardEvent, + uint32_t aKeyFlags, + const nsAString* aCommitString, + bool* aSucceeded) { if (aSucceeded) { *aSucceeded = false; } nsRefPtr<TextEventDispatcher> kungfuDeathGrip(mDispatcher); - nsresult rv = IsValidStateForComposition(); + bool wasComposing = IsComposing(); + + EventDispatcherResult dispatcherResult = + MaybeDispatchKeydownForComposition(aKeyboardEvent, aKeyFlags); + if (NS_WARN_IF(NS_FAILED(dispatcherResult.mResult)) || + !dispatcherResult.mCanContinue) { + return dispatcherResult.mResult; + } + + // Even if the preceding keydown event was consumed, if the composition + // was already started, we shouldn't prevent the commit of composition. + nsresult rv = NS_OK; + if (dispatcherResult.mDoDefault || wasComposing) { + // Preceding keydown event may cause destroying the widget. + if (NS_FAILED(IsValidStateForComposition())) { + return NS_OK; + } + nsEventStatus status = nsEventStatus_eIgnore; + rv = mDispatcher->CommitComposition(status, aCommitString); + if (aSucceeded) { + *aSucceeded = status != nsEventStatus_eConsumeNoDefault; + } + } + + MaybeDispatchKeyupForComposition(aKeyboardEvent, aKeyFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - nsEventStatus status = nsEventStatus_eIgnore; - rv = mDispatcher->CommitComposition(status, aCommitString); - if (NS_WARN_IF(NS_FAILED(rv))) { - return rv; - } - if (aSucceeded) { - *aSucceeded = status != nsEventStatus_eConsumeNoDefault; - } - return rv; + return NS_OK; } NS_IMETHODIMP -TextInputProcessor::CancelComposition() +TextInputProcessor::CancelComposition(nsIDOMKeyEvent* aDOMKeyEvent, + uint32_t aKeyFlags, + uint8_t aOptionalArgc) { MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome()); - return CancelCompositionInternal(); + + WidgetKeyboardEvent* keyboardEvent; + nsresult rv = + PrepareKeyboardEventForComposition(aDOMKeyEvent, aKeyFlags, aOptionalArgc, + keyboardEvent); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return CancelCompositionInternal(keyboardEvent, aKeyFlags); } nsresult -TextInputProcessor::CancelCompositionInternal() +TextInputProcessor::CancelCompositionInternal( + const WidgetKeyboardEvent* aKeyboardEvent, + uint32_t aKeyFlags) { nsRefPtr<TextEventDispatcher> kungfuDeathGrip(mDispatcher); - nsresult rv = IsValidStateForComposition(); + + EventDispatcherResult dispatcherResult = + MaybeDispatchKeydownForComposition(aKeyboardEvent, aKeyFlags); + if (NS_WARN_IF(NS_FAILED(dispatcherResult.mResult)) || + !dispatcherResult.mCanContinue) { + return dispatcherResult.mResult; + } + + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = mDispatcher->CommitComposition(status, &EmptyString()); + + MaybeDispatchKeyupForComposition(aKeyboardEvent, aKeyFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - nsEventStatus status = nsEventStatus_eIgnore; - return mDispatcher->CommitComposition(status, &EmptyString()); + return NS_OK; } NS_IMETHODIMP TextInputProcessor::NotifyIME(TextEventDispatcher* aTextEventDispatcher, const IMENotification& aNotification) { // If This is called while this is being initialized, ignore the call. if (!mDispatcher) { @@ -490,33 +728,43 @@ TextInputProcessor::Keydown(nsIDOMKeyEve uint8_t aOptionalArgc, bool* aDoDefault) { MOZ_RELEASE_ASSERT(aDoDefault, "aDoDefault must not be nullptr"); MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome()); if (!aOptionalArgc) { aKeyFlags = 0; } - *aDoDefault = false; if (NS_WARN_IF(!aDOMKeyEvent)) { return NS_ERROR_INVALID_ARG; } - WidgetKeyboardEvent* originalKeyEvent = aDOMKeyEvent->GetInternalNSEvent()->AsKeyboardEvent(); if (NS_WARN_IF(!originalKeyEvent)) { return NS_ERROR_INVALID_ARG; } + return KeydownInternal(*originalKeyEvent, aKeyFlags, true, *aDoDefault); +} + +nsresult +TextInputProcessor::KeydownInternal(const WidgetKeyboardEvent& aKeyboardEvent, + uint32_t aKeyFlags, + bool aAllowToDispatchKeypress, + bool& aDoDefault) +{ + aDoDefault = false; + // We shouldn't modify the internal WidgetKeyboardEvent. - WidgetKeyboardEvent keyEvent(*originalKeyEvent); + WidgetKeyboardEvent keyEvent(aKeyboardEvent); nsresult rv = PrepareKeyboardEventToDispatch(keyEvent, aKeyFlags); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - *aDoDefault = !(aKeyFlags & KEY_DEFAULT_PREVENTED); + + aDoDefault = !(aKeyFlags & KEY_DEFAULT_PREVENTED); if (WidgetKeyboardEvent::GetModifierForKeyName(keyEvent.mKeyNameIndex)) { ModifierKeyData modifierKeyData(keyEvent); if (WidgetKeyboardEvent::IsLockableModifier(keyEvent.mKeyNameIndex)) { // If the modifier key is lockable modifier key such as CapsLock, // let's toggle modifier key state at keydown. ToggleModifierKey(modifierKeyData); } else { @@ -533,58 +781,69 @@ TextInputProcessor::Keydown(nsIDOMKeyEve keyEvent.modifiers = GetActiveModifiers(); nsRefPtr<TextEventDispatcher> kungfuDeathGrip(mDispatcher); rv = IsValidStateForComposition(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - nsEventStatus status = *aDoDefault ? nsEventStatus_eIgnore : - nsEventStatus_eConsumeNoDefault; + nsEventStatus status = aDoDefault ? nsEventStatus_eIgnore : + nsEventStatus_eConsumeNoDefault; if (!mDispatcher->DispatchKeyboardEvent(NS_KEY_DOWN, keyEvent, status)) { // If keydown event isn't dispatched, we don't need to dispatch keypress // events. return NS_OK; } - mDispatcher->MaybeDispatchKeypressEvents(keyEvent, status); + if (aAllowToDispatchKeypress) { + mDispatcher->MaybeDispatchKeypressEvents(keyEvent, status); + } - *aDoDefault = (status != nsEventStatus_eConsumeNoDefault); + aDoDefault = (status != nsEventStatus_eConsumeNoDefault); return NS_OK; } NS_IMETHODIMP TextInputProcessor::Keyup(nsIDOMKeyEvent* aDOMKeyEvent, uint32_t aKeyFlags, uint8_t aOptionalArgc, bool* aDoDefault) { MOZ_RELEASE_ASSERT(aDoDefault, "aDoDefault must not be nullptr"); MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome()); if (!aOptionalArgc) { aKeyFlags = 0; } - *aDoDefault = false; if (NS_WARN_IF(!aDOMKeyEvent)) { return NS_ERROR_INVALID_ARG; } - WidgetKeyboardEvent* originalKeyEvent = aDOMKeyEvent->GetInternalNSEvent()->AsKeyboardEvent(); if (NS_WARN_IF(!originalKeyEvent)) { return NS_ERROR_INVALID_ARG; } + return KeyupInternal(*originalKeyEvent, aKeyFlags, *aDoDefault); +} + +nsresult +TextInputProcessor::KeyupInternal(const WidgetKeyboardEvent& aKeyboardEvent, + uint32_t aKeyFlags, + bool& aDoDefault) +{ + aDoDefault = false; + // We shouldn't modify the internal WidgetKeyboardEvent. - WidgetKeyboardEvent keyEvent(*originalKeyEvent); + WidgetKeyboardEvent keyEvent(aKeyboardEvent); nsresult rv = PrepareKeyboardEventToDispatch(keyEvent, aKeyFlags); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - *aDoDefault = !(aKeyFlags & KEY_DEFAULT_PREVENTED); + + aDoDefault = !(aKeyFlags & KEY_DEFAULT_PREVENTED); if (WidgetKeyboardEvent::GetModifierForKeyName(keyEvent.mKeyNameIndex)) { if (!WidgetKeyboardEvent::IsLockableModifier(keyEvent.mKeyNameIndex)) { // Inactivate modifier flag before dispatching keyup event (i.e., keyup // event shouldn't indicate the releasing modifier is active. InactivateModifierKey(ModifierKeyData(keyEvent)); } if (aKeyFlags & KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT) { @@ -596,20 +855,20 @@ TextInputProcessor::Keyup(nsIDOMKeyEvent keyEvent.modifiers = GetActiveModifiers(); nsRefPtr<TextEventDispatcher> kungfuDeathGrip(mDispatcher); rv = IsValidStateForComposition(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - nsEventStatus status = *aDoDefault ? nsEventStatus_eIgnore : - nsEventStatus_eConsumeNoDefault; + nsEventStatus status = aDoDefault ? nsEventStatus_eIgnore : + nsEventStatus_eConsumeNoDefault; mDispatcher->DispatchKeyboardEvent(NS_KEY_UP, keyEvent, status); - *aDoDefault = (status != nsEventStatus_eConsumeNoDefault); + aDoDefault = (status != nsEventStatus_eConsumeNoDefault); return NS_OK; } NS_IMETHODIMP TextInputProcessor::GetModifierState(const nsAString& aModifierKeyName, bool* aActive) { MOZ_RELEASE_ASSERT(aActive, "aActive must not be null"); @@ -636,16 +895,34 @@ TextInputProcessor::ShareModifierStateOf if (!other->mModifierKeyDataArray) { other->mModifierKeyDataArray = new ModifierKeyDataArray(); } mModifierKeyDataArray = other->mModifierKeyDataArray; return NS_OK; } /****************************************************************************** + * TextInputProcessor::AutoPendingCompositionResetter + ******************************************************************************/ +TextInputProcessor::AutoPendingCompositionResetter:: + AutoPendingCompositionResetter(TextInputProcessor* aTIP) + : mTIP(aTIP) +{ + MOZ_RELEASE_ASSERT(mTIP.get(), "mTIP must not be null"); +} + +TextInputProcessor::AutoPendingCompositionResetter:: + ~AutoPendingCompositionResetter() +{ + if (mTIP->mDispatcher) { + mTIP->mDispatcher->ClearPendingComposition(); + } +} + +/****************************************************************************** * TextInputProcessor::ModifierKeyData ******************************************************************************/ TextInputProcessor::ModifierKeyData::ModifierKeyData( const WidgetKeyboardEvent& aKeyboardEvent) : mKeyNameIndex(aKeyboardEvent.mKeyNameIndex) , mCodeNameIndex(aKeyboardEvent.mCodeNameIndex) { mModifier = WidgetKeyboardEvent::GetModifierForKeyName(mKeyNameIndex);
--- a/dom/base/TextInputProcessor.h +++ b/dom/base/TextInputProcessor.h @@ -1,16 +1,17 @@ /* -*- Mode: C++; 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/. */ #ifndef mozilla_dom_textinputprocessor_h_ #define mozilla_dom_textinputprocessor_h_ +#include "mozilla/Attributes.h" #include "mozilla/EventForwards.h" #include "mozilla/TextEventDispatcherListener.h" #include "nsAutoPtr.h" #include "nsITextInputProcessor.h" #include "nsITextInputProcessorCallback.h" #include "nsTArray.h" namespace mozilla { @@ -36,28 +37,82 @@ public: const IMENotification& aNotification) MOZ_OVERRIDE; NS_IMETHOD_(void) OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) MOZ_OVERRIDE; protected: virtual ~TextInputProcessor(); private: + bool IsComposing() const; nsresult BeginInputTransactionInternal( nsIDOMWindow* aWindow, nsITextInputProcessorCallback* aCallback, bool aForTests, bool& aSucceeded); - nsresult CommitCompositionInternal(const nsAString* aCommitString = nullptr, - bool* aSucceeded = nullptr); - nsresult CancelCompositionInternal(); + nsresult CommitCompositionInternal( + const WidgetKeyboardEvent* aKeyboardEvent = nullptr, + uint32_t aKeyFlags = 0, + const nsAString* aCommitString = nullptr, + bool* aSucceeded = nullptr); + nsresult CancelCompositionInternal( + const WidgetKeyboardEvent* aKeyboardEvent = nullptr, + uint32_t aKeyFlags = 0); + nsresult KeydownInternal(const WidgetKeyboardEvent& aKeyboardEvent, + uint32_t aKeyFlags, + bool aAllowToDispatchKeypress, + bool& aDoDefault); + nsresult KeyupInternal(const WidgetKeyboardEvent& aKeyboardEvent, + uint32_t aKeyFlags, + bool& aDoDefault); nsresult IsValidStateForComposition(); void UnlinkFromTextEventDispatcher(); nsresult PrepareKeyboardEventToDispatch(WidgetKeyboardEvent& aKeyboardEvent, uint32_t aKeyFlags); + bool IsValidEventTypeForComposition( + const WidgetKeyboardEvent& aKeyboardEvent) const; + nsresult PrepareKeyboardEventForComposition( + nsIDOMKeyEvent* aDOMKeyEvent, + uint32_t& aKeyFlags, + uint8_t aOptionalArgc, + WidgetKeyboardEvent*& aKeyboardEvent); + + struct EventDispatcherResult + { + nsresult mResult; + bool mDoDefault; + bool mCanContinue; + + EventDispatcherResult() + : mResult(NS_OK) + , mDoDefault(true) + , mCanContinue(true) + { + } + }; + EventDispatcherResult MaybeDispatchKeydownForComposition( + const WidgetKeyboardEvent* aKeyboardEvent, + uint32_t aKeyFlags); + EventDispatcherResult MaybeDispatchKeyupForComposition( + const WidgetKeyboardEvent* aKeyboardEvent, + uint32_t aKeyFlags); + + /** + * AutoPendingCompositionResetter guarantees to clear all pending composition + * data in its destructor. + */ + class MOZ_STACK_CLASS AutoPendingCompositionResetter + { + public: + explicit AutoPendingCompositionResetter(TextInputProcessor* aTIP); + ~AutoPendingCompositionResetter(); + + private: + nsRefPtr<TextInputProcessor> mTIP; + }; /** * TextInputProcessor manages modifier state both with .key and .code. * For example, left shift key up shouldn't cause inactivating shift state * while right shift key is being pressed. */ struct ModifierKeyData {
--- a/dom/base/test/chrome/window_nsITextInputProcessor.xul +++ b/dom/base/test/chrome/window_nsITextInputProcessor.xul @@ -250,52 +250,52 @@ function runBeginInputTransactionMethodT description + "text, compositionend and input events should be fired by TIP1.commitComposition()"); is(events[0].type, "text", description + "events[0] should be text"); is(events[1].type, "compositionend", description + "events[1] should be compositionend"); is(events[2].type, "input", description + "events[2] should be input"); - // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitComposition("bar"). + // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitComposition(null, 0, "bar"). events = []; input.addEventListener("compositionstart", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransaction(window, simpleCallback), - description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.commitComposition(null, 0, \"bar\");"); }, false); input.addEventListener("compositionupdate", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransaction(window, simpleCallback), - description + "TIP2 shouldn't be able to begin input transaction during compositionupdate event handler TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction during compositionupdate event handler TIP1.commitComposition(null, 0, \"bar\");"); }, false); input.addEventListener("text", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransaction(window, simpleCallback), - description + "TIP2 shouldn't be able to begin input transaction during text event handler TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction during text event handler TIP1.commitComposition(null, 0, \"bar\");"); }, false); input.addEventListener("compositionend", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransaction(window, simpleCallback), - description + "TIP2 shouldn't be able to begin input transaction during compositionend event handler TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction during compositionend event handler TIP1.commitComposition(null, 0, \"bar\");"); }, false); input.addEventListener("input", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransaction(window, simpleCallback), - description + "TIP2 shouldn't be able to begin input transaction during input event handler TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction during input event handler TIP1.commitComposition(null, 0, \"bar\");"); }, false); TIP1.beginInputTransaction(window, simpleCallback); - TIP1.commitComposition("bar"); + TIP1.commitComposition(null, 0, "bar"); is(events.length, 5, - description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitComposition(\"bar\")"); + description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitComposition(null, 0, \"bar\")"); is(events[0].type, "compositionstart", description + "events[0] should be compositionstart"); is(events[1].type, "compositionupdate", description + "events[1] should be compositionupdate"); is(events[2].type, "text", description + "events[2] should be text"); is(events[3].type, "compositionend", description + "events[3] should be compositionend"); @@ -470,52 +470,52 @@ function runBeginInputTransactionMethodT description + "text, compositionend and input events should be fired by TIP1.commitComposition()"); is(events[0].type, "text", description + "events[0] should be text"); is(events[1].type, "compositionend", description + "events[1] should be compositionend"); is(events[2].type, "input", description + "events[2] should be input"); - // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitComposition("bar"). + // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitComposition(null, 0, "bar"). events = []; input.addEventListener("compositionstart", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransactionForTests(window), - description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.commitComposition(null, 0, \"bar\");"); }, false); input.addEventListener("compositionupdate", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransactionForTests(window), - description + "TIP2 shouldn't be able to begin input transaction for tests during compositionupdate event handler TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction for tests during compositionupdate event handler TIP1.commitComposition(null, 0, \"bar\");"); }, false); input.addEventListener("text", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransactionForTests(window), - description + "TIP2 shouldn't be able to begin input transaction for tests during text event handler TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction for tests during text event handler TIP1.commitComposition(null, 0, \"bar\");"); }, false); input.addEventListener("compositionend", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransactionForTests(window), - description + "TIP2 shouldn't be able to begin input transaction for tests during compositionend event handler TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction for tests during compositionend event handler TIP1.commitComposition(null, 0, \"bar\");"); }, false); input.addEventListener("input", function (aEvent) { events.push(aEvent); input.removeEventListener(aEvent.type, arguments.callee, false); ok(!TIP2.beginInputTransactionForTests(window), - description + "TIP2 shouldn't be able to begin input transaction for tests during input event handler TIP1.commitComposition(\"bar\");"); + description + "TIP2 shouldn't be able to begin input transaction for tests during input event handler TIP1.commitComposition(null, 0, \"bar\");"); }, false); TIP1.beginInputTransactionForTests(window); - TIP1.commitComposition("bar"); + TIP1.commitComposition(null, 0, "bar"); is(events.length, 5, - description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitComposition(\"bar\")"); + description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitComposition(null, 0, \"bar\")"); is(events[0].type, "compositionstart", description + "events[0] should be compositionstart"); is(events[1].type, "compositionupdate", description + "events[1] should be compositionupdate"); is(events[2].type, "text", description + "events[2] should be text"); is(events[3].type, "compositionend", description + "events[3] should be compositionend"); @@ -640,29 +640,29 @@ function runBeginInputTransactionMethodT } catch (e) { ok(e.message.contains("NS_ERROR_NOT_INITIALIZED"), description + "TIP1.flushPendingComposition() should cause throwing an exception including NS_ERROR_NOT_INITIALIZED"); } finally { is(input.value, "", description + "The input element should not have commit string"); } - // Let's check if commitComposition("bar") throws an exception after ownership is stolen. + // Let's check if commitComposition(null, 0, "bar") throws an exception after ownership is stolen. ok(TIP1.beginInputTransactionForTests(window), description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition"); ok(TIP2.beginInputTransactionForTests(window), description + "TIP2.beginInputTransactionForTests() should succeed because there is no composition"); input.value = ""; try { - TIP1.commitComposition("bar"); + TIP1.commitComposition(null, 0, "bar"); ok(false, - description + "TIP1.commitComposition(\"bar\") should cause throwing an exception because TIP2 took the ownership"); + description + "TIP1.commitComposition(null, 0, \"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"); + description + "TIP1.commitComposition(null, 0, \"bar\") should cause throwing an exception including NS_ERROR_NOT_INITIALIZED"); } finally { is(input.value, "", description + "The input element should not have commit string"); } // Let's check if keydown() throws an exception after ownership is stolen. ok(TIP1.beginInputTransactionForTests(window), description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition"); @@ -946,17 +946,17 @@ function runCompositionTests() description + "modifying composition string from empty string should cause compositionupdate"); is(events[0].data, "buzz", description + "compositionupdate caused by modifying composition string from empty string should have new composition string in its data"); is(input.value, "FOobuzz", description + "new composition string should be appended to the focused editor"); // Committing with different string reset(); - TIP.commitComposition("bar"); + TIP.commitComposition(null, 0, "bar"); is(events.length, 2, description + "committing with different string should cause compositionupdate and compositionend"); is(events[0].type, "compositionupdate", description + "committing with different string should cause compositionupdate first"); is(events[0].data, "bar", description + "compositionupdate caused by committing with different string should have the committing string in its data"); is(events[1].type, "compositionend", description + "committing with different string should cause compositionend after compositionupdate"); @@ -969,29 +969,29 @@ function runCompositionTests() TIP.setPendingCompositionString("buzz"); TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE); TIP.flushPendingComposition(); is(input.value, "FOobarbuzz", description + "new composition string should be appended to the focused editor"); // Committing with same string reset(); - TIP.commitComposition("buzz"); + TIP.commitComposition(null, 0, "buzz"); is(events.length, 1, description + "committing with same string should cause only compositionend"); is(events[0].type, "compositionend", description + "committing with same string should cause compositionend"); is(events[0].data, "buzz", description + "compositionend caused by committing with same string should have the committing string in its data"); is(input.value, "FOobarbuzz", description + "new committed string should be appended to the focused editor"); // Inserting commit string directly reset(); - TIP.commitComposition("boo!"); + TIP.commitComposition(null, 0, "boo!"); is(events.length, 3, description + "committing text directly should cause compositionstart, compositionupdate and compositionend"); is(events[0].type, "compositionstart", description + "committing text directly should cause compositionstart first"); is(events[1].type, "compositionupdate", description + "committing text directly should cause compositionupdate after compositionstart"); is(events[1].data, "boo!", description + "compositionupdate caused by committing text directly should have the committing text in its data"); @@ -1002,16 +1002,566 @@ function runCompositionTests() is(input.value, "FOobarbuzzboo!", description + "committing text directly should append the committing text to the focused editor"); window.removeEventListener("compositionstart", handler, false); window.removeEventListener("compositionupdate", handler, false); window.removeEventListener("compositionend", handler, false); } +function runCompositionWithKeyEventTests() +{ + var description = "runCompositionWithKeyEventTests(): "; + + var TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed"); + + var events; + + function reset() + { + events = []; + } + + function handler(aEvent) + { + events.push(aEvent); + } + + window.addEventListener("compositionstart", handler, false); + window.addEventListener("compositionupdate", handler, false); + window.addEventListener("compositionend", handler, false); + window.addEventListener("keydown", handler, false); + window.addEventListener("keypress", handler, false); + window.addEventListener("keyup", handler, false); + + input.value = ""; + input.focus(); + + var printableKeyEvent = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + var enterKeyEvent = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + var escKeyEvent = new KeyboardEvent("", { key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }); + var convertKeyEvent = new KeyboardEvent("", { key: "Convert", code: "Convert", keyCode: KeyboardEvent.DOM_VK_CONVERT }); + var backspaceKeyEvent = new KeyboardEvent("", { key: "Backspace", code: "Backspace", keyCode: KeyboardEvent.DOM_VK_BACK_SPACE }); + + SpecialPowers.setBoolPref("dom.keyboardevent.dispatch_during_composition", false); + + // nsITextInputProcessor.startComposition() + reset(); + TIP.startComposition(printableKeyEvent); + is(events.length, 2, + description + "startComposition(printableKeyEvent) should cause keydown and compositionstart"); + is(events[0].type, "keydown", + description + "startComposition(printableKeyEvent) should cause keydown"); + is(events[1].type, "compositionstart", + description + "startComposition(printableKeyEvent) should cause compositionstart"); + is(input.value, "", + description + "startComposition(printableKeyEvent) shouldn't modify the focused editor"); + + // Setting composition string "foo" as a raw clause + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + + reset(); + TIP.flushPendingComposition(printableKeyEvent); + is(events.length, 1, + description + "flushPendingComposition(KeyupInternal) after startComposition() should cause compositionupdate"); + is(events[0].type, "compositionupdate", + description + "flushPendingComposition(KeyupInternal) after startComposition() should cause compositionupdate"); + is(events[0].data, "foo", + description + "compositionupdate caused by flushPendingComposition(KeyupInternal) should have new composition string in its data"); + is(input.value, "foo", + description + "modifying composition string should cause modifying the focused editor"); + + // Changing the raw clause to a selected clause + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_SELECTED_CLAUSE); + + reset(); + TIP.flushPendingComposition(convertKeyEvent); + is(events.length, 0, + description + "flushPendingComposition(convertKeyEvent) changing only clause information shouldn't cause compositionupdate"); + is(input.value, "foo", + description + "modifying composition clause shouldn't cause modifying the focused editor"); + + // Separating the selected clause to two clauses + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE); + TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE); + TIP.setCaretInPendingComposition(2); + + reset(); + TIP.flushPendingComposition(convertKeyEvent); + is(events.length, 0, + description + "flushPendingComposition(convertKeyEvent) separating a clause information shouldn't cause compositionupdate"); + is(input.value, "foo", + description + "separating composition clause shouldn't cause modifying the focused editor"); + + // Modifying the composition string + TIP.setPendingCompositionString("FOo"); + TIP.appendClauseToPendingComposition(2, TIP.ATTR_SELECTED_CLAUSE); + TIP.appendClauseToPendingComposition(1, TIP.ATTR_CONVERTED_CLAUSE); + TIP.setCaretInPendingComposition(2); + + reset(); + TIP.flushPendingComposition(convertKeyEvent); + is(events.length, 1, + description + "flushPendingComposition(convertKeyEvent) causing modifying composition string should cause compositionupdate"); + is(events[0].type, "compositionupdate", + description + "flushPendingComposition(convertKeyEvent) causing modifying composition string should cause compositionupdate"); + is(events[0].data, "FOo", + description + "compositionupdate caused by flushPendingComposition(convertKeyEvent) should have new composition string in its data"); + is(input.value, "FOo", + description + "modifying composition clause shouldn't cause modifying the focused editor"); + + // Committing the composition string + reset(); + TIP.commitComposition(enterKeyEvent); + is(events.length, 2, + description + "commitComposition(enterKeyEvent) should cause compositionend and keyup but shoudn't cause compositionupdate"); + is(events[0].type, "compositionend", + description + "commitComposition(enterKeyEvent) should cause compositionend"); + is(events[0].data, "FOo", + description + "compositionend caused by commitComposition(enterKeyEvent) should have the committed string in its data"); + is(events[1].type, "keyup", + description + "commitComposition(enterKeyEvent) should cause keyup"); + is(input.value, "FOo", + description + "commitComposition(enterKeyEvent) shouldn't cause modifying the focused editor"); + + // Starting new composition without a call of startComposition() + TIP.setPendingCompositionString("bar"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + + reset(); + TIP.flushPendingComposition(printableKeyEvent); + is(events.length, 3, + description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause both compositionstart and compositionupdate"); + is(events[0].type, "keydown", + description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause keydown"); + is(events[1].type, "compositionstart", + description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause compositionstart"); + is(events[2].type, "compositionupdate", + description + "flushPendingComposition(printableKeyEvent) without a call of startComposition() should cause compositionupdate after compositionstart"); + is(events[2].data, "bar", + description + "compositionupdate caused by flushPendingComposition(printableKeyEvent) without a call of startComposition() should have the composition string in its data"); + is(input.value, "FOobar", + description + "new composition string should cause appending composition string to the focused editor"); + + // Canceling the composition + reset(); + TIP.cancelComposition(escKeyEvent); + is(events.length, 3, + description + "cancelComposition(escKeyEvent) should cause both compositionupdate and compositionend"); + is(events[0].type, "compositionupdate", + description + "cancelComposition(escKeyEvent) should cause compositionupdate"); + is(events[0].data, "", + description + "compositionupdate caused by cancelComposition(escKeyEvent) should have empty string in its data"); + is(events[1].type, "compositionend", + description + "cancelComposition(escKeyEvent) should cause compositionend after compositionupdate"); + is(events[1].data, "", + description + "compositionend caused by cancelComposition(escKeyEvent) should have empty string in its data"); + is(events[2].type, "keyup", + description + "cancelComposition(escKeyEvent) should cause keyup after compositionend"); + is(input.value, "FOo", + description + "canceled composition string should be removed from the focused editor"); + + // Starting composition explicitly and canceling it + reset(); + TIP.startComposition(printableKeyEvent); + TIP.cancelComposition(escKeyEvent); + is(events.length, 4, + description + "canceling composition immediately after startComposition() should cause keydown, compositionstart, compositionend and keyup"); + is(events[0].type, "keydown", + description + "canceling composition immediately after startComposition() should cause keydown first"); + is(events[1].type, "compositionstart", + description + "canceling composition immediately after startComposition() should cause compositionstart after keydown"); + is(events[2].type, "compositionend", + description + "canceling composition immediately after startComposition() should cause compositionend after compositionstart"); + is(events[2].data, "", + description + "compositionend caused by canceling composition should have empty string in its data"); + is(events[3].type, "keyup", + description + "canceling composition immediately after startComposition() should cause keyup after compositionend"); + is(input.value, "FOo", + description + "canceling composition shouldn't modify the focused editor"); + + // Create composition for next test. + TIP.setPendingCompositionString("bar"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.flushPendingComposition(); + is(input.value, "FOobar", + description + "The focused editor should have new composition string \"bar\""); + + // Allow to set empty composition string + reset(); + TIP.flushPendingComposition(backspaceKeyEvent); + is(events.length, 1, + description + "making composition string empty should cause only compositionupdate"); + is(events[0].type, "compositionupdate", + description + "making composition string empty should cause compositionupdate"); + is(events[0].data, "", + description + "compositionupdate caused by making composition string empty should have empty string in its data"); + + // Allow to insert new composition string without compositionend/compositionstart + TIP.setPendingCompositionString("buzz"); + TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE); + + reset(); + TIP.flushPendingComposition(printableKeyEvent); + is(events.length, 1, + description + "modifying composition string from empty string should cause only compositionupdate"); + is(events[0].type, "compositionupdate", + description + "modifying composition string from empty string should cause compositionupdate"); + is(events[0].data, "buzz", + description + "compositionupdate caused by modifying composition string from empty string should have new composition string in its data"); + is(input.value, "FOobuzz", + description + "new composition string should be appended to the focused editor"); + + // Committing with different string + reset(); + TIP.commitComposition(printableKeyEvent, 0, "bar"); + is(events.length, 3, + description + "committing with different string should cause compositionupdate and compositionend"); + is(events[0].type, "compositionupdate", + description + "committing with different string should cause compositionupdate first"); + is(events[0].data, "bar", + description + "compositionupdate caused by committing with different string should have the committing string in its data"); + is(events[1].type, "compositionend", + description + "committing with different string should cause compositionend after compositionupdate"); + is(events[1].data, "bar", + description + "compositionend caused by committing with different string should have the committing string in its data"); + is(events[2].type, "keyup", + description + "committing with different string should cause keyup after compositionend"); + is(input.value, "FOobar", + description + "new committed string should be appended to the focused editor"); + + // Appending new composition string + TIP.setPendingCompositionString("buzz"); + TIP.appendClauseToPendingComposition(4, TIP.ATTR_RAW_CLAUSE); + TIP.flushPendingComposition(); + is(input.value, "FOobarbuzz", + description + "new composition string should be appended to the focused editor"); + + // Committing with same string + reset(); + TIP.commitComposition(enterKeyEvent, 0, "buzz"); + is(events.length, 2, + description + "committing with same string should cause only compositionend"); + is(events[0].type, "compositionend", + description + "committing with same string should cause compositionend"); + is(events[0].data, "buzz", + description + "compositionend caused by committing with same string should have the committing string in its data"); + is(events[1].type, "keyup", + description + "committing with same string should cause keyup after compositionend"); + is(input.value, "FOobarbuzz", + description + "new committed string should be appended to the focused editor"); + + // Inserting commit string directly + reset(); + TIP.commitComposition(printableKeyEvent, 0, "boo!"); + is(events.length, 5, + description + "committing text directly should cause compositionstart, compositionupdate and compositionend"); + is(events[0].type, "keydown", + description + "committing text directly should cause keydown first"); + is(events[1].type, "compositionstart", + description + "committing text directly should cause compositionstart after keydown"); + is(events[2].type, "compositionupdate", + description + "committing text directly should cause compositionupdate after compositionstart"); + is(events[2].data, "boo!", + description + "compositionupdate caused by committing text directly should have the committing text in its data"); + is(events[3].type, "compositionend", + description + "committing text directly should cause compositionend after compositionupdate"); + is(events[3].data, "boo!", + description + "compositionend caused by committing text directly should have the committing text in its data"); + is(events[4].type, "keyup", + description + "committing text directly should cause keyup after compositionend"); + is(input.value, "FOobarbuzzboo!", + description + "committing text directly should append the committing text to the focused editor"); + + SpecialPowers.setBoolPref("dom.keyboardevent.dispatch_during_composition", true); + + // Even if "dom.keyboardevent.dispatch_during_composition" is true, keypress event shouldn't be fired during composition + reset(); + TIP.startComposition(printableKeyEvent); + is(events.length, 3, + description + "TIP.startComposition(printableKeyEvent) should cause keydown, compositionstart and keyup (keypress event shouldn't be fired during composition)"); + is(events[0].type, "keydown", + description + "TIP.startComposition(printableKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)"); + is(events[1].type, "compositionstart", + description + "TIP.startComposition(printableKeyEvent) should cause compositionstart (keypress event shouldn't be fired during composition)"); + is(events[2].type, "keyup", + description + "TIP.startComposition(printableKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)"); + + // TIP.flushPendingComposition(printableKeyEvent) should cause keydown and keyup events if "dom.keyboardevent.dispatch_during_composition" is true + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + + reset(); + TIP.flushPendingComposition(printableKeyEvent); + is(events.length, 3, + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown, compositionupdate and keyup (keypress event shouldn't be fired during composition)"); + is(events[0].type, "keydown", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)"); + is(events[1].type, "compositionupdate", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause compositionupdate (keypress event shouldn't be fired during composition)"); + is(events[2].type, "keyup", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)"); + + // TIP.commitComposition(enterKeyEvent) should cause keydown and keyup events if "dom.keyboardevent.dispatch_during_composition" is true + reset(); + TIP.commitComposition(enterKeyEvent); + is(events.length, 3, + description + "TIP.commitComposition(enterKeyEvent) should cause keydown, compositionend and keyup (keypress event shouldn't be fired during composition)"); + is(events[0].type, "keydown", + description + "TIP.commitComposition(enterKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)"); + is(events[1].type, "compositionend", + description + "TIP.commitComposition(enterKeyEvent) should cause compositionend (keypress event shouldn't be fired during composition)"); + is(events[2].type, "keyup", + description + "TIP.commitComposition(enterKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)"); + + // TIP.cancelComposition(escKeyEvent) should cause keydown and keyup events if "dom.keyboardevent.dispatch_during_composition" is true + TIP.startComposition(); + reset(); + TIP.cancelComposition(escKeyEvent); + is(events.length, 3, + description + "TIP.cancelComposition(escKeyEvent) should cause keydown, compositionend and keyup (keypress event shouldn't be fired during composition)"); + is(events[0].type, "keydown", + description + "TIP.cancelComposition(escKeyEvent) should cause keydown (keypress event shouldn't be fired during composition)"); + is(events[1].type, "compositionend", + description + "TIP.cancelComposition(escKeyEvent) should cause compositionend (keypress event shouldn't be fired during composition)"); + is(events[2].type, "keyup", + description + "TIP.cancelComposition(escKeyEvent) should cause keyup (keypress event shouldn't be fired during composition)"); + + var printableKeydownEvent = new KeyboardEvent("keydown", { key: "b", code: "KeyB", keyCode: KeyboardEvent.DOM_VK_B }); + var enterKeydownEvent = new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + var escKeydownEvent = new KeyboardEvent("keydown", { key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }); + + // TIP.startComposition(printableKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true + reset(); + TIP.startComposition(printableKeydownEvent); + is(events.length, 2, + description + "TIP.startComposition(printableKeydownEvent) should cause keydown and compositionstart (keyup event shouldn't be fired)"); + is(events[0].type, "keydown", + description + "TIP.startComposition(printableKeydownEvent) should cause keydown (keyup event shouldn't be fired)"); + is(events[1].type, "compositionstart", + description + "TIP.startComposition(printableKeydownEvent) should cause compositionstart (keyup event shouldn't be fired)"); + + // TIP.flushPendingComposition(printableKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + + reset(); + TIP.flushPendingComposition(printableKeydownEvent); + is(events.length, 2, + description + "TIP.flushPendingComposition(printableKeydownEvent) should cause keydown and compositionupdate (keyup event shouldn't be fired)"); + is(events[0].type, "keydown", + description + "TIP.flushPendingComposition(printableKeydownEvent) should cause keydown (keyup event shouldn't be fired)"); + is(events[1].type, "compositionupdate", + description + "TIP.flushPendingComposition(printableKeydownEvent) should cause compositionupdate (keyup event shouldn't be fired)"); + + // TIP.commitComposition(enterKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true + reset(); + TIP.commitComposition(enterKeydownEvent); + is(events.length, 2, + description + "TIP.commitComposition(enterKeydownEvent) should cause keydown and compositionend (keyup event shouldn't be fired)"); + is(events[0].type, "keydown", + description + "TIP.commitComposition(enterKeydownEvent) should cause keydown (keyup event shouldn't be fired)"); + is(events[1].type, "compositionend", + description + "TIP.commitComposition(enterKeydownEvent) should cause compositionend (keyup event shouldn't be fired)"); + + // TIP.cancelComposition(escKeydownEvent) shouldn't cause keyup event even if "dom.keyboardevent.dispatch_during_composition" is true + TIP.startComposition(); + reset(); + TIP.cancelComposition(escKeydownEvent); + is(events.length, 2, + description + "TIP.cancelComposition(escKeydownEvent) should cause keydown and compositionend (keyup event shouldn't be fired)"); + is(events[0].type, "keydown", + description + "TIP.cancelComposition(escKeydownEvent) should cause keydown (keyup event shouldn't be fired)"); + is(events[1].type, "compositionend", + description + "TIP.cancelComposition(escKeydownEvent) should cause compositionend (keyup event shouldn't be fired)"); + + SpecialPowers.clearUserPref("dom.keyboardevent.dispatch_during_composition"); + + window.removeEventListener("compositionstart", handler, false); + window.removeEventListener("compositionupdate", handler, false); + window.removeEventListener("compositionend", handler, false); + window.removeEventListener("keydown", handler, false); + window.removeEventListener("keypress", handler, false); + window.removeEventListener("keyup", handler, false); +} + +function runConsumingKeydownBeforeCompositionTests() +{ + var description = "runConsumingKeydownBeforeCompositionTests(): "; + + var TIP = createTIP(); + ok(TIP.beginInputTransactionForTests(window), + description + "TIP.beginInputTransactionForTests() should succeed"); + + var events; + + function reset() + { + events = []; + } + + function handler(aEvent) + { + events.push(aEvent); + if (aEvent.type == "keydown") { + aEvent.preventDefault(); + } + } + + window.addEventListener("compositionstart", handler, false); + window.addEventListener("compositionupdate", handler, false); + window.addEventListener("compositionend", handler, false); + window.addEventListener("keydown", handler, false); + window.addEventListener("keypress", handler, false); + window.addEventListener("keyup", handler, false); + + input.value = ""; + input.focus(); + + var printableKeyEvent = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + var enterKeyEvent = new KeyboardEvent("", { key: "Enter", code: "Enter", keyCode: KeyboardEvent.DOM_VK_RETURN }); + var escKeyEvent = new KeyboardEvent("", { key: "Escape", code: "Escape", keyCode: KeyboardEvent.DOM_VK_ESCAPE }); + + SpecialPowers.setBoolPref("dom.keyboardevent.dispatch_during_composition", false); + + // If keydown before compositionstart is consumed, composition shouldn't be started. + reset(); + ok(!TIP.startComposition(printableKeyEvent), + description + "TIP.startComposition(printableKeyEvent) should return false because it's keydown is consumed"); + is(events.length, 2, + description + "TIP.startComposition(printableKeyEvent) should cause only keydown and keyup events"); + is(events[0].type, "keydown", + description + "TIP.startComposition(printableKeyEvent) should cause keydown event first"); + is(events[1].type, "keyup", + description + "TIP.startComposition(printableKeyEvent) should cause keyup event after keydown"); + ok(!TIP.hasComposition, + description + "TIP.startComposition(printableKeyEvent) shouldn't cause composition"); + is(input.value, "", + description + "TIP.startComposition(printableKeyEvent) shouldn't cause inserting text"); + + // If keydown before compositionstart caused by flushPendingComposition(printableKeyEvent) is consumed, composition shouldn't be started. + reset(); + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + ok(!TIP.flushPendingComposition(printableKeyEvent), + description + "TIP.flushPendingComposition(printableKeyEvent) should return false because it's keydown is consumed"); + is(events.length, 2, + description + "TIP.flushPendingComposition(printableKeyEvent) should cause only keydown and keyup events"); + is(events[0].type, "keydown", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown event first"); + is(events[1].type, "keyup", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keyup event after keydown"); + ok(!TIP.hasComposition, + description + "TIP.flushPendingComposition(printableKeyEvent) shouldn't cause composition"); + is(input.value, "", + description + "TIP.flushPendingComposition(printableKeyEvent) shouldn't cause inserting text"); + + // If keydown before compositionstart is consumed, composition shouldn't be started. + reset(); + ok(!TIP.commitComposition(printableKeyEvent, 0, "foo"), + description + "TIP.commitComposition(printableKeyEvent, 0, \"foo\") should return false because it's keydown is consumed"); + is(events.length, 2, + description + "TIP.commitComposition(printableKeyEvent, 0, \"foo\") should cause only keydown and keyup events"); + is(events[0].type, "keydown", + description + "TIP.commitComposition(printableKeyEvent, 0, \"foo\") should cause keydown event first"); + is(events[1].type, "keyup", + description + "TIP.commitComposition(printableKeyEvent, 0, \"foo\") should cause keyup event after keydown"); + ok(!TIP.hasComposition, + description + "TIP.commitComposition(printableKeyEvent, 0, \"foo\") shouldn't cause composition"); + is(input.value, "", + description + "TIP.commitComposition(printableKeyEvent, 0, \"foo\") shouldn't cause inserting text"); + + SpecialPowers.setBoolPref("dom.keyboardevent.dispatch_during_composition", true); + + // If composition is already started, TIP.flushPendingComposition(printableKeyEvent) shouldn't be canceled. + TIP.startComposition(); + ok(TIP.hasComposition, + description + "Before TIP.flushPendingComposition(printableKeyEvent), composition should've been created"); + reset(); + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + ok(TIP.flushPendingComposition(printableKeyEvent), + description + "TIP.flushPendingComposition(printableKeyEvent) should return true even if preceding keydown is consumed because there was a composition already"); + is(events.length, 3, + description + "TIP.flushPendingComposition(printableKeyEvent) should cause only keydown and keyup events"); + is(events[0].type, "keydown", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keydown event first"); + is(events[1].type, "compositionupdate", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause compositionupdate event after keydown"); + is(events[2].type, "keyup", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause keyup event after compositionupdate"); + ok(TIP.hasComposition, + description + "TIP.flushPendingComposition(printableKeyEvent) shouldn't cause canceling composition"); + is(input.value, "foo", + description + "TIP.flushPendingComposition(printableKeyEvent) should cause inserting text even if preceding keydown is consumed because there was a composition already"); + + // If composition is already started, TIP.commitComposition(enterKeyEvent) shouldn't be canceled. + reset(); + ok(TIP.commitComposition(enterKeyEvent), + description + "TIP.commitComposition(enterKeyEvent) should return true even if preceding keydown is consumed because there was a composition already"); + is(events.length, 3, + description + "TIP.commitComposition(enterKeyEvent) should cause keydown, compositionend and keyup events"); + is(events[0].type, "keydown", + description + "TIP.commitComposition(enterKeyEvent) should cause keydown event first"); + is(events[1].type, "compositionend", + description + "TIP.commitComposition(enterKeyEvent) should cause compositionend event after keydown"); + is(events[2].type, "keyup", + description + "TIP.commitComposition(enterKeyEvent) should cause keyup event after compositionend"); + ok(!TIP.hasComposition, + description + "TIP.commitComposition(enterKeyEvent) should cause committing composition even if preceding keydown is consumed because there was a composition already"); + is(input.value, "foo", + description + "TIP.commitComposition(enterKeyEvent) should commit composition even if preceding keydown is consumed because there was a composition already"); + + // cancelComposition() should work even if preceding keydown event is consumed. + input.value = ""; + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(); + ok(TIP.hasComposition, + description + "Before TIP.cancelComposition(escKeyEvent), composition should've been created"); + is(input.value, "foo", + description + "Before TIP.cancelComposition(escKeyEvent) should have composition string"); + reset(); + TIP.cancelComposition(escKeyEvent); + is(events.length, 4, + description + "TIP.cancelComposition(escKeyEvent) should cause keydown, compositionupdate, compositionend and keyup events even if preceding keydown is consumed because there was a composition already"); + is(events[0].type, "keydown", + description + "TIP.cancelComposition(escKeyEvent) should cause keydown event first"); + is(events[1].type, "compositionupdate", + description + "TIP.cancelComposition(escKeyEvent) should cause compositionupdate event after keydown"); + is(events[2].type, "compositionend", + description + "TIP.cancelComposition(escKeyEvent) should cause compositionend event after compositionupdate"); + is(events[3].type, "keyup", + description + "TIP.cancelComposition(escKeyEvent) should cause keyup event after compositionend"); + ok(!TIP.hasComposition, + description + "TIP.cancelComposition(escKeyEvent) should cause canceling composition even if preceding keydown is consumed because there was a composition already"); + is(input.value, "", + description + "TIP.cancelComposition(escKeyEvent) should cancel composition even if preceding keydown is consumed because there was a composition already"); + + SpecialPowers.clearUserPref("dom.keyboardevent.dispatch_during_composition"); + + window.removeEventListener("compositionstart", handler, false); + window.removeEventListener("compositionupdate", handler, false); + window.removeEventListener("compositionend", handler, false); + window.removeEventListener("keydown", handler, false); + window.removeEventListener("keypress", handler, false); + window.removeEventListener("keyup", handler, false); +} + function runKeyTests() { var description = "runKeyTests(): "; const kModifiers = [ "Alt", "AltGraph", "CapsLock", "Control", "Fn", "FnLock", "Meta", "NumLock", "ScrollLock", "Shift", "Symbol", "SymbolLock", "OS" ]; var TIP = createTIP(); @@ -2129,24 +2679,24 @@ function runErrorTests() TIP.commitComposition(); ok(false, description + "commitComposition() should fail if there is no composition"); } catch (e) { ok(e.message.contains("NS_ERROR_FAILURE"), description + "commitComposition() should cause NS_ERROR_FAILURE if there is no composition"); } - // commitComposition("") should throw an exception if there is no composition + // commitComposition(null, 0, "") should throw an exception if there is no composition try { - TIP.commitComposition(""); + TIP.commitComposition(null, 0, ""); ok(false, - description + "commitComposition(\"\") should fail if there is no composition"); + description + "commitComposition(null, 0, \"\") should fail if there is no composition"); } catch (e) { ok(e.message.contains("NS_ERROR_FAILURE"), - description + "commitComposition(\"\") should cause NS_ERROR_FAILURE if there is no composition"); + description + "commitComposition(null, 0, \"\") should cause NS_ERROR_FAILURE if there is no composition"); } // Pending composition string should allow to flush without clause information (for compatibility) try { TIP.setPendingCompositionString("foo"); TIP.flushPendingComposition(); ok(true, description + "flushPendingComposition() should succeed even if appendClauseToPendingComposition() has never been called"); @@ -2347,16 +2897,91 @@ function runErrorTests() var keyEvent = new KeyboardEvent("", { key: "Enter", code: "ShiftLeft" }); TIP.keyup(keyEvent, TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT); ok(false, description + "keydown(KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT) should fail if the .key value isn't a modifier key"); } catch (e) { ok(e.message.contains("NS_ERROR_ILLEGAL_VALUE"), description + "keydown(KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT) should cause NS_ERROR_ILLEGAL_VALUE if the .key value isn't a modifier key"); } + + // The type of key events specified to composition methods should be "" or "keydown". + var kKeyEventTypes = [ + { type: "keydown", valid: true }, + { type: "keypress", valid: false }, + { type: "keyup", valid: false }, + { type: "", valid: true }, + { type: "mousedown", valid: false }, + { type: "foo", valid: false }, + ]; + for (var i = 0; i < kKeyEventTypes.length; i++) { + var keyEvent = + new KeyboardEvent(kKeyEventTypes[i].type, { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A }); + var testDescription = description + "type=\"" + kKeyEventTypes[i].type + "\", "; + try { + TIP.startComposition(keyEvent); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.startComposition(keyEvent) should not accept the event type"); + TIP.cancelComposition(); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.startComposition(keyEvent) should not throw an exception for the event type"); + ok(e.message.contains("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.startComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + } + try { + TIP.setPendingCompositionString("foo"); + TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); + TIP.setCaretInPendingComposition(3); + TIP.flushPendingComposition(keyEvent); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.flushPendingComposition(keyEvent) should not accept the event type"); + TIP.cancelComposition(); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.flushPendingComposition(keyEvent) should not throw an exception for the event type"); + ok(e.message.contains("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.flushPendingComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + } + try { + TIP.startComposition(); + TIP.commitComposition(keyEvent); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.commitComposition(keyEvent) should not accept the event type"); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.commitComposition(keyEvent) should not throw an exception for the event type"); + ok(e.message.contains("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.commitComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + TIP.cancelComposition(); + } + try { + TIP.commitComposition(keyEvent, 0, "foo"); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.commitComposition(keyEvent, 0, \"foo\") should not accept the event type"); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.commitComposition(keyEvent, 0, \"foo\") should not throw an exception for the event type"); + ok(e.message.contains("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.commitComposition(keyEvent, 0, \"foo\") should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + } + try { + TIP.startComposition(); + TIP.cancelComposition(keyEvent); + ok(kKeyEventTypes[i].valid, + testDescription + "TIP.cancelComposition(keyEvent) should not accept the event type"); + } catch (e) { + ok(!kKeyEventTypes[i].valid, + testDescription + "TIP.cancelComposition(keyEvent) should not throw an exception for the event type"); + ok(e.message.contains("NS_ERROR_ILLEGAL_VALUE"), + testDescription + "TIP.cancelComposition(keyEvent) should cause NS_ERROR_ILLEGAL_VALUE if the key event type isn't valid"); + TIP.cancelComposition(); + } + input.value = ""; + } } function runCommitCompositionTests() { var description = "runCommitCompositionTests(): "; var TIP = createTIP(); ok(TIP.beginInputTransactionForTests(window), @@ -2369,49 +2994,49 @@ function runCommitCompositionTests() TIP.setPendingCompositionString("foo"); TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); TIP.setCaretInPendingComposition(3); TIP.flushPendingComposition(); TIP.commitComposition(); is(input.value, "foo", description + "commitComposition() should commit the composition with the last data"); - // commitComposition("") should commit the composition with empty string. + // commitComposition(null, 0, "") should commit the composition with empty string. input.value = ""; TIP.setPendingCompositionString("foo"); TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); TIP.setCaretInPendingComposition(3); TIP.flushPendingComposition(); - TIP.commitComposition(""); + TIP.commitComposition(null, 0, ""); is(input.value, "", - description + "commitComposition(\"\") should commit the composition with empty string"); + description + "commitComposition(null, 0, \"\") should commit the composition with empty string"); - // commitComposition(null) should commit the composition with empty string. + // commitComposition(null, 0, null) should commit the composition with empty string. input.value = ""; TIP.setPendingCompositionString("foo"); TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); TIP.setCaretInPendingComposition(3); TIP.flushPendingComposition(); - TIP.commitComposition(null); + TIP.commitComposition(null, 0, null); is(input.value, "", - description + "commitComposition(null) should commit the composition with empty string"); + description + "commitComposition(null, 0, null) should commit the composition with empty string"); - // commitComposition(undefined) should commit the composition with the last data. + // commitComposition(null, 0, undefined) should commit the composition with the last data. input.value = ""; TIP.setPendingCompositionString("foo"); TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); TIP.setCaretInPendingComposition(3); TIP.flushPendingComposition(); - TIP.commitComposition(undefined); + TIP.commitComposition(null, 0, undefined); todo_is(input.value, "foo", - description + "commitComposition(undefined) should commit the composition with the last data"); + description + "commitComposition(null, 0, undefined) should commit the composition with the last data"); function doCommit(aText) { - TIP.commitComposition(aText); + TIP.commitComposition(null, 0, aText); } // doCommit() should commit the composition with the last data. input.value = ""; TIP.setPendingCompositionString("foo"); TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); TIP.setCaretInPendingComposition(3); TIP.flushPendingComposition(); @@ -2446,17 +3071,17 @@ function runCommitCompositionTests() TIP.setCaretInPendingComposition(3); TIP.flushPendingComposition(); doCommit(undefined); todo_is(input.value, "foo", description + "doCommit(undefined) should commit the composition with the last data"); function doCommitWithNullCheck(aText) { - TIP.commitComposition(aText ? aText : ""); + TIP.commitComposition(null, 0, aText ? aText : ""); } // doCommitWithNullCheck() should commit the composition with the last data. input.value = ""; TIP.setPendingCompositionString("foo"); TIP.appendClauseToPendingComposition(3, TIP.ATTR_RAW_CLAUSE); TIP.setCaretInPendingComposition(3); TIP.flushPendingComposition(); @@ -2707,16 +3332,18 @@ function runCallbackTests(aForTests) function runTests() { textareaInFrame = iframe.contentDocument.getElementById("textarea"); runBeginInputTransactionMethodTests(); runReleaseTests(); runCompositionTests(); + runCompositionWithKeyEventTests(); + runConsumingKeydownBeforeCompositionTests(); runKeyTests(); runErrorTests(); runCommitCompositionTests(); runCallbackTests(false); runCallbackTests(true); runUnloadTests1(function () { runUnloadTests2(function () { finish();
--- a/dom/inputmethod/forms.js +++ b/dom/inputmethod/forms.js @@ -1302,17 +1302,17 @@ let CompositionManager = { } this._isStarted = this._textInputProcessor.flushPendingComposition(); }, endComposition: function cm_endComposition(text) { if (!this._isStarted) { return; } - this._textInputProcessor.commitComposition(text ? text : ""); + this._textInputProcessor.commitComposition(null, 0, text ? text : ""); this._isStarted = false; }, // Composition ends due to external actions. onCompositionEnd: function cm_onCompositionEnd() { if (!this._isStarted) { return; }
--- a/dom/interfaces/base/nsITextInputProcessor.idl +++ b/dom/interfaces/base/nsITextInputProcessor.idl @@ -23,92 +23,135 @@ interface nsITextInputProcessorCallback; * * 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.beginInputTransaction(window, callback)) { * return; // You failed to get the rights to make composition * } + * // Create a keyboard event if the following compositionc change is caused + * // by a key event. + * var keyEvent = + * new KeyboardEvent("", { key: "foo", code: "bar", keyCode: buzz }); * // 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); * // Flush the pending composition - * if (!TIP.flushPendingComposition()) { + * if (!TIP.flushPendingComposition(keyEvent)) { * // If it returns false, it fails to start composition. * return; * } * * Example #2 JS-IME can separate composition string to two or more clauses: * + * // Create a keyboard event if the following compositionc change is caused + * // by a key event. + * var keyEvent = + * new KeyboardEvent("", { key: "foo", code: "bar", keyCode: buzz }); * // First, set composition string again * TIP.setPendingCompositionString("some-words-are-inputted"); * // Then, if "are" is selected to convert, there are 3 clauses: * TIP.appendClauseToPendingComposition(11, TIP.ATTR_CONVERTED_CLAUSE); * TIP.appendClauseToPendingComposition(3, TIP.ATTR_SELECTED_CLAUSE); * TIP.appendClauseToPendingComposition(9, TIP.ATTR_CONVERTED_CLAUSE); * // Show caret at the beginning of the selected clause * TIP.setCaretInPendingComposition(11); * // Flush the pending composition. Note that if there is a composition, * // flushPendingComposition() won't return false. - * TIP.flushPendingComposition(); + * TIP.flushPendingComposition(keyEvent); * * Example #3 JS-IME can commit composition with specific string with this: * + * // Create a keyboard event if the following compositionc change is caused + * // by a key event. + * var keyEvent1 = + * new KeyboardEvent("", { key: "foo", code: "bar", keyCode: buzz }); * // First, there is a composition. * TIP.setPendingCompositionString("some-words-directly-inputted"); * TIP.appendClauseToPendingComposition(28, TIP.ATTR_RAW_CLAUSE); - * TIP.flushPendingComposition(); + * TIP.flushPendingComposition(keyEvent1); + * // Create a keyboard event if the following commit composition is caused + * // by a key event. + * var keyEvent2 = + * new KeyboardEvent("", { key: "foo", code: "bar", keyCode: buzz }); * // This is useful when user selects a commit string from candidate list UI * // which is provided by JS-IME. - * TIP.commitComposition("selected-words-from-candidate-list"); + * TIP.commitComposition(keyEvent2, "selected-words-from-candidate-list"); * * Example #4 JS-IME can commit composition with the last composition string * without specifying commit string: * + * // Create a keyboard event if the following compositionc change is caused + * // by a key event. + * var keyEvent1 = + * new KeyboardEvent("", { key: "foo", code: "bar", keyCode: buzz }); * // First, there is a composition. * TIP.setPendingCompositionString("some-words-will-be-commited"); * TIP.appendClauseToPendingComposition(27, TIP.ATTR_RAW_CLAUSE); - * TIP.flushPendingComposition(); + * TIP.flushPendingComposition(keyEvent1); + * // Create a keyboard event if the following commit is caused by a key + * // event. + * var keyEvent2 = + * new KeyboardEvent("", { key: "Enter", code: "Enter", + keyCode: KeyboardEvent.DOM_VK_RETURN }); * // This is useful when user just type Enter key. - * TIP.commitComposition(); + * TIP.commitComposition(keyEvent2); * * Example #5 JS-IME can cancel composition with this: * + * // Create a keyboard event if the following composition change is caused + * // by a key event. + * var keyEvent1 = + * new KeyboardEvent("", { key: "foo", code: "bar", keyCode: buzz }); * // First, there is a composition. * TIP.setPendingCompositionString("some-words-will-be-canceled"); * TIP.appendClauseToPendingComposition(27, TIP.ATTR_RAW_CLAUSE); - * TIP.flushPendingComposition(); + * TIP.flushPendingComposition(keyEvent1); + * // Create a keyboard event if the following canceling composition is + * // caused by a key event. + * var keyEvent2 = + * new KeyboardEvent("", { key: "Escape", code: "Escape", + keyCode: KeyboardEvent.DOM_VK_ESCAPE }); * // This is useful when user doesn't want to commit the composition. * // FYI: This is same as TIP.commitComposition("") for now. - * TIP.cancelComposition(); + * TIP.cancelComposition(keyEvent2); * * Example #6 JS-IME can insert text only with commitComposition(): * + * // Create a keyboard event if the following inserting text is caused by a + * // key event. + * var keyEvent1 = + * new KeyboardEvent("", { key: "foo", code: "bar", keyCode: buzz }); * if (!TIP.beginInputTransaction(window, callback)) { * return; // You failed to get the rights to make composition * } - * TIP.commitComposition("Some words"); + * TIP.commitComposition(keyEvent1, "Some words"); * * Example #7 JS-IME can start composition explicitly: * * if (!TIP.beginInputTransaction(window, callback)) { * return; // You failed to get the rights to make composition * } + * // Create a keyboard event if the following starting composition is caused + * // by a key event. + * var keyEvent1 = + * new KeyboardEvent("", { key: "foo", code: "bar", keyCode: buzz }); * // 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()) { + * if (!TIP.startComposition(keyEvent1)) { * // Failed to start composition. * return; * } * // And when user selects a result from UI of JS-IME, commit with it. - * TIP.commitComposition("selected-words"); + * // Then, the key event should be null. + * TIP.commitComposition(null, "selected-words"); * * Example #8 JS-IME or JS-Keyboard should dispatch key events even during * composition (non-printable key case): * * if (!TIP.beginInputTransaction(window, callback)) { * return; // You failed to get the rights to dispatch key events * } * @@ -206,20 +249,26 @@ interface nsITextInputProcessorCallback; * * // keyup of one of shift key doesn't cause inactivating "Shift" state. * TIP.keyup(rightShift); * * // This causes inactivating "Shift" state completely. * TIP.keyup(leftShift); */ -[scriptable, builtinclass, uuid(bb19b843-dbe2-41c5-8485-794121351d3a)] +[scriptable, builtinclass, uuid(9c3e5c81-52ca-4087-b708-cbfcbac2a055)] interface nsITextInputProcessor : nsISupports { /** + * Returns true if this instance was dispatched compositionstart but hasn't + * dispatched compositionend yet. + */ + readonly attribute boolean hasComposition; + + /** * When you create an instance, you must call beginInputTransaction() 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. @@ -247,21 +296,28 @@ interface nsITextInputProcessor : nsISup /** * 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. * + * @param aKeyboardEvent Key event which causes starting composition. + * If its type value is "keydown", this method + * dispatches only keydown event first. Otherwise, + * dispatches keydown first and keyup at last. + * @param aKeyFlags See KEY_* constants. * @return Returns true if composition starts normally. * Otherwise, returns false because it might be * canceled by the web application. */ - boolean startComposition(); + [optional_argc] + boolean startComposition([optional] in nsIDOMKeyEvent aKeyboardEvent, + [optional] in unsigned long aKeyFlags); /** * Set new composition string. Pending composition will be flushed by * a call of flushPendingComposition(). However, if the new composition * string isn't empty, you need to call appendClauseToPendingComposition() to * fill all characters of aString with one or more clauses before flushing. * Note that if you need to commit or cancel composition, use * commitComposition() or cancelComposition(). @@ -328,55 +384,78 @@ interface nsITextInputProcessor : nsISup * * Note that compositionstart will be automatically dispatched if this is * called when there is no composition. * * Note that if sum of lengths of appended clauses are not same as composition * string or caret offset is larger than the composition string length, this * throws an exception. * + * @param aKeyboardEvent Key event which causes the composition string. + * If its type value is "keydown", this method + * dispatches only keydown event first. Otherwise, + * dispatches keydown first and keyup at last. + * @param aKeyFlags See KEY_* constants. * @return Returns true if there is a composition already or * starting composition automatically. * Otherwise, i.e., if it cannot start composition * automatically, e.g., canceled by web apps, returns * false. */ - boolean flushPendingComposition(); + [optional_argc] + boolean flushPendingComposition( + [optional] in nsIDOMKeyEvent aKeyboardEvent, + [optional] in unsigned long aKeyFlags); /** * commitComposition() will commit composition. * If there is a composition which is started by the instance, this commits * the composition. Otherwise, throws an exception. * * Note that if you tries to commit composition without specifying commit * string when there is no composition, this throws an exception. * + * @param aKeyboardEvent Key event which causes the commit composition. + * If its type value is "keydown", this method + * dispatches only keydown event first. Otherwise, + * dispatches keydown first and keyup at last. + * @param aKeyFlags See KEY_* constants. * @param aCommitString This is optional. If this is not specified, * composition will be committed with the last * composing string. Otherwise, committed with * the specified string. * @return Returns true if there is a composition already or * starting composition automatically. * Otherwise, i.e., if it cannot start composition * automatically, e.g., canceled by web apps, returns * false. */ [optional_argc] - boolean commitComposition([optional] in DOMString aCommitString); + boolean commitComposition([optional] in nsIDOMKeyEvent aKeyboardEvent, + [optional] in unsigned long aKeyFlags, + [optional] in DOMString aCommitString); /** * cancelComposition() will cancel composition. This is for now the same as * calling commitComposition(""). However, in the future, this might work * better. If your IME needs to cancel composition, use this instead of * commitComposition(). * * Note that if you tries to cancel composition when there is no composition, * this throws an exception. + * + * @param aKeyboardEvent Key event which causes the canceling composition. + * If its type value is "keydown", this method + * dispatches only keydown event first. Otherwise, + * dispatches keydown first and keyup at last. + * @param aKeyFlags See KEY_* constants. */ - void cancelComposition(); + [optional_argc] + void cancelComposition([optional] in nsIDOMKeyEvent aKeyboardEvent, + [optional] in unsigned long aKeyFlags); // Specifying KEY_DEFAULT_PREVENTED can dispatch key events whose // defaultPrevented are true. Note that if this is specified, keypress event // won't be fired. const unsigned long KEY_DEFAULT_PREVENTED = 0x00000001; // If KEY_NON_PRINTABLE_KEY is specified and the .key value isn't valid // key name, the methods will throws an exception. In other words, this // flag prevents to dispatch key events with wrong key values and to cause
--- a/testing/mochitest/tests/SimpleTest/EventUtils.js +++ b/testing/mochitest/tests/SimpleTest/EventUtils.js @@ -920,17 +920,17 @@ function synthesizeComposition(aEvent, a return false; } switch (aEvent.type) { case "compositionstart": return TIP.startComposition(); case "compositioncommitasis": return TIP.commitComposition(); case "compositioncommit": - return TIP.commitComposition(aEvent.data); + return TIP.commitComposition(null, 0, aEvent.data); default: return false; } } /** * Synthesize a compositionchange event which causes a DOM text event and * compositionupdate event if it's necessary. *
--- a/widget/TextEventDispatcher.h +++ b/widget/TextEventDispatcher.h @@ -155,16 +155,24 @@ public: * SetCaretInPendingComposition(). */ nsresult FlushPendingComposition(nsEventStatus& aStatus) { return mPendingComposition.Flush(this, aStatus); } /** + * ClearPendingComposition() makes this instance forget pending composition. + */ + void ClearPendingComposition() + { + mPendingComposition.Clear(); + } + + /** * @see nsIWidget::NotifyIME() */ nsresult NotifyIME(const IMENotification& aIMENotification); /** * DispatchKeyboardEvent() maybe dispatches aKeyboardEvent. * * @param aMessage Must be NS_KEY_DOWN or NS_KEY_UP.
--- a/widget/TextEvents.h +++ b/widget/TextEvents.h @@ -183,16 +183,21 @@ public: { if (mCodeNameIndex == CODE_NAME_INDEX_USE_STRING) { aCodeName = mCodeValue; return; } GetDOMCodeName(mCodeNameIndex, aCodeName); } + bool IsModifierKeyEvent() const + { + return GetModifierForKeyName(mKeyNameIndex) != MODIFIER_NONE; + } + static void Shutdown(); /** * ComputeLocationFromCodeValue() returns one of .location value * (nsIDOMKeyEvent::DOM_KEY_LOCATION_*) which is the most preferred value * for the specified specified code value. */ static uint32_t ComputeLocationFromCodeValue(CodeNameIndex aCodeNameIndex);