diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /widget/windows/WinIMEHandler.cpp | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/windows/WinIMEHandler.cpp')
-rw-r--r-- | widget/windows/WinIMEHandler.cpp | 1180 |
1 files changed, 1180 insertions, 0 deletions
diff --git a/widget/windows/WinIMEHandler.cpp b/widget/windows/WinIMEHandler.cpp new file mode 100644 index 0000000000..e5733cc8d6 --- /dev/null +++ b/widget/windows/WinIMEHandler.cpp @@ -0,0 +1,1180 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WinIMEHandler.h" + +#include "IMMHandler.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEvents.h" +#include "mozilla/WindowsVersion.h" +#include "nsWindowDefs.h" +#include "WinTextEventDispatcherListener.h" + +#include "TSFTextStore.h" + +#include "OSKInputPaneManager.h" +#include "nsLookAndFeel.h" +#include "nsWindow.h" +#include "WinUtils.h" +#include "nsIWindowsRegKey.h" +#include "nsIWindowsUIUtils.h" + +#ifdef ACCESSIBILITY +# include "nsAccessibilityService.h" +#endif // #ifdef ACCESSIBILITY + +#include "shellapi.h" +#include "shlobj.h" +#include "powrprof.h" +#include "setupapi.h" +#include "cfgmgr32.h" + +#include "FxRWindowManager.h" +#include "VRShMem.h" +#include "moz_external_vr.h" + +const char* kOskPathPrefName = "ui.osk.on_screen_keyboard_path"; +const char* kOskEnabled = "ui.osk.enabled"; +const char* kOskDetectPhysicalKeyboard = "ui.osk.detect_physical_keyboard"; +const char* kOskRequireWin10 = "ui.osk.require_win10"; +const char* kOskDebugReason = "ui.osk.debug.keyboardDisplayReason"; + +namespace mozilla { +namespace widget { + +/****************************************************************************** + * IMEHandler + ******************************************************************************/ + +nsWindow* IMEHandler::sFocusedWindow = nullptr; +InputContextAction::Cause IMEHandler::sLastContextActionCause = + InputContextAction::CAUSE_UNKNOWN; +bool IMEHandler::sMaybeEditable = false; +bool IMEHandler::sForceDisableCurrentIMM_IME = false; +bool IMEHandler::sNativeCaretIsCreated = false; +bool IMEHandler::sHasNativeCaretBeenRequested = false; + +bool IMEHandler::sIsInTSFMode = false; +bool IMEHandler::sIsIMMEnabled = true; +bool IMEHandler::sAssociateIMCOnlyWhenIMM_IMEActive = false; +decltype(SetInputScopes)* IMEHandler::sSetInputScopes = nullptr; + +static POWER_PLATFORM_ROLE sPowerPlatformRole = PlatformRoleUnspecified; +static bool sDeterminedPowerPlatformRole = false; + +// static +void IMEHandler::Initialize() { + TSFTextStore::Initialize(); + sIsInTSFMode = TSFTextStore::IsInTSFMode(); + sIsIMMEnabled = + !sIsInTSFMode || Preferences::GetBool("intl.tsf.support_imm", true); + sAssociateIMCOnlyWhenIMM_IMEActive = + sIsIMMEnabled && + Preferences::GetBool("intl.tsf.associate_imc_only_when_imm_ime_is_active", + false); + if (!sIsInTSFMode) { + // When full TSFTextStore is not available, try to use SetInputScopes API + // to enable at least InputScope. Use GET_MODULE_HANDLE_EX_FLAG_PIN to + // ensure that msctf.dll will not be unloaded. + HMODULE module = nullptr; + if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN, L"msctf.dll", + &module)) { + sSetInputScopes = reinterpret_cast<decltype(SetInputScopes)*>( + GetProcAddress(module, "SetInputScopes")); + } + } + + IMMHandler::Initialize(); + + sForceDisableCurrentIMM_IME = IMMHandler::IsActiveIMEInBlockList(); +} + +// static +void IMEHandler::Terminate() { + if (sIsInTSFMode) { + TSFTextStore::Terminate(); + sIsInTSFMode = false; + } + + IMMHandler::Terminate(); + WinTextEventDispatcherListener::Shutdown(); +} + +// static +void* IMEHandler::GetNativeData(nsWindow* aWindow, uint32_t aDataType) { + if (aDataType == NS_RAW_NATIVE_IME_CONTEXT) { + if (IsTSFAvailable()) { + return TSFTextStore::GetThreadManager(); + } + IMEContext context(aWindow); + if (context.IsValid()) { + return context.get(); + } + // If IMC isn't associated with the window, IME is disabled on the window + // now. In such case, we should return default IMC instead. + const IMEContext& defaultIMC = aWindow->DefaultIMC(); + if (defaultIMC.IsValid()) { + return defaultIMC.get(); + } + // If there is no default IMC, we should return the pointer to the window + // since if we return nullptr, IMEStateManager cannot manage composition + // with TextComposition instance. This is possible if no IME is installed, + // but composition may occur with dead key sequence. + return aWindow; + } + + void* result = TSFTextStore::GetNativeData(aDataType); + if (!result || !(*(static_cast<void**>(result)))) { + return nullptr; + } + // XXX During the TSF module test, sIsInTSFMode must be true. After that, + // the value should be restored but currently, there is no way for that. + // When the TSF test is enabled again, we need to fix this. Perhaps, + // sending a message can fix this. + sIsInTSFMode = true; + return result; +} + +// static +bool IMEHandler::ProcessRawKeyMessage(const MSG& aMsg) { + if (IsTSFAvailable()) { + return TSFTextStore::ProcessRawKeyMessage(aMsg); + } + return false; // noting to do in IMM mode. +} + +// static +bool IMEHandler::ProcessMessage(nsWindow* aWindow, UINT aMessage, + WPARAM& aWParam, LPARAM& aLParam, + MSGResult& aResult) { + // If we're putting native caret over our caret, Windows dispatches + // EVENT_OBJECT_LOCATIONCHANGE event on other applications which hook + // the event with ::SetWinEventHook() and handles WM_GETOBJECT for + // OBJID_CARET (this is request of caret from such applications) instead + // of us. If a11y module is active, it observes every our caret change + // and put native caret over it automatically. However, if other + // applications require only caret information, activating a11y module is + // overwork and such applications may requires carets only in editors. + // Therefore, if it'd be possible, IMEHandler should put native caret over + // our caret, but there is a problem. Some versions of ATOK (Japanese TIP) + // refer native caret and if there is, the behavior is worse than the + // behavior without native caret. Therefore, we shouldn't put native caret + // as far as possible. + if (!sHasNativeCaretBeenRequested && aMessage == WM_GETOBJECT && + static_cast<DWORD>(aLParam) == OBJID_CARET) { + // So, when we receive first WM_GETOBJECT for OBJID_CARET, let's start to + // create native caret for such applications. + sHasNativeCaretBeenRequested = true; + // If an editable element has focus, we can put native caret now. + // XXX Should we avoid doing this if there is composition? + MaybeCreateNativeCaret(aWindow); + } + + if (IsTSFAvailable()) { + TSFTextStore::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult); + if (aResult.mConsumed) { + return true; + } + // If we don't support IMM in TSF mode, we don't use IMMHandler. + if (!sIsIMMEnabled) { + return false; + } + // IME isn't implemented with IMM, IMMHandler shouldn't handle any + // messages. + if (!IsIMMActive()) { + return false; + } + } + + bool keepGoing = + IMMHandler::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult); + + // If user changes active IME to an IME which is listed in our block list, + // we should disassociate IMC from the window for preventing the IME to work + // and crash. + if (aMessage == WM_INPUTLANGCHANGE) { + bool disableIME = IMMHandler::IsActiveIMEInBlockList(); + if (disableIME != sForceDisableCurrentIMM_IME) { + bool enable = + !disableIME && WinUtils::IsIMEEnabled(aWindow->InputContextRef()); + AssociateIMEContext(aWindow, enable); + sForceDisableCurrentIMM_IME = disableIME; + } + } + + return keepGoing; +} + +// static +bool IMEHandler::IsA11yHandlingNativeCaret() { +#ifndef ACCESSIBILITY + return false; +#else // #ifndef ACCESSIBILITY + // Let's assume that when there is the service, it handles native caret. + return GetAccService() != nullptr; +#endif // #ifndef ACCESSIBILITY #else +} + +// static +bool IMEHandler::IsIMMActive() { return TSFTextStore::IsIMM_IMEActive(); } + +// static +bool IMEHandler::IsComposing() { + if (IsTSFAvailable()) { + return TSFTextStore::IsComposing() || IMMHandler::IsComposing(); + } + + return IMMHandler::IsComposing(); +} + +// static +bool IMEHandler::IsComposingOn(nsWindow* aWindow) { + if (IsTSFAvailable()) { + return TSFTextStore::IsComposingOn(aWindow) || + IMMHandler::IsComposingOn(aWindow); + } + + return IMMHandler::IsComposingOn(aWindow); +} + +// static +nsresult IMEHandler::NotifyIME(nsWindow* aWindow, + const IMENotification& aIMENotification) { + if (IsTSFAvailable()) { + switch (aIMENotification.mMessage) { + case NOTIFY_IME_OF_SELECTION_CHANGE: { + nsresult rv = TSFTextStore::OnSelectionChange(aIMENotification); + // If IMM IME is active, we need to notify IMMHandler of updating + // composition change. It will adjust candidate window position or + // composition window position. + bool isIMMActive = IsIMMActive(); + if (isIMMActive) { + IMMHandler::OnUpdateComposition(aWindow); + } + IMMHandler::OnSelectionChange(aWindow, aIMENotification, isIMMActive); + return rv; + } + case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: + // If IMM IME is active, we need to notify IMMHandler of updating + // composition change. It will adjust candidate window position or + // composition window position. + if (IsIMMActive()) { + IMMHandler::OnUpdateComposition(aWindow); + } else { + TSFTextStore::OnUpdateComposition(); + } + return NS_OK; + case NOTIFY_IME_OF_TEXT_CHANGE: + return TSFTextStore::OnTextChange(aIMENotification); + case NOTIFY_IME_OF_FOCUS: { + sFocusedWindow = aWindow; + IMMHandler::OnFocusChange(true, aWindow); + nsresult rv = TSFTextStore::OnFocusChange(true, aWindow, + aWindow->GetInputContext()); + MaybeCreateNativeCaret(aWindow); + IMEHandler::MaybeShowOnScreenKeyboard(aWindow, + aWindow->GetInputContext()); + return rv; + } + case NOTIFY_IME_OF_BLUR: + sFocusedWindow = nullptr; + IMEHandler::MaybeDismissOnScreenKeyboard(aWindow); + IMMHandler::OnFocusChange(false, aWindow); + return TSFTextStore::OnFocusChange(false, aWindow, + aWindow->GetInputContext()); + case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT: + // If IMM IME is active, we should send a mouse button event via IMM. + if (IsIMMActive()) { + return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification); + } + return TSFTextStore::OnMouseButtonEvent(aIMENotification); + case REQUEST_TO_COMMIT_COMPOSITION: + if (TSFTextStore::IsComposingOn(aWindow)) { + TSFTextStore::CommitComposition(false); + } else if (IsIMMActive()) { + IMMHandler::CommitComposition(aWindow); + } + return NS_OK; + case REQUEST_TO_CANCEL_COMPOSITION: + if (TSFTextStore::IsComposingOn(aWindow)) { + TSFTextStore::CommitComposition(true); + } else if (IsIMMActive()) { + IMMHandler::CancelComposition(aWindow); + } + return NS_OK; + case NOTIFY_IME_OF_POSITION_CHANGE: + return TSFTextStore::OnLayoutChange(); + default: + return NS_ERROR_NOT_IMPLEMENTED; + } + } + + switch (aIMENotification.mMessage) { + case REQUEST_TO_COMMIT_COMPOSITION: + IMMHandler::CommitComposition(aWindow); + return NS_OK; + case REQUEST_TO_CANCEL_COMPOSITION: + IMMHandler::CancelComposition(aWindow); + return NS_OK; + case NOTIFY_IME_OF_POSITION_CHANGE: + case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: + IMMHandler::OnUpdateComposition(aWindow); + return NS_OK; + case NOTIFY_IME_OF_SELECTION_CHANGE: + IMMHandler::OnSelectionChange(aWindow, aIMENotification, true); + // IMMHandler::OnSelectionChange() cannot work without its singleton + // instance. Therefore, IMEHandler needs to create native caret instead + // if it's necessary. + MaybeCreateNativeCaret(aWindow); + return NS_OK; + case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT: + return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification); + case NOTIFY_IME_OF_FOCUS: + sFocusedWindow = aWindow; + IMMHandler::OnFocusChange(true, aWindow); + IMEHandler::MaybeShowOnScreenKeyboard(aWindow, + aWindow->GetInputContext()); + MaybeCreateNativeCaret(aWindow); + return NS_OK; + case NOTIFY_IME_OF_BLUR: + sFocusedWindow = nullptr; + IMEHandler::MaybeDismissOnScreenKeyboard(aWindow); + IMMHandler::OnFocusChange(false, aWindow); + // If a plugin gets focus while TSF has focus, we need to notify TSF of + // the blur. + if (TSFTextStore::ThinksHavingFocus()) { + return TSFTextStore::OnFocusChange(false, aWindow, + aWindow->GetInputContext()); + } + return NS_OK; + default: + return NS_ERROR_NOT_IMPLEMENTED; + } +} + +// static +IMENotificationRequests IMEHandler::GetIMENotificationRequests() { + if (IsTSFAvailable()) { + if (!sIsIMMEnabled) { + return TSFTextStore::GetIMENotificationRequests(); + } + // Even if TSF is available, the active IME may be an IMM-IME. + // Unfortunately, changing the result of GetIMENotificationRequests() while + // an editor has focus isn't supported by IMEContentObserver nor + // ContentCacheInParent. Therefore, we need to request whole notifications + // which are necessary either IMMHandler or TSFTextStore. + return IMMHandler::GetIMENotificationRequests() | + TSFTextStore::GetIMENotificationRequests(); + } + + return IMMHandler::GetIMENotificationRequests(); +} + +// static +TextEventDispatcherListener* +IMEHandler::GetNativeTextEventDispatcherListener() { + return WinTextEventDispatcherListener::GetInstance(); +} + +// static +bool IMEHandler::GetOpenState(nsWindow* aWindow) { + if (IsTSFAvailable() && !IsIMMActive()) { + return TSFTextStore::GetIMEOpenState(); + } + + IMEContext context(aWindow); + return context.GetOpenState(); +} + +// static +void IMEHandler::OnDestroyWindow(nsWindow* aWindow) { + // When focus is in remote process, but the window is being destroyed, we + // need to clean up TSFTextStore here since NOTIFY_IME_OF_BLUR won't reach + // here because BrowserParent already lost the reference to the nsWindow when + // it receives from the remote process. + if (sFocusedWindow == aWindow) { + MOZ_ASSERT(aWindow->GetInputContext().IsOriginContentProcess(), + "input context of focused widget should've been set by a remote " + "process " + "if IME focus isn't cleared before destroying the widget"); + NotifyIME(aWindow, IMENotification(NOTIFY_IME_OF_BLUR)); + } + + // We need to do nothing here for TSF. Just restore the default context + // if it's been disassociated. + if (!sIsInTSFMode) { + // MSDN says we need to set IS_DEFAULT to avoid memory leak when we use + // SetInputScopes API. Use an empty string to do this. + SetInputScopeForIMM32(aWindow, u""_ns, u""_ns, false); + } + AssociateIMEContext(aWindow, true); +} + +// static +bool IMEHandler::NeedsToAssociateIMC() { + return !sForceDisableCurrentIMM_IME && + (!sAssociateIMCOnlyWhenIMM_IMEActive || !IsIMMActive()); +} + +// static +void IMEHandler::SetInputContext(nsWindow* aWindow, InputContext& aInputContext, + const InputContextAction& aAction) { + sLastContextActionCause = aAction.mCause; + // FYI: If there is no composition, this call will do nothing. + NotifyIME(aWindow, IMENotification(REQUEST_TO_COMMIT_COMPOSITION)); + + if (aInputContext.mHTMLInputInputmode.EqualsLiteral("none")) { + IMEHandler::MaybeDismissOnScreenKeyboard(aWindow, Sync::Yes); + } else if (aAction.UserMightRequestOpenVKB()) { + IMEHandler::MaybeShowOnScreenKeyboard(aWindow, aInputContext); + } + + bool enable = WinUtils::IsIMEEnabled(aInputContext); + bool adjustOpenState = (enable && aInputContext.mIMEState.mOpen != + IMEState::DONT_CHANGE_OPEN_STATE); + bool open = + (adjustOpenState && aInputContext.mIMEState.mOpen == IMEState::OPEN); + + // Note that even while a plugin has focus, we need to notify TSF of that. + if (sIsInTSFMode) { + TSFTextStore::SetInputContext(aWindow, aInputContext, aAction); + if (IsTSFAvailable()) { + if (sIsIMMEnabled) { + // Associate IMC with aWindow only when it's necessary. + AssociateIMEContext(aWindow, enable && NeedsToAssociateIMC()); + } + if (adjustOpenState) { + TSFTextStore::SetIMEOpenState(open); + } + return; + } + } else { + // Set at least InputScope even when TextStore is not available. + SetInputScopeForIMM32(aWindow, aInputContext.mHTMLInputType, + aInputContext.mHTMLInputInputmode, + aInputContext.mInPrivateBrowsing); + } + + AssociateIMEContext(aWindow, enable); + + IMEContext context(aWindow); + if (adjustOpenState) { + context.SetOpenState(open); + } +} + +// static +void IMEHandler::AssociateIMEContext(nsWindowBase* aWindowBase, bool aEnable) { + IMEContext context(aWindowBase); + if (aEnable) { + context.AssociateDefaultContext(); + return; + } + // Don't disassociate the context after the window is destroyed. + if (aWindowBase->Destroyed()) { + return; + } + context.Disassociate(); +} + +// static +void IMEHandler::InitInputContext(nsWindow* aWindow, + InputContext& aInputContext) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->GetWindowHandle(), + "IMEHandler::SetInputContext() requires non-nullptr HWND"); + + static bool sInitialized = false; + if (!sInitialized) { + sInitialized = true; + // Some TIPs like QQ Input (Simplified Chinese) may need normal window + // (i.e., windows except message window) when initializing themselves. + // Therefore, we need to initialize TSF/IMM modules after first normal + // window is created. InitInputContext() should be called immediately + // after creating each normal window, so, here is a good place to + // initialize these modules. + Initialize(); + } + + // For a11y, the default enabled state should be 'enabled'. + aInputContext.mIMEState.mEnabled = IMEEnabled::Enabled; + + if (sIsInTSFMode) { + TSFTextStore::SetInputContext( + aWindow, aInputContext, + InputContextAction(InputContextAction::CAUSE_UNKNOWN, + InputContextAction::WIDGET_CREATED)); + // IME context isn't necessary in pure TSF mode. + if (!sIsIMMEnabled) { + AssociateIMEContext(aWindow, false); + } + return; + } + +#ifdef DEBUG + // NOTE: IMC may be null if IMM module isn't installed. + IMEContext context(aWindow); + MOZ_ASSERT(context.IsValid() || !CurrentKeyboardLayoutHasIME()); +#endif // #ifdef DEBUG +} + +#ifdef DEBUG +// static +bool IMEHandler::CurrentKeyboardLayoutHasIME() { + if (sIsInTSFMode) { + return TSFTextStore::CurrentKeyboardLayoutHasIME(); + } + + return IMMHandler::IsIMEAvailable(); +} +#endif // #ifdef DEBUG + +// static +void IMEHandler::OnKeyboardLayoutChanged() { + // Be aware, this method won't be called until TSFStaticSink starts to + // observe active TIP change. If you need to be notified of this, you + // need to create TSFStaticSink::Observe() or something and call it + // TSFStaticSink::EnsureInitActiveTIPKeyboard() forcibly. + + if (!sIsIMMEnabled || !IsTSFAvailable()) { + return; + } + + // We don't need to do anything when sAssociateIMCOnlyWhenIMM_IMEActive is + // false because IMContext won't be associated/disassociated when changing + // active keyboard layout/IME. + if (!sAssociateIMCOnlyWhenIMM_IMEActive) { + return; + } + + // If there is no TSFTextStore which has focus, i.e., no editor has focus, + // nothing to do here. + nsWindowBase* windowBase = TSFTextStore::GetEnabledWindowBase(); + if (!windowBase) { + return; + } + + // If IME isn't available, nothing to do here. + InputContext inputContext = windowBase->GetInputContext(); + if (!WinUtils::IsIMEEnabled(inputContext)) { + return; + } + + // Associate or Disassociate IMC if it's necessary. + // Note that this does nothing if the window has already associated with or + // disassociated from the window. + AssociateIMEContext(windowBase, NeedsToAssociateIMC()); +} + +// static +void IMEHandler::SetInputScopeForIMM32(nsWindow* aWindow, + const nsAString& aHTMLInputType, + const nsAString& aHTMLInputInputmode, + bool aInPrivateBrowsing) { + if (sIsInTSFMode || !sSetInputScopes || aWindow->Destroyed()) { + return; + } + AutoTArray<InputScope, 3> scopes; + + // IME may refer only first input scope, but we will append inputmode's + // input scopes since IME may refer it like Chrome. + AppendInputScopeFromType(aHTMLInputType, scopes); + AppendInputScopeFromInputmode(aHTMLInputInputmode, scopes); + + if (aInPrivateBrowsing) { + scopes.AppendElement(IS_PRIVATE); + } + + if (scopes.IsEmpty()) { + // At least, 1 item is necessary. + scopes.AppendElement(IS_DEFAULT); + } + + sSetInputScopes(aWindow->GetWindowHandle(), scopes.Elements(), + scopes.Length(), nullptr, 0, nullptr, nullptr); +} + +// static +void IMEHandler::AppendInputScopeFromInputmode(const nsAString& aInputmode, + nsTArray<InputScope>& aScopes) { + if (aInputmode.EqualsLiteral("mozAwesomebar")) { + // Even if Awesomebar has focus, user may not input URL directly. + // However, on-screen keyboard for URL should be shown because it has + // some useful additional keys like ".com" and they are not hindrances + // even when inputting non-URL text, e.g., words to search something in + // the web. On the other hand, a lot of Microsoft's IMEs and Google + // Japanese Input make their open state "closed" automatically if we + // notify them of URL as the input scope. However, this is very annoying + // for the users when they try to input some words to search the web or + // bookmark/history items. Therefore, if they are active, we need to + // notify them of the default input scope for avoiding this issue. + // FYI: We cannot check active TIP without TSF. Therefore, if it's + // not in TSF mode, this will check only if active IMM-IME is Google + // Japanese Input. Google Japanese Input is a TIP of TSF basically. + // However, if the OS is Win7 or it's installed on Win7 but has not + // been updated yet even after the OS is upgraded to Win8 or later, + // it's installed as IMM-IME. + if (TSFTextStore::ShouldSetInputScopeOfURLBarToDefault()) { + return; + } + // Don't append IS_SEARCH here for showing on-screen keyboard for URL. + if (!aScopes.Contains(IS_URL)) { + aScopes.AppendElement(IS_URL); + } + return; + } + + // https://html.spec.whatwg.org/dev/interaction.html#attr-inputmode + if (aInputmode.EqualsLiteral("url")) { + if (!aScopes.Contains(IS_SEARCH)) { + aScopes.AppendElement(IS_URL); + } + return; + } + if (aInputmode.EqualsLiteral("email")) { + if (!aScopes.Contains(IS_EMAIL_SMTPEMAILADDRESS)) { + aScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS); + } + return; + } + if (aInputmode.EqualsLiteral("tel")) { + if (!aScopes.Contains(IS_TELEPHONE_FULLTELEPHONENUMBER)) { + aScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER); + } + if (!aScopes.Contains(IS_TELEPHONE_LOCALNUMBER)) { + aScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER); + } + return; + } + if (aInputmode.EqualsLiteral("numeric")) { + if (!aScopes.Contains(IS_DIGITS)) { + aScopes.AppendElement(IS_DIGITS); + } + return; + } + if (aInputmode.EqualsLiteral("decimal")) { + if (!aScopes.Contains(IS_NUMBER)) { + aScopes.AppendElement(IS_NUMBER); + } + return; + } + if (aInputmode.EqualsLiteral("search")) { + if (!aScopes.Contains(IS_SEARCH)) { + aScopes.AppendElement(IS_SEARCH); + } + return; + } +} + +// static +void IMEHandler::AppendInputScopeFromType(const nsAString& aHTMLInputType, + nsTArray<InputScope>& aScopes) { + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html + if (aHTMLInputType.EqualsLiteral("url")) { + aScopes.AppendElement(IS_URL); + return; + } + if (aHTMLInputType.EqualsLiteral("search")) { + aScopes.AppendElement(IS_SEARCH); + return; + } + if (aHTMLInputType.EqualsLiteral("email")) { + aScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS); + return; + } + if (aHTMLInputType.EqualsLiteral("password")) { + aScopes.AppendElement(IS_PASSWORD); + return; + } + if (aHTMLInputType.EqualsLiteral("datetime") || + aHTMLInputType.EqualsLiteral("datetime-local")) { + aScopes.AppendElement(IS_DATE_FULLDATE); + aScopes.AppendElement(IS_TIME_FULLTIME); + return; + } + if (aHTMLInputType.EqualsLiteral("date") || + aHTMLInputType.EqualsLiteral("month") || + aHTMLInputType.EqualsLiteral("week")) { + aScopes.AppendElement(IS_DATE_FULLDATE); + return; + } + if (aHTMLInputType.EqualsLiteral("time")) { + aScopes.AppendElement(IS_TIME_FULLTIME); + return; + } + if (aHTMLInputType.EqualsLiteral("tel")) { + aScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER); + aScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER); + return; + } + if (aHTMLInputType.EqualsLiteral("number")) { + aScopes.AppendElement(IS_NUMBER); + return; + } +} + +// static +void IMEHandler::MaybeShowOnScreenKeyboard(nsWindow* aWindow, + const InputContext& aInputContext) { + if (aInputContext.mHTMLInputInputmode.EqualsLiteral("none")) { + return; + } +#ifdef NIGHTLY_BUILD + if (FxRWindowManager::GetInstance()->IsFxRWindow(sFocusedWindow)) { + mozilla::gfx::VRShMem shmem(nullptr, true /*aRequiresMutex*/); + shmem.SendIMEState(FxRWindowManager::GetInstance()->GetWindowID(), + mozilla::gfx::VRFxEventState::FOCUS); + return; + } +#endif // NIGHTLY_BUILD + if (!IsWin8OrLater() || !Preferences::GetBool(kOskEnabled, true) || + GetOnScreenKeyboardWindow() || !IMEHandler::NeedOnScreenKeyboard()) { + return; + } + + // On Windows 10 we require tablet mode, unless the user has set the relevant + // Windows setting to enable the on-screen keyboard in desktop mode. + // We might be disabled specifically on Win8(.1), so we check that afterwards. + if (IsWin10OrLater()) { + if (!IsInTabletMode() && !AutoInvokeOnScreenKeyboardInDesktopMode()) { + return; + } + } else if (Preferences::GetBool(kOskRequireWin10, true)) { + return; + } + + IMEHandler::ShowOnScreenKeyboard(aWindow); +} + +// static +void IMEHandler::MaybeDismissOnScreenKeyboard(nsWindow* aWindow, Sync aSync) { +#ifdef NIGHTLY_BUILD + if (FxRWindowManager::GetInstance()->IsFxRWindow(aWindow)) { + mozilla::gfx::VRShMem shmem(nullptr, true /*aRequiresMutex*/); + shmem.SendIMEState(FxRWindowManager::GetInstance()->GetWindowID(), + mozilla::gfx::VRFxEventState::BLUR); + } +#endif // NIGHTLY_BUILD + if (!IsWin8OrLater()) { + return; + } + + if (aSync == Sync::Yes) { + DismissOnScreenKeyboard(aWindow); + return; + } + + RefPtr<nsWindow> window(aWindow); + NS_DispatchToCurrentThreadQueue( + NS_NewRunnableFunction("IMEHandler::MaybeDismissOnScreenKeyboard", + [window]() { + if (window->Destroyed()) { + return; + } + if (!sFocusedWindow) { + DismissOnScreenKeyboard(window); + } + }), + EventQueuePriority::Idle); +} + +// static +bool IMEHandler::WStringStartsWithCaseInsensitive(const std::wstring& aHaystack, + const std::wstring& aNeedle) { + std::wstring lowerCaseHaystack(aHaystack); + std::wstring lowerCaseNeedle(aNeedle); + std::transform(lowerCaseHaystack.begin(), lowerCaseHaystack.end(), + lowerCaseHaystack.begin(), ::tolower); + std::transform(lowerCaseNeedle.begin(), lowerCaseNeedle.end(), + lowerCaseNeedle.begin(), ::tolower); + return wcsstr(lowerCaseHaystack.c_str(), lowerCaseNeedle.c_str()) == + lowerCaseHaystack.c_str(); +} + +// Returns false if a physical keyboard is detected on Windows 8 and up, +// or there is some other reason why an onscreen keyboard is not necessary. +// Returns true if no keyboard is found and this device looks like it needs +// an on-screen keyboard for text input. +// static +bool IMEHandler::NeedOnScreenKeyboard() { + // This function is only supported for Windows 8 and up. + if (!IsWin8OrLater()) { + Preferences::SetString(kOskDebugReason, L"IKPOS: Requires Win8+."); + return false; + } + + if (!Preferences::GetBool(kOskDetectPhysicalKeyboard, true)) { + Preferences::SetString(kOskDebugReason, L"IKPOS: Detection disabled."); + return true; + } + + // If the last focus cause was not user-initiated (ie a result of code + // setting focus to an element) then don't auto-show a keyboard. This + // avoids cases where the keyboard would pop up "just" because e.g. a + // web page chooses to focus a search field on the page, even when that + // really isn't what the user is trying to do at that moment. + if (!InputContextAction::IsHandlingUserInput(sLastContextActionCause)) { + return false; + } + + // This function should be only invoked for machines with touch screens. + if ((::GetSystemMetrics(SM_DIGITIZER) & NID_INTEGRATED_TOUCH) != + NID_INTEGRATED_TOUCH) { + Preferences::SetString(kOskDebugReason, L"IKPOS: Touch screen not found."); + return false; + } + + // If the device is docked, the user is treating the device as a PC. + if (::GetSystemMetrics(SM_SYSTEMDOCKED) != 0) { + Preferences::SetString(kOskDebugReason, L"IKPOS: System docked."); + return false; + } + + // To determine whether a keyboard is present on the device, we do the + // following:- + // 1. If the platform role is that of a mobile or slate device, check the + // system metric SM_CONVERTIBLESLATEMODE to see if it is being used + // in slate mode. If it is, also check that the last input was a touch. + // If all of this is true, then we should show the on-screen keyboard. + + // 2. If step 1 didn't determine we should show the keyboard, we check if + // this device has keyboards attached to it. + + // Check if the device is being used as a laptop or a tablet. This can be + // checked by first checking the role of the device and then the + // corresponding system metric (SM_CONVERTIBLESLATEMODE). If it is being + // used as a tablet then we want the OSK to show up. + if (!sDeterminedPowerPlatformRole) { + sDeterminedPowerPlatformRole = true; + sPowerPlatformRole = WinUtils::GetPowerPlatformRole(); + } + + // If this a mobile or slate (tablet) device, check if it is in slate mode. + // If the last input was touch, ignore whether or not a keyboard is present. + if ((sPowerPlatformRole == PlatformRoleMobile || + sPowerPlatformRole == PlatformRoleSlate) && + ::GetSystemMetrics(SM_CONVERTIBLESLATEMODE) == 0 && + sLastContextActionCause == InputContextAction::CAUSE_TOUCH) { + Preferences::SetString( + kOskDebugReason, + L"IKPOS: Mobile/Slate Platform role, in slate mode with touch event."); + return true; + } + + return !IMEHandler::IsKeyboardPresentOnSlate(); +} + +// Uses the Setup APIs to enumerate the attached keyboards and returns true +// if the keyboard count is 1 or more. While this will work in most cases +// it won't work if there are devices which expose keyboard interfaces which +// are attached to the machine. +// Based on IsKeyboardPresentOnSlate() in Chromium's base/win/win_util.cc. +// static +bool IMEHandler::IsKeyboardPresentOnSlate() { + const GUID KEYBOARD_CLASS_GUID = { + 0x4D36E96B, + 0xE325, + 0x11CE, + {0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18}}; + + // Query for all the keyboard devices. + HDEVINFO device_info = ::SetupDiGetClassDevs(&KEYBOARD_CLASS_GUID, nullptr, + nullptr, DIGCF_PRESENT); + if (device_info == INVALID_HANDLE_VALUE) { + Preferences::SetString(kOskDebugReason, L"IKPOS: No keyboard info."); + return false; + } + + // Enumerate all keyboards and look for ACPI\PNP and HID\VID devices. If + // the count is more than 1 we assume that a keyboard is present. This is + // under the assumption that there will always be one keyboard device. + for (DWORD i = 0;; ++i) { + SP_DEVINFO_DATA device_info_data = {0}; + device_info_data.cbSize = sizeof(device_info_data); + if (!::SetupDiEnumDeviceInfo(device_info, i, &device_info_data)) { + break; + } + + // Get the device ID. + wchar_t device_id[MAX_DEVICE_ID_LEN]; + CONFIGRET status = ::CM_Get_Device_ID(device_info_data.DevInst, device_id, + MAX_DEVICE_ID_LEN, 0); + if (status == CR_SUCCESS) { + static const std::wstring BT_HID_DEVICE = L"HID\\{00001124"; + static const std::wstring BT_HOGP_DEVICE = L"HID\\{00001812"; + // To reduce the scope of the hack we only look for ACPI and HID\\VID + // prefixes in the keyboard device ids. + if (IMEHandler::WStringStartsWithCaseInsensitive(device_id, L"ACPI") || + IMEHandler::WStringStartsWithCaseInsensitive(device_id, + L"HID\\VID") || + IMEHandler::WStringStartsWithCaseInsensitive(device_id, + BT_HID_DEVICE) || + IMEHandler::WStringStartsWithCaseInsensitive(device_id, + BT_HOGP_DEVICE)) { + // The heuristic we are using is to check the count of keyboards and + // return true if the API's report one or more keyboards. Please note + // that this will break for non keyboard devices which expose a + // keyboard PDO. + Preferences::SetString(kOskDebugReason, + L"IKPOS: Keyboard presence confirmed."); + return true; + } + } + } + Preferences::SetString(kOskDebugReason, + L"IKPOS: Lack of keyboard confirmed."); + return false; +} + +// static +bool IMEHandler::IsInTabletMode() { + nsCOMPtr<nsIWindowsUIUtils> uiUtils( + do_GetService("@mozilla.org/windows-ui-utils;1")); + if (NS_WARN_IF(!uiUtils)) { + Preferences::SetString(kOskDebugReason, + L"IITM: nsIWindowsUIUtils not available."); + return false; + } + bool isInTabletMode = false; + uiUtils->GetInTabletMode(&isInTabletMode); + if (isInTabletMode) { + Preferences::SetString(kOskDebugReason, L"IITM: GetInTabletMode=true."); + } else { + Preferences::SetString(kOskDebugReason, L"IITM: GetInTabletMode=false."); + } + return isInTabletMode; +} + +static bool ReadEnableDesktopModeAutoInvoke(uint32_t aRoot, + nsIWindowsRegKey* aRegKey, + uint32_t& aValue) { + nsresult rv; + rv = aRegKey->Open(aRoot, u"SOFTWARE\\Microsoft\\TabletTip\\1.7"_ns, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) { + Preferences::SetString(kOskDebugReason, + L"AIOSKIDM: failed opening regkey."); + return false; + } + // EnableDesktopModeAutoInvoke is an opt-in option from the Windows + // Settings to "Automatically show the touch keyboard in windowed apps + // when there's no keyboard attached to your device." If the user has + // opted-in to this behavior, the tablet-mode requirement is skipped. + rv = aRegKey->ReadIntValue(u"EnableDesktopModeAutoInvoke"_ns, &aValue); + if (NS_FAILED(rv)) { + Preferences::SetString(kOskDebugReason, + L"AIOSKIDM: failed reading value of regkey."); + return false; + } + return true; +} + +// static +bool IMEHandler::AutoInvokeOnScreenKeyboardInDesktopMode() { + nsresult rv; + nsCOMPtr<nsIWindowsRegKey> regKey( + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv)); + if (NS_WARN_IF(NS_FAILED(rv))) { + Preferences::SetString(kOskDebugReason, + L"AIOSKIDM: " + L"nsIWindowsRegKey not available"); + return false; + } + + uint32_t value; + if (!ReadEnableDesktopModeAutoInvoke(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + regKey, value) && + !ReadEnableDesktopModeAutoInvoke(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, + regKey, value)) { + return false; + } + if (!!value) { + Preferences::SetString(kOskDebugReason, L"AIOSKIDM: regkey value=true."); + } else { + Preferences::SetString(kOskDebugReason, L"AIOSKIDM: regkey value=false."); + } + return !!value; +} + +// Based on DisplayVirtualKeyboard() in Chromium's base/win/win_util.cc. +// static +void IMEHandler::ShowOnScreenKeyboard(nsWindow* aWindow) { + if (IsWin10AnniversaryUpdateOrLater()) { + OSKInputPaneManager::ShowOnScreenKeyboard(aWindow->GetWindowHandle()); + return; + } + + nsAutoString cachedPath; + nsresult result = Preferences::GetString(kOskPathPrefName, cachedPath); + if (NS_FAILED(result) || cachedPath.IsEmpty()) { + wchar_t path[MAX_PATH]; + // The path to TabTip.exe is defined at the following registry key. + // This is pulled out of the 64-bit registry hive directly. + const wchar_t kRegKeyName[] = + L"Software\\Classes\\CLSID\\" + L"{054AAE20-4BEA-4347-8A35-64A533254A9D}\\LocalServer32"; + if (!WinUtils::GetRegistryKey(HKEY_LOCAL_MACHINE, kRegKeyName, nullptr, + path, sizeof path)) { + return; + } + + std::wstring wstrpath(path); + // The path provided by the registry will often contain + // %CommonProgramFiles%, which will need to be replaced if it is present. + size_t commonProgramFilesOffset = wstrpath.find(L"%CommonProgramFiles%"); + if (commonProgramFilesOffset != std::wstring::npos) { + // The path read from the registry contains the %CommonProgramFiles% + // environment variable prefix. On 64 bit Windows the + // SHGetKnownFolderPath function returns the common program files path + // with the X86 suffix for the FOLDERID_ProgramFilesCommon value. + // To get the correct path to TabTip.exe we first read the environment + // variable CommonProgramW6432 which points to the desired common + // files path. Failing that we fallback to the SHGetKnownFolderPath API. + + // We then replace the %CommonProgramFiles% value with the actual common + // files path found in the process. + std::wstring commonProgramFilesPath; + std::vector<wchar_t> commonProgramFilesPathW6432; + DWORD bufferSize = + ::GetEnvironmentVariableW(L"CommonProgramW6432", nullptr, 0); + if (bufferSize) { + commonProgramFilesPathW6432.resize(bufferSize); + ::GetEnvironmentVariableW(L"CommonProgramW6432", + commonProgramFilesPathW6432.data(), + bufferSize); + commonProgramFilesPath = + std::wstring(commonProgramFilesPathW6432.data()); + } else { + PWSTR path = nullptr; + HRESULT hres = SHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, 0, + nullptr, &path); + if (FAILED(hres) || !path) { + return; + } + commonProgramFilesPath = + static_cast<const wchar_t*>(nsDependentString(path).get()); + ::CoTaskMemFree(path); + } + wstrpath.replace(commonProgramFilesOffset, + wcslen(L"%CommonProgramFiles%"), commonProgramFilesPath); + } + + cachedPath.Assign(wstrpath.data()); + Preferences::SetString(kOskPathPrefName, cachedPath); + } + + const char16_t* cachedPathPtr; + cachedPath.GetData(&cachedPathPtr); + ShellExecuteW(nullptr, L"", char16ptr_t(cachedPathPtr), nullptr, nullptr, + SW_SHOW); +} + +// Based on DismissVirtualKeyboard() in Chromium's base/win/win_util.cc. +// static +void IMEHandler::DismissOnScreenKeyboard(nsWindow* aWindow) { + // Dismiss the virtual keyboard if it's open + if (IsWin10AnniversaryUpdateOrLater()) { + OSKInputPaneManager::DismissOnScreenKeyboard(aWindow->GetWindowHandle()); + return; + } + + HWND osk = GetOnScreenKeyboardWindow(); + if (osk) { + ::PostMessage(osk, WM_SYSCOMMAND, SC_CLOSE, 0); + } +} + +// static +HWND IMEHandler::GetOnScreenKeyboardWindow() { + const wchar_t kOSKClassName[] = L"IPTip_Main_Window"; + HWND osk = ::FindWindowW(kOSKClassName, nullptr); + if (::IsWindow(osk) && ::IsWindowEnabled(osk) && ::IsWindowVisible(osk)) { + return osk; + } + return nullptr; +} + +bool IMEHandler::MaybeCreateNativeCaret(nsWindow* aWindow) { + MOZ_ASSERT(aWindow); + + if (IsA11yHandlingNativeCaret()) { + return false; + } + + if (!sHasNativeCaretBeenRequested) { + // If we have not received WM_GETOBJECT for OBJID_CARET, there may be new + // application which requires our caret information. For kicking its + // window event proc, we should fire a window event here. + // (If there is such application, sHasNativeCaretBeenRequested will be set + // to true later.) + // FYI: If we create native caret and move its position, native caret + // causes EVENT_OBJECT_LOCATIONCHANGE event with OBJID_CARET and + // OBJID_CLIENT. + ::NotifyWinEvent(EVENT_OBJECT_LOCATIONCHANGE, aWindow->GetWindowHandle(), + OBJID_CARET, OBJID_CLIENT); + return false; + } + + MaybeDestroyNativeCaret(); + + // If focused content is not text editable, we don't support caret + // caret information without a11y module. + if (!aWindow->GetInputContext().mIMEState.IsEditable()) { + return false; + } + + WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, aWindow); + aWindow->InitEvent(queryCaretRectEvent); + + WidgetQueryContentEvent::Options options; + options.mRelativeToInsertionPoint = true; + queryCaretRectEvent.InitForQueryCaretRect(0, options); + + aWindow->DispatchWindowEvent(&queryCaretRectEvent); + if (NS_WARN_IF(queryCaretRectEvent.Failed())) { + return false; + } + + return CreateNativeCaret(aWindow, queryCaretRectEvent.mReply->mRect); +} + +bool IMEHandler::CreateNativeCaret(nsWindow* aWindow, + const LayoutDeviceIntRect& aCaretRect) { + MOZ_ASSERT(aWindow); + + MOZ_ASSERT(!IsA11yHandlingNativeCaret()); + + sNativeCaretIsCreated = + ::CreateCaret(aWindow->GetWindowHandle(), nullptr, aCaretRect.Width(), + aCaretRect.Height()); + if (!sNativeCaretIsCreated) { + return false; + } + nsWindow* toplevelWindow = aWindow->GetTopLevelWindow(false); + if (NS_WARN_IF(!toplevelWindow)) { + MaybeDestroyNativeCaret(); + return false; + } + + LayoutDeviceIntPoint caretPosition(aCaretRect.TopLeft()); + if (toplevelWindow != aWindow) { + caretPosition += toplevelWindow->WidgetToScreenOffset(); + caretPosition -= aWindow->WidgetToScreenOffset(); + } + + ::SetCaretPos(caretPosition.x, caretPosition.y); + return true; +} + +void IMEHandler::MaybeDestroyNativeCaret() { + if (!sNativeCaretIsCreated) { + return; + } + ::DestroyCaret(); + sNativeCaretIsCreated = false; +} + +} // namespace widget +} // namespace mozilla |