From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- dom/inputmethod/HardwareKeyHandler.cpp | 562 ++++++ dom/inputmethod/HardwareKeyHandler.h | 224 +++ dom/inputmethod/InputMethod.manifest | 2 + dom/inputmethod/Keyboard.jsm | 644 +++++++ dom/inputmethod/MozKeyboard.js | 1255 ++++++++++++++ dom/inputmethod/forms.js | 1561 +++++++++++++++++ dom/inputmethod/jar.mn | 6 + dom/inputmethod/mochitest/bug1110030_helper.js | 267 +++ dom/inputmethod/mochitest/chrome.ini | 52 + dom/inputmethod/mochitest/file_blank.html | 4 + dom/inputmethod/mochitest/file_inputmethod.html | 25 + dom/inputmethod/mochitest/file_test_app.html | 11 + .../mochitest/file_test_bug1066515.html | 6 + .../mochitest/file_test_bug1137557.html | 6 + .../mochitest/file_test_bug1175399.html | 1 + dom/inputmethod/mochitest/file_test_empty_app.html | 10 + .../file_test_focus_blur_manage_events.html | 22 + .../mochitest/file_test_sendkey_cancel.html | 14 + .../mochitest/file_test_setSupportsSwitching.html | 5 + .../mochitest/file_test_simple_manage_events.html | 1 + dom/inputmethod/mochitest/file_test_sms_app.html | 14 + .../mochitest/file_test_sms_app_1066515.html | 14 + dom/inputmethod/mochitest/file_test_sync_edit.html | 1 + .../mochitest/file_test_two_inputs.html | 1 + .../mochitest/file_test_two_selects.html | 1 + dom/inputmethod/mochitest/file_test_unload.html | 1 + .../mochitest/file_test_unload_action.html | 1 + dom/inputmethod/mochitest/inputmethod_common.js | 24 + dom/inputmethod/mochitest/test_basic.html | 212 +++ dom/inputmethod/mochitest/test_bug1026997.html | 101 ++ dom/inputmethod/mochitest/test_bug1043828.html | 183 ++ dom/inputmethod/mochitest/test_bug1059163.html | 87 + dom/inputmethod/mochitest/test_bug1066515.html | 93 + dom/inputmethod/mochitest/test_bug1137557.html | 1799 ++++++++++++++++++++ dom/inputmethod/mochitest/test_bug1175399.html | 62 + dom/inputmethod/mochitest/test_bug944397.html | 107 ++ dom/inputmethod/mochitest/test_bug949059.html | 40 + dom/inputmethod/mochitest/test_bug953044.html | 52 + dom/inputmethod/mochitest/test_bug960946.html | 108 ++ dom/inputmethod/mochitest/test_bug978918.html | 77 + .../mochitest/test_focus_blur_manage_events.html | 199 +++ .../test_forward_hardware_key_to_ime.html | 149 ++ .../mochitest/test_input_registry_events.html | 251 +++ dom/inputmethod/mochitest/test_sendkey_cancel.html | 67 + .../mochitest/test_setSupportsSwitching.html | 130 ++ .../mochitest/test_simple_manage_events.html | 154 ++ dom/inputmethod/mochitest/test_sync_edit.html | 81 + dom/inputmethod/mochitest/test_two_inputs.html | 184 ++ dom/inputmethod/mochitest/test_two_selects.html | 182 ++ dom/inputmethod/mochitest/test_unload.html | 167 ++ dom/inputmethod/moz.build | 41 + dom/inputmethod/nsIHardwareKeyHandler.idl | 142 ++ 52 files changed, 9403 insertions(+) create mode 100644 dom/inputmethod/HardwareKeyHandler.cpp create mode 100644 dom/inputmethod/HardwareKeyHandler.h create mode 100644 dom/inputmethod/InputMethod.manifest create mode 100644 dom/inputmethod/Keyboard.jsm create mode 100644 dom/inputmethod/MozKeyboard.js create mode 100644 dom/inputmethod/forms.js create mode 100644 dom/inputmethod/jar.mn create mode 100644 dom/inputmethod/mochitest/bug1110030_helper.js create mode 100644 dom/inputmethod/mochitest/chrome.ini create mode 100644 dom/inputmethod/mochitest/file_blank.html create mode 100644 dom/inputmethod/mochitest/file_inputmethod.html create mode 100644 dom/inputmethod/mochitest/file_test_app.html create mode 100644 dom/inputmethod/mochitest/file_test_bug1066515.html create mode 100644 dom/inputmethod/mochitest/file_test_bug1137557.html create mode 100644 dom/inputmethod/mochitest/file_test_bug1175399.html create mode 100644 dom/inputmethod/mochitest/file_test_empty_app.html create mode 100644 dom/inputmethod/mochitest/file_test_focus_blur_manage_events.html create mode 100644 dom/inputmethod/mochitest/file_test_sendkey_cancel.html create mode 100644 dom/inputmethod/mochitest/file_test_setSupportsSwitching.html create mode 100644 dom/inputmethod/mochitest/file_test_simple_manage_events.html create mode 100644 dom/inputmethod/mochitest/file_test_sms_app.html create mode 100644 dom/inputmethod/mochitest/file_test_sms_app_1066515.html create mode 100644 dom/inputmethod/mochitest/file_test_sync_edit.html create mode 100644 dom/inputmethod/mochitest/file_test_two_inputs.html create mode 100644 dom/inputmethod/mochitest/file_test_two_selects.html create mode 100644 dom/inputmethod/mochitest/file_test_unload.html create mode 100644 dom/inputmethod/mochitest/file_test_unload_action.html create mode 100644 dom/inputmethod/mochitest/inputmethod_common.js create mode 100644 dom/inputmethod/mochitest/test_basic.html create mode 100644 dom/inputmethod/mochitest/test_bug1026997.html create mode 100644 dom/inputmethod/mochitest/test_bug1043828.html create mode 100644 dom/inputmethod/mochitest/test_bug1059163.html create mode 100644 dom/inputmethod/mochitest/test_bug1066515.html create mode 100644 dom/inputmethod/mochitest/test_bug1137557.html create mode 100644 dom/inputmethod/mochitest/test_bug1175399.html create mode 100644 dom/inputmethod/mochitest/test_bug944397.html create mode 100644 dom/inputmethod/mochitest/test_bug949059.html create mode 100644 dom/inputmethod/mochitest/test_bug953044.html create mode 100644 dom/inputmethod/mochitest/test_bug960946.html create mode 100644 dom/inputmethod/mochitest/test_bug978918.html create mode 100644 dom/inputmethod/mochitest/test_focus_blur_manage_events.html create mode 100644 dom/inputmethod/mochitest/test_forward_hardware_key_to_ime.html create mode 100644 dom/inputmethod/mochitest/test_input_registry_events.html create mode 100644 dom/inputmethod/mochitest/test_sendkey_cancel.html create mode 100644 dom/inputmethod/mochitest/test_setSupportsSwitching.html create mode 100644 dom/inputmethod/mochitest/test_simple_manage_events.html create mode 100644 dom/inputmethod/mochitest/test_sync_edit.html create mode 100644 dom/inputmethod/mochitest/test_two_inputs.html create mode 100644 dom/inputmethod/mochitest/test_two_selects.html create mode 100644 dom/inputmethod/mochitest/test_unload.html create mode 100644 dom/inputmethod/moz.build create mode 100644 dom/inputmethod/nsIHardwareKeyHandler.idl (limited to 'dom/inputmethod') 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::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 + 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 = do_QueryInterface(aTarget); + nsPresContext* presContext = GetPresContext(aTarget); + RefPtr 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 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 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 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 originalRootWindow = GetRootWindow(aTarget); + nsCOMPtr 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 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 remoteLoaderOwner = do_QueryInterface(aTarget); + if (NS_WARN_IF(!remoteLoaderOwner)) { + return false; + } + + RefPtr 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(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 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 presShell = GetPresShell(aTarget); + if (NS_WARN_IF(!presShell)) { + return false; + } + return presShell->CanDispatchEvent(&aEvent); +} + +already_AddRefed +HardwareKeyHandler::GetRootWindow(nsINode* aNode) +{ + // Get nsIPresShell's pointer first + nsCOMPtr presShell = GetPresShell(aNode); + if (NS_WARN_IF(!presShell)) { + return nullptr; + } + nsCOMPtr rootWindow = presShell->GetRootWindow(); + return rootWindow.forget(); +} + +already_AddRefed +HardwareKeyHandler::GetCurrentTarget() +{ + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (NS_WARN_IF(!fm)) { + return nullptr; + } + + nsCOMPtr focusedWindow; + fm->GetFocusedWindow(getter_AddRefs(focusedWindow)); + if (NS_WARN_IF(!focusedWindow)) { + return nullptr; + } + + auto* ourWindow = nsPIDOMWindowOuter::From(focusedWindow); + + nsCOMPtr rootWindow = ourWindow->GetPrivateRoot(); + if (NS_WARN_IF(!rootWindow)) { + return nullptr; + } + + nsCOMPtr focusedFrame; + nsCOMPtr 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 htmlDocument = do_QueryInterface(document); + if (htmlDocument) { + nsCOMPtr body; + htmlDocument->GetBody(getter_AddRefs(body)); + nsCOMPtr bodyContent = do_QueryInterface(body); + if (bodyContent) { + focusedContent = bodyContent; + } + } + } + + return focusedContent ? focusedContent.forget() : nullptr; +} + +nsPresContext* +HardwareKeyHandler::GetPresContext(nsINode* aNode) +{ + // Get nsIPresShell's pointer first + nsCOMPtr presShell = GetPresShell(aNode); + if (NS_WARN_IF(!presShell)) { + return nullptr; + } + + // then use nsIPresShell to get nsPresContext's pointer + return presShell->GetPresContext(); +} + +already_AddRefed +HardwareKeyHandler::GetPresShell(nsINode* aNode) +{ + nsIDocument* doc = aNode->OwnerDoc(); + if (NS_WARN_IF(!doc)) { + return nullptr; + } + + nsCOMPtr presShell = doc->GetShell(); + if (NS_WARN_IF(!presShell)) { + return nullptr; + } + + return presShell.forget(); +} + +/* static */ +already_AddRefed +HardwareKeyHandler::GetInstance() +{ + if (!XRE_IsParentProcess()) { + return nullptr; + } + + if (!sInstance) { + sInstance = new HardwareKeyHandler(); + ClearOnShutdown(&sInstance); + } + + RefPtr 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 is a helper class for adding reference counting mechanism. +struct KeyboardInfo : public RefCounted +{ + 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 EventQueueDeallocator : public nsDequeFunctor +{ + virtual void* operator() (void* aObject) + { + RefPtr releaseMe = dont_AddRef(static_cast(aObject)); + return nullptr; + } +}; + +// The type-safe queue to be used to store the KeyboardInfo data +template +class EventQueue : private nsDeque +{ +public: + EventQueue() + : nsDeque(new EventQueueDeallocator()) + { + }; + + ~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 PopFront() + { + RefPtr rv = dont_AddRef(static_cast(nsDeque::PopFront())); + return rv.forget(); + } + + inline void RemoveFront() + { + RefPtr releaseMe = PopFront(); + } + + inline T* PeekFront() + { + return static_cast(nsDeque::PeekFront()); + } + + void Clear() + { + while (GetSize() > 0) { + RemoveFront(); + } + } +}; + +class HardwareKeyHandler : public nsIHardwareKeyHandler +{ +public: + HardwareKeyHandler(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIHARDWAREKEYHANDLER + + static already_AddRefed 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 GetRootWindow(nsINode* aNode); + + already_AddRefed GetCurrentTarget(); + + nsPresContext* GetPresContext(nsINode* aNode); + + already_AddRefed GetPresShell(nsINode* aNode); + + static StaticRefPtr 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 mEventQueue; + + // Hold the pointer to the latest keydown's data + RefPtr 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 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 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
, 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 has a nested anonymous 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 and + + 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 @@ + 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 @@ + + + + + + + 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 @@ + + + + +

+ + + + + + + + + + + + + + + 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 @@ + + + + + + + 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 @@ + + + + + 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 @@ + 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 @@ + + + +
Httvb
+ + + + + + 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 @@ + + + +
fxos
hello world
+ + + + + + 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 @@ + 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 @@ + 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 @@ + 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 @@ +
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 @@ + 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 @@ + + + + + Basic test for InputMethod API. + + + + + +Mozilla Bug 932145 +

+
+
+
+ + + 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 @@ + + + + + SelectionChange on InputMethod API. + + + + + +Mozilla Bug 1026997 +

+
+
+
+ + + 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 @@ + + + + + Basic test for Switching Keyboards. + + + + + +Mozilla Bug 1043828 +

+
+
+
+ + + 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 @@ + + + + + Basic test for repeat sendKey events + + + + + +Mozilla Bug 1059163 +

+
+
+
+ + + 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 @@ + + + + + + Test for Bug 1066515 + + + + + +Mozilla Bug 1066515 +

+
+
+
+ + 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 @@ + + + + + Test for new API arguments accepting D3E properties + + + + + +Mozilla Bug 1137557 +

+
+
+
+ + + 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 @@ + + + + + Test focus when page unloads + + + + + +Mozilla Bug 1175399 +

+
+
+
+ + + 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 @@ + + + + + Basic test for InputMethod API. + + + + + +Mozilla Bug 944397 +

+
+
+
+ + + 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 @@ + + + + + Test "mgmt" property of MozInputMethod. + + + + + +Mozilla Bug 949059 +

+
+
+
+ + + 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 @@ + + + + + Basic test for InputMethod API. + + + + + +Mozilla Bug 953044 +

+
+
+
+ + + 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 @@ + + + + + Basic test for repeat sendKey events + + + + + +Mozilla Bug 960946 +

+
+
+
+ + + 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 @@ + + + + + Basic test for InputMethod API. + + + + + +Mozilla Bug 978918 +

+
+
+
+ + + 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 @@ + + + + + Test inputcontextfocus and inputcontextblur event + + + + + +Mozilla Bug 1201407 +

+
+
+
+ + + 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 @@ + + + + + Forwarding Hardware Key to InputMethod + + + + + + + + +Mozilla Bug 1110030 +

+
+
+
+ + 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 @@ + + + + + Test addinputrequest and removeinputrequest event + + + + + +Mozilla Bug 1201407 +

+
+
+
+ + 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 @@ + + + + + SendKey with canceled keydown test for InputMethod API. + + + + + +Mozilla Bug 952080 +

+
+
+
+ + + 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 @@ + + + + + Test inputcontext#inputType and MozInputMethodManager#supportsSwitching() + + + + + +Mozilla Bug 1197682 +

+
+
+
+ + 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 @@ + + + + + Test simple manage notification events on MozInputMethodManager + + + + + +Mozilla Bug 1201407 +

+
+
+
+ + + 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 @@ + + + + + Sync edit of an input + + + + + +Mozilla Bug 1079455 +

+
+
+
+ + + 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 @@ + + + + + Test switching between two inputs + + + + + +Mozilla Bug 1057898 +Mozilla Bug 952741 +

+
+
+
+ + + 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 @@ + + + + + Test switching between two inputs + + + + + +Mozilla Bug 1079728 +

+
+
+
+ + + 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 @@ + + + + + Test focus when page unloads + + + + + +Mozilla Bug 1122463 +Mozilla Bug 820057 +

+
+
+
+ + + 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; + %} +}; -- cgit v1.2.3