diff options
Diffstat (limited to 'dom/inputmethod')
52 files changed, 9403 insertions, 0 deletions
diff --git a/dom/inputmethod/HardwareKeyHandler.cpp b/dom/inputmethod/HardwareKeyHandler.cpp new file mode 100644 index 000000000..737c30e5b --- /dev/null +++ b/dom/inputmethod/HardwareKeyHandler.cpp @@ -0,0 +1,562 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "HardwareKeyHandler.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/dom/TabParent.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/TextEvents.h" +#include "nsDeque.h" +#include "nsFocusManager.h" +#include "nsFrameLoader.h" +#include "nsIContent.h" +#include "nsIDOMHTMLDocument.h" +#include "nsIDOMHTMLElement.h" +#include "nsPIDOMWindow.h" +#include "nsPresContext.h" +#include "nsPresShell.h" + +namespace mozilla { + +using namespace dom; + +NS_IMPL_ISUPPORTS(HardwareKeyHandler, nsIHardwareKeyHandler) + +StaticRefPtr<HardwareKeyHandler> HardwareKeyHandler::sInstance; + +HardwareKeyHandler::HardwareKeyHandler() + : mInputMethodAppConnected(false) +{ +} + +HardwareKeyHandler::~HardwareKeyHandler() +{ +} + +NS_IMETHODIMP +HardwareKeyHandler::OnInputMethodAppConnected() +{ + if (NS_WARN_IF(mInputMethodAppConnected)) { + return NS_ERROR_UNEXPECTED; + } + + mInputMethodAppConnected = true; + + return NS_OK; +} + +NS_IMETHODIMP +HardwareKeyHandler::OnInputMethodAppDisconnected() +{ + if (NS_WARN_IF(!mInputMethodAppConnected)) { + return NS_ERROR_UNEXPECTED; + } + + mInputMethodAppConnected = false; + return NS_OK; +} + +NS_IMETHODIMP +HardwareKeyHandler::RegisterListener(nsIHardwareKeyEventListener* aListener) +{ + // Make sure the listener is not nullptr and there is no available + // hardwareKeyEventListener now + if (NS_WARN_IF(!aListener)) { + return NS_ERROR_NULL_POINTER; + } + + if (NS_WARN_IF(mHardwareKeyEventListener)) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + mHardwareKeyEventListener = do_GetWeakReference(aListener); + + if (NS_WARN_IF(!mHardwareKeyEventListener)) { + return NS_ERROR_NULL_POINTER; + } + + return NS_OK; +} + +NS_IMETHODIMP +HardwareKeyHandler::UnregisterListener() +{ + // Clear the HardwareKeyEventListener + mHardwareKeyEventListener = nullptr; + return NS_OK; +} + +bool +HardwareKeyHandler::ForwardKeyToInputMethodApp(nsINode* aTarget, + WidgetKeyboardEvent* aEvent, + nsEventStatus* aEventStatus) +{ + MOZ_ASSERT(aTarget, "No target provided"); + MOZ_ASSERT(aEvent, "No event provided"); + + // No need to forward hardware key event to IME + // if key's defaultPrevented is true + if (aEvent->mFlags.mDefaultPrevented) { + return false; + } + + // No need to forward hardware key event to IME if IME is disabled + if (!mInputMethodAppConnected) { + return false; + } + + // No need to forward hardware key event to IME + // if this key event is generated by IME itself(from nsITextInputProcessor) + if (aEvent->mIsSynthesizedByTIP) { + return false; + } + + // No need to forward hardware key event to IME + // if the key event is handling or already handled + if (aEvent->mInputMethodAppState != WidgetKeyboardEvent::eNotHandled) { + return false; + } + + // No need to forward hardware key event to IME + // if there is no nsIHardwareKeyEventListener in use + nsCOMPtr<nsIHardwareKeyEventListener> + keyHandler(do_QueryReferent(mHardwareKeyEventListener)); + if (!keyHandler) { + return false; + } + + // Set the flags to specify the keyboard event is in forwarding phase. + aEvent->mInputMethodAppState = WidgetKeyboardEvent::eHandling; + + // For those keypress events coming after their heading keydown's reply + // already arrives, they should be dispatched directly instead of + // being stored into the event queue. Otherwise, without the heading keydown + // in the event queue, the stored keypress will never be withdrawn to be fired. + if (aEvent->mMessage == eKeyPress && mEventQueue.IsEmpty()) { + DispatchKeyPress(aTarget, *aEvent, *aEventStatus); + return true; + } + + // Push the key event into queue for reuse when its reply arrives. + KeyboardInfo* copiedInfo = + new KeyboardInfo(aTarget, + *aEvent, + aEventStatus ? *aEventStatus : nsEventStatus_eIgnore); + + // No need to forward hardware key event to IME if the event queue is full + if (!mEventQueue.Push(copiedInfo)) { + delete copiedInfo; + return false; + } + + // We only forward keydown and keyup event to input-method-app + // because input-method-app will generate keypress by itself. + if (aEvent->mMessage == eKeyPress) { + return true; + } + + // Create a keyboard event to pass into + // nsIHardwareKeyEventListener.onHardwareKey + nsCOMPtr<EventTarget> eventTarget = do_QueryInterface(aTarget); + nsPresContext* presContext = GetPresContext(aTarget); + RefPtr<KeyboardEvent> keyboardEvent = + NS_NewDOMKeyboardEvent(eventTarget, presContext, aEvent->AsKeyboardEvent()); + // Duplicate the internal event data in the heap for the keyboardEvent, + // or the internal data from |aEvent| in the stack may be destroyed by others. + keyboardEvent->DuplicatePrivateData(); + + // Forward the created keyboard event to input-method-app + bool isSent = false; + keyHandler->OnHardwareKey(keyboardEvent, &isSent); + + // Pop the pending key event if it can't be forwarded + if (!isSent) { + mEventQueue.RemoveFront(); + } + + return isSent; +} + +NS_IMETHODIMP +HardwareKeyHandler::OnHandledByInputMethodApp(const nsAString& aType, + uint16_t aDefaultPrevented) +{ + // We can not handle this reply because the pending events had been already + // removed from the forwarding queue before this reply arrives. + if (mEventQueue.IsEmpty()) { + return NS_OK; + } + + RefPtr<KeyboardInfo> keyInfo = mEventQueue.PopFront(); + + // Only allow keydown and keyup to call this method + if (NS_WARN_IF(aType.EqualsLiteral("keydown") && + keyInfo->mEvent.mMessage != eKeyDown) || + NS_WARN_IF(aType.EqualsLiteral("keyup") && + keyInfo->mEvent.mMessage != eKeyUp)) { + return NS_ERROR_INVALID_ARG; + } + + // The value of defaultPrevented depends on whether or not + // the key is consumed by input-method-app + SetDefaultPrevented(keyInfo->mEvent, aDefaultPrevented); + + // Set the flag to specify the reply phase + keyInfo->mEvent.mInputMethodAppState = WidgetKeyboardEvent::eHandled; + + // Check whether the event is still valid to be fired + if (CanDispatchEvent(keyInfo->mTarget, keyInfo->mEvent)) { + // If the key's defaultPrevented is true, it means that the + // input-method-app has already consumed this key, + // so we can dispatch |mozbrowserafterkey*| directly if + // preference "dom.beforeAfterKeyboardEvent.enabled" is enabled. + if (keyInfo->mEvent.mFlags.mDefaultPrevented) { + DispatchAfterKeyEvent(keyInfo->mTarget, keyInfo->mEvent); + // Otherwise, it means that input-method-app doesn't handle this key, + // so we need to dispatch it to its current event target. + } else { + DispatchToTargetApp(keyInfo->mTarget, + keyInfo->mEvent, + keyInfo->mStatus); + } + } + + // No need to do further processing if the event is not keydown + if (keyInfo->mEvent.mMessage != eKeyDown) { + return NS_OK; + } + + // Update the latest keydown data: + // Release the holding reference to the previous keydown's data and + // add a reference count to the current keydown's data. + mLatestKeyDownInfo = keyInfo; + + // Handle the pending keypress event once keydown's reply arrives: + // It may have many keypress events per keydown on some platforms, + // so we use loop to dispatch keypress events. + // (But Gonk dispatch only one keypress per keydown) + // However, if there is no keypress after this keydown, + // then those following keypress will be handled in + // ForwardKeyToInputMethodApp directly. + for (KeyboardInfo* keypressInfo; + !mEventQueue.IsEmpty() && + (keypressInfo = mEventQueue.PeekFront()) && + keypressInfo->mEvent.mMessage == eKeyPress; + mEventQueue.RemoveFront()) { + DispatchKeyPress(keypressInfo->mTarget, + keypressInfo->mEvent, + keypressInfo->mStatus); + } + + return NS_OK; +} + +bool +HardwareKeyHandler::DispatchKeyPress(nsINode* aTarget, + WidgetKeyboardEvent& aEvent, + nsEventStatus& aStatus) +{ + MOZ_ASSERT(aTarget, "No target provided"); + MOZ_ASSERT(aEvent.mMessage == eKeyPress, "Event is not keypress"); + + // No need to dispatch keypress to the event target + // if the keydown event is consumed by the input-method-app. + if (mLatestKeyDownInfo && + mLatestKeyDownInfo->mEvent.mFlags.mDefaultPrevented) { + return false; + } + + // No need to dispatch keypress to the event target + // if the previous keydown event is modifier key's + if (mLatestKeyDownInfo && + mLatestKeyDownInfo->mEvent.IsModifierKeyEvent()) { + return false; + } + + // No need to dispatch keypress to the event target + // if it's invalid to be dispatched + if (!CanDispatchEvent(aTarget, aEvent)) { + return false; + } + + // Set the flag to specify the reply phase. + aEvent.mInputMethodAppState = WidgetKeyboardEvent::eHandled; + + // Dispatch the pending keypress event + bool ret = DispatchToTargetApp(aTarget, aEvent, aStatus); + + // Re-trigger EventStateManager::PostHandleKeyboardEvent for keypress + PostHandleKeyboardEvent(aTarget, aEvent, aStatus); + + return ret; +} + +void +HardwareKeyHandler::DispatchAfterKeyEvent(nsINode* aTarget, + WidgetKeyboardEvent& aEvent) +{ + MOZ_ASSERT(aTarget, "No target provided"); + + if (!PresShell::BeforeAfterKeyboardEventEnabled() || + aEvent.mMessage == eKeyPress) { + return; + } + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(aTarget); + if (NS_WARN_IF(presShell)) { + presShell->DispatchAfterKeyboardEvent(aTarget, + aEvent, + aEvent.mFlags.mDefaultPrevented); + } +} + +bool +HardwareKeyHandler::DispatchToTargetApp(nsINode* aTarget, + WidgetKeyboardEvent& aEvent, + nsEventStatus& aStatus) +{ + MOZ_ASSERT(aTarget, "No target provided"); + + // Get current focused element as the event target + nsCOMPtr<nsIContent> currentTarget = GetCurrentTarget(); + if (NS_WARN_IF(!currentTarget)) { + return false; + } + + // The event target should be set to the current focused element. + // However, it might have security issue if the event is dispatched to + // the unexpected application, and it might cause unexpected operation + // in the new app. + nsCOMPtr<nsPIDOMWindowOuter> originalRootWindow = GetRootWindow(aTarget); + nsCOMPtr<nsPIDOMWindowOuter> currentRootWindow = GetRootWindow(currentTarget); + if (currentRootWindow != originalRootWindow) { + NS_WARNING("The root window is changed during the event is dispatching"); + return false; + } + + // If the current focused element is still in the same app, + // then we can use it as the current target to dispatch event. + nsCOMPtr<nsIPresShell> presShell = GetPresShell(currentTarget); + if (!presShell) { + return false; + } + + if (!presShell->CanDispatchEvent(&aEvent)) { + return false; + } + + // In-process case: the event target is in the current process + if (!PresShell::IsTargetIframe(currentTarget)) { + DispatchToCurrentProcess(presShell, currentTarget, aEvent, aStatus); + + if (presShell->CanDispatchEvent(&aEvent)) { + DispatchAfterKeyEvent(aTarget, aEvent); + } + + return true; + } + + // OOP case: the event target is in the child process + return DispatchToCrossProcess(aTarget, aEvent); + + // After the oop target receives the event from TabChild::RecvRealKeyEvent + // and return the result through TabChild::SendDispatchAfterKeyboardEvent, + // the |mozbrowserafterkey*| will be fired from + // TabParent::RecvDispatchAfterKeyboardEvent, so we don't need to dispatch + // |mozbrowserafterkey*| by ourselves in this module. +} + +void +HardwareKeyHandler::DispatchToCurrentProcess(nsIPresShell* presShell, + nsIContent* aTarget, + WidgetKeyboardEvent& aEvent, + nsEventStatus& aStatus) +{ + EventDispatcher::Dispatch(aTarget, presShell->GetPresContext(), + &aEvent, nullptr, &aStatus, nullptr); +} + +bool +HardwareKeyHandler::DispatchToCrossProcess(nsINode* aTarget, + WidgetKeyboardEvent& aEvent) +{ + nsCOMPtr<nsIFrameLoaderOwner> remoteLoaderOwner = do_QueryInterface(aTarget); + if (NS_WARN_IF(!remoteLoaderOwner)) { + return false; + } + + RefPtr<nsFrameLoader> remoteFrameLoader = + remoteLoaderOwner->GetFrameLoader(); + if (NS_WARN_IF(!remoteFrameLoader)) { + return false; + } + + uint32_t eventMode; + remoteFrameLoader->GetEventMode(&eventMode); + if (eventMode == nsIFrameLoader::EVENT_MODE_DONT_FORWARD_TO_CHILD) { + return false; + } + + PBrowserParent* remoteBrowser = remoteFrameLoader->GetRemoteBrowser(); + TabParent* remote = static_cast<TabParent*>(remoteBrowser); + if (NS_WARN_IF(!remote)) { + return false; + } + + return remote->SendRealKeyEvent(aEvent); +} + +void +HardwareKeyHandler::PostHandleKeyboardEvent(nsINode* aTarget, + WidgetKeyboardEvent& aEvent, + nsEventStatus& aStatus) +{ + MOZ_ASSERT(aTarget, "No target provided"); + + nsPresContext* presContext = GetPresContext(aTarget); + + RefPtr<mozilla::EventStateManager> esm = presContext->EventStateManager(); + bool dispatchedToChildProcess = PresShell::IsTargetIframe(aTarget); + esm->PostHandleKeyboardEvent(&aEvent, aStatus, dispatchedToChildProcess); +} + +void +HardwareKeyHandler::SetDefaultPrevented(WidgetKeyboardEvent& aEvent, + uint16_t aDefaultPrevented) { + if (aDefaultPrevented & DEFAULT_PREVENTED) { + aEvent.mFlags.mDefaultPrevented = true; + } + + if (aDefaultPrevented & DEFAULT_PREVENTED_BY_CHROME) { + aEvent.mFlags.mDefaultPreventedByChrome = true; + } + + if (aDefaultPrevented & DEFAULT_PREVENTED_BY_CONTENT) { + aEvent.mFlags.mDefaultPreventedByContent = true; + } +} + +bool +HardwareKeyHandler::CanDispatchEvent(nsINode* aTarget, + WidgetKeyboardEvent& aEvent) +{ + nsCOMPtr<nsIPresShell> presShell = GetPresShell(aTarget); + if (NS_WARN_IF(!presShell)) { + return false; + } + return presShell->CanDispatchEvent(&aEvent); +} + +already_AddRefed<nsPIDOMWindowOuter> +HardwareKeyHandler::GetRootWindow(nsINode* aNode) +{ + // Get nsIPresShell's pointer first + nsCOMPtr<nsIPresShell> presShell = GetPresShell(aNode); + if (NS_WARN_IF(!presShell)) { + return nullptr; + } + nsCOMPtr<nsPIDOMWindowOuter> rootWindow = presShell->GetRootWindow(); + return rootWindow.forget(); +} + +already_AddRefed<nsIContent> +HardwareKeyHandler::GetCurrentTarget() +{ + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (NS_WARN_IF(!fm)) { + return nullptr; + } + + nsCOMPtr<mozIDOMWindowProxy> focusedWindow; + fm->GetFocusedWindow(getter_AddRefs(focusedWindow)); + if (NS_WARN_IF(!focusedWindow)) { + return nullptr; + } + + auto* ourWindow = nsPIDOMWindowOuter::From(focusedWindow); + + nsCOMPtr<nsPIDOMWindowOuter> rootWindow = ourWindow->GetPrivateRoot(); + if (NS_WARN_IF(!rootWindow)) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowOuter> focusedFrame; + nsCOMPtr<nsIContent> focusedContent = + fm->GetFocusedDescendant(rootWindow, true, getter_AddRefs(focusedFrame)); + + // If there is no focus, then we use document body instead + if (NS_WARN_IF(!focusedContent || !focusedContent->GetPrimaryFrame())) { + nsIDocument* document = ourWindow->GetExtantDoc(); + if (NS_WARN_IF(!document)) { + return nullptr; + } + + focusedContent = document->GetRootElement(); + + nsCOMPtr<nsIDOMHTMLDocument> htmlDocument = do_QueryInterface(document); + if (htmlDocument) { + nsCOMPtr<nsIDOMHTMLElement> body; + htmlDocument->GetBody(getter_AddRefs(body)); + nsCOMPtr<nsIContent> bodyContent = do_QueryInterface(body); + if (bodyContent) { + focusedContent = bodyContent; + } + } + } + + return focusedContent ? focusedContent.forget() : nullptr; +} + +nsPresContext* +HardwareKeyHandler::GetPresContext(nsINode* aNode) +{ + // Get nsIPresShell's pointer first + nsCOMPtr<nsIPresShell> presShell = GetPresShell(aNode); + if (NS_WARN_IF(!presShell)) { + return nullptr; + } + + // then use nsIPresShell to get nsPresContext's pointer + return presShell->GetPresContext(); +} + +already_AddRefed<nsIPresShell> +HardwareKeyHandler::GetPresShell(nsINode* aNode) +{ + nsIDocument* doc = aNode->OwnerDoc(); + if (NS_WARN_IF(!doc)) { + return nullptr; + } + + nsCOMPtr<nsIPresShell> presShell = doc->GetShell(); + if (NS_WARN_IF(!presShell)) { + return nullptr; + } + + return presShell.forget(); +} + +/* static */ +already_AddRefed<HardwareKeyHandler> +HardwareKeyHandler::GetInstance() +{ + if (!XRE_IsParentProcess()) { + return nullptr; + } + + if (!sInstance) { + sInstance = new HardwareKeyHandler(); + ClearOnShutdown(&sInstance); + } + + RefPtr<HardwareKeyHandler> service = sInstance.get(); + return service.forget(); +} + +} // namespace mozilla diff --git a/dom/inputmethod/HardwareKeyHandler.h b/dom/inputmethod/HardwareKeyHandler.h new file mode 100644 index 000000000..7520c40cd --- /dev/null +++ b/dom/inputmethod/HardwareKeyHandler.h @@ -0,0 +1,224 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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_HardwareKeyHandler_h_ +#define mozilla_HardwareKeyHandler_h_ + +#include "mozilla/EventForwards.h" // for nsEventStatus +#include "mozilla/StaticPtr.h" +#include "mozilla/TextEvents.h" +#include "nsCOMPtr.h" +#include "nsDeque.h" +#include "nsIHardwareKeyHandler.h" +#include "nsIWeakReferenceUtils.h" // for nsWeakPtr + +class nsIContent; +class nsINode; +class nsIPresShell; +class nsPIDOMWindowOuter; +class nsPresContext; + +namespace mozilla { + +// This module will copy the events' data into its event queue for reuse +// after receiving input-method-app's reply, so we use the following struct +// for storing these information. +// RefCounted<T> is a helper class for adding reference counting mechanism. +struct KeyboardInfo : public RefCounted<KeyboardInfo> +{ + MOZ_DECLARE_REFCOUNTED_TYPENAME(KeyboardInfo) + + nsINode* mTarget; + WidgetKeyboardEvent mEvent; + nsEventStatus mStatus; + + KeyboardInfo(nsINode* aTarget, + WidgetKeyboardEvent& aEvent, + nsEventStatus aStatus) + : mTarget(aTarget) + , mEvent(aEvent) + , mStatus(aStatus) + { + } +}; + +// The following is the type-safe wrapper around nsDeque +// for storing events' data. +// The T must be one class that supports reference counting mechanism. +// The EventQueueDeallocator will be called in nsDeque::~nsDeque() or +// nsDeque::Erase() to deallocate the objects. nsDeque::Erase() will remove +// and delete all items in the queue. See more from nsDeque.h. +template <class T> +class EventQueueDeallocator : public nsDequeFunctor +{ + virtual void* operator() (void* aObject) + { + RefPtr<T> releaseMe = dont_AddRef(static_cast<T*>(aObject)); + return nullptr; + } +}; + +// The type-safe queue to be used to store the KeyboardInfo data +template <class T> +class EventQueue : private nsDeque +{ +public: + EventQueue() + : nsDeque(new EventQueueDeallocator<T>()) + { + }; + + ~EventQueue() + { + Clear(); + } + + inline size_t GetSize() + { + return nsDeque::GetSize(); + } + + bool IsEmpty() + { + return !nsDeque::GetSize(); + } + + inline bool Push(T* aItem) + { + MOZ_ASSERT(aItem); + NS_ADDREF(aItem); + size_t sizeBefore = GetSize(); + nsDeque::Push(aItem); + if (GetSize() != sizeBefore + 1) { + NS_RELEASE(aItem); + return false; + } + return true; + } + + inline already_AddRefed<T> PopFront() + { + RefPtr<T> rv = dont_AddRef(static_cast<T*>(nsDeque::PopFront())); + return rv.forget(); + } + + inline void RemoveFront() + { + RefPtr<T> releaseMe = PopFront(); + } + + inline T* PeekFront() + { + return static_cast<T*>(nsDeque::PeekFront()); + } + + void Clear() + { + while (GetSize() > 0) { + RemoveFront(); + } + } +}; + +class HardwareKeyHandler : public nsIHardwareKeyHandler +{ +public: + HardwareKeyHandler(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIHARDWAREKEYHANDLER + + static already_AddRefed<HardwareKeyHandler> GetInstance(); + + virtual bool ForwardKeyToInputMethodApp(nsINode* aTarget, + WidgetKeyboardEvent* aEvent, + nsEventStatus* aEventStatus) override; + +private: + virtual ~HardwareKeyHandler(); + + // Return true if the keypress is successfully dispatched. + // Otherwise, return false. + bool DispatchKeyPress(nsINode* aTarget, + WidgetKeyboardEvent& aEvent, + nsEventStatus& aStatus); + + void DispatchAfterKeyEvent(nsINode* aTarget, WidgetKeyboardEvent& aEvent); + + void DispatchToCurrentProcess(nsIPresShell* aPresShell, + nsIContent* aTarget, + WidgetKeyboardEvent& aEvent, + nsEventStatus& aStatus); + + bool DispatchToCrossProcess(nsINode* aTarget, WidgetKeyboardEvent& aEvent); + + // This method will dispatch not only key* event to its event target, + // no mather it's in the current process or in its child process, + // but also mozbrowserafterkey* to the corresponding target if it needs. + // Return true if the key is successfully dispatched. + // Otherwise, return false. + bool DispatchToTargetApp(nsINode* aTarget, + WidgetKeyboardEvent& aEvent, + nsEventStatus& aStatus); + + // This method will be called after dispatching keypress to its target, + // if the input-method-app doesn't handle the key. + // In normal dispatching path, EventStateManager::PostHandleKeyboardEvent + // will be called when event is keypress. + // However, the ::PostHandleKeyboardEvent mentioned above will be aborted + // when we try to forward key event to the input-method-app. + // If the input-method-app consumes the key, then we don't need to do anything + // because the input-method-app will generate a new key event by itself. + // On the other hand, if the input-method-app doesn't consume the key, + // then we need to dispatch the key event by ourselves + // and call ::PostHandleKeyboardEvent again after the event is forwarded. + // Note that the EventStateManager::PreHandleEvent is already called before + // forwarding, so we don't need to call it in this module. + void PostHandleKeyboardEvent(nsINode* aTarget, + WidgetKeyboardEvent& aEvent, + nsEventStatus& aStatus); + + void SetDefaultPrevented(WidgetKeyboardEvent& aEvent, + uint16_t aDefaultPrevented); + + // Check whether the event is valid to be fired. + // This method should be called every time before dispatching next event. + bool CanDispatchEvent(nsINode* aTarget, + WidgetKeyboardEvent& aEvent); + + already_AddRefed<nsPIDOMWindowOuter> GetRootWindow(nsINode* aNode); + + already_AddRefed<nsIContent> GetCurrentTarget(); + + nsPresContext* GetPresContext(nsINode* aNode); + + already_AddRefed<nsIPresShell> GetPresShell(nsINode* aNode); + + static StaticRefPtr<HardwareKeyHandler> sInstance; + + // The event queue is used to store the forwarded keyboard events. + // Those stored events will be dispatched if input-method-app doesn't + // consume them. + EventQueue<KeyboardInfo> mEventQueue; + + // Hold the pointer to the latest keydown's data + RefPtr<KeyboardInfo> mLatestKeyDownInfo; + + // input-method-app needs to register a listener by + // |nsIHardwareKeyHandler.registerListener| to receive + // the hardware keyboard event, and |nsIHardwareKeyHandler.registerListener| + // will set an nsIHardwareKeyEventListener to mHardwareKeyEventListener. + // Then, mHardwareKeyEventListener is used to forward the event + // to the input-method-app. + nsWeakPtr mHardwareKeyEventListener; + + // To keep tracking the input-method-app is active or disabled. + bool mInputMethodAppConnected; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_HardwareKeyHandler_h_ diff --git a/dom/inputmethod/InputMethod.manifest b/dom/inputmethod/InputMethod.manifest new file mode 100644 index 000000000..5dc073508 --- /dev/null +++ b/dom/inputmethod/InputMethod.manifest @@ -0,0 +1,2 @@ +component {4607330d-e7d2-40a4-9eb8-43967eae0142} MozKeyboard.js +contract @mozilla.org/b2g-inputmethod;1 {4607330d-e7d2-40a4-9eb8-43967eae0142} diff --git a/dom/inputmethod/Keyboard.jsm b/dom/inputmethod/Keyboard.jsm new file mode 100644 index 000000000..22f87ffbc --- /dev/null +++ b/dom/inputmethod/Keyboard.jsm @@ -0,0 +1,644 @@ +/* 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/. */ + +'use strict'; + +this.EXPORTED_SYMBOLS = ['Keyboard']; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster"); + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +XPCOMUtils.defineLazyGetter(this, "appsService", function() { + return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService); +}); + +XPCOMUtils.defineLazyGetter(this, "hardwareKeyHandler", function() { +#ifdef MOZ_B2G + return Cc["@mozilla.org/HardwareKeyHandler;1"] + .getService(Ci.nsIHardwareKeyHandler); +#else + return null; +#endif +}); + +var Utils = { + getMMFromMessage: function u_getMMFromMessage(msg) { + let mm; + try { + mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader.messageManager; + } catch(e) { + mm = msg.target; + } + + return mm; + }, + checkPermissionForMM: function u_checkPermissionForMM(mm, permName) { + return mm.assertPermission(permName); + } +}; + +this.Keyboard = { +#ifdef MOZ_B2G + // For receving keyboard event fired from hardware before it's dispatched, + // |this| object is used to be the listener to get the forwarded event. + // As the listener, |this| object must implement nsIHardwareKeyEventListener + // and nsSupportsWeakReference. + // Please see nsIHardwareKeyHandler.idl to get more information. + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIHardwareKeyEventListener, + Ci.nsISupportsWeakReference + ]), +#endif + _isConnectedToHardwareKeyHandler: false, + _formMM: null, // The current web page message manager. + _keyboardMM: null, // The keyboard app message manager. + _keyboardID: -1, // The keyboard app's ID number. -1 = invalid + _nextKeyboardID: 0, // The ID number counter. + _systemMMs: [], // The message managers registered to handle system async + // messages. + _supportsSwitchingTypes: [], + _systemMessageNames: [ + 'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions', + 'SetSupportsSwitchingTypes', 'RegisterSync', 'Unregister' + ], + + _messageNames: [ + 'RemoveFocus', + 'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker', + 'SwitchToNextInputMethod', 'HideInputMethod', + 'SendKey', 'GetContext', + 'SetComposition', 'EndComposition', + 'RegisterSync', 'Unregister', + 'ReplyHardwareKeyEvent' + ], + + get formMM() { + if (this._formMM && !Cu.isDeadWrapper(this._formMM)) + return this._formMM; + + return null; + }, + + set formMM(mm) { + this._formMM = mm; + }, + + sendToForm: function(name, data) { + if (!this.formMM) { + dump("Keyboard.jsm: Attempt to send message " + name + + " to form but no message manager exists.\n"); + + return; + } + try { + this.formMM.sendAsyncMessage(name, data); + } catch(e) { } + }, + + sendToKeyboard: function(name, data) { + try { + this._keyboardMM.sendAsyncMessage(name, data); + } catch(e) { + return false; + } + return true; + }, + + sendToSystem: function(name, data) { + if (!this._systemMMs.length) { + dump("Keyboard.jsm: Attempt to send message " + name + + " to system but no message manager registered.\n"); + + return; + } + + this._systemMMs.forEach((mm, i) => { + data.inputManageId = i; + mm.sendAsyncMessage(name, data); + }); + }, + + init: function keyboardInit() { + Services.obs.addObserver(this, 'inprocess-browser-shown', false); + Services.obs.addObserver(this, 'remote-browser-shown', false); + Services.obs.addObserver(this, 'oop-frameloader-crashed', false); + Services.obs.addObserver(this, 'message-manager-close', false); + + // For receiving the native hardware keyboard event + if (hardwareKeyHandler) { + hardwareKeyHandler.registerListener(this); + } + + for (let name of this._messageNames) { + ppmm.addMessageListener('Keyboard:' + name, this); + } + + for (let name of this._systemMessageNames) { + ppmm.addMessageListener('System:' + name, this); + } + + this.inputRegistryGlue = new InputRegistryGlue(); + }, + + // This method will be registered into nsIHardwareKeyHandler: + // Send the initialized dictionary retrieved from the native keyboard event + // to input-method-app for generating a new event. + onHardwareKey: function onHardwareKeyReceived(evt) { + return this.sendToKeyboard('Keyboard:ReceiveHardwareKeyEvent', { + type: evt.type, + keyDict: evt.initDict + }); + }, + + observe: function keyboardObserve(subject, topic, data) { + let frameLoader = null; + let mm = null; + + if (topic == 'message-manager-close') { + mm = subject; + } else { + frameLoader = subject.QueryInterface(Ci.nsIFrameLoader); + mm = frameLoader.messageManager; + } + + if (topic == 'oop-frameloader-crashed' || + topic == 'message-manager-close') { + if (this.formMM == mm) { + // The application has been closed unexpectingly. Let's tell the + // keyboard app that the focus has been lost. + this.sendToKeyboard('Keyboard:Blur', {}); + // Notify system app to hide keyboard. + this.sendToSystem('System:Blur', {}); + // XXX: To be removed when content migrate away from mozChromeEvents. + SystemAppProxy.dispatchEvent({ + type: 'inputmethod-contextchange', + inputType: 'blur' + }); + + this.formMM = null; + } + } else { + // Ignore notifications that aren't from a BrowserOrApp + if (!frameLoader.ownerIsMozBrowserOrAppFrame) { + return; + } + this.initFormsFrameScript(mm); + } + }, + + initFormsFrameScript: function(mm) { + mm.addMessageListener('Forms:Focus', this); + mm.addMessageListener('Forms:Blur', this); + mm.addMessageListener('Forms:SelectionChange', this); + mm.addMessageListener('Forms:SetSelectionRange:Result:OK', this); + mm.addMessageListener('Forms:SetSelectionRange:Result:Error', this); + mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this); + mm.addMessageListener('Forms:ReplaceSurroundingText:Result:Error', this); + mm.addMessageListener('Forms:SendKey:Result:OK', this); + mm.addMessageListener('Forms:SendKey:Result:Error', this); + mm.addMessageListener('Forms:SequenceError', this); + mm.addMessageListener('Forms:GetContext:Result:OK', this); + mm.addMessageListener('Forms:SetComposition:Result:OK', this); + mm.addMessageListener('Forms:EndComposition:Result:OK', this); + }, + + receiveMessage: function keyboardReceiveMessage(msg) { + // If we get a 'Keyboard:XXX'/'System:XXX' message, check that the sender + // has the required permission. + let mm; + + // Assert the permission based on the prefix of the message. + let permName; + if (msg.name.startsWith("Keyboard:")) { + permName = "input"; + } else if (msg.name.startsWith("System:")) { + permName = "input-manage"; + } + + // There is no permission to check (nor we need to get the mm) + // for Form: messages. + if (permName) { + mm = Utils.getMMFromMessage(msg); + if (!mm) { + dump("Keyboard.jsm: Message " + msg.name + " has no message manager."); + return; + } + if (!Utils.checkPermissionForMM(mm, permName)) { + dump("Keyboard.jsm: Message " + msg.name + + " from a content process with no '" + permName + "' privileges.\n"); + return; + } + } + + // we don't process kb messages (other than register) + // if they come from a kb that we're currently not regsitered for. + // this decision is made with the kbID kept by us and kb app + let kbID = null; + if ('kbID' in msg.data) { + kbID = msg.data.kbID; + } + + if (0 === msg.name.indexOf('Keyboard:') && + ('Keyboard:RegisterSync' !== msg.name && this._keyboardID !== kbID) + ) { + return; + } + + switch (msg.name) { + case 'Forms:Focus': + this.handleFocus(msg); + break; + case 'Forms:Blur': + this.handleBlur(msg); + break; + case 'Forms:SelectionChange': + case 'Forms:SetSelectionRange:Result:OK': + case 'Forms:ReplaceSurroundingText:Result:OK': + case 'Forms:SendKey:Result:OK': + case 'Forms:SendKey:Result:Error': + case 'Forms:SequenceError': + case 'Forms:GetContext:Result:OK': + case 'Forms:SetComposition:Result:OK': + case 'Forms:EndComposition:Result:OK': + case 'Forms:SetSelectionRange:Result:Error': + case 'Forms:ReplaceSurroundingText:Result:Error': + let name = msg.name.replace(/^Forms/, 'Keyboard'); + this.forwardEvent(name, msg); + break; + + case 'System:SetValue': + this.setValue(msg); + break; + case 'Keyboard:RemoveFocus': + case 'System:RemoveFocus': + this.removeFocus(); + break; + case 'System:RegisterSync': { + if (this._systemMMs.length !== 0) { + dump('Keyboard.jsm Warning: There are more than one content page ' + + 'with input-manage permission. There will be undeterministic ' + + 'responses to addInput()/removeInput() if both content pages are ' + + 'trying to respond to the same request event.\n'); + } + + let id = this._systemMMs.length; + this._systemMMs.push(mm); + + return id; + } + + case 'System:Unregister': + this._systemMMs.splice(msg.data.id, 1); + + break; + case 'System:SetSelectedOption': + this.setSelectedOption(msg); + break; + case 'System:SetSelectedOptions': + this.setSelectedOption(msg); + break; + case 'System:SetSupportsSwitchingTypes': + this.setSupportsSwitchingTypes(msg); + break; + case 'Keyboard:SetSelectionRange': + this.setSelectionRange(msg); + break; + case 'Keyboard:ReplaceSurroundingText': + this.replaceSurroundingText(msg); + break; + case 'Keyboard:SwitchToNextInputMethod': + this.switchToNextInputMethod(); + break; + case 'Keyboard:ShowInputMethodPicker': + this.showInputMethodPicker(); + break; + case 'Keyboard:SendKey': + this.sendKey(msg); + break; + case 'Keyboard:GetContext': + this.getContext(msg); + break; + case 'Keyboard:SetComposition': + this.setComposition(msg); + break; + case 'Keyboard:EndComposition': + this.endComposition(msg); + break; + case 'Keyboard:RegisterSync': + this._keyboardMM = mm; + if (kbID) { + // keyboard identifies itself, use its kbID + // this msg would be async, so no need to return + this._keyboardID = kbID; + }else{ + // generate the id for the keyboard + this._keyboardID = this._nextKeyboardID; + this._nextKeyboardID++; + // this msg is sync, + // and we want to return the id back to inputmethod + return this._keyboardID; + } + break; + case 'Keyboard:Unregister': + this._keyboardMM = null; + this._keyboardID = -1; + break; + case 'Keyboard:ReplyHardwareKeyEvent': + if (hardwareKeyHandler) { + let reply = msg.data; + hardwareKeyHandler.onHandledByInputMethodApp(reply.type, + reply.defaultPrevented); + } + break; + } + }, + + handleFocus: function keyboardHandleFocus(msg) { + // Set the formMM to the new message manager received. + let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader.messageManager; + this.formMM = mm; + + // Notify the nsIHardwareKeyHandler that the input-method-app is active now. + if (hardwareKeyHandler && !this._isConnectedToHardwareKeyHandler) { + this._isConnectedToHardwareKeyHandler = true; + hardwareKeyHandler.onInputMethodAppConnected(); + } + + // Notify the current active input app to gain focus. + this.forwardEvent('Keyboard:Focus', msg); + + // Notify System app, used also to render value selectors for now; + // that's why we need the info about choices / min / max here as well... + this.sendToSystem('System:Focus', msg.data); + + // XXX: To be removed when content migrate away from mozChromeEvents. + SystemAppProxy.dispatchEvent({ + type: 'inputmethod-contextchange', + inputType: msg.data.inputType, + value: msg.data.value, + choices: JSON.stringify(msg.data.choices), + min: msg.data.min, + max: msg.data.max + }); + }, + + handleBlur: function keyboardHandleBlur(msg) { + let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader.messageManager; + // A blur message can't be sent to the keyboard if the focus has + // already been taken away at first place. + // This check is here to prevent problem caused by out-of-order + // ipc messages from two processes. + if (mm !== this.formMM) { + return; + } + + // unset formMM + this.formMM = null; + + // Notify the nsIHardwareKeyHandler that + // the input-method-app is disabled now. + if (hardwareKeyHandler && this._isConnectedToHardwareKeyHandler) { + this._isConnectedToHardwareKeyHandler = false; + hardwareKeyHandler.onInputMethodAppDisconnected(); + } + + this.forwardEvent('Keyboard:Blur', msg); + this.sendToSystem('System:Blur', {}); + + // XXX: To be removed when content migrate away from mozChromeEvents. + SystemAppProxy.dispatchEvent({ + type: 'inputmethod-contextchange', + inputType: 'blur' + }); + }, + + forwardEvent: function keyboardForwardEvent(newEventName, msg) { + this.sendToKeyboard(newEventName, msg.data); + }, + + setSelectedOption: function keyboardSetSelectedOption(msg) { + this.sendToForm('Forms:Select:Choice', msg.data); + }, + + setSelectedOptions: function keyboardSetSelectedOptions(msg) { + this.sendToForm('Forms:Select:Choice', msg.data); + }, + + setSelectionRange: function keyboardSetSelectionRange(msg) { + this.sendToForm('Forms:SetSelectionRange', msg.data); + }, + + setValue: function keyboardSetValue(msg) { + this.sendToForm('Forms:Input:Value', msg.data); + }, + + removeFocus: function keyboardRemoveFocus() { + if (!this.formMM) { + return; + } + + this.sendToForm('Forms:Select:Blur', {}); + }, + + replaceSurroundingText: function keyboardReplaceSurroundingText(msg) { + this.sendToForm('Forms:ReplaceSurroundingText', msg.data); + }, + + showInputMethodPicker: function keyboardShowInputMethodPicker() { + this.sendToSystem('System:ShowAll', {}); + + // XXX: To be removed with mozContentEvent support from shell.js + SystemAppProxy.dispatchEvent({ + type: "inputmethod-showall" + }); + }, + + switchToNextInputMethod: function keyboardSwitchToNextInputMethod() { + this.sendToSystem('System:Next', {}); + + // XXX: To be removed with mozContentEvent support from shell.js + SystemAppProxy.dispatchEvent({ + type: "inputmethod-next" + }); + }, + + sendKey: function keyboardSendKey(msg) { + this.sendToForm('Forms:Input:SendKey', msg.data); + }, + + getContext: function keyboardGetContext(msg) { + if (!this.formMM) { + return; + } + + this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', { + types: this._supportsSwitchingTypes + }); + + this.sendToForm('Forms:GetContext', msg.data); + }, + + setComposition: function keyboardSetComposition(msg) { + this.sendToForm('Forms:SetComposition', msg.data); + }, + + endComposition: function keyboardEndComposition(msg) { + this.sendToForm('Forms:EndComposition', msg.data); + }, + + setSupportsSwitchingTypes: function setSupportsSwitchingTypes(msg) { + this._supportsSwitchingTypes = msg.data.types; + this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', msg.data); + }, + // XXX: To be removed with mozContentEvent support from shell.js + setLayouts: function keyboardSetLayouts(layouts) { + // The input method plugins may not have loaded yet, + // cache the layouts so on init we can respond immediately instead + // of going back and forth between keyboard_manager + var types = []; + + Object.keys(layouts).forEach((type) => { + if (layouts[type] > 1) { + types.push(type); + } + }); + + this._supportsSwitchingTypes = types; + + this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', { + types: types + }); + } +}; + +function InputRegistryGlue() { + this._messageId = 0; + this._msgMap = new Map(); + + ppmm.addMessageListener('InputRegistry:Add', this); + ppmm.addMessageListener('InputRegistry:Remove', this); + ppmm.addMessageListener('System:InputRegistry:Add:Done', this); + ppmm.addMessageListener('System:InputRegistry:Remove:Done', this); +}; + +InputRegistryGlue.prototype.receiveMessage = function(msg) { + let mm = Utils.getMMFromMessage(msg); + + let permName = msg.name.startsWith("System:") ? "input-mgmt" : "input"; + if (!Utils.checkPermissionForMM(mm, permName)) { + dump("InputRegistryGlue message " + msg.name + + " from a content process with no " + permName + " privileges."); + return; + } + + switch (msg.name) { + case 'InputRegistry:Add': + this.addInput(msg, mm); + + break; + + case 'InputRegistry:Remove': + this.removeInput(msg, mm); + + break; + + case 'System:InputRegistry:Add:Done': + case 'System:InputRegistry:Remove:Done': + this.returnMessage(msg.data); + + break; + } +}; + +InputRegistryGlue.prototype.addInput = function(msg, mm) { + let msgId = this._messageId++; + this._msgMap.set(msgId, { + mm: mm, + requestId: msg.data.requestId + }); + + let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId); + + Keyboard.sendToSystem('System:InputRegistry:Add', { + id: msgId, + manifestURL: manifestURL, + inputId: msg.data.inputId, + inputManifest: msg.data.inputManifest + }); + + // XXX: To be removed when content migrate away from mozChromeEvents. + SystemAppProxy.dispatchEvent({ + type: 'inputregistry-add', + id: msgId, + manifestURL: manifestURL, + inputId: msg.data.inputId, + inputManifest: msg.data.inputManifest + }); +}; + +InputRegistryGlue.prototype.removeInput = function(msg, mm) { + let msgId = this._messageId++; + this._msgMap.set(msgId, { + mm: mm, + requestId: msg.data.requestId + }); + + let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId); + + Keyboard.sendToSystem('System:InputRegistry:Remove', { + id: msgId, + manifestURL: manifestURL, + inputId: msg.data.inputId + }); + + // XXX: To be removed when content migrate away from mozChromeEvents. + SystemAppProxy.dispatchEvent({ + type: 'inputregistry-remove', + id: msgId, + manifestURL: manifestURL, + inputId: msg.data.inputId + }); +}; + +InputRegistryGlue.prototype.returnMessage = function(detail) { + if (!this._msgMap.has(detail.id)) { + dump('InputRegistryGlue: Ignoring already handled message response. ' + + 'id=' + detail.id + '\n'); + return; + } + + let { mm, requestId } = this._msgMap.get(detail.id); + this._msgMap.delete(detail.id); + + if (Cu.isDeadWrapper(mm)) { + dump('InputRegistryGlue: Message manager has already died.\n'); + return; + } + + if (!('error' in detail)) { + mm.sendAsyncMessage('InputRegistry:Result:OK', { + requestId: requestId + }); + } else { + mm.sendAsyncMessage('InputRegistry:Result:Error', { + error: detail.error, + requestId: requestId + }); + } +}; + +this.Keyboard.init(); diff --git a/dom/inputmethod/MozKeyboard.js b/dom/inputmethod/MozKeyboard.js new file mode 100644 index 000000000..3996f3e5d --- /dev/null +++ b/dom/inputmethod/MozKeyboard.js @@ -0,0 +1,1255 @@ +/* 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/. */ + +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", "nsISyncMessageSender"); + +XPCOMUtils.defineLazyServiceGetter(this, "tm", + "@mozilla.org/thread-manager;1", "nsIThreadManager"); + +/* + * A WeakMap to map input method iframe window to + * it's active status, kbID, and ipcHelper. + */ +var WindowMap = { + // WeakMap of <window, object> pairs. + _map: null, + + /* + * Set the object associated to the window and return it. + */ + _getObjForWin: function(win) { + if (!this._map) { + this._map = new WeakMap(); + } + if (this._map.has(win)) { + return this._map.get(win); + } else { + let obj = { + active: false, + kbID: undefined, + ipcHelper: null + }; + this._map.set(win, obj); + + return obj; + } + }, + + /* + * Check if the given window is active. + */ + isActive: function(win) { + if (!this._map || !win) { + return false; + } + + return this._getObjForWin(win).active; + }, + + /* + * Set the active status of the given window. + */ + setActive: function(win, isActive) { + if (!win) { + return; + } + let obj = this._getObjForWin(win); + obj.active = isActive; + }, + + /* + * Get the keyboard ID (assigned by Keyboard.jsm) of the given window. + */ + getKbID: function(win) { + if (!this._map || !win) { + return undefined; + } + + let obj = this._getObjForWin(win); + return obj.kbID; + }, + + /* + * Set the keyboard ID (assigned by Keyboard.jsm) of the given window. + */ + setKbID: function(win, kbID) { + if (!win) { + return; + } + let obj = this._getObjForWin(win); + obj.kbID = kbID; + }, + + /* + * Get InputContextDOMRequestIpcHelper instance attached to this window. + */ + getInputContextIpcHelper: function(win) { + if (!win) { + return; + } + let obj = this._getObjForWin(win); + if (!obj.ipcHelper) { + obj.ipcHelper = new InputContextDOMRequestIpcHelper(win); + } + return obj.ipcHelper; + }, + + /* + * Unset InputContextDOMRequestIpcHelper instance. + */ + unsetInputContextIpcHelper: function(win) { + if (!win) { + return; + } + let obj = this._getObjForWin(win); + if (!obj.ipcHelper) { + return; + } + obj.ipcHelper = null; + } +}; + +var cpmmSendAsyncMessageWithKbID = function (self, msg, data) { + data.kbID = WindowMap.getKbID(self._window); + cpmm.sendAsyncMessage(msg, data); +}; + +/** + * ============================================== + * InputMethodManager + * ============================================== + */ +function MozInputMethodManager(win) { + this._window = win; +} + +MozInputMethodManager.prototype = { + supportsSwitchingForCurrentInputContext: false, + _window: null, + + classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"), + + QueryInterface: XPCOMUtils.generateQI([]), + + set oninputcontextfocus(handler) { + this.__DOM_IMPL__.setEventHandler("oninputcontextfocus", handler); + }, + + get oninputcontextfocus() { + return this.__DOM_IMPL__.getEventHandler("oninputcontextfocus"); + }, + + set oninputcontextblur(handler) { + this.__DOM_IMPL__.setEventHandler("oninputcontextblur", handler); + }, + + get oninputcontextblur() { + return this.__DOM_IMPL__.getEventHandler("oninputcontextblur"); + }, + + set onshowallrequest(handler) { + this.__DOM_IMPL__.setEventHandler("onshowallrequest", handler); + }, + + get onshowallrequest() { + return this.__DOM_IMPL__.getEventHandler("onshowallrequest"); + }, + + set onnextrequest(handler) { + this.__DOM_IMPL__.setEventHandler("onnextrequest", handler); + }, + + get onnextrequest() { + return this.__DOM_IMPL__.getEventHandler("onnextrequest"); + }, + + set onaddinputrequest(handler) { + this.__DOM_IMPL__.setEventHandler("onaddinputrequest", handler); + }, + + get onaddinputrequest() { + return this.__DOM_IMPL__.getEventHandler("onaddinputrequest"); + }, + + set onremoveinputrequest(handler) { + this.__DOM_IMPL__.setEventHandler("onremoveinputrequest", handler); + }, + + get onremoveinputrequest() { + return this.__DOM_IMPL__.getEventHandler("onremoveinputrequest"); + }, + + showAll: function() { + if (!WindowMap.isActive(this._window)) { + return; + } + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ShowInputMethodPicker', {}); + }, + + next: function() { + if (!WindowMap.isActive(this._window)) { + return; + } + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SwitchToNextInputMethod', {}); + }, + + supportsSwitching: function() { + if (!WindowMap.isActive(this._window)) { + return false; + } + return this.supportsSwitchingForCurrentInputContext; + }, + + hide: function() { + if (!WindowMap.isActive(this._window)) { + return; + } + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RemoveFocus', {}); + }, + + setSupportsSwitchingTypes: function(types) { + cpmm.sendAsyncMessage('System:SetSupportsSwitchingTypes', { + types: types + }); + }, + + handleFocus: function(data) { + let detail = new MozInputContextFocusEventDetail(this._window, data); + let wrappedDetail = + this._window.MozInputContextFocusEventDetail._create(this._window, detail); + let event = new this._window.CustomEvent('inputcontextfocus', + { cancelable: true, detail: wrappedDetail }); + + let handled = !this.__DOM_IMPL__.dispatchEvent(event); + + // A gentle warning if the event is not preventDefault() by the content. + if (!handled) { + dump('MozKeyboard.js: A frame with input-manage permission did not' + + ' handle the inputcontextfocus event dispatched.\n'); + } + }, + + handleBlur: function(data) { + let event = + new this._window.Event('inputcontextblur', { cancelable: true }); + + let handled = !this.__DOM_IMPL__.dispatchEvent(event); + + // A gentle warning if the event is not preventDefault() by the content. + if (!handled) { + dump('MozKeyboard.js: A frame with input-manage permission did not' + + ' handle the inputcontextblur event dispatched.\n'); + } + }, + + dispatchShowAllRequestEvent: function() { + this._fireSimpleEvent('showallrequest'); + }, + + dispatchNextRequestEvent: function() { + this._fireSimpleEvent('nextrequest'); + }, + + _fireSimpleEvent: function(eventType) { + let event = new this._window.Event(eventType); + let handled = !this.__DOM_IMPL__.dispatchEvent(event, { cancelable: true }); + + // A gentle warning if the event is not preventDefault() by the content. + if (!handled) { + dump('MozKeyboard.js: A frame with input-manage permission did not' + + ' handle the ' + eventType + ' event dispatched.\n'); + } + }, + + handleAddInput: function(data) { + let p = this._fireInputRegistryEvent('addinputrequest', data); + if (!p) { + return; + } + + p.then(() => { + cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', { + id: data.id + }); + }, (error) => { + cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', { + id: data.id, + error: error || 'Unknown Error' + }); + }); + }, + + handleRemoveInput: function(data) { + let p = this._fireInputRegistryEvent('removeinputrequest', data); + if (!p) { + return; + } + + p.then(() => { + cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', { + id: data.id + }); + }, (error) => { + cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', { + id: data.id, + error: error || 'Unknown Error' + }); + }); + }, + + _fireInputRegistryEvent: function(eventType, data) { + let detail = new MozInputRegistryEventDetail(this._window, data); + let wrappedDetail = + this._window.MozInputRegistryEventDetail._create(this._window, detail); + let event = new this._window.CustomEvent(eventType, + { cancelable: true, detail: wrappedDetail }); + let handled = !this.__DOM_IMPL__.dispatchEvent(event); + + // A gentle warning if the event is not preventDefault() by the content. + if (!handled) { + dump('MozKeyboard.js: A frame with input-manage permission did not' + + ' handle the ' + eventType + ' event dispatched.\n'); + + return null; + } + return detail.takeChainedPromise(); + } +}; + +function MozInputContextFocusEventDetail(win, data) { + this.type = data.type; + this.inputType = data.inputType; + this.value = data.value; + // Exposed as MozInputContextChoicesInfo dictionary defined in WebIDL + this.choices = data.choices; + this.min = data.min; + this.max = data.max; +} +MozInputContextFocusEventDetail.prototype = { + classID: Components.ID("{e0794208-ac50-40e8-b22e-6ee0b4c4e6e8}"), + QueryInterface: XPCOMUtils.generateQI([]), + + type: undefined, + inputType: undefined, + value: '', + choices: null, + min: undefined, + max: undefined +}; + +function MozInputRegistryEventDetail(win, data) { + this._window = win; + + this.manifestURL = data.manifestURL; + this.inputId = data.inputId; + // Exposed as MozInputMethodInputManifest dictionary defined in WebIDL + this.inputManifest = data.inputManifest; + + this._chainedPromise = Promise.resolve(); +} +MozInputRegistryEventDetail.prototype = { + classID: Components.ID("{02130070-9b3e-4f38-bbd9-f0013aa36717}"), + QueryInterface: XPCOMUtils.generateQI([]), + + _window: null, + + manifestURL: undefined, + inputId: undefined, + inputManifest: null, + + waitUntil: function(p) { + // Need an extra protection here since waitUntil will be an no-op + // when chainedPromise is already returned. + if (!this._chainedPromise) { + throw new this._window.DOMException( + 'Must call waitUntil() within the event handling loop.', + 'InvalidStateError'); + } + + this._chainedPromise = this._chainedPromise + .then(function() { return p; }); + }, + + takeChainedPromise: function() { + var p = this._chainedPromise; + this._chainedPromise = null; + return p; + } +}; + +/** + * ============================================== + * InputMethod + * ============================================== + */ +function MozInputMethod() { } + +MozInputMethod.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + + _window: null, + _inputcontext: null, + _wrappedInputContext: null, + _mgmt: null, + _wrappedMgmt: null, + _supportsSwitchingTypes: [], + _inputManageId: undefined, + + classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIDOMGlobalPropertyInitializer, + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]), + + init: function mozInputMethodInit(win) { + this._window = win; + this._mgmt = new MozInputMethodManager(win); + this._wrappedMgmt = win.MozInputMethodManager._create(win, this._mgmt); + this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + + Services.obs.addObserver(this, "inner-window-destroyed", false); + + cpmm.addWeakMessageListener('Keyboard:Focus', this); + cpmm.addWeakMessageListener('Keyboard:Blur', this); + cpmm.addWeakMessageListener('Keyboard:SelectionChange', this); + cpmm.addWeakMessageListener('Keyboard:GetContext:Result:OK', this); + cpmm.addWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this); + cpmm.addWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this); + cpmm.addWeakMessageListener('InputRegistry:Result:OK', this); + cpmm.addWeakMessageListener('InputRegistry:Result:Error', this); + + if (this._hasInputManagePerm(win)) { + this._inputManageId = cpmm.sendSyncMessage('System:RegisterSync', {})[0]; + cpmm.addWeakMessageListener('System:Focus', this); + cpmm.addWeakMessageListener('System:Blur', this); + cpmm.addWeakMessageListener('System:ShowAll', this); + cpmm.addWeakMessageListener('System:Next', this); + cpmm.addWeakMessageListener('System:InputRegistry:Add', this); + cpmm.addWeakMessageListener('System:InputRegistry:Remove', this); + } + }, + + uninit: function mozInputMethodUninit() { + this._window = null; + this._mgmt = null; + this._wrappedMgmt = null; + + cpmm.removeWeakMessageListener('Keyboard:Focus', this); + cpmm.removeWeakMessageListener('Keyboard:Blur', this); + cpmm.removeWeakMessageListener('Keyboard:SelectionChange', this); + cpmm.removeWeakMessageListener('Keyboard:GetContext:Result:OK', this); + cpmm.removeWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this); + cpmm.removeWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this); + cpmm.removeWeakMessageListener('InputRegistry:Result:OK', this); + cpmm.removeWeakMessageListener('InputRegistry:Result:Error', this); + this.setActive(false); + + if (typeof this._inputManageId === 'number') { + cpmm.sendAsyncMessage('System:Unregister', { + 'id': this._inputManageId + }); + cpmm.removeWeakMessageListener('System:Focus', this); + cpmm.removeWeakMessageListener('System:Blur', this); + cpmm.removeWeakMessageListener('System:ShowAll', this); + cpmm.removeWeakMessageListener('System:Next', this); + cpmm.removeWeakMessageListener('System:InputRegistry:Add', this); + cpmm.removeWeakMessageListener('System:InputRegistry:Remove', this); + } + }, + + receiveMessage: function mozInputMethodReceiveMsg(msg) { + if (msg.name.startsWith('Keyboard') && + !WindowMap.isActive(this._window)) { + return; + } + + let data = msg.data; + + if (msg.name.startsWith('System') && + this._inputManageId !== data.inputManageId) { + return; + } + delete data.inputManageId; + + let resolver = ('requestId' in data) ? + this.takePromiseResolver(data.requestId) : null; + + switch(msg.name) { + case 'Keyboard:Focus': + // XXX Bug 904339 could receive 'text' event twice + this.setInputContext(data); + break; + case 'Keyboard:Blur': + this.setInputContext(null); + break; + case 'Keyboard:SelectionChange': + if (this.inputcontext) { + this._inputcontext.updateSelectionContext(data, false); + } + break; + case 'Keyboard:GetContext:Result:OK': + this.setInputContext(data); + break; + case 'Keyboard:SupportsSwitchingTypesChange': + this._supportsSwitchingTypes = data.types; + break; + case 'Keyboard:ReceiveHardwareKeyEvent': + if (!Ci.nsIHardwareKeyHandler) { + break; + } + + let defaultPrevented = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED; + + // |event.preventDefault()| is allowed to be called only when + // |event.cancelable| is true + if (this._inputcontext && data.keyDict.cancelable) { + defaultPrevented |= this._inputcontext.forwardHardwareKeyEvent(data); + } + + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ReplyHardwareKeyEvent', { + type: data.type, + defaultPrevented: defaultPrevented + }); + break; + case 'InputRegistry:Result:OK': + resolver.resolve(); + + break; + + case 'InputRegistry:Result:Error': + resolver.reject(data.error); + + break; + + case 'System:Focus': + this._mgmt.handleFocus(data); + break; + + case 'System:Blur': + this._mgmt.handleBlur(data); + break; + + case 'System:ShowAll': + this._mgmt.dispatchShowAllRequestEvent(); + break; + + case 'System:Next': + this._mgmt.dispatchNextRequestEvent(); + break; + + case 'System:InputRegistry:Add': + this._mgmt.handleAddInput(data); + break; + + case 'System:InputRegistry:Remove': + this._mgmt.handleRemoveInput(data); + break; + } + }, + + observe: function mozInputMethodObserve(subject, topic, data) { + let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (wId == this.innerWindowID) + this.uninit(); + }, + + get mgmt() { + return this._wrappedMgmt; + }, + + get inputcontext() { + if (!WindowMap.isActive(this._window)) { + return null; + } + return this._wrappedInputContext; + }, + + set oninputcontextchange(handler) { + this.__DOM_IMPL__.setEventHandler("oninputcontextchange", handler); + }, + + get oninputcontextchange() { + return this.__DOM_IMPL__.getEventHandler("oninputcontextchange"); + }, + + setInputContext: function mozKeyboardContextChange(data) { + if (this._inputcontext) { + this._inputcontext.destroy(); + this._inputcontext = null; + this._wrappedInputContext = null; + this._mgmt.supportsSwitchingForCurrentInputContext = false; + } + + if (data) { + this._mgmt.supportsSwitchingForCurrentInputContext = + (this._supportsSwitchingTypes.indexOf(data.inputType) !== -1); + + this._inputcontext = new MozInputContext(data); + this._inputcontext.init(this._window); + // inputcontext will be exposed as a WebIDL object. Create its + // content-side object explicitly to avoid Bug 1001325. + this._wrappedInputContext = + this._window.MozInputContext._create(this._window, this._inputcontext); + } + + let event = new this._window.Event("inputcontextchange"); + this.__DOM_IMPL__.dispatchEvent(event); + }, + + setActive: function mozInputMethodSetActive(isActive) { + if (WindowMap.isActive(this._window) === isActive) { + return; + } + + WindowMap.setActive(this._window, isActive); + + if (isActive) { + // Activate current input method. + // If there is already an active context, then this will trigger + // a GetContext:Result:OK event, and we can initialize ourselves. + // Otherwise silently ignored. + + // get keyboard ID from Keyboard.jsm, + // or if we already have it, get it from our map + // Note: if we need to get it from Keyboard.jsm, + // we have to use a synchronous message + var kbID = WindowMap.getKbID(this._window); + if (kbID) { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RegisterSync', {}); + } else { + let res = cpmm.sendSyncMessage('Keyboard:RegisterSync', {}); + WindowMap.setKbID(this._window, res[0]); + } + + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:GetContext', {}); + } else { + // Deactive current input method. + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:Unregister', {}); + if (this._inputcontext) { + this.setInputContext(null); + } + } + }, + + addInput: function(inputId, inputManifest) { + return this.createPromiseWithId(function(resolverId) { + let appId = this._window.document.nodePrincipal.appId; + + cpmm.sendAsyncMessage('InputRegistry:Add', { + requestId: resolverId, + inputId: inputId, + inputManifest: inputManifest, + appId: appId + }); + }.bind(this)); + }, + + removeInput: function(inputId) { + return this.createPromiseWithId(function(resolverId) { + let appId = this._window.document.nodePrincipal.appId; + + cpmm.sendAsyncMessage('InputRegistry:Remove', { + requestId: resolverId, + inputId: inputId, + appId: appId + }); + }.bind(this)); + }, + + setValue: function(value) { + cpmm.sendAsyncMessage('System:SetValue', { + 'value': value + }); + }, + + setSelectedOption: function(index) { + cpmm.sendAsyncMessage('System:SetSelectedOption', { + 'index': index + }); + }, + + setSelectedOptions: function(indexes) { + cpmm.sendAsyncMessage('System:SetSelectedOptions', { + 'indexes': indexes + }); + }, + + removeFocus: function() { + cpmm.sendAsyncMessage('System:RemoveFocus', {}); + }, + + // Only the system app needs that, so instead of testing a permission which + // is allowed for all chrome:// url, we explicitly test that this is the + // system app's start URL. + _hasInputManagePerm: function(win) { + let url = win.location.href; + let systemAppIndex; + try { + systemAppIndex = Services.prefs.getCharPref('b2g.system_startup_url'); + } catch(e) { + dump('MozKeyboard.jsm: no system app startup url set (pref is b2g.system_startup_url)'); + } + + dump(`MozKeyboard.jsm expecting ${systemAppIndex}\n`); + return url == systemAppIndex; + } +}; + +/** + * ============================================== + * InputContextDOMRequestIpcHelper + * ============================================== + */ +function InputContextDOMRequestIpcHelper(win) { + this.initDOMRequestHelper(win, + ["Keyboard:GetText:Result:OK", + "Keyboard:GetText:Result:Error", + "Keyboard:SetSelectionRange:Result:OK", + "Keyboard:ReplaceSurroundingText:Result:OK", + "Keyboard:SendKey:Result:OK", + "Keyboard:SendKey:Result:Error", + "Keyboard:SetComposition:Result:OK", + "Keyboard:EndComposition:Result:OK", + "Keyboard:SequenceError"]); +} + +InputContextDOMRequestIpcHelper.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + _inputContext: null, + + attachInputContext: function(inputCtx) { + if (this._inputContext) { + throw new Error("InputContextDOMRequestIpcHelper: detach the context first."); + } + + this._inputContext = inputCtx; + }, + + // Unset ourselves when the window is destroyed. + uninit: function() { + WindowMap.unsetInputContextIpcHelper(this._window); + }, + + detachInputContext: function() { + // All requests that are still pending need to be invalidated + // because the context is no longer valid. + this.forEachPromiseResolver(k => { + this.takePromiseResolver(k).reject("InputContext got destroyed"); + }); + + this._inputContext = null; + }, + + receiveMessage: function(msg) { + if (!this._inputContext) { + dump('InputContextDOMRequestIpcHelper received message without context attached.\n'); + return; + } + + this._inputContext.receiveMessage(msg); + } +}; + +function MozInputContextSelectionChangeEventDetail(ctx, ownAction) { + this._ctx = ctx; + this.ownAction = ownAction; +} + +MozInputContextSelectionChangeEventDetail.prototype = { + classID: Components.ID("ef35443e-a400-4ae3-9170-c2f4e05f7aed"), + QueryInterface: XPCOMUtils.generateQI([]), + + ownAction: false, + + get selectionStart() { + return this._ctx.selectionStart; + }, + + get selectionEnd() { + return this._ctx.selectionEnd; + } +}; + +function MozInputContextSurroundingTextChangeEventDetail(ctx, ownAction) { + this._ctx = ctx; + this.ownAction = ownAction; +} + +MozInputContextSurroundingTextChangeEventDetail.prototype = { + classID: Components.ID("1c50fdaf-74af-4b2e-814f-792caf65a168"), + QueryInterface: XPCOMUtils.generateQI([]), + + ownAction: false, + + get text() { + return this._ctx.text; + }, + + get textBeforeCursor() { + return this._ctx.textBeforeCursor; + }, + + get textAfterCursor() { + return this._ctx.textAfterCursor; + } +}; + +/** + * ============================================== + * HardwareInput + * ============================================== + */ +function MozHardwareInput() { +} + +MozHardwareInput.prototype = { + classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"), + QueryInterface: XPCOMUtils.generateQI([]), +}; + +/** + * ============================================== + * InputContext + * ============================================== + */ +function MozInputContext(data) { + this._context = { + type: data.type, + inputType: data.inputType, + inputMode: data.inputMode, + lang: data.lang, + selectionStart: data.selectionStart, + selectionEnd: data.selectionEnd, + text: data.value + }; + + this._contextId = data.contextId; +} + +MozInputContext.prototype = { + _window: null, + _context: null, + _contextId: -1, + _ipcHelper: null, + _hardwareinput: null, + _wrappedhardwareinput: null, + + classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]), + + init: function ic_init(win) { + this._window = win; + + this._ipcHelper = WindowMap.getInputContextIpcHelper(win); + this._ipcHelper.attachInputContext(this); + this._hardwareinput = new MozHardwareInput(); + this._wrappedhardwareinput = + this._window.MozHardwareInput._create(this._window, this._hardwareinput); + }, + + destroy: function ic_destroy() { + // A consuming application might still hold a cached version of + // this object. After destroying all methods will throw because we + // cannot create new promises anymore, but we still hold + // (outdated) information in the context. So let's clear that out. + for (var k in this._context) { + if (this._context.hasOwnProperty(k)) { + this._context[k] = null; + } + } + + this._ipcHelper.detachInputContext(); + this._ipcHelper = null; + + this._window = null; + this._hardwareinput = null; + this._wrappedhardwareinput = null; + }, + + receiveMessage: function ic_receiveMessage(msg) { + if (!msg || !msg.json) { + dump('InputContext received message without data\n'); + return; + } + + let json = msg.json; + let resolver = this._ipcHelper.takePromiseResolver(json.requestId); + + if (!resolver) { + dump('InputContext received invalid requestId.\n'); + return; + } + + // Update context first before resolving promise to avoid race condition + if (json.selectioninfo) { + this.updateSelectionContext(json.selectioninfo, true); + } + + switch (msg.name) { + case "Keyboard:SendKey:Result:OK": + resolver.resolve(true); + break; + case "Keyboard:SendKey:Result:Error": + resolver.reject(json.error); + break; + case "Keyboard:GetText:Result:OK": + resolver.resolve(json.text); + break; + case "Keyboard:GetText:Result:Error": + resolver.reject(json.error); + break; + case "Keyboard:SetSelectionRange:Result:OK": + case "Keyboard:ReplaceSurroundingText:Result:OK": + resolver.resolve( + Cu.cloneInto(json.selectioninfo, this._window)); + break; + case "Keyboard:SequenceError": + // Occurs when a new element got focus, but the inputContext was + // not invalidated yet... + resolver.reject("InputContext has expired"); + break; + case "Keyboard:SetComposition:Result:OK": // Fall through. + case "Keyboard:EndComposition:Result:OK": + resolver.resolve(true); + break; + default: + dump("Could not find a handler for " + msg.name); + resolver.reject(); + break; + } + }, + + updateSelectionContext: function ic_updateSelectionContext(data, ownAction) { + if (!this._context) { + return; + } + + let selectionDirty = + this._context.selectionStart !== data.selectionStart || + this._context.selectionEnd !== data.selectionEnd; + let surroundDirty = selectionDirty || data.text !== this._contextId.text; + + this._context.text = data.text; + this._context.selectionStart = data.selectionStart; + this._context.selectionEnd = data.selectionEnd; + + if (selectionDirty) { + let selectionChangeDetail = + new MozInputContextSelectionChangeEventDetail(this, ownAction); + let wrappedSelectionChangeDetail = + this._window.MozInputContextSelectionChangeEventDetail + ._create(this._window, selectionChangeDetail); + let selectionChangeEvent = new this._window.CustomEvent("selectionchange", + { cancelable: false, detail: wrappedSelectionChangeDetail }); + + this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent); + } + + if (surroundDirty) { + let surroundingTextChangeDetail = + new MozInputContextSurroundingTextChangeEventDetail(this, ownAction); + let wrappedSurroundingTextChangeDetail = + this._window.MozInputContextSurroundingTextChangeEventDetail + ._create(this._window, surroundingTextChangeDetail); + let selectionChangeEvent = new this._window.CustomEvent("surroundingtextchange", + { cancelable: false, detail: wrappedSurroundingTextChangeDetail }); + + this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent); + } + }, + + // tag name of the input field + get type() { + return this._context.type; + }, + + // type of the input field + get inputType() { + return this._context.inputType; + }, + + get inputMode() { + return this._context.inputMode; + }, + + get lang() { + return this._context.lang; + }, + + getText: function ic_getText(offset, length) { + let text; + if (offset && length) { + text = this._context.text.substr(offset, length); + } else if (offset) { + text = this._context.text.substr(offset); + } else { + text = this._context.text; + } + + return this._window.Promise.resolve(text); + }, + + get selectionStart() { + return this._context.selectionStart; + }, + + get selectionEnd() { + return this._context.selectionEnd; + }, + + get text() { + return this._context.text; + }, + + get textBeforeCursor() { + let text = this._context.text; + let start = this._context.selectionStart; + return (start < 100) ? + text.substr(0, start) : + text.substr(start - 100, 100); + }, + + get textAfterCursor() { + let text = this._context.text; + let start = this._context.selectionStart; + let end = this._context.selectionEnd; + return text.substr(start, end - start + 100); + }, + + get hardwareinput() { + return this._wrappedhardwareinput; + }, + + setSelectionRange: function ic_setSelectionRange(start, length) { + let self = this; + return this._sendPromise(function(resolverId) { + cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetSelectionRange', { + contextId: self._contextId, + requestId: resolverId, + selectionStart: start, + selectionEnd: start + length + }); + }); + }, + + get onsurroundingtextchange() { + return this.__DOM_IMPL__.getEventHandler("onsurroundingtextchange"); + }, + + set onsurroundingtextchange(handler) { + this.__DOM_IMPL__.setEventHandler("onsurroundingtextchange", handler); + }, + + get onselectionchange() { + return this.__DOM_IMPL__.getEventHandler("onselectionchange"); + }, + + set onselectionchange(handler) { + this.__DOM_IMPL__.setEventHandler("onselectionchange", handler); + }, + + replaceSurroundingText: function ic_replaceSurrText(text, offset, length) { + let self = this; + return this._sendPromise(function(resolverId) { + cpmmSendAsyncMessageWithKbID(self, 'Keyboard:ReplaceSurroundingText', { + contextId: self._contextId, + requestId: resolverId, + text: text, + offset: offset || 0, + length: length || 0 + }); + }); + }, + + deleteSurroundingText: function ic_deleteSurrText(offset, length) { + return this.replaceSurroundingText(null, offset, length); + }, + + sendKey: function ic_sendKey(dictOrKeyCode, charCode, modifiers, repeat) { + if (typeof dictOrKeyCode === 'number') { + // XXX: modifiers are ignored in this API method. + + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', { + contextId: this._contextId, + requestId: resolverId, + method: 'sendKey', + keyCode: dictOrKeyCode, + charCode: charCode, + repeat: repeat + }); + }); + } else if (typeof dictOrKeyCode === 'object') { + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', { + contextId: this._contextId, + requestId: resolverId, + method: 'sendKey', + keyboardEventDict: this._getkeyboardEventDict(dictOrKeyCode) + }); + }); + } else { + // XXX: Should not reach here; implies WebIDL binding error. + throw new TypeError('Unknown argument passed.'); + } + }, + + keydown: function ic_keydown(dict) { + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', { + contextId: this._contextId, + requestId: resolverId, + method: 'keydown', + keyboardEventDict: this._getkeyboardEventDict(dict) + }); + }); + }, + + keyup: function ic_keyup(dict) { + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', { + contextId: this._contextId, + requestId: resolverId, + method: 'keyup', + keyboardEventDict: this._getkeyboardEventDict(dict) + }); + }); + }, + + setComposition: function ic_setComposition(text, cursor, clauses, dict) { + let self = this; + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetComposition', { + contextId: self._contextId, + requestId: resolverId, + text: text, + cursor: (typeof cursor !== 'undefined') ? cursor : text.length, + clauses: clauses || null, + keyboardEventDict: this._getkeyboardEventDict(dict) + }); + }); + }, + + endComposition: function ic_endComposition(text, dict) { + let self = this; + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(self, 'Keyboard:EndComposition', { + contextId: self._contextId, + requestId: resolverId, + text: text || '', + keyboardEventDict: this._getkeyboardEventDict(dict) + }); + }); + }, + + // Generate a new keyboard event by the received keyboard dictionary + // and return defaultPrevented's result of the event after dispatching. + forwardHardwareKeyEvent: function ic_forwardHardwareKeyEvent(data) { + if (!Ci.nsIHardwareKeyHandler) { + return; + } + + if (!this._context) { + return Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED; + } + let evt = new this._window.KeyboardEvent(data.type, + Cu.cloneInto(data.keyDict, + this._window)); + this._hardwareinput.__DOM_IMPL__.dispatchEvent(evt); + return this._getDefaultPreventedValue(evt); + }, + + _getDefaultPreventedValue: function(evt) { + if (!Ci.nsIHardwareKeyHandler) { + return; + } + + let flags = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED; + + if (evt.defaultPrevented) { + flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED; + } + + if (evt.defaultPreventedByChrome) { + flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CHROME; + } + + if (evt.defaultPreventedByContent) { + flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CONTENT; + } + + return flags; + }, + + _sendPromise: function(callback) { + let self = this; + return this._ipcHelper.createPromiseWithId(function(aResolverId) { + if (!WindowMap.isActive(self._window)) { + self._ipcHelper.removePromiseResolver(aResolverId); + reject('Input method is not active.'); + return; + } + callback(aResolverId); + }); + }, + + // Take a MozInputMethodKeyboardEventDict dict, creates a keyboardEventDict + // object that can be sent to forms.js + _getkeyboardEventDict: function(dict) { + if (typeof dict !== 'object' || !dict.key) { + return; + } + + var keyboardEventDict = { + key: dict.key, + code: dict.code, + repeat: dict.repeat, + flags: 0 + }; + + if (dict.printable) { + keyboardEventDict.flags |= + Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY; + } + + if (/^[a-zA-Z0-9]$/.test(dict.key)) { + // keyCode must follow the key value in this range; + // disregard the keyCode from content. + keyboardEventDict.keyCode = dict.key.toUpperCase().charCodeAt(0); + } else if (typeof dict.keyCode === 'number') { + // Allow keyCode to be specified for other key values. + keyboardEventDict.keyCode = dict.keyCode; + + // Allow keyCode to be explicitly set to zero. + if (dict.keyCode === 0) { + keyboardEventDict.flags |= + Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO; + } + } + + return keyboardEventDict; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozInputMethod]); diff --git a/dom/inputmethod/forms.js b/dom/inputmethod/forms.js new file mode 100644 index 000000000..1884f2b4d --- /dev/null +++ b/dom/inputmethod/forms.js @@ -0,0 +1,1561 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* 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/. */ + +"use strict"; + +dump("###################################### forms.js loaded\n"); + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +XPCOMUtils.defineLazyServiceGetter(Services, "fm", + "@mozilla.org/focus-manager;1", + "nsIFocusManager"); + +/* + * A WeakMap to map window to objects keeping it's TextInputProcessor instance. + */ +var WindowMap = { + // WeakMap of <window, object> pairs. + _map: null, + + /* + * Set the object associated to the window and return it. + */ + _getObjForWin: function(win) { + if (!this._map) { + this._map = new WeakMap(); + } + if (this._map.has(win)) { + return this._map.get(win); + } else { + let obj = { + tip: null + }; + this._map.set(win, obj); + + return obj; + } + }, + + getTextInputProcessor: function(win) { + if (!win) { + return; + } + let obj = this._getObjForWin(win); + let tip = obj.tip + + if (!tip) { + tip = obj.tip = Cc["@mozilla.org/text-input-processor;1"] + .createInstance(Ci.nsITextInputProcessor); + } + + if (!tip.beginInputTransaction(win, textInputProcessorCallback)) { + tip = obj.tip = null; + } + return tip; + } +}; + +const RESIZE_SCROLL_DELAY = 20; +// In content editable node, when there are hidden elements such as <br>, it +// may need more than one (usually less than 3 times) move/extend operations +// to change the selection range. If we cannot change the selection range +// with more than 20 opertations, we are likely being blocked and cannot change +// the selection range any more. +const MAX_BLOCKED_COUNT = 20; + +var HTMLDocument = Ci.nsIDOMHTMLDocument; +var HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement; +var HTMLBodyElement = Ci.nsIDOMHTMLBodyElement; +var HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement; +var HTMLInputElement = Ci.nsIDOMHTMLInputElement; +var HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement; +var HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; +var HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement; +var HTMLOptionElement = Ci.nsIDOMHTMLOptionElement; + +function guessKeyNameFromKeyCode(KeyboardEvent, aKeyCode) { + switch (aKeyCode) { + case KeyboardEvent.DOM_VK_CANCEL: + return "Cancel"; + case KeyboardEvent.DOM_VK_HELP: + return "Help"; + case KeyboardEvent.DOM_VK_BACK_SPACE: + return "Backspace"; + case KeyboardEvent.DOM_VK_TAB: + return "Tab"; + case KeyboardEvent.DOM_VK_CLEAR: + return "Clear"; + case KeyboardEvent.DOM_VK_RETURN: + return "Enter"; + case KeyboardEvent.DOM_VK_SHIFT: + return "Shift"; + case KeyboardEvent.DOM_VK_CONTROL: + return "Control"; + case KeyboardEvent.DOM_VK_ALT: + return "Alt"; + case KeyboardEvent.DOM_VK_PAUSE: + return "Pause"; + case KeyboardEvent.DOM_VK_EISU: + return "Eisu"; + case KeyboardEvent.DOM_VK_ESCAPE: + return "Escape"; + case KeyboardEvent.DOM_VK_CONVERT: + return "Convert"; + case KeyboardEvent.DOM_VK_NONCONVERT: + return "NonConvert"; + case KeyboardEvent.DOM_VK_ACCEPT: + return "Accept"; + case KeyboardEvent.DOM_VK_MODECHANGE: + return "ModeChange"; + case KeyboardEvent.DOM_VK_PAGE_UP: + return "PageUp"; + case KeyboardEvent.DOM_VK_PAGE_DOWN: + return "PageDown"; + case KeyboardEvent.DOM_VK_END: + return "End"; + case KeyboardEvent.DOM_VK_HOME: + return "Home"; + case KeyboardEvent.DOM_VK_LEFT: + return "ArrowLeft"; + case KeyboardEvent.DOM_VK_UP: + return "ArrowUp"; + case KeyboardEvent.DOM_VK_RIGHT: + return "ArrowRight"; + case KeyboardEvent.DOM_VK_DOWN: + return "ArrowDown"; + case KeyboardEvent.DOM_VK_SELECT: + return "Select"; + case KeyboardEvent.DOM_VK_PRINT: + return "Print"; + case KeyboardEvent.DOM_VK_EXECUTE: + return "Execute"; + case KeyboardEvent.DOM_VK_PRINTSCREEN: + return "PrintScreen"; + case KeyboardEvent.DOM_VK_INSERT: + return "Insert"; + case KeyboardEvent.DOM_VK_DELETE: + return "Delete"; + case KeyboardEvent.DOM_VK_WIN: + return "OS"; + case KeyboardEvent.DOM_VK_CONTEXT_MENU: + return "ContextMenu"; + case KeyboardEvent.DOM_VK_SLEEP: + return "Standby"; + case KeyboardEvent.DOM_VK_F1: + return "F1"; + case KeyboardEvent.DOM_VK_F2: + return "F2"; + case KeyboardEvent.DOM_VK_F3: + return "F3"; + case KeyboardEvent.DOM_VK_F4: + return "F4"; + case KeyboardEvent.DOM_VK_F5: + return "F5"; + case KeyboardEvent.DOM_VK_F6: + return "F6"; + case KeyboardEvent.DOM_VK_F7: + return "F7"; + case KeyboardEvent.DOM_VK_F8: + return "F8"; + case KeyboardEvent.DOM_VK_F9: + return "F9"; + case KeyboardEvent.DOM_VK_F10: + return "F10"; + case KeyboardEvent.DOM_VK_F11: + return "F11"; + case KeyboardEvent.DOM_VK_F12: + return "F12"; + case KeyboardEvent.DOM_VK_F13: + return "F13"; + case KeyboardEvent.DOM_VK_F14: + return "F14"; + case KeyboardEvent.DOM_VK_F15: + return "F15"; + case KeyboardEvent.DOM_VK_F16: + return "F16"; + case KeyboardEvent.DOM_VK_F17: + return "F17"; + case KeyboardEvent.DOM_VK_F18: + return "F18"; + case KeyboardEvent.DOM_VK_F19: + return "F19"; + case KeyboardEvent.DOM_VK_F20: + return "F20"; + case KeyboardEvent.DOM_VK_F21: + return "F21"; + case KeyboardEvent.DOM_VK_F22: + return "F22"; + case KeyboardEvent.DOM_VK_F23: + return "F23"; + case KeyboardEvent.DOM_VK_F24: + return "F24"; + case KeyboardEvent.DOM_VK_NUM_LOCK: + return "NumLock"; + case KeyboardEvent.DOM_VK_SCROLL_LOCK: + return "ScrollLock"; + case KeyboardEvent.DOM_VK_VOLUME_MUTE: + return "AudioVolumeMute"; + case KeyboardEvent.DOM_VK_VOLUME_DOWN: + return "AudioVolumeDown"; + case KeyboardEvent.DOM_VK_VOLUME_UP: + return "AudioVolumeUp"; + case KeyboardEvent.DOM_VK_META: + return "Meta"; + case KeyboardEvent.DOM_VK_ALTGR: + return "AltGraph"; + case KeyboardEvent.DOM_VK_ATTN: + return "Attn"; + case KeyboardEvent.DOM_VK_CRSEL: + return "CrSel"; + case KeyboardEvent.DOM_VK_EXSEL: + return "ExSel"; + case KeyboardEvent.DOM_VK_EREOF: + return "EraseEof"; + case KeyboardEvent.DOM_VK_PLAY: + return "Play"; + default: + return "Unidentified"; + } +} + +var FormVisibility = { + /** + * Searches upwards in the DOM for an element that has been scrolled. + * + * @param {HTMLElement} node element to start search at. + * @return {Window|HTMLElement|Null} null when none are found window/element otherwise. + */ + findScrolled: function fv_findScrolled(node) { + let win = node.ownerDocument.defaultView; + + while (!(node instanceof HTMLBodyElement)) { + + // We can skip elements that have not been scrolled. + // We only care about top now remember to add the scrollLeft + // check if we decide to care about the X axis. + if (node.scrollTop !== 0) { + // the element has been scrolled so we may need to adjust + // where we think the root element is located. + // + // Otherwise it may seem visible but be scrolled out of the viewport + // inside this scrollable node. + return node; + } else { + // this node does not effect where we think + // the node is even if it is scrollable it has not hidden + // the element we are looking for. + node = node.parentNode; + continue; + } + } + + // we also care about the window this is the more + // common case where the content is larger then + // the viewport/screen. + if (win.scrollMaxX != win.scrollMinX || win.scrollMaxY != win.scrollMinY) { + return win; + } + + return null; + }, + + /** + * Checks if "top and "bottom" points of the position is visible. + * + * @param {Number} top position. + * @param {Number} height of the element. + * @param {Number} maxHeight of the window. + * @return {Boolean} true when visible. + */ + yAxisVisible: function fv_yAxisVisible(top, height, maxHeight) { + return (top > 0 && (top + height) < maxHeight); + }, + + /** + * Searches up through the dom for scrollable elements + * which are not currently visible (relative to the viewport). + * + * @param {HTMLElement} element to start search at. + * @param {Object} pos .top, .height and .width of element. + */ + scrollablesVisible: function fv_scrollablesVisible(element, pos) { + while ((element = this.findScrolled(element))) { + if (element.window && element.self === element) + break; + + // remember getBoundingClientRect does not care + // about scrolling only where the element starts + // in the document. + let offset = element.getBoundingClientRect(); + + // the top of both the scrollable area and + // the form element itself are in the same document. + // We adjust the "top" so if the elements coordinates + // are relative to the viewport in the current document. + let adjustedTop = pos.top - offset.top; + + let visible = this.yAxisVisible( + adjustedTop, + pos.height, + offset.height + ); + + if (!visible) + return false; + + element = element.parentNode; + } + + return true; + }, + + /** + * Verifies the element is visible in the viewport. + * Handles scrollable areas, frames and scrollable viewport(s) (windows). + * + * @param {HTMLElement} element to verify. + * @return {Boolean} true when visible. + */ + isVisible: function fv_isVisible(element) { + // scrollable frames can be ignored we just care about iframes... + let rect = element.getBoundingClientRect(); + let parent = element.ownerDocument.defaultView; + + // used to calculate the inner position of frames / scrollables. + // The intent was to use this information to scroll either up or down. + // scrollIntoView(true) will _break_ some web content so we can't do + // this today. If we want that functionality we need to manually scroll + // the individual elements. + let pos = { + top: rect.top, + height: rect.height, + width: rect.width + }; + + let visible = true; + + do { + let frame = parent.frameElement; + visible = visible && + this.yAxisVisible(pos.top, pos.height, parent.innerHeight) && + this.scrollablesVisible(element, pos); + + // nothing we can do about this now... + // In the future we can use this information to scroll + // only the elements we need to at this point as we should + // have all the details we need to figure out how to scroll. + if (!visible) + return false; + + if (frame) { + let frameRect = frame.getBoundingClientRect(); + + pos.top += frameRect.top + frame.clientTop; + } + } while ( + (parent !== parent.parent) && + (parent = parent.parent) + ); + + return visible; + } +}; + +// This object implements nsITextInputProcessorCallback +var textInputProcessorCallback = { + onNotify: function(aTextInputProcessor, aNotification) { + try { + switch (aNotification.type) { + case "request-to-commit": + // TODO: Send a notification through asyncMessage to the keyboard here. + aTextInputProcessor.commitComposition(); + + break; + case "request-to-cancel": + // TODO: Send a notification through asyncMessage to the keyboard here. + aTextInputProcessor.cancelComposition(); + + break; + + case "notify-detached": + // TODO: Send a notification through asyncMessage to the keyboard here. + break; + + // TODO: Manage _focusedElement for text input from here instead. + // (except for <select> which will be need to handled elsewhere) + case "notify-focus": + break; + + case "notify-blur": + break; + } + } catch (e) { + return false; + } + return true; + } +}; + +var FormAssistant = { + init: function fa_init() { + addEventListener("focus", this, true, false); + addEventListener("blur", this, true, false); + addEventListener("resize", this, true, false); + // We should not blur the fucus if the submit event is cancelled, + // therefore we are binding our event listener in the bubbling phase here. + addEventListener("submit", this, false, false); + addEventListener("pagehide", this, true, false); + addEventListener("beforeunload", this, true, false); + addEventListener("input", this, true, false); + addEventListener("keydown", this, true, false); + addEventListener("keyup", this, true, false); + addMessageListener("Forms:Select:Choice", this); + addMessageListener("Forms:Input:Value", this); + addMessageListener("Forms:Select:Blur", this); + addMessageListener("Forms:SetSelectionRange", this); + addMessageListener("Forms:ReplaceSurroundingText", this); + addMessageListener("Forms:Input:SendKey", this); + addMessageListener("Forms:GetContext", this); + addMessageListener("Forms:SetComposition", this); + addMessageListener("Forms:EndComposition", this); + }, + + ignoredInputTypes: new Set([ + 'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image', + 'range' + ]), + + isHandlingFocus: false, + selectionStart: -1, + selectionEnd: -1, + text: "", + + scrollIntoViewTimeout: null, + _focusedElement: null, + _focusCounter: 0, // up one for every time we focus a new element + _focusDeleteObserver: null, + _focusContentObserver: null, + _documentEncoder: null, + _editor: null, + _editing: false, + _selectionPrivate: null, + + get focusedElement() { + if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement)) + this._focusedElement = null; + + return this._focusedElement; + }, + + set focusedElement(val) { + this._focusCounter++; + this._focusedElement = val; + }, + + setFocusedElement: function fa_setFocusedElement(element) { + let self = this; + + if (element === this.focusedElement) + return; + + if (this.focusedElement) { + this.focusedElement.removeEventListener('compositionend', this); + if (this._focusDeleteObserver) { + this._focusDeleteObserver.disconnect(); + this._focusDeleteObserver = null; + } + if (this._focusContentObserver) { + this._focusContentObserver.disconnect(); + this._focusContentObserver = null; + } + if (this._selectionPrivate) { + this._selectionPrivate.removeSelectionListener(this); + this._selectionPrivate = null; + } + } + + this._documentEncoder = null; + if (this._editor) { + // When the nsIFrame of the input element is reconstructed by + // CSS restyling, the editor observers are removed. Catch + // [nsIEditor.removeEditorObserver] failure exception if that + // happens. + try { + this._editor.removeEditorObserver(this); + } catch (e) {} + this._editor = null; + } + + if (element) { + element.addEventListener('compositionend', this); + if (isContentEditable(element)) { + this._documentEncoder = getDocumentEncoder(element); + } + this._editor = getPlaintextEditor(element); + if (this._editor) { + // Add a nsIEditorObserver to monitor the text content of the focused + // element. + this._editor.addEditorObserver(this); + + let selection = this._editor.selection; + if (selection) { + this._selectionPrivate = selection.QueryInterface(Ci.nsISelectionPrivate); + this._selectionPrivate.addSelectionListener(this); + } + } + + // If our focusedElement is removed from DOM we want to handle it properly + let MutationObserver = element.ownerDocument.defaultView.MutationObserver; + this._focusDeleteObserver = new MutationObserver(function(mutations) { + var del = [].some.call(mutations, function(m) { + return [].some.call(m.removedNodes, function(n) { + return n.contains(element); + }); + }); + if (del && element === self.focusedElement) { + self.unhandleFocus(); + } + }); + + this._focusDeleteObserver.observe(element.ownerDocument.body, { + childList: true, + subtree: true + }); + + // If contenteditable, also add a mutation observer on its content and + // call selectionChanged when a change occurs + if (isContentEditable(element)) { + this._focusContentObserver = new MutationObserver(function() { + this.updateSelection(); + }.bind(this)); + + this._focusContentObserver.observe(element, { + childList: true, + subtree: true + }); + } + } + + this.focusedElement = element; + }, + + notifySelectionChanged: function(aDocument, aSelection, aReason) { + this.updateSelection(); + }, + + get documentEncoder() { + return this._documentEncoder; + }, + + // Get the nsIPlaintextEditor object of current input field. + get editor() { + return this._editor; + }, + + // Implements nsIEditorObserver get notification when the text content of + // current input field has changed. + EditAction: function fa_editAction() { + if (this._editing || !this.isHandlingFocus) { + return; + } + this.sendInputState(this.focusedElement); + }, + + handleEvent: function fa_handleEvent(evt) { + let target = evt.composedTarget; + + let range = null; + switch (evt.type) { + case "focus": + if (!target) { + break; + } + + // Focusing on Window, Document or iFrame should focus body + if (target instanceof HTMLHtmlElement) { + target = target.document.body; + } else if (target instanceof HTMLDocument) { + target = target.body; + } else if (target instanceof HTMLIFrameElement) { + target = target.contentDocument ? target.contentDocument.body + : null; + } + + if (!target) { + break; + } + + if (isContentEditable(target)) { + this.handleFocus(this.getTopLevelEditable(target)); + this.updateSelection(); + break; + } + + if (this.isFocusableElement(target)) { + this.handleFocus(target); + this.updateSelection(); + } + break; + + case "pagehide": + case "beforeunload": + // We are only interested to the pagehide and beforeunload events from + // the root document. + if (target && target != content.document) { + break; + } + // fall through + case "submit": + if (this.focusedElement && !evt.defaultPrevented) { + this.focusedElement.blur(); + } + break; + + case "blur": + if (this.focusedElement) { + this.unhandleFocus(); + } + break; + + case "resize": + if (!this.isHandlingFocus) + return; + + if (this.scrollIntoViewTimeout) { + content.clearTimeout(this.scrollIntoViewTimeout); + this.scrollIntoViewTimeout = null; + } + + // We may receive multiple resize events in quick succession, so wait + // a bit before scrolling the input element into view. + if (this.focusedElement) { + this.scrollIntoViewTimeout = content.setTimeout(function () { + this.scrollIntoViewTimeout = null; + if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) { + scrollSelectionOrElementIntoView(this.focusedElement); + } + }.bind(this), RESIZE_SCROLL_DELAY); + } + break; + + case "keydown": + if (!this.focusedElement || this._editing) { + break; + } + + CompositionManager.endComposition(''); + break; + + case "keyup": + if (!this.focusedElement || this._editing) { + break; + } + + CompositionManager.endComposition(''); + break; + + case "compositionend": + if (!this.focusedElement) { + break; + } + + CompositionManager.onCompositionEnd(); + break; + } + }, + + receiveMessage: function fa_receiveMessage(msg) { + let target = this.focusedElement; + let json = msg.json; + + // To not break mozKeyboard contextId is optional + if ('contextId' in json && + json.contextId !== this._focusCounter && + json.requestId) { + // Ignore messages that are meant for a previously focused element + sendAsyncMessage("Forms:SequenceError", { + requestId: json.requestId, + error: "Expected contextId " + this._focusCounter + + " but was " + json.contextId + }); + return; + } + + if (!target) { + return; + } + + this._editing = true; + switch (msg.name) { + case "Forms:Input:Value": { + CompositionManager.endComposition(''); + + target.value = json.value; + + let event = target.ownerDocument.createEvent('HTMLEvents'); + event.initEvent('input', true, false); + target.dispatchEvent(event); + break; + } + + case "Forms:Input:SendKey": + CompositionManager.endComposition(''); + + let win = target.ownerDocument.defaultView; + let tip = WindowMap.getTextInputProcessor(win); + if (!tip) { + if (json.requestId) { + sendAsyncMessage("Forms:SendKey:Result:Error", { + requestId: json.requestId, + error: "Unable to start input transaction." + }); + } + + break; + } + + // If we receive a keyboardEventDict from json, that means the user + // is calling the method with the new arguments. + // Otherwise, we would have to construct our own keyboardEventDict + // based on legacy values we have received. + let keyboardEventDict = json.keyboardEventDict; + let flags = 0; + + if (keyboardEventDict) { + if ('flags' in keyboardEventDict) { + flags = keyboardEventDict.flags; + } + } else { + // The naive way to figure out if the key to dispatch is printable. + let printable = !!json.charCode; + + // For printable keys, the value should be the actual character. + // For non-printable keys, it should be a value in the D3E spec. + // Here we make some educated guess for it. + let key = printable ? + String.fromCharCode(json.charCode) : + guessKeyNameFromKeyCode(win.KeyboardEvent, json.keyCode); + + // keyCode from content is only respected when the key is not an + // an alphanumeric character. We also ask TextInputProcessor not to + // infer this value for non-printable keys to keep the original + // behavior. + let keyCode = (printable && /^[a-zA-Z0-9]$/.test(key)) ? + key.toUpperCase().charCodeAt(0) : + json.keyCode; + + keyboardEventDict = { + key: key, + keyCode: keyCode, + // We don't have any information to tell the virtual key the + // user have interacted with. + code: "", + // We do not have the information to infer location of the virtual key + // either (and we would need TextInputProcessor not to compute it). + location: 0, + // This indicates the key is triggered for repeats. + repeat: json.repeat + }; + + flags = tip.KEY_KEEP_KEY_LOCATION_STANDARD; + if (!printable) { + flags |= tip.KEY_NON_PRINTABLE_KEY; + } + if (!keyboardEventDict.keyCode) { + flags |= tip.KEY_KEEP_KEYCODE_ZERO; + } + } + + let keyboardEvent = new win.KeyboardEvent("", keyboardEventDict); + + let keydownDefaultPrevented = false; + try { + switch (json.method) { + case 'sendKey': { + let consumedFlags = tip.keydown(keyboardEvent, flags); + keydownDefaultPrevented = + !!(tip.KEYDOWN_IS_CONSUMED & consumedFlags); + if (!keyboardEventDict.repeat) { + tip.keyup(keyboardEvent, flags); + } + break; + } + case 'keydown': { + let consumedFlags = tip.keydown(keyboardEvent, flags); + keydownDefaultPrevented = + !!(tip.KEYDOWN_IS_CONSUMED & consumedFlags); + break; + } + case 'keyup': { + tip.keyup(keyboardEvent, flags); + + break; + } + } + } catch (err) { + dump("forms.js:" + err.toString() + "\n"); + + if (json.requestId) { + if (err instanceof Ci.nsIException && + err.result == Cr.NS_ERROR_ILLEGAL_VALUE) { + sendAsyncMessage("Forms:SendKey:Result:Error", { + requestId: json.requestId, + error: "The values specified are illegal." + }); + } else { + sendAsyncMessage("Forms:SendKey:Result:Error", { + requestId: json.requestId, + error: "Unable to type into destroyed input." + }); + } + } + + break; + } + + if (json.requestId) { + if (keydownDefaultPrevented) { + sendAsyncMessage("Forms:SendKey:Result:Error", { + requestId: json.requestId, + error: "Key event(s) was cancelled." + }); + } else { + sendAsyncMessage("Forms:SendKey:Result:OK", { + requestId: json.requestId, + selectioninfo: this.getSelectionInfo() + }); + } + } + + break; + + case "Forms:Select:Choice": + let options = target.options; + let valueChanged = false; + if ("index" in json) { + if (options.selectedIndex != json.index) { + options.selectedIndex = json.index; + valueChanged = true; + } + } else if ("indexes" in json) { + for (let i = 0; i < options.length; i++) { + let newValue = (json.indexes.indexOf(i) != -1); + if (options.item(i).selected != newValue) { + options.item(i).selected = newValue; + valueChanged = true; + } + } + } + + // only fire onchange event if any selected option is changed + if (valueChanged) { + let event = target.ownerDocument.createEvent('HTMLEvents'); + event.initEvent('change', true, true); + target.dispatchEvent(event); + } + break; + + case "Forms:Select:Blur": { + if (this.focusedElement) { + this.focusedElement.blur(); + } + + break; + } + + case "Forms:SetSelectionRange": { + CompositionManager.endComposition(''); + + let start = json.selectionStart; + let end = json.selectionEnd; + + if (!setSelectionRange(target, start, end)) { + if (json.requestId) { + sendAsyncMessage("Forms:SetSelectionRange:Result:Error", { + requestId: json.requestId, + error: "failed" + }); + } + break; + } + + if (json.requestId) { + sendAsyncMessage("Forms:SetSelectionRange:Result:OK", { + requestId: json.requestId, + selectioninfo: this.getSelectionInfo() + }); + } + break; + } + + case "Forms:ReplaceSurroundingText": { + CompositionManager.endComposition(''); + + if (!replaceSurroundingText(target, + json.text, + json.offset, + json.length)) { + if (json.requestId) { + sendAsyncMessage("Forms:ReplaceSurroundingText:Result:Error", { + requestId: json.requestId, + error: "failed" + }); + } + break; + } + + if (json.requestId) { + sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", { + requestId: json.requestId, + selectioninfo: this.getSelectionInfo() + }); + } + break; + } + + case "Forms:GetContext": { + let obj = getJSON(target, this._focusCounter); + sendAsyncMessage("Forms:GetContext:Result:OK", obj); + break; + } + + case "Forms:SetComposition": { + CompositionManager.setComposition(target, json.text, json.cursor, + json.clauses, json.keyboardEventDict); + sendAsyncMessage("Forms:SetComposition:Result:OK", { + requestId: json.requestId, + selectioninfo: this.getSelectionInfo() + }); + break; + } + + case "Forms:EndComposition": { + CompositionManager.endComposition(json.text, json.keyboardEventDict); + sendAsyncMessage("Forms:EndComposition:Result:OK", { + requestId: json.requestId, + selectioninfo: this.getSelectionInfo() + }); + break; + } + } + this._editing = false; + + }, + + handleFocus: function fa_handleFocus(target) { + if (this.focusedElement === target) + return; + + if (target instanceof HTMLOptionElement) + target = target.parentNode; + + this.setFocusedElement(target); + this.sendInputState(target); + this.isHandlingFocus = true; + }, + + unhandleFocus: function fa_unhandleFocus() { + this.setFocusedElement(null); + this.isHandlingFocus = false; + this.selectionStart = -1; + this.selectionEnd = -1; + this.text = ""; + sendAsyncMessage("Forms:Blur", {}); + }, + + isFocusableElement: function fa_isFocusableElement(element) { + if (element instanceof HTMLSelectElement || + element instanceof HTMLTextAreaElement) + return true; + + if (element instanceof HTMLOptionElement && + element.parentNode instanceof HTMLSelectElement) + return true; + + return (element instanceof HTMLInputElement && + !this.ignoredInputTypes.has(element.type) && + !element.readOnly); + }, + + getTopLevelEditable: function fa_getTopLevelEditable(element) { + function retrieveTopLevelEditable(element) { + while (element && !isContentEditable(element)) + element = element.parentNode; + + return element; + } + + return retrieveTopLevelEditable(element) || element; + }, + + sendInputState: function(element) { + sendAsyncMessage("Forms:Focus", getJSON(element, this._focusCounter)); + }, + + getSelectionInfo: function fa_getSelectionInfo() { + let element = this.focusedElement; + let range = getSelectionRange(element); + + let text = isContentEditable(element) ? getContentEditableText(element) + : element.value; + + let changed = this.selectionStart !== range[0] || + this.selectionEnd !== range[1] || + this.text !== text; + + this.selectionStart = range[0]; + this.selectionEnd = range[1]; + this.text = text; + + return { + selectionStart: range[0], + selectionEnd: range[1], + text: text, + changed: changed + }; + }, + + _selectionTimeout: null, + + // Notify when the selection range changes + updateSelection: function fa_updateSelection() { + // A call to setSelectionRange on input field causes 2 selection changes + // one to [0,0] and one to actual value. Both are sent in same tick. + // Prevent firing two events in that scenario, always only use the last 1. + // + // It is also a workaround for Bug 1053048, which prevents + // getSelectionInfo() accessing selectionStart or selectionEnd in the + // callback function of nsISelectionListener::NotifySelectionChanged(). + if (this._selectionTimeout) { + content.clearTimeout(this._selectionTimeout); + } + this._selectionTimeout = content.setTimeout(function() { + if (!this.focusedElement) { + return; + } + let selectionInfo = this.getSelectionInfo(); + if (selectionInfo.changed) { + sendAsyncMessage("Forms:SelectionChange", selectionInfo); + } + }.bind(this), 0); + } +}; + +FormAssistant.init(); + +function isContentEditable(element) { + if (!element) { + return false; + } + + if (element.isContentEditable || element.designMode == "on") + return true; + + return element.ownerDocument && element.ownerDocument.designMode == "on"; +} + +function isPlainTextField(element) { + if (!element) { + return false; + } + + return element instanceof HTMLTextAreaElement || + (element instanceof HTMLInputElement && + element.mozIsTextField(false)); +} + +function getJSON(element, focusCounter) { + // <input type=number> has a nested anonymous <input type=text> element that + // takes focus on behalf of the number control when someone tries to focus + // the number control. If |element| is such an anonymous text control then we + // need it's number control here in order to get the correct 'type' etc.: + element = element.ownerNumberControl || element; + + let type = element.tagName.toLowerCase(); + let inputType = (element.type || "").toLowerCase(); + let value = element.value || ""; + let max = element.max || ""; + let min = element.min || ""; + + // Treat contenteditable element as a special text area field + if (isContentEditable(element)) { + type = "contenteditable"; + inputType = "textarea"; + value = getContentEditableText(element); + } + + // Until the input type=date/datetime/range have been implemented + // let's return their real type even if the platform returns 'text' + let attributeInputType = element.getAttribute("type") || ""; + + if (attributeInputType) { + let inputTypeLowerCase = attributeInputType.toLowerCase(); + switch (inputTypeLowerCase) { + case "datetime": + case "datetime-local": + case "month": + case "week": + case "range": + inputType = inputTypeLowerCase; + break; + } + } + + // Gecko has some support for @inputmode but behind a preference and + // it is disabled by default. + // Gaia is then using @x-inputmode has its proprietary way to set + // inputmode for fields. This shouldn't be used outside of pre-installed + // apps because the attribute is going to disappear as soon as a definitive + // solution will be find. + let inputMode = element.getAttribute('x-inputmode'); + if (inputMode) { + inputMode = inputMode.toLowerCase(); + } else { + inputMode = ''; + } + + let range = getSelectionRange(element); + + return { + "contextId": focusCounter, + + "type": type, + "inputType": inputType, + "inputMode": inputMode, + + "choices": getListForElement(element), + "value": value, + "selectionStart": range[0], + "selectionEnd": range[1], + "max": max, + "min": min, + "lang": element.lang || "" + }; +} + +function getListForElement(element) { + if (!(element instanceof HTMLSelectElement)) + return null; + + let optionIndex = 0; + let result = { + "multiple": element.multiple, + "choices": [] + }; + + // Build up a flat JSON array of the choices. + // In HTML, it's possible for select element choices to be under a + // group header (but not recursively). We distinguish between headers + // and entries using the boolean "list.group". + let children = element.children; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + + if (child instanceof HTMLOptGroupElement) { + result.choices.push({ + "group": true, + "text": child.label || child.firstChild.data, + "disabled": child.disabled + }); + + let subchildren = child.children; + for (let j = 0; j < subchildren.length; j++) { + let subchild = subchildren[j]; + result.choices.push({ + "group": false, + "inGroup": true, + "text": subchild.text, + "disabled": child.disabled || subchild.disabled, + "selected": subchild.selected, + "optionIndex": optionIndex++ + }); + } + } else if (child instanceof HTMLOptionElement) { + result.choices.push({ + "group": false, + "inGroup": false, + "text": child.text, + "disabled": child.disabled, + "selected": child.selected, + "optionIndex": optionIndex++ + }); + } + } + + return result; +}; + +// Create a plain text document encode from the focused element. +function getDocumentEncoder(element) { + let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"] + .createInstance(Ci.nsIDocumentEncoder); + let flags = Ci.nsIDocumentEncoder.SkipInvisibleContent | + Ci.nsIDocumentEncoder.OutputRaw | + Ci.nsIDocumentEncoder.OutputDropInvisibleBreak | + // Bug 902847. Don't trim trailing spaces of a line. + Ci.nsIDocumentEncoder.OutputDontRemoveLineEndingSpaces | + Ci.nsIDocumentEncoder.OutputLFLineBreak | + Ci.nsIDocumentEncoder.OutputNonTextContentAsPlaceholder; + encoder.init(element.ownerDocument, "text/plain", flags); + return encoder; +} + +// Get the visible content text of a content editable element +function getContentEditableText(element) { + if (!element || !isContentEditable(element)) { + return null; + } + + let doc = element.ownerDocument; + let range = doc.createRange(); + range.selectNodeContents(element); + let encoder = FormAssistant.documentEncoder; + encoder.setRange(range); + return encoder.encodeToString(); +} + +function getSelectionRange(element) { + let start = 0; + let end = 0; + if (isPlainTextField(element)) { + // Get the selection range of <input> and <textarea> elements + start = element.selectionStart; + end = element.selectionEnd; + } else if (isContentEditable(element)){ + // Get the selection range of contenteditable elements + let win = element.ownerDocument.defaultView; + let sel = win.getSelection(); + if (sel && sel.rangeCount > 0) { + start = getContentEditableSelectionStart(element, sel); + end = start + getContentEditableSelectionLength(element, sel); + } else { + dump("Failed to get window.getSelection()\n"); + } + } + return [start, end]; + } + +function getContentEditableSelectionStart(element, selection) { + let doc = element.ownerDocument; + let range = doc.createRange(); + range.setStart(element, 0); + range.setEnd(selection.anchorNode, selection.anchorOffset); + let encoder = FormAssistant.documentEncoder; + encoder.setRange(range); + return encoder.encodeToString().length; +} + +function getContentEditableSelectionLength(element, selection) { + let encoder = FormAssistant.documentEncoder; + encoder.setRange(selection.getRangeAt(0)); + return encoder.encodeToString().length; +} + +function setSelectionRange(element, start, end) { + let isTextField = isPlainTextField(element); + + // Check the parameters + + if (!isTextField && !isContentEditable(element)) { + // Skip HTMLOptionElement and HTMLSelectElement elements, as they don't + // support the operation of setSelectionRange + return false; + } + + let text = isTextField ? element.value : getContentEditableText(element); + let length = text.length; + if (start < 0) { + start = 0; + } + if (end > length) { + end = length; + } + if (start > end) { + start = end; + } + + if (isTextField) { + // Set the selection range of <input> and <textarea> elements + element.setSelectionRange(start, end, "forward"); + return true; + } else { + // set the selection range of contenteditable elements + let win = element.ownerDocument.defaultView; + let sel = win.getSelection(); + + // Move the caret to the start position + sel.collapse(element, 0); + for (let i = 0; i < start; i++) { + sel.modify("move", "forward", "character"); + } + + // Avoid entering infinite loop in case we cannot change the selection + // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918 + let oldStart = getContentEditableSelectionStart(element, sel); + let counter = 0; + while (oldStart < start) { + sel.modify("move", "forward", "character"); + let newStart = getContentEditableSelectionStart(element, sel); + if (oldStart == newStart) { + counter++; + if (counter > MAX_BLOCKED_COUNT) { + return false; + } + } else { + counter = 0; + oldStart = newStart; + } + } + + // Extend the selection to the end position + for (let i = start; i < end; i++) { + sel.modify("extend", "forward", "character"); + } + + // Avoid entering infinite loop in case we cannot change the selection + // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918 + counter = 0; + let selectionLength = end - start; + let oldSelectionLength = getContentEditableSelectionLength(element, sel); + while (oldSelectionLength < selectionLength) { + sel.modify("extend", "forward", "character"); + let newSelectionLength = getContentEditableSelectionLength(element, sel); + if (oldSelectionLength == newSelectionLength ) { + counter++; + if (counter > MAX_BLOCKED_COUNT) { + return false; + } + } else { + counter = 0; + oldSelectionLength = newSelectionLength; + } + } + return true; + } +} + +/** + * Scroll the given element into view. + * + * Calls scrollSelectionIntoView for contentEditable elements. + */ +function scrollSelectionOrElementIntoView(element) { + let editor = getPlaintextEditor(element); + if (editor) { + editor.selectionController.scrollSelectionIntoView( + Ci.nsISelectionController.SELECTION_NORMAL, + Ci.nsISelectionController.SELECTION_FOCUS_REGION, + Ci.nsISelectionController.SCROLL_SYNCHRONOUS); + } else { + element.scrollIntoView(false); + } +} + +// Get nsIPlaintextEditor object from an input field +function getPlaintextEditor(element) { + let editor = null; + // Get nsIEditor + if (isPlainTextField(element)) { + // Get from the <input> and <textarea> elements + editor = element.QueryInterface(Ci.nsIDOMNSEditableElement).editor; + } else if (isContentEditable(element)) { + // Get from content editable element + let win = element.ownerDocument.defaultView; + let editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + if (editingSession) { + editor = editingSession.getEditorForWindow(win); + } + } + if (editor) { + editor.QueryInterface(Ci.nsIPlaintextEditor); + } + return editor; +} + +function replaceSurroundingText(element, text, offset, length) { + let editor = FormAssistant.editor; + if (!editor) { + return false; + } + + // Check the parameters. + if (length < 0) { + length = 0; + } + + // Change selection range before replacing. For content editable element, + // searching the node for setting selection range is not needed when the + // selection is collapsed within a text node. + let fastPathHit = false; + if (!isPlainTextField(element)) { + let sel = element.ownerDocument.defaultView.getSelection(); + let node = sel.anchorNode; + if (sel.isCollapsed && node && node.nodeType == 3 /* TEXT_NODE */) { + let start = sel.anchorOffset + offset; + let end = start + length; + // Fallback to setSelectionRange() if the replacement span multiple nodes. + if (start >= 0 && end <= node.textContent.length) { + fastPathHit = true; + sel.collapse(node, start); + sel.extend(node, end); + } + } + } + if (!fastPathHit) { + let range = getSelectionRange(element); + let start = range[0] + offset; + if (start < 0) { + start = 0; + } + let end = start + length; + if (start != range[0] || end != range[1]) { + if (!setSelectionRange(element, start, end)) { + return false; + } + } + } + + if (length) { + // Delete the selected text. + editor.deleteSelection(Ci.nsIEditor.ePrevious, Ci.nsIEditor.eStrip); + } + + if (text) { + // We don't use CR but LF + // see https://bugzilla.mozilla.org/show_bug.cgi?id=902847 + text = text.replace(/\r/g, '\n'); + // Insert the text to be replaced with. + editor.insertText(text); + } + return true; +} + +var CompositionManager = { + _isStarted: false, + _tip: null, + _KeyboardEventForWin: null, + _clauseAttrMap: { + 'raw-input': + Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE, + 'selected-raw-text': + Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE, + 'converted-text': + Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE, + 'selected-converted-text': + Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE + }, + + setComposition: function cm_setComposition(element, text, cursor, clauses, dict) { + // Check parameters. + if (!element) { + return; + } + let len = text.length; + if (cursor > len) { + cursor = len; + } + let clauseLens = []; + let clauseAttrs = []; + if (clauses) { + let remainingLength = len; + for (let i = 0; i < clauses.length; i++) { + if (clauses[i]) { + let clauseLength = clauses[i].length || 0; + // Make sure the total clauses length is not bigger than that of the + // composition string. + if (clauseLength > remainingLength) { + clauseLength = remainingLength; + } + remainingLength -= clauseLength; + clauseLens.push(clauseLength); + clauseAttrs.push(this._clauseAttrMap[clauses[i].selectionType] || + Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE); + } + } + // If the total clauses length is less than that of the composition + // string, extend the last clause to the end of the composition string. + if (remainingLength > 0) { + clauseLens[clauseLens.length - 1] += remainingLength; + } + } else { + clauseLens.push(len); + clauseAttrs.push(Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE); + } + + let win = element.ownerDocument.defaultView; + let tip = WindowMap.getTextInputProcessor(win); + if (!tip) { + return; + } + // Update the composing text. + tip.setPendingCompositionString(text); + for (var i = 0; i < clauseLens.length; i++) { + if (!clauseLens[i]) { + continue; + } + tip.appendClauseToPendingComposition(clauseLens[i], clauseAttrs[i]); + } + if (cursor >= 0) { + tip.setCaretInPendingComposition(cursor); + } + + if (!dict) { + this._isStarted = tip.flushPendingComposition(); + } else { + let keyboardEvent = new win.KeyboardEvent("", dict); + let flags = dict.flags; + this._isStarted = tip.flushPendingComposition(keyboardEvent, flags); + } + + if (this._isStarted) { + this._tip = tip; + this._KeyboardEventForWin = win.KeyboardEvent; + } + }, + + endComposition: function cm_endComposition(text, dict) { + if (!this._isStarted) { + return; + } + let tip = this._tip; + if (!tip) { + return; + } + + text = text || ""; + if (!dict) { + tip.commitCompositionWith(text); + } else { + let keyboardEvent = new this._KeyboardEventForWin("", dict); + let flags = dict.flags; + tip.commitCompositionWith(text, keyboardEvent, flags); + } + + this._isStarted = false; + this._tip = null; + this._KeyboardEventForWin = null; + }, + + // Composition ends due to external actions. + onCompositionEnd: function cm_onCompositionEnd() { + if (!this._isStarted) { + return; + } + + this._isStarted = false; + this._tip = null; + this._KeyboardEventForWin = null; + } +}; diff --git a/dom/inputmethod/jar.mn b/dom/inputmethod/jar.mn new file mode 100644 index 000000000..c7bb9a0fe --- /dev/null +++ b/dom/inputmethod/jar.mn @@ -0,0 +1,6 @@ +# 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/. + +toolkit.jar: + content/global/forms.js (forms.js) diff --git a/dom/inputmethod/mochitest/bug1110030_helper.js b/dom/inputmethod/mochitest/bug1110030_helper.js new file mode 100644 index 000000000..54f15825b --- /dev/null +++ b/dom/inputmethod/mochitest/bug1110030_helper.js @@ -0,0 +1,267 @@ +// *********************************** +// * Global variables +// *********************************** +const kIsWin = navigator.platform.indexOf("Win") == 0; + +// Bit value for the keyboard events +const kKeyDown = 0x01; +const kKeyPress = 0x02; +const kKeyUp = 0x04; + +// Pair the event name to its bit value +const kEventCode = { + 'keydown' : kKeyDown, + 'keypress' : kKeyPress, + 'keyup' : kKeyUp +}; + +// Holding the current test case's infomation: +var gCurrentTest; + +// The current used input method of this test +var gInputMethod; + +// *********************************** +// * Utilities +// *********************************** +function addKeyEventListeners(eventTarget, handler) +{ + Object.keys(kEventCode).forEach(function(type) { + eventTarget.addEventListener(type, handler); + }); +} + +function eventToCode(type) +{ + return kEventCode[type]; +} + +// To test key events that will be generated by input method here, +// we need to convert alphabets to native key code. +// (Our input method for testing will handle alphabets) +// On the other hand, to test key events that will not be generated by IME, +// we use 0-9 for such case in our testing. +function guessNativeKeyCode(key) +{ + let nativeCodeName = (kIsWin)? 'WIN_VK_' : 'MAC_VK_ANSI_'; + if (/^[A-Z]$/.test(key)) { + nativeCodeName += key; + } else if (/^[a-z]$/.test(key)) { + nativeCodeName += key.toUpperCase(); + } else if (/^[0-9]$/.test(key)) { + nativeCodeName += key.toString(); + } else { + return 0; + } + + return eval(nativeCodeName); +} + +// *********************************** +// * Frame loader and frame scripts +// *********************************** +function frameScript() +{ + function handler(e) { + sendAsyncMessage("forwardevent", { type: e.type, key: e.key }); + } + function notifyFinish(e) { + if (e.type != 'keyup') return; + sendAsyncMessage("finish"); + } + let input = content.document.getElementById('test-input'); + input.addEventListener('keydown', handler); + input.addEventListener('keypress', handler); + input.addEventListener('keyup', handler); + input.addEventListener('keyup', notifyFinish); +} + +function loadTestFrame(goNext) { + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_empty_app.html'; + iframe.setAttribute('mozbrowser', true); + + iframe.addEventListener("mozbrowserloadend", function onloadend() { + iframe.removeEventListener("mozbrowserloadend", onloadend); + iframe.focus(); + var mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + mm.addMessageListener("forwardevent", function(msg) { + inputtextEventReceiver(msg.json); + }); + mm.addMessageListener("finish", function(msg) { + if(goNext) { + goNext(); + } + }); + mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", false); + return; + }); + + document.body.appendChild(iframe); +} + +// *********************************** +// * Event firer and listeners +// *********************************** +function fireEvent(callback) +{ + let key = gCurrentTest.key; + synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, guessNativeKeyCode(key), {}, + key, key, (callback) ? callback : null); +} + +function hardwareEventReceiver(evt) +{ + if (!gCurrentTest) { + return; + } + gCurrentTest.hardwareinput.receivedEvents |= eventToCode(evt.type); + gCurrentTest.hardwareinput.receivedKeys += evt.key; +} + +function inputtextEventReceiver(evt) +{ + if (!gCurrentTest) { + return; + } + gCurrentTest.inputtext.receivedEvents |= eventToCode(evt.type); + gCurrentTest.inputtext.receivedKeys += evt.key; +} + +// *********************************** +// * Event verifier +// *********************************** +function verifyResults(test) +{ + // Verify results received from inputcontent.hardwareinput + is(test.hardwareinput.receivedEvents, + test.hardwareinput.expectedEvents, + "received events from inputcontent.hardwareinput are wrong"); + + is(test.hardwareinput.receivedKeys, + test.hardwareinput.expectedKeys, + "received keys from inputcontent.hardwareinput are wrong"); + + // Verify results received from actual input text + is(test.inputtext.receivedEvents, + test.inputtext.expectedEvents, + "received events from input text are wrong"); + + is(test.inputtext.receivedKeys, + test.inputtext.expectedKeys, + "received keys from input text are wrong"); +} + +function areEventsSame(test) +{ + return (test.hardwareinput.receivedEvents == + test.hardwareinput.expectedEvents) && + (test.inputtext.receivedEvents == + test.inputtext.expectedEvents); +} + +// *********************************** +// * Input Method +// *********************************** +// The method input used in this test +// only handles alphabets +function InputMethod(inputContext) +{ + this._inputContext = inputContext; + this.init(); +} + +InputMethod.prototype = { + init: function im_init() { + this._setKepMap(); + }, + + handler: function im_handler(evt) { + // Ignore the key if the event is defaultPrevented + if (evt.defaultPrevented) { + return; + } + + // Finish if there is no _inputContext + if (!this._inputContext) { + return; + } + + // Generate the keyDict for inputcontext.keydown/keyup + let keyDict = this._generateKeyDict(evt); + + // Ignore the key if IME doesn't want to handle it + if (!keyDict) { + return; + } + + // Call preventDefault if the key will be handled. + evt.preventDefault(); + + // Call inputcontext.keydown/keyup + this._inputContext[evt.type](keyDict); + }, + + mapKey: function im_keymapping(key) { + if (!this._mappingTable) { + return; + } + return this._mappingTable[key]; + }, + + _setKepMap: function im_setKeyMap() { + // A table to map characters: + // { + // 'A': 'B' + // 'a': 'b' + // 'B': 'C' + // 'b': 'c' + // .. + // .. + // 'Z': 'A', + // 'z': 'a', + // } + this._mappingTable = {}; + + let rotation = 1; + + for (let i = 0 ; i < 26 ; i++) { + // Convert 'A' to 'B', 'B' to 'C', ..., 'Z' to 'A' + this._mappingTable[String.fromCharCode(i + 'A'.charCodeAt(0))] = + String.fromCharCode((i+rotation)%26 + 'A'.charCodeAt(0)); + + // Convert 'a' to 'b', 'b' to 'c', ..., 'z' to 'a' + this._mappingTable[String.fromCharCode(i + 'a'.charCodeAt(0))] = + String.fromCharCode((i+rotation)%26 + 'a'.charCodeAt(0)); + } + }, + + _generateKeyDict: function im_generateKeyDict(evt) { + + let mappedKey = this.mapKey(evt.key); + + if (!mappedKey) { + return; + } + + let keyDict = { + key: mappedKey, + code: this._guessCodeFromKey(mappedKey), + repeat: evt.repeat, + }; + + return keyDict; + }, + + _guessCodeFromKey: function im_guessCodeFromKey(key) { + if (/^[A-Z]$/.test(key)) { + return "Key" + key; + } else if (/^[a-z]$/.test(key)) { + return "Key" + key.toUpperCase(); + } else if (/^[0-9]$/.test(key)) { + return "Digit" + key.toString(); + } else { + return 0; + } + }, +}; diff --git a/dom/inputmethod/mochitest/chrome.ini b/dom/inputmethod/mochitest/chrome.ini new file mode 100644 index 000000000..7575c2215 --- /dev/null +++ b/dom/inputmethod/mochitest/chrome.ini @@ -0,0 +1,52 @@ +[DEFAULT] +# dom/inputmethod only made sense on B2G +skip-if = true +support-files = + bug1110030_helper.js + inputmethod_common.js + file_inputmethod.html + file_blank.html + file_test_app.html + file_test_bug1066515.html + file_test_bug1137557.html + file_test_bug1175399.html + file_test_empty_app.html + file_test_focus_blur_manage_events.html + file_test_sendkey_cancel.html + file_test_setSupportsSwitching.html + file_test_simple_manage_events.html + file_test_sms_app.html + file_test_sms_app_1066515.html + file_test_sync_edit.html + file_test_two_inputs.html + file_test_two_selects.html + file_test_unload.html + file_test_unload_action.html + +[test_basic.html] +[test_bug944397.html] +[test_bug949059.html] +[test_bug953044.html] +[test_bug960946.html] +[test_bug978918.html] +[test_bug1026997.html] +[test_bug1043828.html] +[test_bug1059163.html] +disabled = fails because receiving bad values +[test_bug1066515.html] +[test_bug1137557.html] +[test_bug1175399.html] +[test_focus_blur_manage_events.html] +disabled = fails because receiving bad events # also depends on bug 1254823 +[test_forward_hardware_key_to_ime.html] +skip-if = true # Test only ran on Mulet +[test_input_registry_events.html] +disabled = timeout on pine +[test_sendkey_cancel.html] +[test_setSupportsSwitching.html] +[test_simple_manage_events.html] +disabled = fails because receiving bad events +[test_sync_edit.html] +[test_two_inputs.html] +[test_two_selects.html] +[test_unload.html] diff --git a/dom/inputmethod/mochitest/file_blank.html b/dom/inputmethod/mochitest/file_blank.html new file mode 100644 index 000000000..7879e1ce9 --- /dev/null +++ b/dom/inputmethod/mochitest/file_blank.html @@ -0,0 +1,4 @@ +<html> +<body> +</body> +</html> diff --git a/dom/inputmethod/mochitest/file_inputmethod.html b/dom/inputmethod/mochitest/file_inputmethod.html new file mode 100644 index 000000000..193cb0505 --- /dev/null +++ b/dom/inputmethod/mochitest/file_inputmethod.html @@ -0,0 +1,25 @@ +<html> +<body> +<script> + var im = navigator.mozInputMethod; + if (im) { + im.oninputcontextchange = onIcc; + + if (im.inputcontext) { + onIcc(); + } + } + + function onIcc() { + var ctx = im.inputcontext; + if (ctx) { + ctx.replaceSurroundingText(location.hash).then(function() { + /* Happy flow */ + }, function(err) { + dump('ReplaceSurroundingText failed ' + err + '\n'); + }); + } + } +</script> +</body> +</html> diff --git a/dom/inputmethod/mochitest/file_test_app.html b/dom/inputmethod/mochitest/file_test_app.html new file mode 100644 index 000000000..3063e9749 --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_app.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<body> +<input id="test-input" type="text" value="Yuan" x-inputmode="verbatim" lang="zh"/> +<script type="application/javascript;version=1.7"> + let input = document.getElementById('test-input'); + input.focus(); + dump('file_test_app.html was loaded.'); +</script> +</body> +</html> diff --git a/dom/inputmethod/mochitest/file_test_bug1066515.html b/dom/inputmethod/mochitest/file_test_bug1066515.html new file mode 100644 index 000000000..6331ec40e --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_bug1066515.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> +<div id="text" contenteditable>Jan Jongboom</div> +</body> +</html> diff --git a/dom/inputmethod/mochitest/file_test_bug1137557.html b/dom/inputmethod/mochitest/file_test_bug1137557.html new file mode 100644 index 000000000..dc0c8d77e --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_bug1137557.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> +<textarea rows=30 cols=30></textarea> +</body> +</html> diff --git a/dom/inputmethod/mochitest/file_test_bug1175399.html b/dom/inputmethod/mochitest/file_test_bug1175399.html new file mode 100644 index 000000000..3fa7da46c --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_bug1175399.html @@ -0,0 +1 @@ +<html><body><input value="First" readonly></body></html> diff --git a/dom/inputmethod/mochitest/file_test_empty_app.html b/dom/inputmethod/mochitest/file_test_empty_app.html new file mode 100644 index 000000000..c071c1a31 --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_empty_app.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<body> +<input id="test-input" type="text" value=""/> +<script type="application/javascript;version=1.7"> + let input = document.getElementById('test-input'); + input.focus(); +</script> +</body> +</html> diff --git a/dom/inputmethod/mochitest/file_test_focus_blur_manage_events.html b/dom/inputmethod/mochitest/file_test_focus_blur_manage_events.html new file mode 100644 index 000000000..bb8c44573 --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_focus_blur_manage_events.html @@ -0,0 +1,22 @@ +<html><body> +<input type="text"> +<input type="search"> +<textarea></textarea> +<p contenteditable></p> +<input type="number"> +<input type="tel"> +<input type="url"> +<input type="email"> +<input type="password"> +<input type="datetime"> +<input type="date" value="2015-08-03" min="1990-01-01" max="2020-01-01"> +<input type="month"> +<input type="week"> +<input type="time"> +<input type="datetime-local"> +<input type="color"> +<select><option selected>foo</option><option disabled>bar</option> +<optgroup label="group"><option>baz</option></optgroup></select> +<select multiple><option selected>foo</option><option disabled>bar</option> +<optgroup label="group"><option>baz</option></optgroup></select> +</body></html> diff --git a/dom/inputmethod/mochitest/file_test_sendkey_cancel.html b/dom/inputmethod/mochitest/file_test_sendkey_cancel.html new file mode 100644 index 000000000..f40ee6959 --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_sendkey_cancel.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<body> +<input id="test-input" type="text" value="Yolo"/> +<script type="application/javascript;version=1.7"> + let input = document.getElementById('test-input'); + input.focus(); + + input.addEventListener('keydown', function(e) { + e.preventDefault(); + }); +</script> +</body> +</html> diff --git a/dom/inputmethod/mochitest/file_test_setSupportsSwitching.html b/dom/inputmethod/mochitest/file_test_setSupportsSwitching.html new file mode 100644 index 000000000..5b5e1733a --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_setSupportsSwitching.html @@ -0,0 +1,5 @@ +<html><body> +<input type="text"> +<input type="number"> +<input type="password"> +</body></html> diff --git a/dom/inputmethod/mochitest/file_test_simple_manage_events.html b/dom/inputmethod/mochitest/file_test_simple_manage_events.html new file mode 100644 index 000000000..5c0c25cf4 --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_simple_manage_events.html @@ -0,0 +1 @@ +<html><body><input type="text"></body></html> diff --git a/dom/inputmethod/mochitest/file_test_sms_app.html b/dom/inputmethod/mochitest/file_test_sms_app.html new file mode 100644 index 000000000..7aa3e6081 --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_sms_app.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<body> + <div id="messages-input" x-inputmode="-moz-sms" contenteditable="true" + autofocus="autofocus">Httvb<br></div> + <script type="application/javascript;version=1.7"> + let input = document.getElementById('messages-input'); + input.focus(); + </script> +</body> +</html> + </div> +</body> +</html> diff --git a/dom/inputmethod/mochitest/file_test_sms_app_1066515.html b/dom/inputmethod/mochitest/file_test_sms_app_1066515.html new file mode 100644 index 000000000..a515c90b5 --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_sms_app_1066515.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<body> + <div id="messages-input" x-inputmode="-moz-sms" contenteditable="true" + autofocus="autofocus">fxos<br>hello <b>world</b></div> + <script type="application/javascript;version=1.7"> + let input = document.getElementById('messages-input'); + input.focus(); + </script> +</body> +</html> + </div> +</body> +</html> diff --git a/dom/inputmethod/mochitest/file_test_sync_edit.html b/dom/inputmethod/mochitest/file_test_sync_edit.html new file mode 100644 index 000000000..c450ad5cf --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_sync_edit.html @@ -0,0 +1 @@ +<html><body><input value="First"></body></html> diff --git a/dom/inputmethod/mochitest/file_test_two_inputs.html b/dom/inputmethod/mochitest/file_test_two_inputs.html new file mode 100644 index 000000000..af7a2866d --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_two_inputs.html @@ -0,0 +1 @@ +<html><body><input value="First"><input value="Second"></body></html> diff --git a/dom/inputmethod/mochitest/file_test_two_selects.html b/dom/inputmethod/mochitest/file_test_two_selects.html new file mode 100644 index 000000000..be2204f6e --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_two_selects.html @@ -0,0 +1 @@ +<html><body><select><option>First</option></select><select><option>Second</option></select></html> diff --git a/dom/inputmethod/mochitest/file_test_unload.html b/dom/inputmethod/mochitest/file_test_unload.html new file mode 100644 index 000000000..d1a939405 --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_unload.html @@ -0,0 +1 @@ +<html><body><form id="form"><input value="First"><input type="submit"></form></body></html> diff --git a/dom/inputmethod/mochitest/file_test_unload_action.html b/dom/inputmethod/mochitest/file_test_unload_action.html new file mode 100644 index 000000000..20a9bb3af --- /dev/null +++ b/dom/inputmethod/mochitest/file_test_unload_action.html @@ -0,0 +1 @@ +<html><body><input value="Second"></body></html> diff --git a/dom/inputmethod/mochitest/inputmethod_common.js b/dom/inputmethod/mochitest/inputmethod_common.js new file mode 100644 index 000000000..ad8103c9f --- /dev/null +++ b/dom/inputmethod/mochitest/inputmethod_common.js @@ -0,0 +1,24 @@ +function inputmethod_setup(callback) { + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestCompleteLog(); + let appInfo = SpecialPowers.Cc['@mozilla.org/xre/app-info;1'] + .getService(SpecialPowers.Ci.nsIXULAppInfo); + if (appInfo.name != 'B2G') { + SpecialPowers.Cu.import("resource://gre/modules/Keyboard.jsm", this); + } + + let prefs = [ + ['dom.mozBrowserFramesEnabled', true], + ['network.disable.ipc.security', true], + // Enable navigator.mozInputMethod. + ['dom.mozInputMethod.enabled', true] + ]; + SpecialPowers.pushPrefEnv({set: prefs}, function() { + SimpleTest.waitForFocus(callback); + }); +} + +function inputmethod_cleanup() { + SpecialPowers.wrap(navigator.mozInputMethod).setActive(false); + SimpleTest.finish(); +} diff --git a/dom/inputmethod/mochitest/test_basic.html b/dom/inputmethod/mochitest/test_basic.html new file mode 100644 index 000000000..bf22e99dd --- /dev/null +++ b/dom/inputmethod/mochitest/test_basic.html @@ -0,0 +1,212 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=932145 +--> +<head> + <title>Basic test for InputMethod API.</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=932145">Mozilla Bug 932145</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +SimpleTest.requestFlakyTimeout("untriaged"); + +// The input context. +var gContext = null; + +inputmethod_setup(function() { + runTest(); +}); + +function runTest() { + let im = navigator.mozInputMethod; + + im.oninputcontextchange = function() { + ok(true, 'inputcontextchange event was fired.'); + im.oninputcontextchange = null; + + gContext = im.inputcontext; + if (!gContext) { + ok(false, 'Should have a non-null inputcontext.'); + inputmethod_cleanup(); + return; + } + + is(gContext.type, 'input', 'The input context type should match.'); + is(gContext.inputType, 'text', 'The inputType should match.'); + is(gContext.inputMode, 'verbatim', 'The inputMode should match.'); + is(gContext.lang, 'zh', 'The language should match.'); + is(gContext.text, 'Yuan', 'Should get the text.'); + is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yuan', + 'Should get the text around the cursor.'); + + test_setSelectionRange(); + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_app.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); +} + +function test_setSelectionRange() { + // Move cursor position to 2. + gContext.setSelectionRange(2, 0).then(function() { + is(gContext.selectionStart, 2, 'selectionStart was set successfully.'); + is(gContext.selectionEnd, 2, 'selectionEnd was set successfully.'); + test_sendKey(); + }, function(e) { + ok(false, 'setSelectionRange failed:' + e.name); + console.error(e); + inputmethod_cleanup(); + }); +} + +function test_sendKey() { + // Add '-' to current cursor posistion and move the cursor position to 3. + gContext.sendKey(0, '-'.charCodeAt(0), 0).then(function() { + is(gContext.text, 'Yu-an', + 'sendKey should changed the input field correctly.'); + is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yu-an', + 'sendKey should changed the input field correctly.'); + test_deleteSurroundingText(); + }, function(e) { + ok(false, 'sendKey failed:' + e.name); + inputmethod_cleanup(); + }); +} + +function test_deleteSurroundingText() { + // Remove one character before current cursor position and move the cursor + // position back to 2. + gContext.deleteSurroundingText(-1, 1).then(function() { + ok(true, 'deleteSurroundingText finished'); + is(gContext.text, 'Yuan', + 'deleteSurroundingText should changed the input field correctly.'); + is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yuan', + 'deleteSurroundingText should changed the input field correctly.'); + test_replaceSurroundingText(); + }, function(e) { + ok(false, 'deleteSurroundingText failed:' + e.name); + inputmethod_cleanup(); + }); +} + +function test_replaceSurroundingText() { + // Replace 'Yuan' with 'Xulei'. + gContext.replaceSurroundingText('Xulei', -2, 4).then(function() { + ok(true, 'replaceSurroundingText finished'); + is(gContext.text, 'Xulei', + 'replaceSurroundingText changed the input field correctly.'); + is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Xulei', + 'replaceSurroundingText changed the input field correctly.'); + test_setComposition(); + }, function(e) { + ok(false, 'replaceSurroundingText failed: ' + e.name); + inputmethod_cleanup(); + }); +} + +function test_setComposition() { + gContext.setComposition('XXX').then(function() { + ok(true, 'setComposition finished'); + test_endComposition(); + }, function(e) { + ok(false, 'setComposition failed: ' + e.name); + inputmethod_cleanup(); + }); +} + +function test_endComposition() { + gContext.endComposition('2013').then(function() { + is(gContext.text, 'Xulei2013', + 'endComposition changed the input field correctly.'); + is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Xulei2013', + 'endComposition changed the input field correctly.'); + test_onSelectionChange(); + }, function (e) { + ok(false, 'endComposition failed: ' + e.name); + inputmethod_cleanup(); + }); +} + +function test_onSelectionChange() { + var sccTimeout = setTimeout(function() { + ok(false, 'selectionchange event not fired'); + cleanup(true); + }, 3000); + + function cleanup(failed) { + gContext.onselectionchange = null; + clearTimeout(sccTimeout); + if (failed) { + inputmethod_cleanup(); + } + else { + test_onSurroundingTextChange(); + } + } + + gContext.onselectionchange = function(evt) { + ok(true, 'onselectionchange fired'); + is(evt.detail.selectionStart, 10); + is(evt.detail.selectionEnd, 10); + ok(evt.detail.ownAction); + }; + + gContext.sendKey(0, 'j'.charCodeAt(0), 0).then(function() { + cleanup(); + }, function(e) { + ok(false, 'sendKey failed: ' + e.name); + cleanup(true); + }); +} + +function test_onSurroundingTextChange() { + var sccTimeout = setTimeout(function() { + ok(false, 'surroundingtextchange event not fired'); + cleanup(true); + }, 3000); + + function cleanup(failed) { + gContext.onsurroundingtextchange = null; + clearTimeout(sccTimeout); + if (failed) { + inputmethod_cleanup(); + } + else { + // in case we want more tests leave this + inputmethod_cleanup(); + } + } + + gContext.onsurroundingtextchange = function(evt) { + ok(true, 'onsurroundingtextchange fired'); + is(evt.detail.text, 'Xulei2013jj'); + is(evt.detail.textBeforeCursor, 'Xulei2013jj'); + is(evt.detail.textAfterCursor, ''); + ok(evt.detail.ownAction); + }; + + gContext.sendKey(0, 'j'.charCodeAt(0), 0).then(function() { + cleanup(); + }, function(e) { + ok(false, 'sendKey failed: ' + e.name); + cleanup(true); + }); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug1026997.html b/dom/inputmethod/mochitest/test_bug1026997.html new file mode 100644 index 000000000..3d44e6cbd --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug1026997.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1026997 +--> +<head> + <title>SelectionChange on InputMethod API.</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1026997">Mozilla Bug 1026997</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +// The frame script running in file_test_app.html. +function appFrameScript() { + let input = content.document.getElementById('test-input'); + + input.focus(); + + function next(start, end) { + input.setSelectionRange(start, end); + } + + addMessageListener("test:KeyBoard:nextSelection", function(event) { + let json = event.json; + next(json[0], json[1]); + }); +} + +function runTest() { + let actions = [ + [0, 4], + [1, 1], + [3, 3], + [2, 3] + ]; + + let counter = 0; + let mm = null; + let ic = null; + + let im = navigator.mozInputMethod; + im.oninputcontextchange = function() { + ok(true, 'inputcontextchange event was fired.'); + im.oninputcontextchange = null; + + ic = im.inputcontext; + if (!ic) { + ok(false, 'Should have a non-null inputcontext.'); + inputmethod_cleanup(); + return; + } + + ic.onselectionchange = function() { + is(ic.selectionStart, actions[counter][0], "start"); + is(ic.selectionEnd, actions[counter][1], "end"); + + if (++counter === actions.length) { + inputmethod_cleanup(); + return; + } + + next(); + }; + + next(); + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + // Create an app frame to recieve keyboard inputs. + let app = document.createElement('iframe'); + app.src = 'file_test_app.html'; + app.setAttribute('mozbrowser', true); + document.body.appendChild(app); + app.addEventListener('mozbrowserloadend', function() { + mm = SpecialPowers.getBrowserFrameMessageManager(app); + mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false); + next(); + }); + + function next() { + if (ic && mm) { + mm.sendAsyncMessage('test:KeyBoard:nextSelection', actions[counter]); + } + } +} +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug1043828.html b/dom/inputmethod/mochitest/test_bug1043828.html new file mode 100644 index 000000000..84c1dc089 --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug1043828.html @@ -0,0 +1,183 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1043828 +--> +<head> + <title>Basic test for Switching Keyboards.</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1043828">Mozilla Bug 1043828</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +SimpleTest.requestFlakyTimeout("untriaged"); + +inputmethod_setup(function() { + runTest(); +}); + +// The KB frame script running in Keyboard B. +function kbFrameScript() { + function tryGetText() { + var ctx = content.navigator.mozInputMethod.inputcontext; + if (ctx) { + var p = ctx.getText(); + p.then(function(){ + sendAsyncMessage('test:InputMethod:getText:Resolve'); + }, function(e){ + sendAsyncMessage('test:InputMethod:getText:Reject'); + }); + } else { + dump("Could not get inputcontext") ; + } + } + + addMessageListener('test:InputMethod:getText:Do', function(){ + tryGetText(); + }); +} + +function runTest() { + let app, keyboardA, keyboardB; + let getTextPromise; + let mmKeyboardA, mmKeyboardB; + + /** + * Test flow: + * 1. Create two keyboard iframes & a mozbrowser iframe with a text field in it & focus the text + * field. + * 2. Set keyboard frame A as active input. Wait 200ms. + * 3. Set keyboard frame B as active input. Wait 200ms. + * 4. Set keyboard frame A as inactive. Wait 200ms. + * 5. Allow frame b to use getText() with inputcontext to get the content from the text field + * iframe. Wait 200ms. + * [Test would succeed if the Promise returned by getText() resolves correctly. + * Test would fail if otherwise] + */ + + let path = location.pathname; + let basePath = location.protocol + '//' + location.host + + path.substring(0, path.lastIndexOf('/')); + + const WAIT_TIME = 200; + + // STEP 1: Create the frames. + function step1() { + // app + app = document.createElement('iframe'); + app.src = basePath + '/file_test_app.html'; + app.setAttribute('mozbrowser', true); + document.body.appendChild(app); + + // keyboards + keyboardA = document.createElement('iframe'); + keyboardA.setAttribute('mozbrowser', true); + document.body.appendChild(keyboardA); + + keyboardB = document.createElement('iframe'); + keyboardB.setAttribute('mozbrowser', true); + document.body.appendChild(keyboardB); + + // simulate two different keyboard apps + let imeUrl = basePath + '/file_blank.html'; + + keyboardA.src = imeUrl; + keyboardB.src = imeUrl; + + var handler = { + handleEvent: function(){ + keyboardB.removeEventListener('mozbrowserloadend', this); + + mmKeyboardB = SpecialPowers.getBrowserFrameMessageManager(keyboardB); + + mmKeyboardB.loadFrameScript('data:,(' + kbFrameScript.toString() + ')();', false); + + mmKeyboardB.addMessageListener('test:InputMethod:getText:Resolve', function() { + info('getText() was resolved'); + inputmethod_cleanup(); + }); + + mmKeyboardB.addMessageListener('test:InputMethod:getText:Reject', function() { + ok(false, 'getText() was rejected'); + inputmethod_cleanup(); + }); + + setTimeout(function(){ + step2(); + }, WAIT_TIME); + } + }; + + keyboardB.addEventListener('mozbrowserloadend', handler); + } + + // STEP 2: Set keyboard A active + function step2() { + info('step2'); + let req = keyboardA.setInputMethodActive(true); + + req.onsuccess = function(){ + setTimeout(function(){ + step3(); + }, WAIT_TIME); + }; + + req.onerror = function(){ + ok(false, 'setInputMethodActive failed: ' + this.error.name); + inputmethod_cleanup(); + }; + } + + // STEP 3: Set keyboard B active + function step3() { + info('step3'); + let req = keyboardB.setInputMethodActive(true); + + req.onsuccess = function(){ + setTimeout(function(){ + step4(); + }, WAIT_TIME); + }; + + req.onerror = function(){ + ok(false, 'setInputMethodActive failed: ' + this.error.name); + inputmethod_cleanup(); + }; + } + + // STEP 4: Set keyboard A inactive + function step4() { + info('step4'); + let req = keyboardA.setInputMethodActive(false); + + req.onsuccess = function(){ + setTimeout(function(){ + step5(); + }, WAIT_TIME); + }; + + req.onerror = function(){ + ok(false, 'setInputMethodActive failed: ' + this.error.name); + inputmethod_cleanup(); + }; + } + + // STEP 5: getText + function step5() { + info('step5'); + mmKeyboardB.sendAsyncMessage('test:InputMethod:getText:Do'); + } + + step1(); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug1059163.html b/dom/inputmethod/mochitest/test_bug1059163.html new file mode 100644 index 000000000..c54a03b0e --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug1059163.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1059163 +--> +<head> + <title>Basic test for repeat sendKey events</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1059163">Mozilla Bug 1059163</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> +inputmethod_setup(function() { + runTest(); +}); + +// The frame script running in the file +function appFrameScript() { + let document = content.document; + let window = content.document.defaultView; + + let t = document.getElementById('text'); + t.focus(); + + let range = document.createRange(); + range.selectNodeContents(t); + range.collapse(false); + let selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + addMessageListener('test:InputMethod:clear', function() { + t.innerHTML = ''; + }); +} + +function runTest() { + let im = navigator.mozInputMethod; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + // Create an app frame to recieve keyboard inputs. + let app = document.createElement('iframe'); + app.src = 'file_test_bug1066515.html'; + app.setAttribute('mozbrowser', true); + document.body.appendChild(app); + + app.addEventListener('mozbrowserloadend', function() { + let mm = SpecialPowers.getBrowserFrameMessageManager(app); + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + + im.oninputcontextchange = function() { + is(im.inputcontext.type, 'contenteditable', 'type'); + is(im.inputcontext.inputType, 'textarea', 'inputType'); + + if (im.inputcontext) { + im.oninputcontextchange = null; + register(); + } + }; + + function register() { + im.inputcontext.onselectionchange = function() { + im.inputcontext.onselectionchange = null; + + is(im.inputcontext.textBeforeCursor, '', 'textBeforeCursor'); + is(im.inputcontext.textAfterCursor, '', 'textAfterCursor'); + is(im.inputcontext.selectionStart, 0, 'selectionStart'); + is(im.inputcontext.selectionEnd, 0, 'selectionEnd'); + + inputmethod_cleanup(); + }; + + mm.sendAsyncMessage('test:InputMethod:clear'); + } + }); +} +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug1066515.html b/dom/inputmethod/mochitest/test_bug1066515.html new file mode 100644 index 000000000..56fe10772 --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug1066515.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1066515 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1066515</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1066515">Mozilla Bug 1066515</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +// The input context. +var gContext = null; + +inputmethod_setup(function() { + runTest(); +}); + +function runTest() { + let im = navigator.mozInputMethod; + + im.oninputcontextchange = function() { + ok(true, 'inputcontextchange event was fired.'); + im.oninputcontextchange = null; + + gContext = im.inputcontext; + if (!gContext) { + ok(false, 'Should have a non-null inputcontext.'); + inputmethod_cleanup(); + return; + } + + test_replaceSurroundingTextWithinTextNode(); + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_sms_app_1066515.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); +} + +function test_replaceSurroundingTextWithinTextNode() { + // Set cursor position after 'f'. + gContext.setSelectionRange(1, 0); + + // Replace 'fxos' to 'Hitooo' which the range is within current text node. + gContext.replaceSurroundingText('Hitooo', -1, 4).then(function() { + gContext.getText().then(function(text) { + is(text, 'Hitooo\nhello world', 'replaceSurroundingText successfully.'); + test_replaceSurroundingTextSpanMultipleNodes(); + }, function(e) { + ok(false, 'getText failed: ' + e.name); + inputmethod_cleanup(); + }); + }, function(e) { + ok(false, 'replaceSurroundingText failed: ' + e.name); + inputmethod_cleanup(); + }); +} + +function test_replaceSurroundingTextSpanMultipleNodes() { + // Set cursor position to the beginning. + gContext.setSelectionRange(0, 0); + + // Replace whole content editable element to 'abc'. + gContext.replaceSurroundingText('abc', 0, 100).then(function() { + gContext.getText().then(function(text) { + is(text, 'abc', 'replaceSurroundingText successfully.'); + inputmethod_cleanup(); + }, function(e) { + ok(false, 'getText failed: ' + e.name); + inputmethod_cleanup(); + }); + }, function(e) { + ok(false, 'replaceSurroundingText failed: ' + e.name); + inputmethod_cleanup(); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/inputmethod/mochitest/test_bug1137557.html b/dom/inputmethod/mochitest/test_bug1137557.html new file mode 100644 index 000000000..1f5053662 --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug1137557.html @@ -0,0 +1,1799 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1137557 +--> +<head> + <title>Test for new API arguments accepting D3E properties</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1137557">Mozilla Bug 1137557</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +let gEventDetails = []; +let gCurrentValue = ''; +let gTestDescription = ''; + +let appFrameScript = function appFrameScript() { + let input = content.document.body.firstElementChild; + + input.focus(); + + function sendEventDetail(evt) { + var eventDetail; + + switch (evt.type) { + case 'compositionstart': + case 'compositionupdate': + case 'compositionend': + eventDetail = { + type: evt.type, + value: input.value, + data: evt.data + }; + break; + + case 'input': + eventDetail = { + type: evt.type, + value: input.value + }; + break; + + default: // keyboard events + eventDetail = { + type: evt.type, + charCode: evt.charCode, + keyCode: evt.keyCode, + key: evt.key, + code: evt.code, + location: evt.location, + repeat: evt.repeat, + value: input.value, + shift: evt.getModifierState('Shift'), + capsLock: evt.getModifierState('CapsLock'), + control: evt.getModifierState('Control'), + alt: evt.getModifierState('Alt') + }; + break; + } + + sendAsyncMessage('test:eventDetail', eventDetail); + } + + input.addEventListener('compositionstart', sendEventDetail); + input.addEventListener('compositionupdate', sendEventDetail); + input.addEventListener('compositionend', sendEventDetail); + input.addEventListener('input', sendEventDetail); + input.addEventListener('keydown', sendEventDetail); + input.addEventListener('keypress', sendEventDetail); + input.addEventListener('keyup', sendEventDetail); +}; + +function waitForInputContextChange() { + return new Promise((resolve) => { + navigator.mozInputMethod.oninputcontextchange = resolve; + }); +} + +function assertEventDetail(expectedDetails, testName) { + is(gEventDetails.length, expectedDetails.length, + testName + ' expects ' + expectedDetails.map(d => d.type).join(', ') + ' events, got ' + gEventDetails.map(d => d.type).join(', ')); + + expectedDetails.forEach((expectedDetail, j) => { + for (let key in expectedDetail) { + is(gEventDetails[j][key], expectedDetail[key], + testName + ' expects ' + key + ' of ' + gEventDetails[j].type + ' to be equal to ' + expectedDetail[key]); + } + }); +} + +function sendKeyAndAssertResult(testdata) { + var dict = testdata.dict; + var testName = gTestDescription + 'sendKey(' + JSON.stringify(dict) + ')'; + var promise = navigator.mozInputMethod.inputcontext.sendKey(dict); + + if (testdata.expectedReject) { + promise = promise + .then(() => { + ok(false, testName + ' should not resolve.'); + }, (e) => { + ok(true, testName + ' rejects.'); + ok(e instanceof testdata.expectedReject, 'Reject with type.'); + }) + + return promise; + } + + promise = promise + .then((res) => { + is(res, true, + testName + ' should resolve to true.'); + + var expectedEventDetail = []; + + var expectedValues = testdata.expectedValues; + + expectedEventDetail.push({ + type: 'keydown', + key: expectedValues.key, + charCode: 0, + code: expectedValues.code || '', + keyCode: expectedValues.keyCode || 0, + location: expectedValues.location ? expectedValues.location : 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + + if (testdata.expectedKeypress) { + expectedEventDetail.push({ + type: 'keypress', + key: expectedValues.key, + charCode: expectedValues.charCode, + code: expectedValues.code || '', + keyCode: expectedValues.charCode ? 0 : expectedValues.keyCode, + location: expectedValues.location ? expectedValues.location : 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + } + + if (testdata.expectedInput) { + switch (testdata.expectedInput) { + case 'Enter': + gCurrentValue += '\n'; + break; + case 'Backspace': + gCurrentValue = + gCurrentValue.substr(0, gCurrentValue.length - 1); + break; + default: + gCurrentValue += testdata.expectedInput; + break; + } + + expectedEventDetail.push({ + type: 'input', + value: gCurrentValue + }); + } + + if (!testdata.expectedRepeat) { + expectedEventDetail.push({ + type: 'keyup', + key: expectedValues.key, + charCode: 0, + code: expectedValues.code || '', + keyCode: expectedValues.keyCode || 0, + location: expectedValues.location ? expectedValues.location : 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + } + + assertEventDetail(expectedEventDetail, testName); + gEventDetails = []; + }, (e) => { + ok(false, testName + ' should not reject. ' + e); + }); + + return promise; +} + +function runSendKeyAlphabetTests() { + gTestDescription = 'runSendKeyAlphabetTests(): '; + var promiseQueue = Promise.resolve(); + + // Test the plain alphabets + var codeA = 'A'.charCodeAt(0); + for (var i = 0; i < 26; i++) { + // callbacks in then() are deferred; must only reference these block-scoped + // variable instead of i. + let keyCode = codeA + i; + let code = 'Key' + String.fromCharCode(keyCode); + + [String.fromCharCode(keyCode), + String.fromCharCode(keyCode).toLowerCase()] + .forEach((chr) => { + // Test plain alphabet + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with keyCode set + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: keyCode + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with keyCode set to keyCode + 1, + // expects keyCode to follow key value and ignore the incorrect value. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: keyCode + 1 + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with code set + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: code + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: code, + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with code set to Digit1, + // expects keyCode to follow key value. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'Digit1' + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'Digit1', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with keyCode set to DOM_VK_1, + // expects keyCode to follow key value. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: KeyboardEvent.DOM_VK_1 + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with code set to Digit1 + // and keyCode set to DOM_VK_1, + // expects keyCode to follow key value. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'Digit1', + keyCode: KeyboardEvent.DOM_VK_1 + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'Digit1', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + }); + } + + return promiseQueue; +} + +function runSendKeyNumberTests() { + gTestDescription = 'runSendKeyNumberTests(): '; + var promiseQueue = Promise.resolve(); + + // Test numbers + var code0 = '0'.charCodeAt(0); + for (var i = 0; i < 10; i++) { + // callbacks in then() are deferred; must only reference these block-scoped + // variable instead of i. + let keyCode = code0 + i; + let chr = String.fromCharCode(keyCode); + + // Test plain number + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain number with keyCode set + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: keyCode + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain number with keyCode set to keyCode + 1, + // expects keyCode to follow key value and ignore the incorrect value. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: keyCode + 1 + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain number with code set + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'Digit' + chr + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'Digit' + chr, + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain upper caps alphabet with code set to KeyA, + // expects keyCode to follow key value. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'KeyA' + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'KeyA', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain upper caps alphabet with code set to KeyA, + // and keyCode set to DOM_VK_A. + // expects keyCode to follow key value. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'KeyA', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + } + + return promiseQueue; +} + +function runSendKeyDvorakTests() { + gTestDescription = 'runSendKeyDvorakTests(): '; + var promiseQueue = Promise.resolve(); + + // Test Dvorak layout emulation + var qwertyCodeForDvorakKeys = [ + 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP', + 'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', + 'KeyH', 'KeyJ', 'KeyK', 'KeyL', 'Semicolon', + 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', + 'KeyM', 'Comma', 'Period', 'Slash']; + var dvorakKeys = 'PYFGCRL' + + 'AOEUIDHTNS' + + 'QJKXBMWVZ'; + for (var i = 0; i < dvorakKeys.length; i++) { + // callbacks in then() are deferred; must only reference these block-scoped + // variable instead of i. + let keyCode = dvorakKeys.charCodeAt(i); + let code = qwertyCodeForDvorakKeys[i]; + + [dvorakKeys.charAt(i), dvorakKeys.charAt(i).toLowerCase()] + .forEach((chr) => { + // Test alphabet with code set to Qwerty code, + // expects keyCode to follow key value. + // (This is *NOT* the expected scenario for emulating a Dvorak keyboard, + // even though expected results are the same.) + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: code + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: code, + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test alphabet with code set to Qwerty code and keyCode set, + // expects keyCode to follow key/keyCode value. + // (This is the expected scenario for emulating a Dvorak keyboard) + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: keyCode, + code: code + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: code, + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + }); + } + + var qwertyCodeForDvorakSymbols = [ + 'Minus', 'Equal', + 'KeyQ', 'KeyW', 'KeyE', 'BracketLeft', 'BracketRight', 'Backslash', + 'Quote', 'KeyZ']; + + var shiftDvorakSymbols = '{}\"<>?+|_:'; + var dvorakSymbols = '[]\',./=\\-;'; + var dvorakSymbolsKeyCodes = [ + KeyboardEvent.DOM_VK_OPEN_BRACKET, + KeyboardEvent.DOM_VK_CLOSE_BRACKET, + KeyboardEvent.DOM_VK_QUOTE, + KeyboardEvent.DOM_VK_COMMA, + KeyboardEvent.DOM_VK_PERIOD, + KeyboardEvent.DOM_VK_SLASH, + KeyboardEvent.DOM_VK_EQUALS, + KeyboardEvent.DOM_VK_BACK_SLASH, + KeyboardEvent.DOM_VK_HYPHEN_MINUS, + KeyboardEvent.DOM_VK_SEMICOLON + ]; + + for (var i = 0; i < dvorakSymbols.length; i++) { + // callbacks in then() are deferred; must only reference these block-scoped + // variable instead of i. + let keyCode = dvorakSymbolsKeyCodes[i]; + let code = qwertyCodeForDvorakSymbols[i]; + + [dvorakSymbols.charAt(i), shiftDvorakSymbols.charAt(i)] + .forEach((chr) => { + // Test symbols with code set to Qwerty code, + // expects keyCode to be 0. + // (This is *NOT* the expected scenario for emulating a Dvorak keyboard) + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: code + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: code, + keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test alphabet with code set to Qwerty code and keyCode set, + // expects keyCode to follow keyCode value. + // (This is the expected scenario for emulating a Dvorak keyboard) + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: keyCode, + code: code + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: code, + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + }); + } + + return promiseQueue; +} + +function runSendKeyDigitKeySymbolsTests() { + gTestDescription = 'runSendKeyDigitKeySymbolsTests(): '; + var promiseQueue = Promise.resolve(); + + var digitKeySymbols = ')!@#$%^&*('; + for (var i = 0; i < digitKeySymbols.length; i++) { + // callbacks in then() are deferred; must only reference these block-scoped + // variable instead of i. + let keyCode = KeyboardEvent['DOM_VK_' + i]; + let chr = digitKeySymbols.charAt(i); + let code = 'Digit' + i; + + // Test plain symbol + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with keyCode set + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: keyCode + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with code set + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: code + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: code, + keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with code set to KeyA, + // expects keyCode to be 0. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'KeyA' + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'KeyA', + keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with keyCode set to DOM_VK_A, + // expects keyCode to follow the keyCode set. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: KeyboardEvent.DOM_VK_A + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: KeyboardEvent.DOM_VK_A, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with code set to KeyA + // expects keyCode to follow the keyCode set. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A, + charCode: chr.charCodeAt(0) + } + }); + }); + } + + return promiseQueue; +} + +function runSendKeyUSKeyboardSymbolsTests() { + gTestDescription = 'runSendKeyUSKeyboardSymbolsTests(): '; + var promiseQueue = Promise.resolve(); + + // Test printable symbols on US Keyboard + var symbols = ' ;:=+,<-_.>/?`~[{\\|]}\'\"'; + var symbolKeyCodes = [ + KeyboardEvent.DOM_VK_SPACE, + KeyboardEvent.DOM_VK_SEMICOLON, + KeyboardEvent.DOM_VK_SEMICOLON, + KeyboardEvent.DOM_VK_EQUALS, + KeyboardEvent.DOM_VK_EQUALS, + KeyboardEvent.DOM_VK_COMMA, + KeyboardEvent.DOM_VK_COMMA, + KeyboardEvent.DOM_VK_HYPHEN_MINUS, + KeyboardEvent.DOM_VK_HYPHEN_MINUS, + KeyboardEvent.DOM_VK_PERIOD, + KeyboardEvent.DOM_VK_PERIOD, + KeyboardEvent.DOM_VK_SLASH, + KeyboardEvent.DOM_VK_SLASH, + KeyboardEvent.DOM_VK_BACK_QUOTE, + KeyboardEvent.DOM_VK_BACK_QUOTE, + KeyboardEvent.DOM_VK_OPEN_BRACKET, + KeyboardEvent.DOM_VK_OPEN_BRACKET, + KeyboardEvent.DOM_VK_BACK_SLASH, + KeyboardEvent.DOM_VK_BACK_SLASH, + KeyboardEvent.DOM_VK_CLOSE_BRACKET, + KeyboardEvent.DOM_VK_CLOSE_BRACKET, + KeyboardEvent.DOM_VK_QUOTE, + KeyboardEvent.DOM_VK_QUOTE + ]; + var symbolCodes = [ + 'Space', + 'Semicolon', + 'Semicolon', + 'Equal', + 'Equal', + 'Comma', + 'Comma', + 'Minus', + 'Minus', + 'Period', + 'Period', + 'Slash', + 'Slash', + 'Backquote', + 'Backquote', + 'BracketLeft', + 'BracketLeft', + 'Backslash', + 'Backslash', + 'BracketRight', + 'BracketRight', + 'Quote', + 'Quote' + ]; + for (var i = 0; i < symbols.length; i++) { + // callbacks in then() are deferred; must only reference these block-scoped + // variable instead of i. + let keyCode = symbolKeyCodes[i]; + let chr = symbols.charAt(i); + let code = symbolCodes[i]; + + // Test plain symbol + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with keyCode set + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: keyCode + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with code set + // expects keyCode to be 0. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: code + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: code, + keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with code set to KeyA, + // expects keyCode to be 0. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'KeyA' + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'KeyA', + keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with keyCode set to DOM_VK_A, + // expects keyCode to follow the keyCode set. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: KeyboardEvent.DOM_VK_A + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: KeyboardEvent.DOM_VK_A, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain symbol with code set to KeyA + // expects keyCode to follow the keyCode set. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A, + charCode: chr.charCodeAt(0) + } + }); + }); + } + + return promiseQueue; +} + +function runSendKeyGreekLettersTests() { + gTestDescription = 'runSendKeyGreekLettersTests(): '; + var promiseQueue = Promise.resolve(); + + // Test Greek letters + var greekLetters = + '\u0391\u0392\u0393\u0394\u0395\u0396\u0397\u0398\u0399\u039a\u039b\u039c' + + '\u039d\u039e\u039f\u03a0\u03a1\u03a3\u03a4\u03a5\u03a6\u03a7\u03a8\u03a9' + + '\u03b1\u03b2\u03b3\u03b4\u03b5\u03b6\u03b7\u03b8\u03b9\u03ba\u03bb\u03bc' + + '\u03bd\u03be\u03bf\u03c0\u03c1\u03c3\u03c4\u03c5\u03c6\u03c7\u03c8\u03c9' + + '\u03c2'; + var greekLettersLayoutMap = + 'ABGDEZHUIKLMNJOPRSTYFXCVABGDEZHUIKLMNJOPRSTYFXCVQ'; + for (var i = 0; i < greekLetters.length; i++) { + // callbacks in then() are deferred; must only reference these block-scoped + // variable instead of i. + let keyCode = greekLettersLayoutMap.charCodeAt(i); + let chr = greekLetters.charAt(i); + let code = 'Key' + greekLettersLayoutMap.charAt(i); + + // Test plain alphabet + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with keyCode set + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + keyCode: keyCode + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: '', + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with code set, + // expects keyCode to be 0. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: code + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: code, + keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with code set to Digit1, + // expects keyCode to be 0. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'Digit1' + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'Digit1', + keyCode: 0, + charCode: chr.charCodeAt(0) + } + }); + }); + + // Test plain alphabet with code set to Digit1, + // and keyCode set to DOM_VK_A. + // expects keyCode to follow the keyCode set. + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: chr, + code: 'Digit1', + keyCode: KeyboardEvent.DOM_VK_A + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: 'Digit1', + keyCode: KeyboardEvent.DOM_VK_A, + charCode: chr.charCodeAt(0) + } + }); + }); + } + + return promiseQueue; +} + +function runSendKeyEnterTests() { + gTestDescription = 'runSendKeyEnterTests(): '; + var promiseQueue = Promise.resolve(); + + // Test Enter with code unset + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: 'Enter' + }, + expectedKeypress: true, + expectedInput: '\n', + expectedValues: { + key: 'Enter', code: '', + keyCode: KeyboardEvent.DOM_VK_RETURN, + charCode: 0 + } + }); + }); + + // Test Enter with code set + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: 'Enter', + code: 'Enter' + }, + expectedKeypress: true, + expectedInput: '\n', + expectedValues: { + key: 'Enter', code: 'Enter', + keyCode: KeyboardEvent.DOM_VK_RETURN, + charCode: 0 + } + }); + }); + + // Test Enter with keyCode explict set to zero + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: { + key: 'Enter', + keyCode: 0 + }, + expectedKeypress: true, + expectedValues: { + key: 'Enter', code: '', + keyCode: 0, + charCode: 0 + } + }); + }); + + return promiseQueue; +} + +function runSendKeyNumpadTests() { + gTestDescription = 'runSendKeyNumpadTests(): '; + var promiseQueue = Promise.resolve(); + + var tests = []; + ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + .forEach(function(key) { + let charCode = key.charCodeAt(0); + + tests.push({ + dict: { + key: key, + code: 'Numpad' + key + }, + expectedKeypress: true, + expectedInput: key, + expectedValues: { + key: key, code: 'Numpad' + key, + keyCode: charCode, charCode: charCode, + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD + } + }); + }); + + [['+', 'NumpadAdd'], + [',', 'NumpadComma'], + ['.', 'NumpadDecimal'], + ['.', 'NumpadComma'], // Locale-specific NumpadComma + [',', 'NumpadDecimal'], // Locale-specific NumpadDecimal + ['/', 'NumpadDivide'], + ['=', 'NumpadEqual'], + // ['#', 'NumpadHash'], // Not supported yet. + ['*', 'NumpadMultiply'], + ['(', 'NumpadParenLeft'], + [')', 'NumpadParenRight'], + // ['*', 'NumpadStar'], // Not supported yet. + ['-', 'NumpadSubtract']].forEach(function([key, code]) { + tests.push({ + dict: { + key: key, + code: code + }, + expectedKeypress: true, + expectedInput: key, + expectedValues: { + key: key, code: code, keyCode: 0, charCode: key.charCodeAt(0), + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD + } + }); + }); + + [ + 'NumpadComma', // Locale-specific NumpadComma -- outputs nothing + 'NumpadClear', + 'NumpadClearEntry', + 'NumpadMemoryAdd', + 'NumpadMemoryClear', + 'NumpadMemoryRecall', + 'NumpadMemoryStore', + 'NumpadMemorySubtract' + ].forEach(function(code) { + tests.push({ + dict: { + key: 'Unidentified', + code: code + }, + expectedKeypress: true, + expectedValues: { + key: 'Unidentified', code: code, keyCode: 0, charCode: 0, + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD + } + }); + }); + + tests.push({ + dict: { + key: 'Enter', + code: 'NumpadEnter' + }, + expectedKeypress: true, + expectedInput: '\n', + expectedValues: { + key: 'Enter', code: 'NumpadEnter', + keyCode: KeyboardEvent.DOM_VK_RETURN, charCode: 0, + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD + } + }); + + tests.push({ + dict: { + key: 'Backspace', + code: 'NumpadBackspace' + }, + expectedKeypress: true, + expectedInput: 'Backspace', // Special value + expectedValues: { + key: 'Backspace', code: 'NumpadBackspace', + keyCode: KeyboardEvent.DOM_VK_BACK_SPACE, charCode: 0, + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD + } + }); + + tests.forEach((test) => { + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult(test); + }); + }); + + return promiseQueue; +} + +function runSendKeyRejectionTests() { + gTestDescription = 'runSendKeyRejectionTests(): '; + var promiseQueue = Promise.resolve(); + + promiseQueue = promiseQueue.then(() => { + return sendKeyAndAssertResult({ + dict: undefined, + expectedReject: TypeError + }); + }); + + return promiseQueue; +} + +function setCompositionAndAssertResult(testdata) { + var dict = testdata.dict; + var testName; + var promise; + + if (dict) { + testName = gTestDescription + + 'setComposition(' + testdata.text + + ', undefined, undefined, ' + + JSON.stringify(dict) + ')'; + promise = navigator.mozInputMethod.inputcontext + .setComposition(testdata.text, undefined, undefined, dict); + } else { + testName = gTestDescription + + 'setComposition(' + testdata.text + ')'; + promise = navigator.mozInputMethod.inputcontext + .setComposition(testdata.text); + } + + if (testdata.expectedReject) { + promise = promise + .then(() => { + ok(false, testName + ' should not resolve.'); + }, (e) => { + ok(true, testName + ' rejects.'); + ok(e instanceof testdata.expectedReject, 'Reject with type.'); + }) + + return promise; + } + + promise = promise + .then((res) => { + is(res, true, + testName + ' should resolve to true.'); + + var expectedEventDetail = []; + + var expectedValues = testdata.expectedValues; + + if (testdata.expectsKeyEvents && + (testdata.startsComposition || + testdata.dispatchKeyboardEventDuringComposition)) { + expectedEventDetail.push({ + type: 'keydown', + key: expectedValues.key, + charCode: 0, + code: expectedValues.code || '', + keyCode: expectedValues.keyCode || 0, + location: 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + } + + if (testdata.startsComposition) { + expectedEventDetail.push({ + type: 'compositionstart', + data: '', + value: gCurrentValue + }); + } + + expectedEventDetail.push({ + type: 'compositionupdate', + data: testdata.text, + value: gCurrentValue + }); + + expectedEventDetail.push({ + type: 'input', + value: gCurrentValue += testdata.expectedInput + }); + + if (testdata.expectsKeyEvents && + testdata.dispatchKeyboardEventDuringComposition) { + expectedEventDetail.push({ + type: 'keyup', + key: expectedValues.key, + charCode: 0, + code: expectedValues.code || '', + keyCode: expectedValues.keyCode || 0, + location: 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + } + + assertEventDetail(expectedEventDetail, testName); + gEventDetails = []; + }, (e) => { + ok(false, testName + ' should not reject. ' + e); + }); + + return promise; +} + +function endCompositionAndAssertResult(testdata) { + var dict = testdata.dict; + var testName; + var promise; + if (dict) { + testName = gTestDescription + + 'endComposition(' + testdata.text + ', ' + JSON.stringify(dict) + ')'; + promise = navigator.mozInputMethod.inputcontext + .endComposition(testdata.text, dict); + } else { + testName = gTestDescription + + 'endComposition(' + testdata.text + ')'; + promise = navigator.mozInputMethod.inputcontext + .endComposition(testdata.text); + } + + if (testdata.expectedReject) { + promise = promise + .then(() => { + ok(false, testName + ' should not resolve.'); + }, (e) => { + ok(true, testName + ' rejects.'); + ok(e instanceof testdata.expectedReject, 'Reject with type.'); + }) + + return promise; + } + + promise = promise + .then((res) => { + is(res, true, + testName + ' should resolve to true.'); + + var expectedEventDetail = []; + + var expectedValues = testdata.expectedValues; + + if (testdata.expectsKeyEvents && + testdata.dispatchKeyboardEventDuringComposition) { + expectedEventDetail.push({ + type: 'keydown', + key: expectedValues.key, + charCode: 0, + code: expectedValues.code || '', + keyCode: expectedValues.keyCode || 0, + location: 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + } + + expectedEventDetail.push({ + type: 'compositionend', + data: testdata.text, + value: gCurrentValue + }); + + expectedEventDetail.push({ + type: 'input', + value: gCurrentValue + }); + + if (testdata.expectsKeyEvents) { + expectedEventDetail.push({ + type: 'keyup', + key: expectedValues.key, + charCode: 0, + code: expectedValues.code || '', + keyCode: expectedValues.keyCode || 0, + location: 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + } + + assertEventDetail(expectedEventDetail, testName); + gEventDetails = []; + }, (e) => { + ok(false, testName + ' should not reject. ' + e); + }); + + return promise; +} + +function runCompositionWithKeyEventTests() { + var promiseQueue = Promise.resolve(); + + [true, false].forEach((dispatchKeyboardEventDuringComposition) => { + gTestDescription = 'runCompositionWithKeyEventTests() (dispatchKeyboardEvent =' + dispatchKeyboardEventDuringComposition + '): '; + + promiseQueue = promiseQueue + .then(() => { + SpecialPowers.setBoolPref( + 'dom.keyboardevent.dispatch_during_composition', + dispatchKeyboardEventDuringComposition); + }) + .then(() => { + return setCompositionAndAssertResult({ + text: 'foo', + expectsKeyEvents: true, + startsComposition: true, + dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition, + expectedInput: 'foo', + dict: { + key: 'a', + code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A + }, + expectedValues: { + key: 'a', + code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A + } + }); + }) + .then(() => { + return setCompositionAndAssertResult({ + text: 'foobar', + expectsKeyEvents: true, + startsComposition: false, + dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition, + expectedInput: 'bar', + dict: { + key: 'a', + code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A + }, + expectedValues: { + key: 'a', + code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A + } + }); + }) + .then(() => { + return endCompositionAndAssertResult({ + text: 'foobar', + expectsKeyEvents: true, + dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition, + expectedInput: '', + dict: { + key: 'a', + code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A + }, + expectedValues: { + key: 'a', + code: 'KeyA', + keyCode: KeyboardEvent.DOM_VK_A + } + }); + }) + .then(() => { + SpecialPowers.clearUserPref( + 'dom.keyboardevent.dispatch_during_composition'); + }); + }); + + return promiseQueue; +} + +function runCompositionWithoutKeyEventTests() { + var promiseQueue = Promise.resolve(); + + gTestDescription = 'runCompositionWithoutKeyEventTests(): '; + + promiseQueue = promiseQueue + .then(() => { + return setCompositionAndAssertResult({ + text: 'foo', + expectsKeyEvents: false, + startsComposition: true, + expectedInput: 'foo' + }); + }) + .then(() => { + return setCompositionAndAssertResult({ + text: 'foobar', + expectsKeyEvents: false, + startsComposition: false, + expectedInput: 'bar' + }); + }) + .then(() => { + return endCompositionAndAssertResult({ + text: 'foobar', + expectsKeyEvents: false, + expectedInput: '' + }); + }); + + return promiseQueue; +} + +function keydownAndAssertResult(testdata) { + var dict = testdata.dict; + var testName = gTestDescription + 'keydown(' + JSON.stringify(dict) + ')'; + var promise = navigator.mozInputMethod.inputcontext.keydown(dict); + + if (testdata.expectedReject) { + promise = promise + .then(() => { + ok(false, testName + ' should not resolve.'); + }, (e) => { + ok(true, testName + ' rejects.'); + ok(e instanceof testdata.expectedReject, 'Reject with type.'); + }) + + return promise; + } + + promise = promise + .then((res) => { + is(res, true, + testName + ' should resolve to true.'); + + var expectedEventDetail = []; + + var expectedValues = testdata.expectedValues; + + expectedEventDetail.push({ + type: 'keydown', + key: expectedValues.key, + charCode: 0, + code: expectedValues.code || '', + keyCode: expectedValues.keyCode || 0, + location: 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + + if (testdata.expectedKeypress) { + expectedEventDetail.push({ + type: 'keypress', + key: expectedValues.key, + charCode: expectedValues.charCode, + code: expectedValues.code || '', + keyCode: expectedValues.charCode ? 0 : expectedValues.keyCode, + location: 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + } + + if (testdata.expectedInput) { + expectedEventDetail.push({ + type: 'input', + value: gCurrentValue += testdata.expectedInput + }); + } + + assertEventDetail(expectedEventDetail, testName); + gEventDetails = []; + }, (e) => { + ok(false, testName + ' should not reject. ' + e); + }); + + return promise; +} + +function keyupAndAssertResult(testdata) { + var dict = testdata.dict; + var testName = gTestDescription + 'keyup(' + JSON.stringify(dict) + ')'; + var promise = navigator.mozInputMethod.inputcontext.keyup(dict); + + if (testdata.expectedReject) { + promise = promise + .then(() => { + ok(false, testName + ' should not resolve.'); + }, (e) => { + ok(true, testName + ' rejects.'); + ok(e instanceof testdata.expectedReject, 'Reject with type.'); + }) + + return promise; + } + + promise = promise + .then((res) => { + is(res, true, + testName + ' should resolve to true.'); + + var expectedEventDetail = []; + + var expectedValues = testdata.expectedValues; + + expectedEventDetail.push({ + type: 'keyup', + key: expectedValues.key, + charCode: 0, + code: expectedValues.code || '', + keyCode: expectedValues.keyCode || 0, + location: 0, + repeat: expectedValues.repeat || false, + value: gCurrentValue, + shift: false, + capsLock: false, + control: false, + alt: false + }); + + assertEventDetail(expectedEventDetail, testName); + gEventDetails = []; + }, (e) => { + ok(false, testName + ' should not reject. ' + e); + }); + + return promise; +} + +function runKeyDownUpTests() { + gTestDescription = 'runKeyDownUpTests(): '; + var promiseQueue = Promise.resolve(); + + let chr = 'a'; + let code = 'KeyA'; + let keyCode = KeyboardEvent.DOM_VK_A; + + promiseQueue = promiseQueue + .then(() => { + return keydownAndAssertResult({ + dict: { + key: chr, + code: code, + keyCode: keyCode + }, + expectedKeypress: true, + expectedInput: chr, + expectedValues: { + key: chr, code: code, + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }) + .then(() => { + return keyupAndAssertResult({ + dict: { + key: chr, + code: code, + keyCode: keyCode + }, + expectedValues: { + key: chr, code: code, + keyCode: keyCode, + charCode: chr.charCodeAt(0) + } + }); + }); + + return promiseQueue; +} + +function runKeyDownUpRejectionTests() { + gTestDescription = 'runKeyDownUpRejectionTests(): '; + var promiseQueue = Promise.resolve(); + + promiseQueue = promiseQueue.then(() => { + return keydownAndAssertResult({ + dict: undefined, + expectedReject: TypeError + }); + }); + + promiseQueue = promiseQueue.then(() => { + return keyupAndAssertResult({ + dict: undefined, + expectedReject: TypeError + }); + }); + + return promiseQueue; +} + +function runRepeatTests() { + gTestDescription = 'runRepeatTests(): '; + var promiseQueue = Promise.resolve(); + + // Test repeat + promiseQueue = promiseQueue + .then(() => { + return sendKeyAndAssertResult({ + dict: { + key: 'A', + repeat: true + }, + expectedKeypress: true, + expectedRepeat: true, + expectedInput: 'A', + expectedValues: { + repeat: true, + key: 'A', code: '', + keyCode: KeyboardEvent.DOM_VK_A, + charCode: 'A'.charCodeAt(0) + } + }); + }) + .then(() => { + return keyupAndAssertResult({ + dict: { + key: 'A' + }, + expectedKeypress: true, + expectedRepeat: true, + expectedInput: 'A', + expectedValues: { + key: 'A', code: '', + keyCode: KeyboardEvent.DOM_VK_A, + charCode: 'A'.charCodeAt(0) + } + }); + }); + + return promiseQueue; +} + +function runTest() { + let im = navigator.mozInputMethod; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_bug1137557.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); + + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + + iframe.addEventListener('mozbrowserloadend', function() { + mm.addMessageListener('test:eventDetail', function(msg) { + gEventDetails.push(msg.data); + }); + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + }); + + waitForInputContextChange() + .then(() => { + var inputcontext = navigator.mozInputMethod.inputcontext; + + ok(!!inputcontext, 'Receving the first input context'); + }) + .then(() => runSendKeyAlphabetTests()) + .then(() => runSendKeyNumberTests()) + .then(() => runSendKeyDvorakTests()) + .then(() => runSendKeyDigitKeySymbolsTests()) + .then(() => runSendKeyUSKeyboardSymbolsTests()) + .then(() => runSendKeyGreekLettersTests()) + .then(() => runSendKeyEnterTests()) + .then(() => runSendKeyNumpadTests()) + .then(() => runSendKeyRejectionTests()) + .then(() => runCompositionWithKeyEventTests()) + .then(() => runCompositionWithoutKeyEventTests()) + .then(() => runKeyDownUpTests()) + .then(() => runKeyDownUpRejectionTests()) + .then(() => runRepeatTests()) + .catch((err) => { + console.error(err); + is(false, err.message); + }) + .then(() => { + var p = waitForInputContextChange(); + + // Revoke our right from using the IM API. + SpecialPowers.wrap(im).setActive(false); + + return p; + }) + .then(() => { + var inputcontext = navigator.mozInputMethod.inputcontext; + + is(inputcontext, null, 'Receving null input context'); + + inputmethod_cleanup(); + }) + .catch((err) => { + console.error(err); + is(false, err.message); + }); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug1175399.html b/dom/inputmethod/mochitest/test_bug1175399.html new file mode 100644 index 000000000..64fc85e88 --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug1175399.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1175399 +--> +<head> + <title>Test focus when page unloads</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175399">Mozilla Bug 1175399</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +let appFrameScript = function appFrameScript() { + let input = content.document.body.firstElementChild; + input.focus(); + + content.setTimeout(function() { + sendAsyncMessage('test:step'); + }); +}; + +function runTest() { + let im = navigator.mozInputMethod; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_bug1175399.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); + + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + im.oninputcontextchange = function() { + is(false, 'should not receive inputcontextchange event'); + }; + + iframe.addEventListener('mozbrowserloadend', function() { + mm.addMessageListener('test:step', function() { + let inputcontext = navigator.mozInputMethod.inputcontext; + is(inputcontext, null, 'inputcontext is null'); + + inputmethod_cleanup(); + }); + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + }); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug944397.html b/dom/inputmethod/mochitest/test_bug944397.html new file mode 100644 index 000000000..4be95f395 --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug944397.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=944397 +--> +<head> + <title>Basic test for InputMethod API.</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=944397">Mozilla Bug 944397</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +SimpleTest.requestFlakyTimeout("untriaged"); + +inputmethod_setup(function() { + runTest(); +}); + +// The frame script running in file_test_app.html. +function appFrameScript() { + let input = content.document.getElementById('test-input'); + input.oninput = function() { + sendAsyncMessage('test:InputMethod:oninput', { + value: input.value + }); + }; +} + +function runTest() { + let app, keyboard; + + /** + * So this test does the following: + * 1. Create a mozbrowser iframe with a text field in it, and focus the text field + * 2. 100ms. after loading we create new keyboard iframe, that will try to execute + * replaceSurroundingText on the current active inputcontext + * 3. That should trigger 'input' event on the said text field + * 4. And if that happens we know everything is OK + */ + + let path = location.pathname; + let basePath = location.protocol + '//' + location.host + + path.substring(0, path.lastIndexOf('/')); + + // STEP 1: Create an app frame to recieve keyboard inputs. + function step1() { + app = document.createElement('iframe'); + app.src = basePath + '/file_test_app.html'; + app.setAttribute('mozbrowser', true); + document.body.appendChild(app); + app.addEventListener('mozbrowserloadend', function() { + let mm = SpecialPowers.getBrowserFrameMessageManager(app); + mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false); + mm.addMessageListener("test:InputMethod:oninput", function(ev) { + step4(SpecialPowers.wrap(ev).json.value); + }); + + step2(); + }); + } + + function step2() { + // STEP 2a: Create a browser frame to load the input method app. + keyboard = document.createElement('iframe'); + keyboard.setAttribute('mozbrowser', true); + document.body.appendChild(keyboard); + + // STEP 2b: Grant input privileges to the keyboard iframe + let imeUrl = basePath + '/file_inputmethod.html#data'; + + // STEP 2c: Tell Gecko to use this iframe as its keyboard app + let req = keyboard.setInputMethodActive(true); + + req.onsuccess = function() { + ok(true, 'setInputMethodActive succeeded.'); + }; + + req.onerror = function() { + ok(false, 'setInputMethodActive failed: ' + this.error.name); + inputmethod_cleanup(); + }; + + // STEP 3: Loads the input method app to the browser frame after a delay. + setTimeout(function() { + keyboard.src = imeUrl; + }, 100); + } + + function step4(val) { + ok(true, 'Keyboard input was received.'); + is(val, '#dataYuan', 'Input value'); + inputmethod_cleanup(); + } + + step1(); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug949059.html b/dom/inputmethod/mochitest/test_bug949059.html new file mode 100644 index 000000000..495f3d63c --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug949059.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=949059 +--> +<head> + <title>Test "mgmt" property of MozInputMethod.</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=949059">Mozilla Bug 949059</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +function runTest() { + let im = navigator.mozInputMethod; + + // Treat current page as an input method and activate it. + SpecialPowers.wrap(im).setActive(true); + ok(im.mgmt, 'The mgmt property should not be null.'); + + // Deactivate current page. + SpecialPowers.wrap(im).setActive(false); + ok(im.mgmt, 'The mgmt property should not be null.'); + + inputmethod_cleanup(); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug953044.html b/dom/inputmethod/mochitest/test_bug953044.html new file mode 100644 index 000000000..2b1f022a6 --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug953044.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=953044 +--> +<head> + <title>Basic test for InputMethod API.</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=953044">Mozilla Bug 953044</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +function runTest() { + // Create an app frame to recieve keyboard inputs. + let app = document.createElement('iframe'); + app.src = 'file_test_app.html'; + app.setAttribute('mozbrowser', true); + document.body.appendChild(app); + + // Create a browser frame to load the input method app. + let keyboard = document.createElement('iframe'); + keyboard.setAttribute('mozbrowser', true); + document.body.appendChild(keyboard); + + // Bug 953044 setInputMethodActive(false) before input method app loads should + // always succeed. + let req = keyboard.setInputMethodActive(false); + req.onsuccess = function() { + ok(true, 'setInputMethodActive before loading succeeded.'); + inputmethod_cleanup(); + }; + + req.onerror = function() { + ok(false, 'setInputMethodActive before loading failed: ' + this.error.name); + inputmethod_cleanup(); + }; +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug960946.html b/dom/inputmethod/mochitest/test_bug960946.html new file mode 100644 index 000000000..f65ff42e7 --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug960946.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=960946 +--> +<head> + <title>Basic test for repeat sendKey events</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=960946">Mozilla Bug 960946</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +// The input context. +var gContext = null; +var gCounter = 0; +var gBackSpaceCounter = 0; +var result = ["keydown", "keypress", "keydown","keypress", + "keydown", "keypress", "keyup" + ]; + +inputmethod_setup(function() { + runTest(); +}); + +var input; +// The frame script running in file_test_backspace_event.html. +function appFrameScript() { + let input = content.document.getElementById('test-input'); + input.onkeydown = input.onkeypress = input.onkeyup = function(event) { + dump('key event was fired in file_test_backspace_event.html: ' + event.type +'\n'); + sendAsyncMessage('test:KeyBoard:keyEvent', {'type':event.type}); + }; +} + +function runTest() { + let im = navigator.mozInputMethod; + + im.oninputcontextchange = function() { + ok(true, 'inputcontextchange event was fired.'); + im.oninputcontextchange = null; + + gContext = im.inputcontext; + if (!gContext) { + ok(false, 'Should have a non-null inputcontext.'); + inputmethod_cleanup(); + return; + } + + test_sendKey(); + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + // Create an app frame to recieve keyboard inputs. + let app = document.createElement('iframe'); + app.src = 'file_test_app.html'; + app.setAttribute('mozbrowser', true); + document.body.appendChild(app); + app.addEventListener('mozbrowserloadend', function() { + let mm = SpecialPowers.getBrowserFrameMessageManager(app); + mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false); + mm.addMessageListener("test:KeyBoard:keyEvent", function(event) { + ok(true, 'Keyboard input was received.'); + is(SpecialPowers.wrap(event).json.type, result[gCounter], "expected event"); + gCounter++; + }); + }); +} + +function test_sendKey() { + // Move cursor position to 4. + gContext.setSelectionRange(4, 0).then(function() { + is(gContext.selectionStart, 4, 'selectionStart was set successfully.'); + is(gContext.selectionEnd, 4, 'selectionEnd was set successfully.'); + for(let i = 0; i < 2; i++) { + test_sendBackspace(true); + } + test_sendBackspace(false); + }, function(e) { + ok(false, 'setSelectionRange failed:' + e.name); + inputmethod_cleanup(); + }); +} + +function test_sendBackspace(repeat) { + // Send backspace + gContext.sendKey(KeyEvent.DOM_VK_BACK_SPACE, 0, 0, repeat).then(function() { + ok(true, 'sendKey success'); + gBackSpaceCounter++; + if (gBackSpaceCounter == 3) { + inputmethod_cleanup(); + } + }, function(e) { + ok(false, 'sendKey failed:' + e.name); + inputmethod_cleanup(); + }); +} +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_bug978918.html b/dom/inputmethod/mochitest/test_bug978918.html new file mode 100644 index 000000000..56e02a57d --- /dev/null +++ b/dom/inputmethod/mochitest/test_bug978918.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=978918 +--> +<head> + <title>Basic test for InputMethod API.</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=978918">Mozilla Bug 978918</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +// The input context. +var gContext = null; + +inputmethod_setup(function() { + runTest(); +}); + +function runTest() { + let im = navigator.mozInputMethod; + + im.oninputcontextchange = function() { + ok(true, 'inputcontextchange event was fired.'); + im.oninputcontextchange = null; + + gContext = im.inputcontext; + if (!gContext) { + ok(false, 'Should have a non-null inputcontext.'); + inputmethod_cleanup(); + return; + } + + test_setSelectionRange(); + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_sms_app.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); +} + +function test_setSelectionRange() { + gContext.setSelectionRange(0, 100).then(function() { + is(gContext.selectionStart, 0, 'selectionStart was set successfully.'); + is(gContext.selectionEnd, 5, 'selectionEnd was set successfully.'); + test_replaceSurroundingText(); + }, function(e) { + ok(false, 'setSelectionRange failed:' + e.name); + inputmethod_cleanup(); + }); +} + +function test_replaceSurroundingText() { + // Replace 'Httvb' with 'Hito'. + gContext.replaceSurroundingText('Hito', 0, 100).then(function() { + ok(true, 'replaceSurroundingText finished'); + inputmethod_cleanup(); + }, function(e) { + ok(false, 'replaceSurroundingText failed: ' + e.name); + inputmethod_cleanup(); + }); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_focus_blur_manage_events.html b/dom/inputmethod/mochitest/test_focus_blur_manage_events.html new file mode 100644 index 000000000..939639050 --- /dev/null +++ b/dom/inputmethod/mochitest/test_focus_blur_manage_events.html @@ -0,0 +1,199 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1201407 +--> +<head> + <title>Test inputcontextfocus and inputcontextblur event</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1201407">Mozilla Bug 1201407</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +let contentFrameMM; + +function setupTestRunner() { + info('setupTestRunner'); + let im = navigator.mozInputMethod; + + let expectedEventDetails = [ + { type: 'input', inputType: 'text' }, + { type: 'input', inputType: 'search' }, + { type: 'textarea', inputType: 'textarea' }, + { type: 'contenteditable', inputType: 'textarea' }, + { type: 'input', inputType: 'number' }, + { type: 'input', inputType: 'tel' }, + { type: 'input', inputType: 'url' }, + { type: 'input', inputType: 'email' }, + { type: 'input', inputType: 'password' }, + { type: 'input', inputType: 'datetime' }, + { type: 'input', inputType: 'date', + value: '2015-08-03', min: '1990-01-01', max: '2020-01-01' }, + { type: 'input', inputType: 'month' }, + { type: 'input', inputType: 'week' }, + { type: 'input', inputType: 'time' }, + { type: 'input', inputType: 'datetime-local' }, + { type: 'input', inputType: 'color' }, + { type: 'select', inputType: 'select-one', + choices: { + multiple: false, + choices: [ + { group: false, inGroup: false, text: 'foo', + disabled: false, selected: true, optionIndex: 0 }, + { group: false, inGroup: false, text: 'bar', + disabled: true, selected: false, optionIndex: 1 }, + { group: true, text: 'group', disabled: false }, + { group: false, inGroup: true, text: 'baz', + disabled: false, selected: false, optionIndex: 2 } ] } + }, + { type: 'select', inputType: 'select-multiple', + choices: { + multiple: true, + choices: [ + { group: false, inGroup: false, text: 'foo', + disabled: false, selected: true, optionIndex: 0 }, + { group: false, inGroup: false, text: 'bar', + disabled: true, selected: false, optionIndex: 1 }, + { group: true, text: 'group', disabled: false }, + { group: false, inGroup: true, text: 'baz', + disabled: false, selected: false, optionIndex: 2 } ] } + } + ]; + + let expectBlur = false; + + function deepAssertObject(obj, expectedObj, desc) { + for (let prop in expectedObj) { + if (typeof expectedObj[prop] === 'object') { + deepAssertObject(obj[prop], expectedObj[prop], desc + '.' + prop); + } else { + is(obj[prop], expectedObj[prop], desc + '.' + prop); + } + } + } + + im.mgmt.oninputcontextfocus = + im.mgmt.oninputcontextblur = function(evt) { + if (expectBlur) { + is(evt.type, 'inputcontextblur', 'evt.type'); + evt.preventDefault(); + expectBlur = false; + + return; + } + + let expectedEventDetail = expectedEventDetails.shift(); + + if (!expectedEventDetail) { + ok(false, 'Receving extra events'); + inputmethod_cleanup(); + + return; + } + + is(evt.type, 'inputcontextfocus', 'evt.type'); + evt.preventDefault(); + expectBlur = true; + + let detail = evt.detail; + deepAssertObject(detail, expectedEventDetail, 'detail'); + + if (expectedEventDetails.length) { + contentFrameMM.sendAsyncMessage('test:next'); + } else { + im.mgmt.oninputcontextfocus = im.mgmt.oninputcontextblur = null; + inputmethod_cleanup(); + } + }; +} + +function setupInputAppFrame() { + info('setupInputAppFrame'); + return new Promise((resolve, reject) => { + let appFrameScript = function appFrameScript() { + let im = content.navigator.mozInputMethod; + + im.mgmt.oninputcontextfocus = + im.mgmt.oninputcontextblur = function(evt) { + sendAsyncMessage('text:appEvent', { type: evt.type }); + }; + + content.document.body.textContent = 'I am a input app'; + }; + + let path = location.pathname; + let basePath = location.protocol + '//' + location.host + + path.substring(0, path.lastIndexOf('/')); + let imeUrl = basePath + '/file_blank.html'; + + let inputAppFrame = document.createElement('iframe'); + inputAppFrame.setAttribute('mozbrowser', true); + inputAppFrame.src = imeUrl; + document.body.appendChild(inputAppFrame); + + let mm = SpecialPowers.getBrowserFrameMessageManager(inputAppFrame); + inputAppFrame.addEventListener('mozbrowserloadend', function() { + mm.addMessageListener('text:appEvent', function(msg) { + ok(false, 'Input app should not receive ' + msg.data.type + ' event.'); + }); + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + + // Set the input app frame to be active + let req = inputAppFrame.setInputMethodActive(true); + resolve(req); + }); + }); +} + +function setupContentFrame() { + info('setupContentFrame'); + return new Promise((resolve, reject) => { + let contentFrameScript = function contentFrameScript() { + let input = content.document.body.firstElementChild; + + let i = 0; + + input.focus(); + + addMessageListener('test:next', function() { + content.document.body.children[++i].focus(); + }); + }; + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_focus_blur_manage_events.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); + + let mm = contentFrameMM = + SpecialPowers.getBrowserFrameMessageManager(iframe); + + iframe.addEventListener('mozbrowserloadend', function() { + mm.loadFrameScript('data:,(' + encodeURIComponent(contentFrameScript.toString()) + ')();', false); + + resolve(); + }); + }); +} + +inputmethod_setup(function() { + Promise.resolve() + .then(() => setupTestRunner()) + .then(() => setupContentFrame()) + .then(() => setupInputAppFrame()) + .catch((e) => { + ok(false, 'Error' + e.toString()); + console.error(e); + }); +}); + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_forward_hardware_key_to_ime.html b/dom/inputmethod/mochitest/test_forward_hardware_key_to_ime.html new file mode 100644 index 000000000..13bd58ce9 --- /dev/null +++ b/dom/inputmethod/mochitest/test_forward_hardware_key_to_ime.html @@ -0,0 +1,149 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1110030 +--> +<head> + <title>Forwarding Hardware Key to InputMethod</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"></script> + <script type="text/javascript" src="bug1110030_helper.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1110030">Mozilla Bug 1110030</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> +// The input context. +var gContext = null; + +// The test cases. +var gTests; + +inputmethod_setup(function() { + setInputContext(); +}); + +function setInputContext() { + let im = navigator.mozInputMethod; + + im.oninputcontextchange = function() { + ok(true, 'inputcontextchange event was fired.'); + im.oninputcontextchange = null; + + gContext = im.inputcontext; + if (!gContext || !gContext.hardwareinput) { + ok(false, 'Should have a non-null inputcontext.hardwareinput'); + inputmethod_cleanup(); + return; + } + + prepareTest(); + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + // verifyResultsAndMoveNext will be called after input#text-input + // receives all expected key events and it will verify results + // and start next test. + loadTestFrame(verifyResultsAndMoveNext); +} + +function prepareTest() +{ + // Set the used input method of this test + gInputMethod = new InputMethod(gContext); + + // Add listenr to hardwareinput + addKeyEventListeners(gContext.hardwareinput, function (evt) { + hardwareEventReceiver(evt); + gInputMethod.handler(evt); + }); + + // Set the test cases + gTests = [ + // Case 1: IME handle the key input + { + key: 'z', + hardwareinput: { + expectedEvents: kKeyDown | kKeyUp, + receivedEvents: 0, + expectedKeys: 'zz', // One for keydown, the other for keyup + receivedKeys: '', + }, + inputtext: { + expectedEvents: kKeyDown | kKeyPress | kKeyUp, + receivedEvents: 0, + expectedKeys: gInputMethod.mapKey('z') + // for keydown + gInputMethod.mapKey('z') + // for keypress + gInputMethod.mapKey('z'), // for keyup + receivedKeys: '', + } + }, + // case 2: IME doesn't handle the key input + { + key: '7', + hardwareinput: { + expectedEvents: kKeyDown | kKeyUp, + receivedEvents: 0, + expectedKeys: '77', // One for keydown, the other for keyup + receivedKeys: '', + }, + inputtext: { + expectedEvents: kKeyDown | kKeyPress | kKeyUp, + receivedEvents: 0, + expectedKeys: '777', // keydown, keypress, keyup all will receive key + receivedKeys: '', + } + }, + // case 3: IME is disable + // This case is same as + // dom/events/test/test_dom_before_after_keyboard_event*.html + ]; + + startTesting(); +} + +function startTesting() +{ + if (gTests.length <= 0) { + finish(); + return; + } + + gCurrentTest = gTests.shift(); + + fireEvent(); +} + +function verifyResultsAndMoveNext() +{ + verifyResults(gCurrentTest); + startTesting(); +} + +function finish() +{ + inputmethod_cleanup(); +} + +function errorHandler(msg) +{ + // Clear the test cases + if (gTests) { + gTests = []; + } + + ok(false, msg); + + inputmethod_cleanup(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/inputmethod/mochitest/test_input_registry_events.html b/dom/inputmethod/mochitest/test_input_registry_events.html new file mode 100644 index 000000000..e2c3c1f9d --- /dev/null +++ b/dom/inputmethod/mochitest/test_input_registry_events.html @@ -0,0 +1,251 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1201407 +--> +<head> + <title>Test addinputrequest and removeinputrequest event</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1201407">Mozilla Bug 1201407</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +let appFrameMM; +let nextStep; + +function setupInputAppFrame() { + info('setupInputAppFrame'); + return new Promise((resolve, reject) => { + let appFrameScript = function appFrameScript() { + let im = content.navigator.mozInputMethod; + + addMessageListener('test:callAddInput', function() { + im.addInput('foo', { + launch_path: 'bar.html', + name: 'Foo', + description: 'foobar', + types: ['text', 'password'] + }) + .then((r) => { + sendAsyncMessage('test:resolved', { resolved: true, result: r }); + }, (e) => { + sendAsyncMessage('test:rejected', { rejected: true, error: e }); + }); + }); + + addMessageListener('test:callRemoveInput', function() { + im.removeInput('foo') + .then((r) => { + sendAsyncMessage('test:resolved', { resolved: true, result: r }); + }, (e) => { + sendAsyncMessage('test:rejected', { rejected: true, error: e }); + }); + }); + + im.mgmt.onaddinputrequest = + im.mgmt.onremoveinputrequest = function(evt) { + sendAsyncMessage('test:appEvent', { type: evt.type }); + }; + + content.document.body.textContent = 'I am a input app'; + }; + + let path = location.pathname; + let basePath = location.protocol + '//' + location.host + + path.substring(0, path.lastIndexOf('/')); + let imeUrl = basePath + '/file_blank.html'; + + let inputAppFrame = document.createElement('iframe'); + inputAppFrame.setAttribute('mozbrowser', true); + // FIXME: Bug 1270790 + inputAppFrame.setAttribute('remote', true); + inputAppFrame.src = imeUrl; + document.body.appendChild(inputAppFrame); + + let mm = appFrameMM = + SpecialPowers.getBrowserFrameMessageManager(inputAppFrame); + + inputAppFrame.addEventListener('mozbrowserloadend', function() { + mm.addMessageListener('test:appEvent', function(msg) { + ok(false, 'Input app should not receive ' + msg.data.type + ' event.'); + }); + mm.addMessageListener('test:resolved', function(msg) { + nextStep && nextStep(msg.data); + }); + mm.addMessageListener('test:rejected', function(msg) { + nextStep && nextStep(msg.data); + }); + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + + resolve(); + }); + }); +} + +function Deferred() { + this.promise = new Promise((res, rej) => { + this.resolve = res; + this.reject = rej; + }); + return this; +} + +function deepAssertObject(obj, expectedObj, desc) { + for (let prop in expectedObj) { + if (typeof expectedObj[prop] === 'object') { + deepAssertObject(obj[prop], expectedObj[prop], desc + '.' + prop); + } else { + is(obj[prop], expectedObj[prop], desc + '.' + prop); + } + } +} + +function setupTestRunner() { + let im = navigator.mozInputMethod; + let d; + + let i = -1; + nextStep = function next(evt) { + i++; + info('Step ' + i); + + switch (i) { + case 0: + appFrameMM.sendAsyncMessage('test:callAddInput'); + + break; + + case 1: + is(evt.type, 'addinputrequest', 'evt.type'); + deepAssertObject(evt.detail, { + inputId: 'foo', + manifestURL: null, // todo + inputManifest: { + launch_path: 'bar.html', + name: 'Foo', + description: 'foobar', + types: ['text', 'password'] + } + }, 'detail'); + + d = new Deferred(); + evt.detail.waitUntil(d.promise); + evt.preventDefault(); + + Promise.resolve().then(next); + break; + + case 2: + d.resolve(); + d = null; + break; + + case 3: + ok(evt.resolved, 'resolved'); + appFrameMM.sendAsyncMessage('test:callAddInput'); + + break; + + case 4: + is(evt.type, 'addinputrequest', 'evt.type'); + + d = new Deferred(); + evt.detail.waitUntil(d.promise); + evt.preventDefault(); + + Promise.resolve().then(next); + break; + + case 5: + d.reject('Foo Error'); + d = null; + break; + + case 6: + ok(evt.rejected, 'rejected'); + is(evt.error, 'Foo Error', 'rejected'); + + + appFrameMM.sendAsyncMessage('test:callRemoveInput'); + + break; + + case 7: + is(evt.type, 'removeinputrequest', 'evt.type'); + deepAssertObject(evt.detail, { + inputId: 'foo', + manifestURL: null // todo + }, 'detail'); + + d = new Deferred(); + evt.detail.waitUntil(d.promise); + evt.preventDefault(); + + Promise.resolve().then(next); + break; + + case 8: + d.resolve(); + d = null; + break; + + case 9: + ok(evt.resolved, 'resolved'); + appFrameMM.sendAsyncMessage('test:callRemoveInput'); + + break; + + case 10: + is(evt.type, 'removeinputrequest', 'evt.type'); + + d = new Deferred(); + evt.detail.waitUntil(d.promise); + evt.preventDefault(); + + Promise.resolve().then(next); + break; + + case 11: + d.reject('Foo Error'); + d = null; + break; + + case 12: + ok(evt.rejected, 'rejected'); + is(evt.error, 'Foo Error', 'rejected'); + inputmethod_cleanup(); + + break; + + default: + ok(false, 'received extra call.'); + inputmethod_cleanup(); + + break; + } + } + + im.mgmt.onaddinputrequest = + im.mgmt.onremoveinputrequest = nextStep; +} + +inputmethod_setup(function() { + Promise.resolve() + .then(() => setupTestRunner()) + .then(() => setupInputAppFrame()) + .then(() => nextStep()) + .catch((e) => { + ok(false, 'Error' + e.toString()); + console.error(e); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/inputmethod/mochitest/test_sendkey_cancel.html b/dom/inputmethod/mochitest/test_sendkey_cancel.html new file mode 100644 index 000000000..8affa8c09 --- /dev/null +++ b/dom/inputmethod/mochitest/test_sendkey_cancel.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=952080 +--> +<head> + <title>SendKey with canceled keydown test for InputMethod API.</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=952080">Mozilla Bug 952080</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +// The input context. +var gContext = null; + +inputmethod_setup(function() { + runTest(); +}); + +function runTest() { + let im = navigator.mozInputMethod; + + im.oninputcontextchange = function() { + ok(true, 'inputcontextchange event was fired.'); + im.oninputcontextchange = null; + + gContext = im.inputcontext; + if (!gContext) { + ok(false, 'Should have a non-null inputcontext.'); + inputmethod_cleanup(); + return; + } + + test(); + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_sendkey_cancel.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); +} + +function test() { + gContext.sendKey(0, 'j', 0).then(function() { + ok(false, 'sendKey was incorrectly resolved'); + + inputmethod_cleanup(); + }, function(e) { + ok(true, 'sendKey was rejected'); + + inputmethod_cleanup(); + }); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_setSupportsSwitching.html b/dom/inputmethod/mochitest/test_setSupportsSwitching.html new file mode 100644 index 000000000..2a7540cde --- /dev/null +++ b/dom/inputmethod/mochitest/test_setSupportsSwitching.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1197682 +--> +<head> + <title>Test inputcontext#inputType and MozInputMethodManager#supportsSwitching()</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1197682">Mozilla Bug 1197682</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +let appFrameScript = function appFrameScript() { + let input = content.document.body.firstElementChild; + + let i = 1; + + input.focus(); + + addMessageListener('test:next', function() { + i++; + switch (i) { + case 2: + content.document.body.children[1].focus(); + i++; // keep the same count with the parent frame. + + break; + + case 4: + content.document.body.lastElementChild.focus(); + i++; // keep the same count with the parent frame. + + break; + + case 6: + content.document.body.lastElementChild.blur(); + + break; + } + }); +}; + +function runTest() { + let im = navigator.mozInputMethod; + + let i = 0; + im.oninputcontextchange = function(evt) { + var inputcontext = navigator.mozInputMethod.inputcontext; + + i++; + switch (i) { + case 1: + ok(!!inputcontext, '1) Receving the input context'); + is(inputcontext.inputType, 'text', '1) input type'); + is(im.mgmt.supportsSwitching(), true, '1) supports switching'); + + mm.sendAsyncMessage('test:next'); + break; + + case 2: + is(inputcontext, null, '2) Receving null inputcontext'); + + break; + + case 3: + ok(!!inputcontext, '3) Receving the input context'); + is(inputcontext.inputType, 'number', '3) input type'); + is(im.mgmt.supportsSwitching(), false, '3) supports switching'); + + mm.sendAsyncMessage('test:next'); + break; + + case 4: + is(inputcontext, null, '4) Receving null inputcontext'); + + break; + + case 5: + ok(!!inputcontext, '5) Receving the input context'); + is(inputcontext.inputType, 'password', '5) input type'); + is(im.mgmt.supportsSwitching(), true, '5) supports switching'); + + mm.sendAsyncMessage('test:next'); + break; + + case 6: + is(inputcontext, null, '6) Receving null inputcontext'); + is(im.mgmt.supportsSwitching(), false, '6) supports switching'); + + inputmethod_cleanup(); + break; + + default: + ok(false, 'Receving extra inputcontextchange calls'); + inputmethod_cleanup(); + + break; + } + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + // Set text and password inputs as supports switching (and not supported for number type) + im.mgmt.setSupportsSwitchingTypes(['text', 'password']); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_setSupportsSwitching.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); + + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + + iframe.addEventListener('mozbrowserloadend', function() { + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/inputmethod/mochitest/test_simple_manage_events.html b/dom/inputmethod/mochitest/test_simple_manage_events.html new file mode 100644 index 000000000..bbcac498d --- /dev/null +++ b/dom/inputmethod/mochitest/test_simple_manage_events.html @@ -0,0 +1,154 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1201407 +--> +<head> + <title>Test simple manage notification events on MozInputMethodManager</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1201407">Mozilla Bug 1201407</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +let appFrameMM; +let nextStep; + +function setupTestRunner() { + info('setupTestRunner'); + let im = navigator.mozInputMethod; + + let i = 0; + im.mgmt.onshowallrequest = + im.mgmt.onnextrequest = nextStep = function(evt) { + i++; + switch (i) { + case 1: + is(evt.type, 'inputcontextchange', '1) inputcontextchange event'); + appFrameMM.sendAsyncMessage('test:callShowAll'); + + break; + + case 2: + is(evt.type, 'showallrequest', '2) showallrequest event'); + ok(evt.target, im.mgmt, '2) evt.target'); + evt.preventDefault(); + + appFrameMM.sendAsyncMessage('test:callNext'); + + break; + + case 3: + is(evt.type, 'nextrequest', '3) nextrequest event'); + ok(evt.target, im.mgmt, '3) evt.target'); + evt.preventDefault(); + + im.mgmt.onshowallrequest = + im.mgmt.onnextrequest = nextStep = null; + + inputmethod_cleanup(); + break; + + default: + ok(false, 'Receving extra events'); + inputmethod_cleanup(); + + break; + } + }; +} + +function setupInputAppFrame() { + info('setupInputAppFrame'); + return new Promise((resolve, reject) => { + let appFrameScript = function appFrameScript() { + let im = content.navigator.mozInputMethod; + + addMessageListener('test:callShowAll', function() { + im.mgmt.showAll(); + }); + + addMessageListener('test:callNext', function() { + im.mgmt.next(); + }); + + im.mgmt.onshowallrequest = + im.mgmt.onnextrequest = function(evt) { + sendAsyncMessage('test:appEvent', { type: evt.type }); + }; + + im.oninputcontextchange = function(evt) { + sendAsyncMessage('test:inputcontextchange', {}); + }; + + content.document.body.textContent = 'I am a input app'; + }; + + let path = location.pathname; + let basePath = location.protocol + '//' + location.host + + path.substring(0, path.lastIndexOf('/')); + let imeUrl = basePath + '/file_blank.html'; + + let inputAppFrame = document.createElement('iframe'); + inputAppFrame.setAttribute('mozbrowser', true); + inputAppFrame.src = imeUrl; + document.body.appendChild(inputAppFrame); + + let mm = appFrameMM = + SpecialPowers.getBrowserFrameMessageManager(inputAppFrame); + + inputAppFrame.addEventListener('mozbrowserloadend', function() { + mm.addMessageListener('test:appEvent', function(msg) { + ok(false, 'Input app should not receive ' + msg.data.type + ' event.'); + }); + mm.addMessageListener('test:inputcontextchange', function(msg) { + nextStep && nextStep({ type: 'inputcontextchange' }); + }); + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + + // Set the input app frame to be active + let req = inputAppFrame.setInputMethodActive(true); + resolve(req); + }); + }); +} + +function setupContentFrame() { + let contentFrameScript = function contentFrameScript() { + let input = content.document.body.firstElementChild; + + input.focus(); + }; + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_simple_manage_events.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); + + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + + iframe.addEventListener('mozbrowserloadend', function() { + mm.loadFrameScript('data:,(' + encodeURIComponent(contentFrameScript.toString()) + ')();', false); + }); +} + +inputmethod_setup(function() { + Promise.resolve() + .then(() => setupTestRunner()) + .then(() => setupContentFrame()) + .then(() => setupInputAppFrame()) + .catch((e) => { + ok(false, 'Error' + e.toString()); + console.error(e); + }); +}); + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_sync_edit.html b/dom/inputmethod/mochitest/test_sync_edit.html new file mode 100644 index 000000000..a32ea23fa --- /dev/null +++ b/dom/inputmethod/mochitest/test_sync_edit.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1079455 +--> +<head> + <title>Sync edit of an input</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1079455">Mozilla Bug 1079455</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +let appFrameScript = function appFrameScript() { + let input = content.document.body.firstElementChild; + + input.focus(); + input.value = 'First1'; + input.blur(); +}; + +function runTest() { + let im = navigator.mozInputMethod; + + let i = 0; + im.oninputcontextchange = function() { + let inputcontext = im.inputcontext; + i++; + switch (i) { + case 1: + ok(!!inputcontext, 'Should receive inputcontext from focus().'); + is(inputcontext.textAfterCursor, 'First'); + + break; + + case 2: + ok(!!inputcontext, 'Should receive inputcontext from value change.'); + is(inputcontext.textBeforeCursor, 'First1'); + + break; + + case 3: + ok(!inputcontext, 'Should lost inputcontext from blur().'); + + inputmethod_cleanup(); + break; + + default: + ok(false, 'Unknown event count.'); + + inputmethod_cleanup(); + } + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_sync_edit.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); + + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + iframe.addEventListener('mozbrowserloadend', function() { + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + }); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_two_inputs.html b/dom/inputmethod/mochitest/test_two_inputs.html new file mode 100644 index 000000000..e0f007cbc --- /dev/null +++ b/dom/inputmethod/mochitest/test_two_inputs.html @@ -0,0 +1,184 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1057898 +https://bugzilla.mozilla.org/show_bug.cgi?id=952741 +--> +<head> + <title>Test switching between two inputs</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1057898">Mozilla Bug 1057898</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=952741">Mozilla Bug 952741</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +let appFrameScript = function appFrameScript() { + let input1 = content.document.body.firstElementChild; + let input2 = content.document.body.children[1]; + + let i = 1; + + input1.focus(); + + addMessageListener('test:next', function() { + i++; + switch (i) { + case 2: + input2.focus(); + i++; // keep the same count with the parent frame. + + break; + + case 4: + input2.blur(); + + break; + + case 5: + input2.focus(); + + break; + + case 6: + input1.focus(); + i++; // keep the same count with the parent frame. + + break; + + case 8: + content.document.body.removeChild(input1); + + break; + + case 9: + input2.focus(); + + break; + + case 10: + content.document.body.removeChild(input2); + + break; + } + }); +}; + +function runTest() { + let im = navigator.mozInputMethod; + + let i = 0; + im.oninputcontextchange = function(evt) { + var inputcontext = navigator.mozInputMethod.inputcontext; + + i++; + switch (i) { + // focus on the first input receives the first input context. + case 1: + ok(!!inputcontext, '1) Receving the first input context'); + is(inputcontext.textAfterCursor, 'First'); + + mm.sendAsyncMessage('test:next'); + break; + + // focus on the second input should implicitly blur the first input + case 2: + is(inputcontext, null, '2) Receving null inputcontext'); + + break; + + // ... and results the second input context. + case 3: + ok(!!inputcontext, '3) Receving the second input context'); + is(inputcontext.textAfterCursor, 'Second'); + + mm.sendAsyncMessage('test:next'); + break; + + // blur on the second input results null input context + case 4: + is(inputcontext, null, '4) Receving null inputcontext'); + + mm.sendAsyncMessage('test:next'); + break; + + // focus on the second input receives the second input context. + case 5: + ok(!!inputcontext, '5) Receving the second input context'); + is(inputcontext.textAfterCursor, 'Second'); + + mm.sendAsyncMessage('test:next'); + break; + + // focus on the second input should implicitly blur the first input + case 6: + is(inputcontext, null, '6) Receving null inputcontext'); + + break; + + // ... and results the second input context. + case 7: + ok(!!inputcontext, '7) Receving the first input context'); + is(inputcontext.textAfterCursor, 'First'); + + mm.sendAsyncMessage('test:next'); + break; + + // remove on the first focused input results null input context + case 8: + is(inputcontext, null, '8) Receving null inputcontext'); + + mm.sendAsyncMessage('test:next'); + break; + + // input context for the second input. + case 9: + ok(!!inputcontext, '9) Receving the second input context'); + is(inputcontext.textAfterCursor, 'Second'); + + mm.sendAsyncMessage('test:next'); + break; + + // remove on the second focused input results null input context + case 10: + is(inputcontext, null, '10) Receving null inputcontext'); + + inputmethod_cleanup(); + break; + + default: + ok(false, 'Receving extra inputcontextchange calls'); + inputmethod_cleanup(); + + break; + } + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_two_inputs.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); + + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + + iframe.addEventListener('mozbrowserloadend', function() { + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + }); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_two_selects.html b/dom/inputmethod/mochitest/test_two_selects.html new file mode 100644 index 000000000..9869b8c14 --- /dev/null +++ b/dom/inputmethod/mochitest/test_two_selects.html @@ -0,0 +1,182 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1079728 +--> +<head> + <title>Test switching between two inputs</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1079728">Mozilla Bug 1079728</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +let appFrameScript = function appFrameScript() { + let select1 = content.document.body.firstElementChild; + let select2 = content.document.body.children[1]; + + let i = 1; + + select1.focus(); + + addMessageListener('test:next', function() { + i++; + switch (i) { + case 2: + select2.focus(); + i++; // keep the same count with the parent frame. + + break; + + case 4: + select2.blur(); + + break; + + case 5: + select2.focus(); + + break; + + case 6: + select1.focus(); + i++; // keep the same count with the parent frame. + + break; + + case 8: + content.document.body.removeChild(select1); + + break; + + case 9: + select2.focus(); + + break; + + case 10: + content.document.body.removeChild(select2); + + break; + } + }); +}; + +function runTest() { + let im = navigator.mozInputMethod; + + let i = 0; + im.oninputcontextchange = function(evt) { + var inputcontext = navigator.mozInputMethod.inputcontext; + + i++; + switch (i) { + // focus on the first input receives the first input context. + case 1: + ok(!!inputcontext, '1) Receving the first input context'); + is(inputcontext.textAfterCursor, 'First'); + + mm.sendAsyncMessage('test:next'); + break; + + // focus on the second input should implicitly blur the first input + case 2: + is(inputcontext, null, '2) Receving null inputcontext'); + + break; + + // ... and results the second input context. + case 3: + ok(!!inputcontext, '3) Receving the second input context'); + is(inputcontext.textAfterCursor, 'Second'); + + mm.sendAsyncMessage('test:next'); + break; + + // blur on the second input results null input context + case 4: + is(inputcontext, null, '4) Receving null inputcontext'); + + mm.sendAsyncMessage('test:next'); + break; + + // focus on the second input receives the second input context. + case 5: + ok(!!inputcontext, '5) Receving the second input context'); + is(inputcontext.textAfterCursor, 'Second'); + + mm.sendAsyncMessage('test:next'); + break; + + // focus on the second input should implicitly blur the first input + case 6: + is(inputcontext, null, '6) Receving null inputcontext'); + + break; + + // ... and results the second input context. + case 7: + ok(!!inputcontext, '7) Receving the first input context'); + is(inputcontext.textAfterCursor, 'First'); + + mm.sendAsyncMessage('test:next'); + break; + + // remove on the first focused input results null input context + case 8: + is(inputcontext, null, '8) Receving null inputcontext'); + + mm.sendAsyncMessage('test:next'); + break; + + // input context for the second input. + case 9: + ok(!!inputcontext, '9) Receving the second input context'); + is(inputcontext.textAfterCursor, 'Second'); + + mm.sendAsyncMessage('test:next'); + break; + + // remove on the second focused input results null input context + case 10: + is(inputcontext, null, '10) Receving null inputcontext'); + + inputmethod_cleanup(); + break; + + default: + ok(false, 'Receving extra inputcontextchange calls'); + inputmethod_cleanup(); + + break; + } + }; + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_two_selects.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); + + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + + iframe.addEventListener('mozbrowserloadend', function() { + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + }); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/mochitest/test_unload.html b/dom/inputmethod/mochitest/test_unload.html new file mode 100644 index 000000000..bef9d1485 --- /dev/null +++ b/dom/inputmethod/mochitest/test_unload.html @@ -0,0 +1,167 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1122463 +https://bugzilla.mozilla.org/show_bug.cgi?id=820057 +--> +<head> + <title>Test focus when page unloads</title> + <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1122463">Mozilla Bug 1122463</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=820057">Mozilla Bug 820057</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +inputmethod_setup(function() { + runTest(); +}); + +let appFrameScript = function appFrameScript() { + let form1 = content.document.body.firstElementChild; + let input1 = form1.firstElementChild; + let submit1 = form1.lastElementChild; + let input2; + + let cancelSubmit = function(evt) { + evt.preventDefault(); + }; + + // Content of the second page. + form1.action = 'file_test_unload_action.html'; + + let i = 1; + + input1.focus(); + + addMessageListener('test:next', function() { + i++; + switch (i) { + case 2: + // Click the submit button, trigger the submit event and make our + // installed event listener preventing the submission. + form1.addEventListener('submit', cancelSubmit); + submit1.click(); + + sendAsyncMessage('test:step'); + + break; + + case 3: + // Actually submit the form. + form1.removeEventListener('submit', cancelSubmit); + submit1.click(); + + break; + + case 4: + if (!content.document.body) { + content.onload = function() { + content.onload = null; + + let input2 = content.document.body.firstElementChild; + input2.focus(); + }; + + return; + } + + input2 = content.document.body.firstElementChild; + input2.focus(); + + break; + + case 5: + content.location.href = 'data:text/html,Hello!'; + + break; + } + }); +}; + +function runTest() { + let im = navigator.mozInputMethod; + + let i = 0; + function nextStep() { + let inputcontext = navigator.mozInputMethod.inputcontext; + + i++; + switch (i) { + // focus on the first input receives the first input context. + case 1: + ok(!!inputcontext, '1) Receving the first input context'); + is(inputcontext.textAfterCursor, 'First'); + + mm.sendAsyncMessage('test:next'); + break; + + // Cancelled submission should not cause us lost focus. + case 2: + ok(!!inputcontext, '2) Receving the first input context'); + is(inputcontext.textAfterCursor, 'First'); + + mm.sendAsyncMessage('test:next'); + break; + + // Real submit and page transition should cause us lost focus. + // XXX: Unless we could delay the page transition, we does not know if + // the inputcontext is lost because of the submit or the pagehide/beforeload + // event. + case 3: + is(inputcontext, null, '3) Receving null inputcontext'); + + mm.sendAsyncMessage('test:next'); + + break; + + // Regaining focus of input in the second page. + case 4: + ok(!!inputcontext, '4) Receving the second input context'); + is(inputcontext.textAfterCursor, 'Second'); + + mm.sendAsyncMessage('test:next'); + + break; + + // Page transition should cause us lost focus + case 5: + is(inputcontext, null, '5) Receving null inputcontext'); + + inputmethod_cleanup(); + + break; + } + } + + // Set current page as an input method. + SpecialPowers.wrap(im).setActive(true); + + let iframe = document.createElement('iframe'); + iframe.src = 'file_test_unload.html'; + iframe.setAttribute('mozbrowser', true); + document.body.appendChild(iframe); + + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + im.oninputcontextchange = nextStep; + + let frameScriptLoaded = false; + iframe.addEventListener('mozbrowserloadend', function() { + if (frameScriptLoaded) + return; + + frameScriptLoaded = true; + mm.addMessageListener('test:step', nextStep); + mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false); + }); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/inputmethod/moz.build b/dom/inputmethod/moz.build new file mode 100644 index 000000000..504e2ebfc --- /dev/null +++ b/dom/inputmethod/moz.build @@ -0,0 +1,41 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +if CONFIG['MOZ_B2G']: + XPIDL_SOURCES += [ + 'nsIHardwareKeyHandler.idl', + ] + + XPIDL_MODULE = 'inputmethod' + + EXPORTS.mozilla += [ + 'HardwareKeyHandler.h', + ] + + SOURCES += [ + 'HardwareKeyHandler.cpp' + ] + + include('/ipc/chromium/chromium-config.mozbuild') + + FINAL_LIBRARY = 'xul' + LOCAL_INCLUDES += [ + '/dom/base', + '/layout/base', + ] + +EXTRA_COMPONENTS += [ + 'InputMethod.manifest', + 'MozKeyboard.js', +] + +EXTRA_PP_JS_MODULES += [ + 'Keyboard.jsm', +] + +JAR_MANIFESTS += ['jar.mn'] + +MOCHITEST_CHROME_MANIFESTS += ['mochitest/chrome.ini'] diff --git a/dom/inputmethod/nsIHardwareKeyHandler.idl b/dom/inputmethod/nsIHardwareKeyHandler.idl new file mode 100644 index 000000000..5bce4d980 --- /dev/null +++ b/dom/inputmethod/nsIHardwareKeyHandler.idl @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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 "nsISupports.idl" + +interface nsIDOMKeyEvent; + +%{C++ +#define NS_HARDWARE_KEY_HANDLER_CID \ + { 0xfb45921b, 0xe0a5, 0x45c6, \ + { 0x90, 0xd0, 0xa6, 0x97, 0xa7, 0x72, 0xc4, 0x2a } } +#define NS_HARDWARE_KEY_HANDLER_CONTRACTID \ + "@mozilla.org/HardwareKeyHandler;1" + +#include "mozilla/EventForwards.h" /* For nsEventStatus */ + +namespace mozilla { +class WidgetKeyboardEvent; +} + +using mozilla::WidgetKeyboardEvent; + +class nsINode; +%} + +/** + * This interface is used to be registered to the nsIHardwareKeyHandler through + * |nsIHardwareKeyHandler.registerListener|. + */ +[scriptable, function, uuid(cd5aeee3-b4b9-459d-85e7-c0671c7a8a2e)] +interface nsIHardwareKeyEventListener : nsISupports +{ + /** + * This method will be invoked by nsIHardwareKeyHandler to forward the native + * keyboard event to the active input method + */ + bool onHardwareKey(in nsIDOMKeyEvent aEvent); +}; + +/** + * This interface has two main roles. One is to send a hardware keyboard event + * to the active input method app and the other is to receive its reply result. + * If a keyboard event is triggered from a hardware keyboard when an editor has + * focus, the event target should be the editor. However, the text input + * processor algorithm is implemented in an input method app and it should + * handle the event earlier than the real event target to do the mapping such + * as character conversion according to the language setting or the type of a + * hardware keyboard. + */ +[scriptable, builtinclass, uuid(25b34270-caad-4d18-a910-860351690639)] +interface nsIHardwareKeyHandler : nsISupports +{ + /** + * Flags used to set the defaultPrevented's result. The default result + * from input-method-app should be set to NO_DEFAULT_PREVENTED. + * (It means the forwarded event isn't consumed by input-method-app.) + * If the input-method-app consumes the forwarded event, + * then the result should be set by DEFAULT_PREVENTED* before reply. + */ + const unsigned short NO_DEFAULT_PREVENTED = 0x0000; + const unsigned short DEFAULT_PREVENTED = 0x0001; + const unsigned short DEFAULT_PREVENTED_BY_CHROME = 0x0002; + const unsigned short DEFAULT_PREVENTED_BY_CONTENT = 0x0004; + + /** + * Registers a listener in input-method-app to receive + * the forwarded hardware keyboard events + * + * @param aListener Listener object to be notified for receiving + * the keyboard event fired from hardware + * @note A listener object must implement + * nsIHardwareKeyEventListener and + * nsSupportsWeakReference + * @see nsIHardwareKeyEventListener + * @see nsSupportsWeakReference + */ + void registerListener(in nsIHardwareKeyEventListener aListener); + + /** + * Unregisters the current listener from input-method-app + */ + void unregisterListener(); + + /** + * Notifies nsIHardwareKeyHandler that input-method-app is active. + */ + void onInputMethodAppConnected(); + + /** + * Notifies nsIHardwareKeyHandler that input-method-app is disabled. + */ + void onInputMethodAppDisconnected(); + + /** + * Input-method-app will pass the processing result that the forwarded + * event is handled or not through this method, and the nsIHardwareKeyHandler + * can use this to receive the reply of |forwardKeyToInputMethodApp| + * from the active input method. + * + * The result should contain the original event type and the info whether + * the default is prevented, also, it is prevented by chrome or content. + * + * @param aEventType The type of an original event. + * @param aDefaultPrevented State that |evt.preventDefault| + * is called by content, chrome or not. + */ + void onHandledByInputMethodApp(in DOMString aType, + in unsigned short aDefaultPrevented); + + /** + * Sends the native keyboard events triggered from hardware to the + * active input method before dispatching to its event target. + * This method only forwards keydown and keyup events. + * If the event isn't allowed to be forwarded, we should continue the + * normal event processing. For those forwarded keydown and keyup events + * We will pause the further event processing to wait for the completion + * of the event handling in the active input method app. + * Once |onHandledByInputMethodApp| is called by the input method app, + * the pending event processing can be resumed according to its reply. + * On the other hand, the keypress will never be sent to the input-method-app. + * Depending on whether the keydown's reply arrives before the keypress event + * comes, the keypress event will be handled directly or pushed into + * the event queue to wait for its heading keydown's reply. + * + * This implementation will call |nsIHardwareKeyEventListener.onHardwareKey|, + * which is registered through |nsIHardwareKeyEventListener.registerListener|, + * to forward the events. + * + * Returns true, if the event is handled in this module. + * Returns false, otherwise. + * + * If it returns false, we should continue the normal event processing. + */ + %{C++ + virtual bool ForwardKeyToInputMethodApp(nsINode* aTarget, + WidgetKeyboardEvent* aEvent, + nsEventStatus* aEventStatus) = 0; + %} +}; |