From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- widget/windows/AudioSession.cpp | 351 + widget/windows/AudioSession.h | 19 + widget/windows/CheckInvariantWrapper.h | 78 + widget/windows/CompositorWidgetChild.cpp | 114 + widget/windows/CompositorWidgetChild.h | 62 + widget/windows/CompositorWidgetParent.cpp | 240 + widget/windows/CompositorWidgetParent.h | 93 + widget/windows/DirectManipulationOwner.cpp | 758 ++ widget/windows/DirectManipulationOwner.h | 56 + widget/windows/GfxInfo.cpp | 2048 +++++ widget/windows/GfxInfo.h | 106 + widget/windows/IEnumFE.cpp | 139 + widget/windows/IEnumFE.h | 88 + widget/windows/IMMHandler.cpp | 2410 +++++ widget/windows/IMMHandler.h | 424 + widget/windows/InProcessWinCompositorWidget.cpp | 363 + widget/windows/InProcessWinCompositorWidget.h | 112 + widget/windows/InkCollector.cpp | 245 + widget/windows/InkCollector.h | 101 + widget/windows/InputDeviceUtils.cpp | 61 + widget/windows/InputDeviceUtils.h | 26 + widget/windows/JumpListBuilder.cpp | 647 ++ widget/windows/JumpListBuilder.h | 70 + widget/windows/JumpListItem.cpp | 575 ++ widget/windows/JumpListItem.h | 131 + widget/windows/KeyboardLayout.cpp | 5383 +++++++++++ widget/windows/KeyboardLayout.h | 1120 +++ widget/windows/LSPAnnotator.cpp | 135 + widget/windows/MediaKeysEventSourceFactory.cpp | 20 + widget/windows/OSKInputPaneManager.cpp | 101 + widget/windows/OSKInputPaneManager.h | 24 + widget/windows/OSKTabTipManager.cpp | 113 + widget/windows/OSKTabTipManager.h | 22 + widget/windows/OSKVRManager.cpp | 35 + widget/windows/OSKVRManager.h | 22 + widget/windows/PCompositorWidget.ipdl | 49 + widget/windows/PlatformWidgetTypes.ipdlh | 34 + widget/windows/RemoteBackbuffer.cpp | 711 ++ widget/windows/RemoteBackbuffer.h | 92 + widget/windows/ScreenHelperWin.cpp | 157 + widget/windows/ScreenHelperWin.h | 26 + widget/windows/ShellHeaderOnlyUtils.h | 178 + widget/windows/SystemStatusBar.cpp | 337 + widget/windows/SystemStatusBar.h | 32 + widget/windows/TSFTextStore.cpp | 7462 ++++++++++++++++ widget/windows/TSFTextStore.h | 1157 +++ widget/windows/TaskbarPreview.cpp | 416 + widget/windows/TaskbarPreview.h | 132 + widget/windows/TaskbarPreviewButton.cpp | 137 + widget/windows/TaskbarPreviewButton.h | 47 + widget/windows/TaskbarTabPreview.cpp | 345 + widget/windows/TaskbarTabPreview.h | 70 + widget/windows/TaskbarWindowPreview.cpp | 326 + widget/windows/TaskbarWindowPreview.h | 85 + widget/windows/ToastNotification.cpp | 891 ++ widget/windows/ToastNotification.h | 71 + widget/windows/ToastNotificationHandler.cpp | 1089 +++ widget/windows/ToastNotificationHandler.h | 151 + widget/windows/ToastNotificationHeaderOnlyUtils.h | 151 + widget/windows/UrlmonHeaderOnlyUtils.h | 76 + widget/windows/WidgetTraceEvent.cpp | 119 + widget/windows/WinCompositorWidget.cpp | 105 + widget/windows/WinCompositorWidget.h | 105 + widget/windows/WinCompositorWindowThread.cpp | 257 + widget/windows/WinCompositorWindowThread.h | 66 + widget/windows/WinEventObserver.cpp | 217 + widget/windows/WinEventObserver.h | 111 + widget/windows/WinHeaderOnlyUtils.h | 837 ++ widget/windows/WinIMEHandler.cpp | 1085 +++ widget/windows/WinIMEHandler.h | 241 + widget/windows/WinMessages.h | 90 + widget/windows/WinModifierKeyState.h | 59 + widget/windows/WinMouseScrollHandler.cpp | 1668 ++++ widget/windows/WinMouseScrollHandler.h | 574 ++ widget/windows/WinPointerEvents.cpp | 190 + widget/windows/WinPointerEvents.h | 150 + widget/windows/WinTaskbar.cpp | 462 + widget/windows/WinTaskbar.h | 46 + widget/windows/WinTextEventDispatcherListener.cpp | 68 + widget/windows/WinTextEventDispatcherListener.h | 50 + widget/windows/WinUtils.cpp | 2145 +++++ widget/windows/WinUtils.h | 735 ++ widget/windows/WinWindowOcclusionTracker.cpp | 1437 +++ widget/windows/WinWindowOcclusionTracker.h | 323 + widget/windows/WindowHook.cpp | 113 + widget/windows/WindowHook.h | 76 + widget/windows/WindowsConsole.cpp | 51 + widget/windows/WindowsConsole.h | 16 + widget/windows/WindowsEMF.cpp | 94 + widget/windows/WindowsEMF.h | 106 + widget/windows/WindowsEventLog.h | 98 + widget/windows/WindowsSMTCProvider.cpp | 723 ++ widget/windows/WindowsSMTCProvider.h | 128 + widget/windows/WindowsUIUtils.cpp | 825 ++ widget/windows/WindowsUIUtils.h | 49 + widget/windows/components.conf | 213 + widget/windows/docs/blocklist.rst | 352 + widget/windows/docs/index.rst | 8 + widget/windows/moz.build | 206 + widget/windows/nsAppShell.cpp | 809 ++ widget/windows/nsAppShell.h | 64 + widget/windows/nsBidiKeyboard.cpp | 169 + widget/windows/nsBidiKeyboard.h | 34 + widget/windows/nsClipboard.cpp | 1334 +++ widget/windows/nsClipboard.h | 106 + widget/windows/nsColorPicker.cpp | 221 + widget/windows/nsColorPicker.h | 55 + widget/windows/nsDataObj.cpp | 2253 +++++ widget/windows/nsDataObj.h | 312 + widget/windows/nsDataObjCollection.cpp | 370 + widget/windows/nsDataObjCollection.h | 92 + widget/windows/nsDeviceContextSpecWin.cpp | 671 ++ widget/windows/nsDeviceContextSpecWin.h | 98 + widget/windows/nsDragService.cpp | 594 ++ widget/windows/nsDragService.h | 67 + widget/windows/nsFilePicker.cpp | 608 ++ widget/windows/nsFilePicker.h | 109 + widget/windows/nsLookAndFeel.cpp | 849 ++ widget/windows/nsLookAndFeel.h | 87 + widget/windows/nsNativeDragSource.cpp | 97 + widget/windows/nsNativeDragSource.h | 68 + widget/windows/nsNativeDragTarget.cpp | 472 + widget/windows/nsNativeDragTarget.h | 103 + widget/windows/nsNativeThemeWin.cpp | 3530 ++++++++ widget/windows/nsNativeThemeWin.h | 175 + widget/windows/nsPrintDialogUtil.cpp | 374 + widget/windows/nsPrintDialogUtil.h | 11 + widget/windows/nsPrintDialogWin.cpp | 180 + widget/windows/nsPrintDialogWin.h | 39 + widget/windows/nsPrintSettingsServiceWin.cpp | 127 + widget/windows/nsPrintSettingsServiceWin.h | 29 + widget/windows/nsPrintSettingsWin.cpp | 477 + widget/windows/nsPrintSettingsWin.h | 62 + widget/windows/nsPrinterWin.cpp | 521 ++ widget/windows/nsPrinterWin.h | 53 + widget/windows/nsSharePicker.cpp | 81 + widget/windows/nsSharePicker.h | 29 + widget/windows/nsSound.cpp | 330 + widget/windows/nsSound.h | 47 + widget/windows/nsToolkit.cpp | 69 + widget/windows/nsToolkit.h | 47 + widget/windows/nsUXThemeConstants.h | 258 + widget/windows/nsUXThemeData.cpp | 358 + widget/windows/nsUXThemeData.h | 113 + widget/windows/nsUserIdleServiceWin.cpp | 22 + widget/windows/nsUserIdleServiceWin.h | 43 + widget/windows/nsWidgetFactory.cpp | 60 + widget/windows/nsWidgetFactory.h | 21 + widget/windows/nsWinGesture.cpp | 388 + widget/windows/nsWinGesture.h | 91 + widget/windows/nsWindow.cpp | 9416 ++++++++++++++++++++ widget/windows/nsWindow.h | 907 ++ widget/windows/nsWindowDbg.cpp | 1499 ++++ widget/windows/nsWindowDbg.h | 124 + widget/windows/nsWindowDefs.h | 129 + widget/windows/nsWindowGfx.cpp | 701 ++ widget/windows/nsWindowGfx.h | 35 + widget/windows/nsWindowLoggedMessages.cpp | 305 + widget/windows/nsWindowLoggedMessages.h | 26 + widget/windows/nsWindowTaskbarConcealer.cpp | 432 + widget/windows/nsWindowTaskbarConcealer.h | 51 + widget/windows/nsdefs.h | 58 + widget/windows/res/aliasb.cur | Bin 0 -> 326 bytes widget/windows/res/cell.cur | Bin 0 -> 326 bytes widget/windows/res/col_resize.cur | Bin 0 -> 326 bytes widget/windows/res/copy.cur | Bin 0 -> 326 bytes widget/windows/res/grab.cur | Bin 0 -> 326 bytes widget/windows/res/grabbing.cur | Bin 0 -> 326 bytes widget/windows/res/none.cur | Bin 0 -> 326 bytes widget/windows/res/row_resize.cur | Bin 0 -> 326 bytes widget/windows/res/select.cur | Bin 0 -> 326 bytes widget/windows/res/vertical_text.cur | Bin 0 -> 326 bytes widget/windows/res/zoom_in.cur | Bin 0 -> 326 bytes widget/windows/res/zoom_out.cur | Bin 0 -> 326 bytes widget/windows/resource.h | 16 + widget/windows/tests/TestUriValidation.cpp | 135 + widget/windows/tests/TestUrisToValidate.h | 471 + widget/windows/tests/TestWinDND.cpp | 696 ++ widget/windows/tests/moz.build | 31 + .../tests/unit/test_windows_alert_service.js | 314 + widget/windows/tests/unit/xpcshell.ini | 4 + widget/windows/touchinjection_sdk80.h | 171 + widget/windows/widget.rc | 30 + 183 files changed, 77440 insertions(+) create mode 100644 widget/windows/AudioSession.cpp create mode 100644 widget/windows/AudioSession.h create mode 100644 widget/windows/CheckInvariantWrapper.h create mode 100644 widget/windows/CompositorWidgetChild.cpp create mode 100644 widget/windows/CompositorWidgetChild.h create mode 100644 widget/windows/CompositorWidgetParent.cpp create mode 100644 widget/windows/CompositorWidgetParent.h create mode 100644 widget/windows/DirectManipulationOwner.cpp create mode 100644 widget/windows/DirectManipulationOwner.h create mode 100644 widget/windows/GfxInfo.cpp create mode 100644 widget/windows/GfxInfo.h create mode 100644 widget/windows/IEnumFE.cpp create mode 100644 widget/windows/IEnumFE.h create mode 100644 widget/windows/IMMHandler.cpp create mode 100644 widget/windows/IMMHandler.h create mode 100644 widget/windows/InProcessWinCompositorWidget.cpp create mode 100644 widget/windows/InProcessWinCompositorWidget.h create mode 100644 widget/windows/InkCollector.cpp create mode 100644 widget/windows/InkCollector.h create mode 100644 widget/windows/InputDeviceUtils.cpp create mode 100644 widget/windows/InputDeviceUtils.h create mode 100644 widget/windows/JumpListBuilder.cpp create mode 100644 widget/windows/JumpListBuilder.h create mode 100644 widget/windows/JumpListItem.cpp create mode 100644 widget/windows/JumpListItem.h create mode 100644 widget/windows/KeyboardLayout.cpp create mode 100644 widget/windows/KeyboardLayout.h create mode 100644 widget/windows/LSPAnnotator.cpp create mode 100644 widget/windows/MediaKeysEventSourceFactory.cpp create mode 100644 widget/windows/OSKInputPaneManager.cpp create mode 100644 widget/windows/OSKInputPaneManager.h create mode 100644 widget/windows/OSKTabTipManager.cpp create mode 100644 widget/windows/OSKTabTipManager.h create mode 100644 widget/windows/OSKVRManager.cpp create mode 100644 widget/windows/OSKVRManager.h create mode 100644 widget/windows/PCompositorWidget.ipdl create mode 100644 widget/windows/PlatformWidgetTypes.ipdlh create mode 100644 widget/windows/RemoteBackbuffer.cpp create mode 100644 widget/windows/RemoteBackbuffer.h create mode 100644 widget/windows/ScreenHelperWin.cpp create mode 100644 widget/windows/ScreenHelperWin.h create mode 100644 widget/windows/ShellHeaderOnlyUtils.h create mode 100644 widget/windows/SystemStatusBar.cpp create mode 100644 widget/windows/SystemStatusBar.h create mode 100644 widget/windows/TSFTextStore.cpp create mode 100644 widget/windows/TSFTextStore.h create mode 100644 widget/windows/TaskbarPreview.cpp create mode 100644 widget/windows/TaskbarPreview.h create mode 100644 widget/windows/TaskbarPreviewButton.cpp create mode 100644 widget/windows/TaskbarPreviewButton.h create mode 100644 widget/windows/TaskbarTabPreview.cpp create mode 100644 widget/windows/TaskbarTabPreview.h create mode 100644 widget/windows/TaskbarWindowPreview.cpp create mode 100644 widget/windows/TaskbarWindowPreview.h create mode 100644 widget/windows/ToastNotification.cpp create mode 100644 widget/windows/ToastNotification.h create mode 100644 widget/windows/ToastNotificationHandler.cpp create mode 100644 widget/windows/ToastNotificationHandler.h create mode 100644 widget/windows/ToastNotificationHeaderOnlyUtils.h create mode 100644 widget/windows/UrlmonHeaderOnlyUtils.h create mode 100644 widget/windows/WidgetTraceEvent.cpp create mode 100644 widget/windows/WinCompositorWidget.cpp create mode 100644 widget/windows/WinCompositorWidget.h create mode 100644 widget/windows/WinCompositorWindowThread.cpp create mode 100644 widget/windows/WinCompositorWindowThread.h create mode 100644 widget/windows/WinEventObserver.cpp create mode 100644 widget/windows/WinEventObserver.h create mode 100644 widget/windows/WinHeaderOnlyUtils.h create mode 100644 widget/windows/WinIMEHandler.cpp create mode 100644 widget/windows/WinIMEHandler.h create mode 100644 widget/windows/WinMessages.h create mode 100644 widget/windows/WinModifierKeyState.h create mode 100644 widget/windows/WinMouseScrollHandler.cpp create mode 100644 widget/windows/WinMouseScrollHandler.h create mode 100644 widget/windows/WinPointerEvents.cpp create mode 100644 widget/windows/WinPointerEvents.h create mode 100644 widget/windows/WinTaskbar.cpp create mode 100644 widget/windows/WinTaskbar.h create mode 100644 widget/windows/WinTextEventDispatcherListener.cpp create mode 100644 widget/windows/WinTextEventDispatcherListener.h create mode 100644 widget/windows/WinUtils.cpp create mode 100644 widget/windows/WinUtils.h create mode 100644 widget/windows/WinWindowOcclusionTracker.cpp create mode 100644 widget/windows/WinWindowOcclusionTracker.h create mode 100644 widget/windows/WindowHook.cpp create mode 100644 widget/windows/WindowHook.h create mode 100644 widget/windows/WindowsConsole.cpp create mode 100644 widget/windows/WindowsConsole.h create mode 100644 widget/windows/WindowsEMF.cpp create mode 100644 widget/windows/WindowsEMF.h create mode 100644 widget/windows/WindowsEventLog.h create mode 100644 widget/windows/WindowsSMTCProvider.cpp create mode 100644 widget/windows/WindowsSMTCProvider.h create mode 100644 widget/windows/WindowsUIUtils.cpp create mode 100644 widget/windows/WindowsUIUtils.h create mode 100644 widget/windows/components.conf create mode 100644 widget/windows/docs/blocklist.rst create mode 100644 widget/windows/docs/index.rst create mode 100644 widget/windows/moz.build create mode 100644 widget/windows/nsAppShell.cpp create mode 100644 widget/windows/nsAppShell.h create mode 100644 widget/windows/nsBidiKeyboard.cpp create mode 100644 widget/windows/nsBidiKeyboard.h create mode 100644 widget/windows/nsClipboard.cpp create mode 100644 widget/windows/nsClipboard.h create mode 100644 widget/windows/nsColorPicker.cpp create mode 100644 widget/windows/nsColorPicker.h create mode 100644 widget/windows/nsDataObj.cpp create mode 100644 widget/windows/nsDataObj.h create mode 100644 widget/windows/nsDataObjCollection.cpp create mode 100644 widget/windows/nsDataObjCollection.h create mode 100644 widget/windows/nsDeviceContextSpecWin.cpp create mode 100644 widget/windows/nsDeviceContextSpecWin.h create mode 100644 widget/windows/nsDragService.cpp create mode 100644 widget/windows/nsDragService.h create mode 100644 widget/windows/nsFilePicker.cpp create mode 100644 widget/windows/nsFilePicker.h create mode 100644 widget/windows/nsLookAndFeel.cpp create mode 100644 widget/windows/nsLookAndFeel.h create mode 100644 widget/windows/nsNativeDragSource.cpp create mode 100644 widget/windows/nsNativeDragSource.h create mode 100644 widget/windows/nsNativeDragTarget.cpp create mode 100644 widget/windows/nsNativeDragTarget.h create mode 100644 widget/windows/nsNativeThemeWin.cpp create mode 100644 widget/windows/nsNativeThemeWin.h create mode 100644 widget/windows/nsPrintDialogUtil.cpp create mode 100644 widget/windows/nsPrintDialogUtil.h create mode 100644 widget/windows/nsPrintDialogWin.cpp create mode 100644 widget/windows/nsPrintDialogWin.h create mode 100644 widget/windows/nsPrintSettingsServiceWin.cpp create mode 100644 widget/windows/nsPrintSettingsServiceWin.h create mode 100644 widget/windows/nsPrintSettingsWin.cpp create mode 100644 widget/windows/nsPrintSettingsWin.h create mode 100644 widget/windows/nsPrinterWin.cpp create mode 100644 widget/windows/nsPrinterWin.h create mode 100644 widget/windows/nsSharePicker.cpp create mode 100644 widget/windows/nsSharePicker.h create mode 100644 widget/windows/nsSound.cpp create mode 100644 widget/windows/nsSound.h create mode 100644 widget/windows/nsToolkit.cpp create mode 100644 widget/windows/nsToolkit.h create mode 100644 widget/windows/nsUXThemeConstants.h create mode 100644 widget/windows/nsUXThemeData.cpp create mode 100644 widget/windows/nsUXThemeData.h create mode 100644 widget/windows/nsUserIdleServiceWin.cpp create mode 100644 widget/windows/nsUserIdleServiceWin.h create mode 100644 widget/windows/nsWidgetFactory.cpp create mode 100644 widget/windows/nsWidgetFactory.h create mode 100644 widget/windows/nsWinGesture.cpp create mode 100644 widget/windows/nsWinGesture.h create mode 100644 widget/windows/nsWindow.cpp create mode 100644 widget/windows/nsWindow.h create mode 100644 widget/windows/nsWindowDbg.cpp create mode 100644 widget/windows/nsWindowDbg.h create mode 100644 widget/windows/nsWindowDefs.h create mode 100644 widget/windows/nsWindowGfx.cpp create mode 100644 widget/windows/nsWindowGfx.h create mode 100644 widget/windows/nsWindowLoggedMessages.cpp create mode 100644 widget/windows/nsWindowLoggedMessages.h create mode 100644 widget/windows/nsWindowTaskbarConcealer.cpp create mode 100644 widget/windows/nsWindowTaskbarConcealer.h create mode 100644 widget/windows/nsdefs.h create mode 100644 widget/windows/res/aliasb.cur create mode 100644 widget/windows/res/cell.cur create mode 100644 widget/windows/res/col_resize.cur create mode 100644 widget/windows/res/copy.cur create mode 100644 widget/windows/res/grab.cur create mode 100644 widget/windows/res/grabbing.cur create mode 100644 widget/windows/res/none.cur create mode 100644 widget/windows/res/row_resize.cur create mode 100644 widget/windows/res/select.cur create mode 100644 widget/windows/res/vertical_text.cur create mode 100644 widget/windows/res/zoom_in.cur create mode 100644 widget/windows/res/zoom_out.cur create mode 100644 widget/windows/resource.h create mode 100644 widget/windows/tests/TestUriValidation.cpp create mode 100644 widget/windows/tests/TestUrisToValidate.h create mode 100644 widget/windows/tests/TestWinDND.cpp create mode 100644 widget/windows/tests/moz.build create mode 100644 widget/windows/tests/unit/test_windows_alert_service.js create mode 100644 widget/windows/tests/unit/xpcshell.ini create mode 100644 widget/windows/touchinjection_sdk80.h create mode 100644 widget/windows/widget.rc (limited to 'widget/windows') diff --git a/widget/windows/AudioSession.cpp b/widget/windows/AudioSession.cpp new file mode 100644 index 0000000000..a38e2a83f6 --- /dev/null +++ b/widget/windows/AudioSession.cpp @@ -0,0 +1,351 @@ +/* -*- 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 +#include +#include +#include + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPtr.h" +#include "nsIStringBundle.h" + +#include "nsCOMPtr.h" +#include "nsID.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "mozilla/Attributes.h" +#include "mozilla/mscom/AgileReference.h" +#include "mozilla/mscom/Utils.h" +#include "mozilla/Mutex.h" +#include "mozilla/WindowsVersion.h" + +namespace mozilla { +namespace widget { + +/* + * To take advantage of what Vista+ have to offer with respect to audio, + * we need to maintain an audio session. This class wraps IAudioSessionControl + * and implements IAudioSessionEvents (for callbacks from Windows) + */ +class AudioSession final : public IAudioSessionEvents { + public: + AudioSession(); + + static AudioSession* GetSingleton(); + + // COM IUnknown + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP QueryInterface(REFIID, void**); + STDMETHODIMP_(ULONG) Release(); + + // IAudioSessionEvents + STDMETHODIMP OnChannelVolumeChanged(DWORD aChannelCount, + float aChannelVolumeArray[], + DWORD aChangedChannel, LPCGUID aContext); + STDMETHODIMP OnDisplayNameChanged(LPCWSTR aDisplayName, LPCGUID aContext); + STDMETHODIMP OnGroupingParamChanged(LPCGUID aGroupingParam, LPCGUID aContext); + STDMETHODIMP OnIconPathChanged(LPCWSTR aIconPath, LPCGUID aContext); + STDMETHODIMP OnSessionDisconnected(AudioSessionDisconnectReason aReason); + STDMETHODIMP OnSimpleVolumeChanged(float aVolume, BOOL aMute, + LPCGUID aContext); + STDMETHODIMP OnStateChanged(AudioSessionState aState); + + void Start(); + void Stop(bool shouldRestart = false); + + nsresult GetSessionData(nsID& aID, nsString& aSessionName, + nsString& aIconPath); + nsresult SetSessionData(const nsID& aID, const nsString& aSessionName, + const nsString& aIconPath); + + private: + ~AudioSession() = default; + + void StopInternal(const MutexAutoLock& aProofOfLock, + bool shouldRestart = false); + + protected: + RefPtr mAudioSessionControl; + nsString mDisplayName; + nsString mIconPath; + nsID mSessionGroupingParameter; + // Guards the IAudioSessionControl + mozilla::Mutex mMutex MOZ_UNANNOTATED; + + ThreadSafeAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD +}; + +StaticRefPtr sService; + +void StartAudioSession() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!sService); + sService = new AudioSession(); + + // Destroy AudioSession only after any background task threads have been + // stopped or abandoned. + ClearOnShutdown(&sService, ShutdownPhase::XPCOMShutdownFinal); + + NS_DispatchBackgroundTask( + NS_NewCancelableRunnableFunction("StartAudioSession", []() -> void { + MOZ_ASSERT(AudioSession::GetSingleton(), + "AudioSession should outlive background threads"); + AudioSession::GetSingleton()->Start(); + })); +} + +void StopAudioSession() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(sService); + NS_DispatchBackgroundTask( + NS_NewRunnableFunction("StopAudioSession", []() -> void { + MOZ_ASSERT(AudioSession::GetSingleton(), + "AudioSession should outlive background threads"); + AudioSession::GetSingleton()->Stop(); + })); +} + +AudioSession* AudioSession::GetSingleton() { + MOZ_ASSERT(mscom::IsCurrentThreadMTA()); + return sService; +} + +// It appears Windows will use us on a background thread ... +NS_IMPL_ADDREF(AudioSession) +NS_IMPL_RELEASE(AudioSession) + +STDMETHODIMP +AudioSession::QueryInterface(REFIID iid, void** ppv) { + const IID IID_IAudioSessionEvents = __uuidof(IAudioSessionEvents); + if ((IID_IUnknown == iid) || (IID_IAudioSessionEvents == iid)) { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +AudioSession::AudioSession() : mMutex("AudioSessionControl") { + // This func must be run on the main thread as + // nsStringBundle is not thread safe otherwise + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(XRE_IsParentProcess(), + "Should only get here in a chrome process!"); + + nsCOMPtr bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + MOZ_ASSERT(bundleService); + + nsCOMPtr bundle; + bundleService->CreateBundle("chrome://branding/locale/brand.properties", + getter_AddRefs(bundle)); + MOZ_ASSERT(bundle); + bundle->GetStringFromName("brandFullName", mDisplayName); + + wchar_t* buffer; + mIconPath.GetMutableData(&buffer, MAX_PATH); + ::GetModuleFileNameW(nullptr, buffer, MAX_PATH); + + [[maybe_unused]] nsresult rv = + nsID::GenerateUUIDInPlace(mSessionGroupingParameter); + MOZ_ASSERT(rv == NS_OK); +} + +// Once we are started Windows will hold a reference to us through our +// IAudioSessionEvents interface that will keep us alive until the appshell +// calls Stop. +void AudioSession::Start() { + MOZ_ASSERT(mscom::IsCurrentThreadMTA()); + + const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); + const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); + const IID IID_IAudioSessionManager = __uuidof(IAudioSessionManager); + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mAudioSessionControl); + MOZ_ASSERT(!mDisplayName.IsEmpty() || !mIconPath.IsEmpty(), + "Should never happen ..."); + + auto scopeExit = MakeScopeExit([&] { StopInternal(lock); }); + + RefPtr enumerator; + HRESULT hr = + ::CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, + IID_IMMDeviceEnumerator, getter_AddRefs(enumerator)); + if (FAILED(hr)) { + return; + } + + RefPtr device; + hr = enumerator->GetDefaultAudioEndpoint( + EDataFlow::eRender, ERole::eMultimedia, getter_AddRefs(device)); + if (FAILED(hr)) { + return; + } + + RefPtr manager; + hr = device->Activate(IID_IAudioSessionManager, CLSCTX_ALL, nullptr, + getter_AddRefs(manager)); + if (FAILED(hr)) { + return; + } + + hr = manager->GetAudioSessionControl(&GUID_NULL, 0, + getter_AddRefs(mAudioSessionControl)); + + if (FAILED(hr) || !mAudioSessionControl) { + return; + } + + // Increments refcount of 'this'. + hr = mAudioSessionControl->RegisterAudioSessionNotification(this); + if (FAILED(hr)) { + return; + } + + hr = mAudioSessionControl->SetGroupingParam( + (LPGUID) & (mSessionGroupingParameter), nullptr); + if (FAILED(hr)) { + return; + } + + hr = mAudioSessionControl->SetDisplayName(mDisplayName.get(), nullptr); + if (FAILED(hr)) { + return; + } + + hr = mAudioSessionControl->SetIconPath(mIconPath.get(), nullptr); + if (FAILED(hr)) { + return; + } + + scopeExit.release(); +} + +void AudioSession::Stop(bool shouldRestart) { + MOZ_ASSERT(mscom::IsCurrentThreadMTA()); + + MutexAutoLock lock(mMutex); + StopInternal(lock, shouldRestart); +} + +void AudioSession::StopInternal(const MutexAutoLock& aProofOfLock, + bool shouldRestart) { + if (!mAudioSessionControl) { + return; + } + + // Decrement refcount of 'this' + mAudioSessionControl->UnregisterAudioSessionNotification(this); + + // Deleting the IAudioSessionControl COM object requires the STA/main thread. + // Audio code may concurrently be running on the main thread and it may + // block waiting for this to complete, creating deadlock. So we destroy the + // IAudioSessionControl on the main thread instead. In order to do that, we + // need to marshall the object to the main thread's apartment with an + // AgileReference. + const IID IID_IAudioSessionControl = __uuidof(IAudioSessionControl); + auto agileAsc = MakeUnique( + IID_IAudioSessionControl, mAudioSessionControl); + mAudioSessionControl = nullptr; + NS_DispatchToMainThread(NS_NewRunnableFunction( + "FreeAudioSession", [agileAsc = std::move(agileAsc), + IID_IAudioSessionControl, shouldRestart] { + RefPtr toDelete; + [[maybe_unused]] HRESULT hr = agileAsc->Resolve( + IID_IAudioSessionControl, getter_AddRefs(toDelete)); + MOZ_ASSERT(SUCCEEDED(hr)); + // Now release the AgileReference which holds our only reference to the + // IAudioSessionControl, then maybe restart. + if (shouldRestart) { + NS_DispatchBackgroundTask( + NS_NewCancelableRunnableFunction("RestartAudioSession", [] { + AudioSession* as = AudioSession::GetSingleton(); + MOZ_ASSERT(as); + as->Start(); + })); + } + })); +} + +void CopynsID(nsID& lhs, const nsID& rhs) { + lhs.m0 = rhs.m0; + lhs.m1 = rhs.m1; + lhs.m2 = rhs.m2; + for (int i = 0; i < 8; i++) { + lhs.m3[i] = rhs.m3[i]; + } +} + +nsresult AudioSession::GetSessionData(nsID& aID, nsString& aSessionName, + nsString& aIconPath) { + CopynsID(aID, mSessionGroupingParameter); + aSessionName = mDisplayName; + aIconPath = mIconPath; + + return NS_OK; +} + +nsresult AudioSession::SetSessionData(const nsID& aID, + const nsString& aSessionName, + const nsString& aIconPath) { + MOZ_ASSERT(!XRE_IsParentProcess(), + "Should never get here in a chrome process!"); + CopynsID(mSessionGroupingParameter, aID); + mDisplayName = aSessionName; + mIconPath = aIconPath; + return NS_OK; +} + +STDMETHODIMP +AudioSession::OnChannelVolumeChanged(DWORD aChannelCount, + float aChannelVolumeArray[], + DWORD aChangedChannel, LPCGUID aContext) { + return S_OK; // NOOP +} + +STDMETHODIMP +AudioSession::OnDisplayNameChanged(LPCWSTR aDisplayName, LPCGUID aContext) { + return S_OK; // NOOP +} + +STDMETHODIMP +AudioSession::OnGroupingParamChanged(LPCGUID aGroupingParam, LPCGUID aContext) { + return S_OK; // NOOP +} + +STDMETHODIMP +AudioSession::OnIconPathChanged(LPCWSTR aIconPath, LPCGUID aContext) { + return S_OK; // NOOP +} + +STDMETHODIMP +AudioSession::OnSessionDisconnected(AudioSessionDisconnectReason aReason) { + Stop(true /* shouldRestart */); + return S_OK; +} + +STDMETHODIMP +AudioSession::OnSimpleVolumeChanged(float aVolume, BOOL aMute, + LPCGUID aContext) { + return S_OK; // NOOP +} + +STDMETHODIMP +AudioSession::OnStateChanged(AudioSessionState aState) { + return S_OK; // NOOP +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/AudioSession.h b/widget/windows/AudioSession.h new file mode 100644 index 0000000000..612a5e209b --- /dev/null +++ b/widget/windows/AudioSession.h @@ -0,0 +1,19 @@ +/* -*- 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 "nsString.h" + +namespace mozilla { +namespace widget { + +// Start the audio session in the current process +void StartAudioSession(); + +// Stop the audio session in the current process +void StopAudioSession(); + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/CheckInvariantWrapper.h b/widget/windows/CheckInvariantWrapper.h new file mode 100644 index 0000000000..da6024ec21 --- /dev/null +++ b/widget/windows/CheckInvariantWrapper.h @@ -0,0 +1,78 @@ +/* -*- 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/. */ + +// A wrapper that uses RAII to ensure that a class invariant is checked +// before and after any public function is called + +#ifndef CHECKINVARIANTWRAPPER_H_ +#define CHECKINVARIANTWRAPPER_H_ + +#include "mozilla/Attributes.h" +#include + +namespace mozilla { + +// +// Wraps an object of type T and allows access to its public API by +// deferencing it using the pointer syntax "->". +// +// Using that operator will return a temporary RAII object that +// calls a method named "CheckInvariant" in its constructor, calls the +// requested method, and then calls "CheckInvariant" again in its +// destructor. +// +// The only thing your class requires is a method with the following signature: +// +// void CheckInvariant() const; +// +template +class CheckInvariantWrapper { + public: + class Wrapper { + public: + explicit Wrapper(T& aObject) : mObject(aObject) { + mObject.CheckInvariant(); + } + ~Wrapper() { mObject.CheckInvariant(); } + + T* operator->() { return &mObject; } + + private: + T& mObject; + }; + + class ConstWrapper { + public: + explicit ConstWrapper(const T& aObject) : mObject(aObject) { + mObject.CheckInvariant(); + } + ~ConstWrapper() { mObject.CheckInvariant(); } + + const T* operator->() const { return &mObject; } + + private: + const T& mObject; + }; + + CheckInvariantWrapper() = default; + + MOZ_IMPLICIT CheckInvariantWrapper(T aObject) : mObject(std::move(aObject)) {} + + template + explicit CheckInvariantWrapper(std::in_place_t, Args&&... args) + : mObject(std::forward(args)...) {} + + const ConstWrapper operator->() const { return ConstWrapper(mObject); } + + Wrapper operator->() { return Wrapper(mObject); } + + private: + T mObject; +}; + +} // namespace mozilla + +#endif // CHECKINVARIANTWRAPPER_H_ diff --git a/widget/windows/CompositorWidgetChild.cpp b/widget/windows/CompositorWidgetChild.cpp new file mode 100644 index 0000000000..b8bef09aee --- /dev/null +++ b/widget/windows/CompositorWidgetChild.cpp @@ -0,0 +1,114 @@ +/* -*- 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 "CompositorWidgetChild.h" +#include "mozilla/Unused.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/widget/CompositorWidgetVsyncObserver.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "nsBaseWidget.h" +#include "VsyncDispatcher.h" +#include "gfxPlatform.h" +#include "RemoteBackbuffer.h" + +namespace mozilla { +namespace widget { + +CompositorWidgetChild::CompositorWidgetChild( + RefPtr aVsyncDispatcher, + RefPtr aVsyncObserver, + const CompositorWidgetInitData& aInitData) + : mVsyncDispatcher(aVsyncDispatcher), + mVsyncObserver(aVsyncObserver), + mCompositorWnd(nullptr), + mWnd(reinterpret_cast( + aInitData.get_WinCompositorWidgetInitData().hWnd())), + mTransparencyMode( + aInitData.get_WinCompositorWidgetInitData().transparencyMode()), + mRemoteBackbufferProvider() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!gfxPlatform::IsHeadless()); + MOZ_ASSERT(mWnd && ::IsWindow(mWnd)); +} + +CompositorWidgetChild::~CompositorWidgetChild() {} + +bool CompositorWidgetChild::Initialize() { + mRemoteBackbufferProvider = std::make_unique(); + if (!mRemoteBackbufferProvider->Initialize(mWnd, OtherPid(), + mTransparencyMode)) { + return false; + } + + auto maybeRemoteHandles = mRemoteBackbufferProvider->CreateRemoteHandles(); + if (!maybeRemoteHandles) { + return false; + } + + Unused << SendInitialize(*maybeRemoteHandles); + + return true; +} + +void CompositorWidgetChild::EnterPresentLock() { + Unused << SendEnterPresentLock(); +} + +void CompositorWidgetChild::LeavePresentLock() { + Unused << SendLeavePresentLock(); +} + +void CompositorWidgetChild::OnDestroyWindow() {} + +bool CompositorWidgetChild::OnWindowResize(const LayoutDeviceIntSize& aSize) { + return true; +} + +void CompositorWidgetChild::OnWindowModeChange(nsSizeMode aSizeMode) {} + +void CompositorWidgetChild::UpdateTransparency(nsTransparencyMode aMode) { + mTransparencyMode = aMode; + mRemoteBackbufferProvider->UpdateTransparencyMode(aMode); + Unused << SendUpdateTransparency(aMode); +} + +void CompositorWidgetChild::NotifyVisibilityUpdated(nsSizeMode aSizeMode, + bool aIsFullyOccluded) { + Unused << SendNotifyVisibilityUpdated(aSizeMode, aIsFullyOccluded); +}; + +void CompositorWidgetChild::ClearTransparentWindow() { + Unused << SendClearTransparentWindow(); +} + +mozilla::ipc::IPCResult CompositorWidgetChild::RecvObserveVsync() { + mVsyncDispatcher->SetCompositorVsyncObserver(mVsyncObserver); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetChild::RecvUnobserveVsync() { + mVsyncDispatcher->SetCompositorVsyncObserver(nullptr); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetChild::RecvUpdateCompositorWnd( + const WindowsHandle& aCompositorWnd, const WindowsHandle& aParentWnd, + UpdateCompositorWndResolver&& aResolve) { + HWND parentWnd = reinterpret_cast(aParentWnd); + if (mWnd == parentWnd) { + mCompositorWnd = reinterpret_cast(aCompositorWnd); + ::SetParent(mCompositorWnd, mWnd); + aResolve(true); + } else { + aResolve(false); + gfxCriticalNote << "Parent winow does not match"; + MOZ_ASSERT_UNREACHABLE("unexpected to happen"); + } + + return IPC_OK(); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/CompositorWidgetChild.h b/widget/windows/CompositorWidgetChild.h new file mode 100644 index 0000000000..f4c8ecd5a5 --- /dev/null +++ b/widget/windows/CompositorWidgetChild.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_windows_CompositorWidgetChild_h +#define widget_windows_CompositorWidgetChild_h + +#include "WinCompositorWidget.h" +#include "mozilla/widget/PCompositorWidgetChild.h" +#include "mozilla/widget/CompositorWidgetVsyncObserver.h" + +namespace mozilla { +class CompositorVsyncDispatcher; + +namespace widget { + +namespace remote_backbuffer { +class Provider; +} + +class CompositorWidgetChild final : public PCompositorWidgetChild, + public PlatformCompositorWidgetDelegate { + public: + CompositorWidgetChild(RefPtr aVsyncDispatcher, + RefPtr aVsyncObserver, + const CompositorWidgetInitData& aInitData); + ~CompositorWidgetChild() override; + + bool Initialize(); + + void EnterPresentLock() override; + void LeavePresentLock() override; + void OnDestroyWindow() override; + bool OnWindowResize(const LayoutDeviceIntSize& aSize) override; + void OnWindowModeChange(nsSizeMode aSizeMode) override; + void UpdateTransparency(nsTransparencyMode aMode) override; + void NotifyVisibilityUpdated(nsSizeMode aSizeMode, + bool aIsFullyOccluded) override; + void ClearTransparentWindow() override; + + mozilla::ipc::IPCResult RecvObserveVsync() override; + mozilla::ipc::IPCResult RecvUnobserveVsync() override; + mozilla::ipc::IPCResult RecvUpdateCompositorWnd( + const WindowsHandle& aCompositorWnd, const WindowsHandle& aParentWnd, + UpdateCompositorWndResolver&& aResolve) override; + + private: + RefPtr mVsyncDispatcher; + RefPtr mVsyncObserver; + HWND mCompositorWnd; + + HWND mWnd; + nsTransparencyMode mTransparencyMode; + + std::unique_ptr mRemoteBackbufferProvider; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_windows_CompositorWidgetChild_h diff --git a/widget/windows/CompositorWidgetParent.cpp b/widget/windows/CompositorWidgetParent.cpp new file mode 100644 index 0000000000..0e3fc026b9 --- /dev/null +++ b/widget/windows/CompositorWidgetParent.cpp @@ -0,0 +1,240 @@ +/* -*- 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 "CompositorWidgetParent.h" + +#include "mozilla/Unused.h" +#include "mozilla/StaticPrefs_layers.h" +#include "mozilla/gfx/DeviceManagerDx.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/layers/Compositor.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/CompositorThread.h" +#include "mozilla/webrender/RenderThread.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "nsWindow.h" +#include "VsyncDispatcher.h" +#include "WinCompositorWindowThread.h" +#include "VRShMem.h" +#include "RemoteBackbuffer.h" + +#include + +namespace mozilla { +namespace widget { + +using namespace mozilla::gfx; +using namespace mozilla; + +CompositorWidgetParent::CompositorWidgetParent( + const CompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions) + : WinCompositorWidget(aInitData.get_WinCompositorWidgetInitData(), + aOptions), + mWnd(reinterpret_cast( + aInitData.get_WinCompositorWidgetInitData().hWnd())), + mTransparencyMode( + aInitData.get_WinCompositorWidgetInitData().transparencyMode()), + mSizeMode(nsSizeMode_Normal), + mIsFullyOccluded(false), + mRemoteBackbufferClient() { + MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU); + MOZ_ASSERT(mWnd && ::IsWindow(mWnd)); +} + +CompositorWidgetParent::~CompositorWidgetParent() {} + +bool CompositorWidgetParent::Initialize( + const RemoteBackbufferHandles& aRemoteHandles) { + mRemoteBackbufferClient = std::make_unique(); + if (!mRemoteBackbufferClient->Initialize(aRemoteHandles)) { + return false; + } + + return true; +} + +bool CompositorWidgetParent::PreRender(WidgetRenderingContext* aContext) { + // This can block waiting for WM_SETTEXT to finish + // Using PreRender is unnecessarily pessimistic because + // we technically only need to block during the present call + // not all of compositor rendering + mPresentLock.Enter(); + return true; +} + +void CompositorWidgetParent::PostRender(WidgetRenderingContext* aContext) { + mPresentLock.Leave(); +} + +LayoutDeviceIntSize CompositorWidgetParent::GetClientSize() { + RECT r; + if (!::GetClientRect(mWnd, &r)) { + return LayoutDeviceIntSize(); + } + return LayoutDeviceIntSize(r.right - r.left, r.bottom - r.top); +} + +already_AddRefed +CompositorWidgetParent::StartRemoteDrawingInRegion( + const LayoutDeviceIntRegion& aInvalidRegion, + layers::BufferMode* aBufferMode) { + MOZ_ASSERT(mRemoteBackbufferClient); + MOZ_ASSERT(aBufferMode); + + // Because we use remote backbuffering, there is no need to use a local + // backbuffer too. + (*aBufferMode) = layers::BufferMode::BUFFER_NONE; + + return mRemoteBackbufferClient->BorrowDrawTarget(); +} + +void CompositorWidgetParent::EndRemoteDrawingInRegion( + gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) { + Unused << mRemoteBackbufferClient->PresentDrawTarget( + aInvalidRegion.ToUnknownRegion()); +} + +bool CompositorWidgetParent::NeedsToDeferEndRemoteDrawing() { return false; } + +already_AddRefed +CompositorWidgetParent::GetBackBufferDrawTarget(gfx::DrawTarget* aScreenTarget, + const gfx::IntRect& aRect, + bool* aOutIsCleared) { + MOZ_CRASH( + "Unexpected call to GetBackBufferDrawTarget() with remote " + "backbuffering in use"); +} + +already_AddRefed +CompositorWidgetParent::EndBackBufferDrawing() { + MOZ_CRASH( + "Unexpected call to EndBackBufferDrawing() with remote " + "backbuffering in use"); +} + +bool CompositorWidgetParent::InitCompositor(layers::Compositor* aCompositor) { + return true; +} + +bool CompositorWidgetParent::HasGlass() const { + MOZ_ASSERT(layers::CompositorThreadHolder::IsInCompositorThread() || + wr::RenderThread::IsInRenderThread()); + + return mTransparencyMode == eTransparencyBorderlessGlass; +} + +bool CompositorWidgetParent::IsHidden() const { return ::IsIconic(mWnd); } + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvInitialize( + const RemoteBackbufferHandles& aRemoteHandles) { + Unused << Initialize(aRemoteHandles); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvEnterPresentLock() { + mPresentLock.Enter(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvLeavePresentLock() { + mPresentLock.Leave(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvUpdateTransparency( + const nsTransparencyMode& aMode) { + mTransparencyMode = aMode; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvNotifyVisibilityUpdated( + const nsSizeMode& aSizeMode, const bool& aIsFullyOccluded) { + mSizeMode = aSizeMode; + mIsFullyOccluded = aIsFullyOccluded; + return IPC_OK(); +} + +nsSizeMode CompositorWidgetParent::CompositorWidgetParent::GetWindowSizeMode() + const { + nsSizeMode sizeMode = mSizeMode; + return sizeMode; +} + +bool CompositorWidgetParent::CompositorWidgetParent::GetWindowIsFullyOccluded() + const { + bool isFullyOccluded = mIsFullyOccluded; + return isFullyOccluded; +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvClearTransparentWindow() { + gfx::CriticalSectionAutoEnter lock(&mPresentLock); + + RefPtr drawTarget = mRemoteBackbufferClient->BorrowDrawTarget(); + if (!drawTarget) { + return IPC_OK(); + } + + IntSize size = drawTarget->GetSize(); + if (size.IsEmpty()) { + return IPC_OK(); + } + + drawTarget->ClearRect(Rect(0, 0, size.width, size.height)); + + Unused << mRemoteBackbufferClient->PresentDrawTarget( + IntRect(0, 0, size.width, size.height)); + + return IPC_OK(); +} + +nsIWidget* CompositorWidgetParent::RealWidget() { return nullptr; } + +void CompositorWidgetParent::ObserveVsync(VsyncObserver* aObserver) { + if (aObserver) { + Unused << SendObserveVsync(); + } else { + Unused << SendUnobserveVsync(); + } + mVsyncObserver = aObserver; +} + +RefPtr CompositorWidgetParent::GetVsyncObserver() const { + MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU); + return mVsyncObserver; +} + +void CompositorWidgetParent::UpdateCompositorWnd(const HWND aCompositorWnd, + const HWND aParentWnd) { + MOZ_ASSERT(layers::CompositorThreadHolder::IsInCompositorThread()); + MOZ_ASSERT(mRootLayerTreeID.isSome()); + + RefPtr self = this; + SendUpdateCompositorWnd(reinterpret_cast(aCompositorWnd), + reinterpret_cast(aParentWnd)) + ->Then( + layers::CompositorThread(), __func__, + [self](const bool& aSuccess) { + if (aSuccess && self->mRootLayerTreeID.isSome() && + layers::CompositorThreadHolder::IsActive()) { + self->mSetParentCompleted = true; + // Schedule composition after ::SetParent() call in parent + // process. + layers::CompositorBridgeParent::ScheduleForcedComposition( + self->mRootLayerTreeID.ref(), wr::RenderReasons::WIDGET); + } + }, + [self](const mozilla::ipc::ResponseRejectReason&) {}); +} + +void CompositorWidgetParent::SetRootLayerTreeID( + const layers::LayersId& aRootLayerTreeId) { + mRootLayerTreeID = Some(aRootLayerTreeId); +} + +void CompositorWidgetParent::ActorDestroy(ActorDestroyReason aWhy) {} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/CompositorWidgetParent.h b/widget/windows/CompositorWidgetParent.h new file mode 100644 index 0000000000..8403f11915 --- /dev/null +++ b/widget/windows/CompositorWidgetParent.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_windows_CompositorWidgetParent_h +#define widget_windows_CompositorWidgetParent_h + +#include "WinCompositorWidget.h" +#include "mozilla/Maybe.h" +#include "mozilla/widget/PCompositorWidgetParent.h" + +namespace mozilla { +namespace widget { + +namespace remote_backbuffer { +class Client; +} + +class CompositorWidgetParent final : public PCompositorWidgetParent, + public WinCompositorWidget { + public: + explicit CompositorWidgetParent(const CompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions); + ~CompositorWidgetParent() override; + + bool Initialize(const RemoteBackbufferHandles& aRemoteHandles); + + bool PreRender(WidgetRenderingContext*) override; + void PostRender(WidgetRenderingContext*) override; + already_AddRefed StartRemoteDrawingInRegion( + const LayoutDeviceIntRegion& aInvalidRegion, + layers::BufferMode* aBufferMode) override; + void EndRemoteDrawingInRegion( + gfx::DrawTarget* aDrawTarget, + const LayoutDeviceIntRegion& aInvalidRegion) override; + bool NeedsToDeferEndRemoteDrawing() override; + LayoutDeviceIntSize GetClientSize() override; + already_AddRefed GetBackBufferDrawTarget( + gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect, + bool* aOutIsCleared) override; + already_AddRefed EndBackBufferDrawing() override; + bool InitCompositor(layers::Compositor* aCompositor) override; + bool IsHidden() const override; + + bool HasGlass() const override; + + nsSizeMode GetWindowSizeMode() const override; + bool GetWindowIsFullyOccluded() const override; + + mozilla::ipc::IPCResult RecvInitialize( + const RemoteBackbufferHandles& aRemoteHandles) override; + mozilla::ipc::IPCResult RecvEnterPresentLock() override; + mozilla::ipc::IPCResult RecvLeavePresentLock() override; + mozilla::ipc::IPCResult RecvUpdateTransparency( + const nsTransparencyMode& aMode) override; + mozilla::ipc::IPCResult RecvNotifyVisibilityUpdated( + const nsSizeMode& aSizeMode, const bool& aIsFullyOccluded) override; + mozilla::ipc::IPCResult RecvClearTransparentWindow() override; + void ActorDestroy(ActorDestroyReason aWhy) override; + + nsIWidget* RealWidget() override; + void ObserveVsync(VsyncObserver* aObserver) override; + RefPtr GetVsyncObserver() const override; + + // PlatformCompositorWidgetDelegate Overrides + void UpdateCompositorWnd(const HWND aCompositorWnd, + const HWND aParentWnd) override; + void SetRootLayerTreeID(const layers::LayersId& aRootLayerTreeId) override; + + private: + RefPtr mVsyncObserver; + Maybe mRootLayerTreeID; + + HWND mWnd; + + gfx::CriticalSection mPresentLock; + + // Transparency handling. + mozilla::Atomic + mTransparencyMode; + + // Visibility handling. + mozilla::Atomic mSizeMode; + mozilla::Atomic mIsFullyOccluded; + + std::unique_ptr mRemoteBackbufferClient; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_windows_CompositorWidgetParent_h diff --git a/widget/windows/DirectManipulationOwner.cpp b/widget/windows/DirectManipulationOwner.cpp new file mode 100644 index 0000000000..340024a911 --- /dev/null +++ b/widget/windows/DirectManipulationOwner.cpp @@ -0,0 +1,758 @@ +/* -*- 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 "DirectManipulationOwner.h" +#include "nsWindow.h" +#include "WinModifierKeyState.h" +#include "InputData.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/SwipeTracker.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/VsyncDispatcher.h" + +#if !defined(__MINGW32__) && !defined(__MINGW64__) + +// Direct Manipulation is only defined for Win8 and newer. +# if defined(_WIN32_WINNT) +# undef _WIN32_WINNT +# define _WIN32_WINNT _WIN32_WINNT_WIN8 +# endif // defined(_WIN32_WINNT) +# if defined(NTDDI_VERSION) +# undef NTDDI_VERSION +# define NTDDI_VERSION NTDDI_WIN8 +# endif // defined(NTDDI_VERSION) + +# include "directmanipulation.h" + +#endif // !defined(__MINGW32__) && !defined(__MINGW64__) + +namespace mozilla { +namespace widget { + +#if !defined(__MINGW32__) && !defined(__MINGW64__) + +class DManipEventHandler : public IDirectManipulationViewportEventHandler, + public IDirectManipulationInteractionEventHandler { + public: + typedef mozilla::LayoutDeviceIntRect LayoutDeviceIntRect; + + friend class DirectManipulationOwner; + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DManipEventHandler) + + STDMETHODIMP QueryInterface(REFIID, void**) override; + + friend class DirectManipulationOwner; + + explicit DManipEventHandler(nsWindow* aWindow, + DirectManipulationOwner* aOwner, + const LayoutDeviceIntRect& aBounds); + + HRESULT STDMETHODCALLTYPE OnViewportStatusChanged( + IDirectManipulationViewport* viewport, DIRECTMANIPULATION_STATUS current, + DIRECTMANIPULATION_STATUS previous) override; + + HRESULT STDMETHODCALLTYPE + OnViewportUpdated(IDirectManipulationViewport* viewport) override; + + HRESULT STDMETHODCALLTYPE + OnContentUpdated(IDirectManipulationViewport* viewport, + IDirectManipulationContent* content) override; + + HRESULT STDMETHODCALLTYPE + OnInteraction(IDirectManipulationViewport2* viewport, + DIRECTMANIPULATION_INTERACTION_TYPE interaction) override; + + void Update(); + + class VObserver final : public mozilla::VsyncObserver { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DManipEventHandler::VObserver, + override) + + public: + void NotifyVsync(const mozilla::VsyncEvent& aVsync) override { + if (mOwner) { + mOwner->Update(); + } + } + explicit VObserver(DManipEventHandler* aOwner) : mOwner(aOwner) {} + + void ClearOwner() { mOwner = nullptr; } + + private: + virtual ~VObserver() {} + DManipEventHandler* mOwner; + }; + + enum class State { eNone, ePanning, eInertia, ePinching }; + void TransitionToState(State aNewState); + + enum class Phase { eStart, eMiddle, eEnd }; + // Return value indicates if we sent an event or not and hence if we should + // update mLastScale. (We only want to send pinch events if the computed + // deltaY for the corresponding WidgetWheelEvent would be non-zero.) + bool SendPinch(Phase aPhase, float aScale); + void SendPan(Phase aPhase, float x, float y, bool aIsInertia); + static void SendPanCommon(nsWindow* aWindow, Phase aPhase, + ScreenPoint aPosition, double aDeltaX, + double aDeltaY, Modifiers aMods, bool aIsInertia); + + static void SynthesizeNativeTouchpadPan( + nsWindow* aWindow, nsIWidget::TouchpadGesturePhase aEventPhase, + LayoutDeviceIntPoint aPoint, double aDeltaX, double aDeltaY, + int32_t aModifierFlags); + + private: + virtual ~DManipEventHandler() = default; + + nsWindow* mWindow; + DirectManipulationOwner* mOwner; + RefPtr mObserver; + float mLastScale; + float mLastXOffset; + float mLastYOffset; + LayoutDeviceIntRect mBounds; + bool mShouldSendPanStart; + bool mShouldSendPinchStart; + State mState = State::eNone; +}; + +DManipEventHandler::DManipEventHandler(nsWindow* aWindow, + DirectManipulationOwner* aOwner, + const LayoutDeviceIntRect& aBounds) + : mWindow(aWindow), + mOwner(aOwner), + mLastScale(1.f), + mLastXOffset(0.f), + mLastYOffset(0.f), + mBounds(aBounds), + mShouldSendPanStart(false), + mShouldSendPinchStart(false) {} + +STDMETHODIMP +DManipEventHandler::QueryInterface(REFIID iid, void** ppv) { + const IID IID_IDirectManipulationViewportEventHandler = + __uuidof(IDirectManipulationViewportEventHandler); + const IID IID_IDirectManipulationInteractionEventHandler = + __uuidof(IDirectManipulationInteractionEventHandler); + + if ((IID_IUnknown == iid) || + (IID_IDirectManipulationViewportEventHandler == iid)) { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + if (IID_IDirectManipulationInteractionEventHandler == iid) { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +HRESULT +DManipEventHandler::OnViewportStatusChanged( + IDirectManipulationViewport* viewport, DIRECTMANIPULATION_STATUS current, + DIRECTMANIPULATION_STATUS previous) { + if (current == previous) { + return S_OK; + } + + if (current == DIRECTMANIPULATION_INERTIA) { + if (previous != DIRECTMANIPULATION_RUNNING || mState != State::ePanning) { + // xxx transition to none? + return S_OK; + } + + TransitionToState(State::eInertia); + } + + if (current == DIRECTMANIPULATION_RUNNING) { + // INERTIA -> RUNNING, should start a new sequence. + if (previous == DIRECTMANIPULATION_INERTIA) { + TransitionToState(State::eNone); + } + } + + if (current != DIRECTMANIPULATION_ENABLED && + current != DIRECTMANIPULATION_READY) { + return S_OK; + } + + // A session has ended, reset the transform. + if (mLastScale != 1.f || mLastXOffset != 0.f || mLastYOffset != 0.f) { + HRESULT hr = + viewport->ZoomToRect(0, 0, mBounds.width, mBounds.height, false); + if (!SUCCEEDED(hr)) { + NS_WARNING("ZoomToRect failed"); + } + } + mLastScale = 1.f; + mLastXOffset = 0.f; + mLastYOffset = 0.f; + + TransitionToState(State::eNone); + + return S_OK; +} + +HRESULT +DManipEventHandler::OnViewportUpdated(IDirectManipulationViewport* viewport) { + return S_OK; +} + +void DManipEventHandler::TransitionToState(State aNewState) { + if (mState == aNewState) { + return; + } + + State prevState = mState; + mState = aNewState; + + // End the previous sequence. + switch (prevState) { + case State::ePanning: { + // ePanning -> *: PanEnd + SendPan(Phase::eEnd, 0.f, 0.f, false); + break; + } + case State::eInertia: { + // eInertia -> *: MomentumEnd + SendPan(Phase::eEnd, 0.f, 0.f, true); + break; + } + case State::ePinching: { + MOZ_ASSERT(aNewState == State::eNone); + // ePinching -> eNone: PinchEnd. ePinching should only transition to + // eNone. + // Only send a pinch end if we sent a pinch start. + if (!mShouldSendPinchStart) { + SendPinch(Phase::eEnd, 0.f); + } + mShouldSendPinchStart = false; + break; + } + case State::eNone: { + // eNone -> *: no cleanup is needed. + break; + } + default: + MOZ_ASSERT(false); + } + + // Start the new sequence. + switch (aNewState) { + case State::ePanning: { + // eInertia, eNone -> ePanning: PanStart. + // We're being called from OnContentUpdated, it has the coords we need to + // pass to SendPan(Phase::eStart), so set mShouldSendPanStart and when we + // return OnContentUpdated will check it and call SendPan(Phase::eStart). + mShouldSendPanStart = true; + break; + } + case State::eInertia: { + // Only ePanning can transition to eInertia. + MOZ_ASSERT(prevState == State::ePanning); + SendPan(Phase::eStart, 0.f, 0.f, true); + break; + } + case State::ePinching: { + // * -> ePinching: PinchStart. + // Pinch gesture may begin with some scroll events. + // We're being called from OnContentUpdated, it has the scale we need to + // pass to SendPinch(Phase::eStart), so set mShouldSendPinchStart and when + // we return OnContentUpdated will check it and call + // SendPinch(Phase::eStart). + mShouldSendPinchStart = true; + break; + } + case State::eNone: { + // * -> eNone: only cleanup is needed. + break; + } + default: + MOZ_ASSERT(false); + } +} + +HRESULT +DManipEventHandler::OnContentUpdated(IDirectManipulationViewport* viewport, + IDirectManipulationContent* content) { + float transform[6]; + HRESULT hr = content->GetContentTransform(transform, ARRAYSIZE(transform)); + if (!SUCCEEDED(hr)) { + NS_WARNING("GetContentTransform failed"); + return S_OK; + } + + float scale = transform[0]; + float xoffset = transform[4]; + float yoffset = transform[5]; + + // Not different from last time. + if (FuzzyEqualsMultiplicative(scale, mLastScale) && xoffset == mLastXOffset && + yoffset == mLastYOffset) { + return S_OK; + } + + // Consider this is a Scroll when scale factor equals 1.0. + if (FuzzyEqualsMultiplicative(scale, 1.f)) { + if (mState == State::eNone) { + TransitionToState(State::ePanning); + } + } else { + // Pinch gesture may begin with some scroll events. + TransitionToState(State::ePinching); + } + + if (mState == State::ePanning || mState == State::eInertia) { + // Accumulate the offset (by not updating mLastX/YOffset) until we have at + // least one pixel. + float dx = std::abs(mLastXOffset - xoffset); + float dy = std::abs(mLastYOffset - yoffset); + if (dx < 1.f && dy < 1.f) { + return S_OK; + } + } + + bool updateLastScale = true; + if (mState == State::ePanning) { + if (mShouldSendPanStart) { + SendPan(Phase::eStart, mLastXOffset - xoffset, mLastYOffset - yoffset, + false); + mShouldSendPanStart = false; + } else { + SendPan(Phase::eMiddle, mLastXOffset - xoffset, mLastYOffset - yoffset, + false); + } + } else if (mState == State::eInertia) { + SendPan(Phase::eMiddle, mLastXOffset - xoffset, mLastYOffset - yoffset, + true); + } else if (mState == State::ePinching) { + if (mShouldSendPinchStart) { + updateLastScale = SendPinch(Phase::eStart, scale); + // Only clear mShouldSendPinchStart if we actually sent the event + // (updateLastScale tells us if we sent an event). + if (updateLastScale) { + mShouldSendPinchStart = false; + } + } else { + updateLastScale = SendPinch(Phase::eMiddle, scale); + } + } + + if (updateLastScale) { + mLastScale = scale; + } + mLastXOffset = xoffset; + mLastYOffset = yoffset; + + return S_OK; +} + +HRESULT +DManipEventHandler::OnInteraction( + IDirectManipulationViewport2* viewport, + DIRECTMANIPULATION_INTERACTION_TYPE interaction) { + if (interaction == DIRECTMANIPULATION_INTERACTION_BEGIN) { + if (!mObserver) { + mObserver = new VObserver(this); + } + + gfxWindowsPlatform::GetPlatform() + ->GetGlobalVsyncDispatcher() + ->AddMainThreadObserver(mObserver); + } + + if (mObserver && interaction == DIRECTMANIPULATION_INTERACTION_END) { + gfxWindowsPlatform::GetPlatform() + ->GetGlobalVsyncDispatcher() + ->RemoveMainThreadObserver(mObserver); + } + + return S_OK; +} + +void DManipEventHandler::Update() { + if (mOwner) { + mOwner->Update(); + } +} + +#endif // !defined(__MINGW32__) && !defined(__MINGW64__) + +void DirectManipulationOwner::Update() { +#if !defined(__MINGW32__) && !defined(__MINGW64__) + if (mDmUpdateManager) { + mDmUpdateManager->Update(nullptr); + } +#endif +} + +DirectManipulationOwner::DirectManipulationOwner(nsWindow* aWindow) + : mWindow(aWindow) {} + +DirectManipulationOwner::~DirectManipulationOwner() { Destroy(); } + +#if !defined(__MINGW32__) && !defined(__MINGW64__) + +bool DManipEventHandler::SendPinch(Phase aPhase, float aScale) { + if (!mWindow) { + return false; + } + + if (aScale == mLastScale && aPhase != Phase::eEnd) { + return false; + } + + PinchGestureInput::PinchGestureType pinchGestureType = + PinchGestureInput::PINCHGESTURE_SCALE; + switch (aPhase) { + case Phase::eStart: + pinchGestureType = PinchGestureInput::PINCHGESTURE_START; + break; + case Phase::eMiddle: + pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE; + break; + case Phase::eEnd: + pinchGestureType = PinchGestureInput::PINCHGESTURE_END; + break; + default: + MOZ_ASSERT_UNREACHABLE("handle all enum values"); + } + + TimeStamp eventTimeStamp = TimeStamp::Now(); + + ModifierKeyState modifierKeyState; + Modifiers mods = modifierKeyState.GetModifiers(); + + ExternalPoint screenOffset = ViewAs( + mWindow->WidgetToScreenOffset(), + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); + + POINT cursor_pos; + ::GetCursorPos(&cursor_pos); + HWND wnd = static_cast(mWindow->GetNativeData(NS_NATIVE_WINDOW)); + ::ScreenToClient(wnd, &cursor_pos); + ScreenPoint position = {(float)cursor_pos.x, (float)cursor_pos.y}; + + PinchGestureInput event{pinchGestureType, + PinchGestureInput::TRACKPAD, + eventTimeStamp, + screenOffset, + position, + 100.0 * ((aPhase == Phase::eEnd) ? 1.f : aScale), + 100.0 * ((aPhase == Phase::eEnd) ? 1.f : mLastScale), + mods}; + + if (!event.SetLineOrPageDeltaY(mWindow)) { + return false; + } + + mWindow->SendAnAPZEvent(event); + + return true; +} + +void DManipEventHandler::SendPan(Phase aPhase, float x, float y, + bool aIsInertia) { + if (!mWindow) { + return; + } + + ModifierKeyState modifierKeyState; + Modifiers mods = modifierKeyState.GetModifiers(); + + POINT cursor_pos; + ::GetCursorPos(&cursor_pos); + HWND wnd = static_cast(mWindow->GetNativeData(NS_NATIVE_WINDOW)); + ::ScreenToClient(wnd, &cursor_pos); + ScreenPoint position = {(float)cursor_pos.x, (float)cursor_pos.y}; + + SendPanCommon(mWindow, aPhase, position, x, y, mods, aIsInertia); +} + +/* static */ +void DManipEventHandler::SendPanCommon(nsWindow* aWindow, Phase aPhase, + ScreenPoint aPosition, double aDeltaX, + double aDeltaY, Modifiers aMods, + bool aIsInertia) { + if (!aWindow) { + return; + } + + PanGestureInput::PanGestureType panGestureType = + PanGestureInput::PANGESTURE_PAN; + if (aIsInertia) { + switch (aPhase) { + case Phase::eStart: + panGestureType = PanGestureInput::PANGESTURE_MOMENTUMSTART; + break; + case Phase::eMiddle: + panGestureType = PanGestureInput::PANGESTURE_MOMENTUMPAN; + break; + case Phase::eEnd: + panGestureType = PanGestureInput::PANGESTURE_MOMENTUMEND; + break; + default: + MOZ_ASSERT_UNREACHABLE("handle all enum values"); + } + } else { + switch (aPhase) { + case Phase::eStart: + panGestureType = PanGestureInput::PANGESTURE_START; + break; + case Phase::eMiddle: + panGestureType = PanGestureInput::PANGESTURE_PAN; + break; + case Phase::eEnd: + panGestureType = PanGestureInput::PANGESTURE_END; + break; + default: + MOZ_ASSERT_UNREACHABLE("handle all enum values"); + } + } + + TimeStamp eventTimeStamp = TimeStamp::Now(); + + PanGestureInput event{panGestureType, eventTimeStamp, aPosition, + ScreenPoint(aDeltaX, aDeltaY), aMods}; + + aWindow->SendAnAPZEvent(event); +} + +#endif // !defined(__MINGW32__) && !defined(__MINGW64__) + +void DirectManipulationOwner::Init(const LayoutDeviceIntRect& aBounds) { +#if !defined(__MINGW32__) && !defined(__MINGW64__) + HRESULT hr = CoCreateInstance( + CLSID_DirectManipulationManager, nullptr, CLSCTX_INPROC_SERVER, + IID_IDirectManipulationManager, getter_AddRefs(mDmManager)); + if (!SUCCEEDED(hr)) { + NS_WARNING("CoCreateInstance(CLSID_DirectManipulationManager failed"); + mDmManager = nullptr; + return; + } + + hr = mDmManager->GetUpdateManager(IID_IDirectManipulationUpdateManager, + getter_AddRefs(mDmUpdateManager)); + if (!SUCCEEDED(hr)) { + NS_WARNING("GetUpdateManager failed"); + mDmManager = nullptr; + mDmUpdateManager = nullptr; + return; + } + + HWND wnd = static_cast(mWindow->GetNativeData(NS_NATIVE_WINDOW)); + + hr = mDmManager->CreateViewport(nullptr, wnd, IID_IDirectManipulationViewport, + getter_AddRefs(mDmViewport)); + if (!SUCCEEDED(hr)) { + NS_WARNING("CreateViewport failed"); + mDmManager = nullptr; + mDmUpdateManager = nullptr; + mDmViewport = nullptr; + return; + } + + DIRECTMANIPULATION_CONFIGURATION configuration = + DIRECTMANIPULATION_CONFIGURATION_INTERACTION | + DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X | + DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y | + DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA | + DIRECTMANIPULATION_CONFIGURATION_RAILS_X | + DIRECTMANIPULATION_CONFIGURATION_RAILS_Y; + if (StaticPrefs::apz_allow_zooming()) { + configuration |= DIRECTMANIPULATION_CONFIGURATION_SCALING; + } + + hr = mDmViewport->ActivateConfiguration(configuration); + if (!SUCCEEDED(hr)) { + NS_WARNING("ActivateConfiguration failed"); + mDmManager = nullptr; + mDmUpdateManager = nullptr; + mDmViewport = nullptr; + return; + } + + hr = mDmViewport->SetViewportOptions( + DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE); + if (!SUCCEEDED(hr)) { + NS_WARNING("SetViewportOptions failed"); + mDmManager = nullptr; + mDmUpdateManager = nullptr; + mDmViewport = nullptr; + return; + } + + mDmHandler = new DManipEventHandler(mWindow, this, aBounds); + + hr = mDmViewport->AddEventHandler(wnd, mDmHandler.get(), + &mDmViewportHandlerCookie); + if (!SUCCEEDED(hr)) { + NS_WARNING("AddEventHandler failed"); + mDmManager = nullptr; + mDmUpdateManager = nullptr; + mDmViewport = nullptr; + mDmHandler = nullptr; + return; + } + + RECT rect = {0, 0, aBounds.Width(), aBounds.Height()}; + hr = mDmViewport->SetViewportRect(&rect); + if (!SUCCEEDED(hr)) { + NS_WARNING("SetViewportRect failed"); + mDmManager = nullptr; + mDmUpdateManager = nullptr; + mDmViewport = nullptr; + mDmHandler = nullptr; + return; + } + + hr = mDmManager->Activate(wnd); + if (!SUCCEEDED(hr)) { + NS_WARNING("manager Activate failed"); + mDmManager = nullptr; + mDmUpdateManager = nullptr; + mDmViewport = nullptr; + mDmHandler = nullptr; + return; + } + + hr = mDmViewport->Enable(); + if (!SUCCEEDED(hr)) { + NS_WARNING("mDmViewport->Enable failed"); + mDmManager = nullptr; + mDmUpdateManager = nullptr; + mDmViewport = nullptr; + mDmHandler = nullptr; + return; + } + + hr = mDmUpdateManager->Update(nullptr); + if (!SUCCEEDED(hr)) { + NS_WARNING("mDmUpdateManager->Update failed"); + mDmManager = nullptr; + mDmUpdateManager = nullptr; + mDmViewport = nullptr; + mDmHandler = nullptr; + return; + } +#endif // !defined(__MINGW32__) && !defined(__MINGW64__) +} + +void DirectManipulationOwner::ResizeViewport( + const LayoutDeviceIntRect& aBounds) { +#if !defined(__MINGW32__) && !defined(__MINGW64__) + if (mDmHandler) { + mDmHandler->mBounds = aBounds; + } + + if (mDmViewport) { + RECT rect = {0, 0, aBounds.Width(), aBounds.Height()}; + HRESULT hr = mDmViewport->SetViewportRect(&rect); + if (!SUCCEEDED(hr)) { + NS_WARNING("SetViewportRect failed"); + } + } +#endif +} + +void DirectManipulationOwner::Destroy() { +#if !defined(__MINGW32__) && !defined(__MINGW64__) + if (mDmHandler) { + mDmHandler->mWindow = nullptr; + mDmHandler->mOwner = nullptr; + if (mDmHandler->mObserver) { + gfxWindowsPlatform::GetPlatform() + ->GetGlobalVsyncDispatcher() + ->RemoveMainThreadObserver(mDmHandler->mObserver); + mDmHandler->mObserver->ClearOwner(); + mDmHandler->mObserver = nullptr; + } + } + + HRESULT hr; + if (mDmViewport) { + hr = mDmViewport->Stop(); + if (!SUCCEEDED(hr)) { + NS_WARNING("mDmViewport->Stop() failed"); + } + + hr = mDmViewport->Disable(); + if (!SUCCEEDED(hr)) { + NS_WARNING("mDmViewport->Disable() failed"); + } + + hr = mDmViewport->RemoveEventHandler(mDmViewportHandlerCookie); + if (!SUCCEEDED(hr)) { + NS_WARNING("mDmViewport->RemoveEventHandler() failed"); + } + + hr = mDmViewport->Abandon(); + if (!SUCCEEDED(hr)) { + NS_WARNING("mDmViewport->Abandon() failed"); + } + } + + if (mWindow) { + HWND wnd = static_cast(mWindow->GetNativeData(NS_NATIVE_WINDOW)); + + if (mDmManager) { + hr = mDmManager->Deactivate(wnd); + if (!SUCCEEDED(hr)) { + NS_WARNING("mDmManager->Deactivate() failed"); + } + } + } + + mDmHandler = nullptr; + mDmViewport = nullptr; + mDmUpdateManager = nullptr; + mDmManager = nullptr; +#endif // !defined(__MINGW32__) && !defined(__MINGW64__) + mWindow = nullptr; +} + +void DirectManipulationOwner::SetContact(UINT aContactId) { +#if !defined(__MINGW32__) && !defined(__MINGW64__) + if (mDmViewport) { + mDmViewport->SetContact(aContactId); + } +#endif +} + +/*static */ void DirectManipulationOwner::SynthesizeNativeTouchpadPan( + nsWindow* aWindow, nsIWidget::TouchpadGesturePhase aEventPhase, + LayoutDeviceIntPoint aPoint, double aDeltaX, double aDeltaY, + int32_t aModifierFlags) { +#if !defined(__MINGW32__) && !defined(__MINGW64__) + DManipEventHandler::SynthesizeNativeTouchpadPan( + aWindow, aEventPhase, aPoint, aDeltaX, aDeltaY, aModifierFlags); +#endif +} + +#if !defined(__MINGW32__) && !defined(__MINGW64__) +/*static */ void DManipEventHandler::SynthesizeNativeTouchpadPan( + nsWindow* aWindow, nsIWidget::TouchpadGesturePhase aEventPhase, + LayoutDeviceIntPoint aPoint, double aDeltaX, double aDeltaY, + int32_t aModifierFlags) { + ScreenPoint position = {(float)aPoint.x, (float)aPoint.y}; + Phase phase = Phase::eStart; + if (aEventPhase == nsIWidget::PHASE_UPDATE) { + phase = Phase::eMiddle; + } + + if (aEventPhase == nsIWidget::PHASE_END) { + phase = Phase::eEnd; + } + SendPanCommon(aWindow, phase, position, aDeltaX, aDeltaY, aModifierFlags, + /* aIsInertia = */ false); +} +#endif + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/DirectManipulationOwner.h b/widget/windows/DirectManipulationOwner.h new file mode 100644 index 0000000000..ab1e8b2307 --- /dev/null +++ b/widget/windows/DirectManipulationOwner.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DirectManipulationOwner_h__ +#define DirectManipulationOwner_h__ + +#include +#include "Units.h" +#include "nsIWidget.h" // for TouchpadGesturePhase + +class nsWindow; +class IDirectManipulationManager; +class IDirectManipulationUpdateManager; +class IDirectManipulationViewport; + +namespace mozilla { +namespace widget { + +class DManipEventHandler; + +class DirectManipulationOwner { + public: + typedef mozilla::LayoutDeviceIntRect LayoutDeviceIntRect; + + explicit DirectManipulationOwner(nsWindow* aWindow); + ~DirectManipulationOwner(); + void Init(const LayoutDeviceIntRect& aBounds); + void ResizeViewport(const LayoutDeviceIntRect& aBounds); + void Destroy(); + + void SetContact(UINT aContactId); + + void Update(); + + static void SynthesizeNativeTouchpadPan( + nsWindow* aWindow, nsIWidget::TouchpadGesturePhase aEventPhase, + LayoutDeviceIntPoint aPoint, double aDeltaX, double aDeltaY, + int32_t aModifierFlags); + + private: + nsWindow* mWindow; +#if !defined(__MINGW32__) && !defined(__MINGW64__) + DWORD mDmViewportHandlerCookie; + RefPtr mDmManager; + RefPtr mDmUpdateManager; + RefPtr mDmViewport; + RefPtr mDmHandler; +#endif +}; + +} // namespace widget +} // namespace mozilla + +#endif // #ifndef DirectManipulationOwner_h__ diff --git a/widget/windows/GfxInfo.cpp b/widget/windows/GfxInfo.cpp new file mode 100644 index 0000000000..4ba1daeec1 --- /dev/null +++ b/widget/windows/GfxInfo.cpp @@ -0,0 +1,2048 @@ +/* -*- 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 "GfxInfo.h" + +#include "gfxConfig.h" +#include "GfxDriverInfo.h" +#include "gfxWindowsPlatform.h" +#include "jsapi.h" +#include "js/PropertyAndElement.h" // JS_SetElement, JS_SetProperty +#include "nsExceptionHandler.h" +#include "nsPrintfCString.h" +#include "nsUnicharUtils.h" +#include "prenv.h" +#include "prprf.h" +#include "xpcpublic.h" + +#include "mozilla/Components.h" +#include "mozilla/Preferences.h" +#include "mozilla/gfx/DeviceManagerDx.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/SSE.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/WindowsProcessMitigations.h" + +#include +#include +#include // for GUID_DEVCLASS_BATTERY +#include // for SetupDi* +#include // for IOCTL_* +#include // for BATTERY_* + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::widget; + +#ifdef DEBUG +NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug) +#endif + +static void AssertNotWin32kLockdown() { + // Check that we are not in Win32k lockdown + MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown(), + "Invalid Windows GfxInfo API with Win32k lockdown"); +} + +/* GetD2DEnabled and GetDwriteEnabled shouldn't be called until after + * gfxPlatform initialization has occurred because they depend on it for + * information. (See bug 591561) */ +nsresult GfxInfo::GetD2DEnabled(bool* aEnabled) { + // Telemetry queries this during XPCOM initialization, and there's no + // gfxPlatform by then. Just bail out if gfxPlatform isn't initialized. + if (!gfxPlatform::Initialized()) { + *aEnabled = false; + return NS_OK; + } + + // We check gfxConfig rather than the actual render mode, since the UI + // process does not use Direct2D if the GPU process is enabled. However, + // content processes can still use Direct2D. + *aEnabled = gfx::gfxConfig::IsEnabled(gfx::Feature::DIRECT2D); + return NS_OK; +} + +nsresult GfxInfo::GetDWriteEnabled(bool* aEnabled) { + *aEnabled = gfxWindowsPlatform::GetPlatform()->DWriteEnabled(); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetDWriteVersion(nsAString& aDwriteVersion) { + gfxWindowsPlatform::GetDLLVersion(L"dwrite.dll", aDwriteVersion); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetHasBattery(bool* aHasBattery) { + AssertNotWin32kLockdown(); + + *aHasBattery = mHasBattery; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetEmbeddedInFirefoxReality(bool* aEmbeddedInFirefoxReality) { + *aEmbeddedInFirefoxReality = gfxVars::FxREmbedded(); + return NS_OK; +} + +#define PIXEL_STRUCT_RGB 1 +#define PIXEL_STRUCT_BGR 2 + +NS_IMETHODIMP +GfxInfo::GetCleartypeParameters(nsAString& aCleartypeParams) { + nsTArray clearTypeParams; + + gfxWindowsPlatform::GetPlatform()->GetCleartypeParams(clearTypeParams); + uint32_t d, numDisplays = clearTypeParams.Length(); + bool displayNames = (numDisplays > 1); + bool foundData = false; + nsString outStr; + + for (d = 0; d < numDisplays; d++) { + ClearTypeParameterInfo& params = clearTypeParams[d]; + + if (displayNames) { + outStr.AppendPrintf( + "%S [ ", static_cast(params.displayName.get())); + } + + if (params.gamma >= 0) { + foundData = true; + outStr.AppendPrintf("Gamma: %.4g ", params.gamma / 1000.0); + } + + if (params.pixelStructure >= 0) { + foundData = true; + if (params.pixelStructure == PIXEL_STRUCT_RGB || + params.pixelStructure == PIXEL_STRUCT_BGR) { + outStr.AppendPrintf( + "Pixel Structure: %s ", + (params.pixelStructure == PIXEL_STRUCT_RGB ? "RGB" : "BGR")); + } else { + outStr.AppendPrintf("Pixel Structure: %d ", params.pixelStructure); + } + } + + if (params.clearTypeLevel >= 0) { + foundData = true; + outStr.AppendPrintf("ClearType Level: %d ", params.clearTypeLevel); + } + + if (params.enhancedContrast >= 0) { + foundData = true; + outStr.AppendPrintf("Enhanced Contrast: %d ", params.enhancedContrast); + } + + if (displayNames) { + outStr.Append(u"] "); + } + } + + if (foundData) { + aCleartypeParams.Assign(outStr); + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetWindowProtocol(nsAString& aWindowProtocol) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GfxInfo::GetDesktopEnvironment(nsAString& aDesktopEnvironment) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GfxInfo::GetTestType(nsAString& aTestType) { return NS_ERROR_NOT_IMPLEMENTED; } + +static nsresult GetKeyValue(const WCHAR* keyLocation, const WCHAR* keyName, + uint32_t& destValue, int type) { + MOZ_ASSERT(type == REG_DWORD || type == REG_QWORD); + HKEY key; + DWORD dwcbData; + DWORD dValue; + DWORD resultType; + LONG result; + nsresult retval = NS_OK; + + result = + RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyLocation, 0, KEY_QUERY_VALUE, &key); + if (result != ERROR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + switch (type) { + case REG_DWORD: { + // We only use this for vram size + dwcbData = sizeof(dValue); + result = RegQueryValueExW(key, keyName, nullptr, &resultType, + (LPBYTE)&dValue, &dwcbData); + if (result == ERROR_SUCCESS && resultType == REG_DWORD) { + destValue = (uint32_t)(dValue / 1024 / 1024); + } else { + retval = NS_ERROR_FAILURE; + } + break; + } + case REG_QWORD: { + // We only use this for vram size + LONGLONG qValue; + dwcbData = sizeof(qValue); + result = RegQueryValueExW(key, keyName, nullptr, &resultType, + (LPBYTE)&qValue, &dwcbData); + if (result == ERROR_SUCCESS && resultType == REG_QWORD) { + destValue = (uint32_t)(qValue / 1024 / 1024); + } else { + retval = NS_ERROR_FAILURE; + } + break; + } + } + RegCloseKey(key); + + return retval; +} + +static nsresult GetKeyValue(const WCHAR* keyLocation, const WCHAR* keyName, + nsAString& destString, int type) { + MOZ_ASSERT(type == REG_MULTI_SZ); + + HKEY key; + DWORD dwcbData; + DWORD resultType; + LONG result; + nsresult retval = NS_OK; + + result = + RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyLocation, 0, KEY_QUERY_VALUE, &key); + if (result != ERROR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + // A chain of null-separated strings; we convert the nulls to spaces + WCHAR wCharValue[1024]; + dwcbData = sizeof(wCharValue); + + result = RegQueryValueExW(key, keyName, nullptr, &resultType, + (LPBYTE)wCharValue, &dwcbData); + if (result == ERROR_SUCCESS && resultType == REG_MULTI_SZ) { + // This bit here could probably be cleaner. + bool isValid = false; + + DWORD strLen = dwcbData / sizeof(wCharValue[0]); + for (DWORD i = 0; i < strLen; i++) { + if (wCharValue[i] == '\0') { + if (i < strLen - 1 && wCharValue[i + 1] == '\0') { + isValid = true; + break; + } else { + wCharValue[i] = ' '; + } + } + } + + // ensure wCharValue is null terminated + wCharValue[strLen - 1] = '\0'; + + if (isValid) destString = wCharValue; + + } else { + retval = NS_ERROR_FAILURE; + } + + RegCloseKey(key); + + return retval; +} + +static nsresult GetKeyValues(const WCHAR* keyLocation, const WCHAR* keyName, + nsTArray& destStrings) { + // First ask for the size of the value + DWORD size; + LONG rv = RegGetValueW(HKEY_LOCAL_MACHINE, keyLocation, keyName, + RRF_RT_REG_MULTI_SZ, nullptr, nullptr, &size); + if (rv != ERROR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + // Create a buffer with the proper size and retrieve the value + WCHAR* wCharValue = new WCHAR[size / sizeof(WCHAR)]; + rv = RegGetValueW(HKEY_LOCAL_MACHINE, keyLocation, keyName, + RRF_RT_REG_MULTI_SZ, nullptr, (LPBYTE)wCharValue, &size); + if (rv != ERROR_SUCCESS) { + delete[] wCharValue; + return NS_ERROR_FAILURE; + } + + // The value is a sequence of null-terminated strings, usually terminated by + // an empty string (\0). RegGetValue ensures that the value is properly + // terminated with a null character. + DWORD i = 0; + DWORD strLen = size / sizeof(WCHAR); + while (i < strLen) { + nsString value(wCharValue + i); + if (!value.IsEmpty()) { + destStrings.AppendElement(value); + } + i += value.Length() + 1; + } + delete[] wCharValue; + + return NS_OK; +} + +// The device ID is a string like PCI\VEN_15AD&DEV_0405&SUBSYS_040515AD +// this function is used to extract the id's out of it +uint32_t ParseIDFromDeviceID(const nsAString& key, const nsAString& prefix, + int length) { + nsAutoString id(key); + ToUpperCase(id); + int32_t start = id.Find(prefix); + if (start != -1) { + id.Cut(0, start + prefix.Length()); + id.Truncate(length); + } + if (id.Equals(L"QCOM", nsCaseInsensitiveStringComparator)) { + // String format assumptions are broken, so use a Qualcomm PCI Vendor ID + // for now. See also GfxDriverInfo::GetDeviceVendor. + return 0x5143; + } + nsresult err; + return id.ToInteger(&err, 16); +} + +// OS version in 16.16 major/minor form +// based on http://msdn.microsoft.com/en-us/library/ms724834(VS.85).aspx +enum { + kWindowsUnknown = 0, + kWindows7 = 0x60001, + kWindows8 = 0x60002, + kWindows8_1 = 0x60003, + kWindows10 = 0xA0000 +}; + +static bool HasBattery() { + // Helper classes to manage lifetimes of Windows structs. + class MOZ_STACK_CLASS HDevInfoHolder final { + public: + explicit HDevInfoHolder(HDEVINFO aHandle) : mHandle(aHandle) {} + + ~HDevInfoHolder() { ::SetupDiDestroyDeviceInfoList(mHandle); } + + private: + HDEVINFO mHandle; + }; + + class MOZ_STACK_CLASS HandleHolder final { + public: + explicit HandleHolder(HANDLE aHandle) : mHandle(aHandle) {} + + ~HandleHolder() { ::CloseHandle(mHandle); } + + private: + HANDLE mHandle; + }; + + HDEVINFO hdev = + ::SetupDiGetClassDevs(&GUID_DEVCLASS_BATTERY, nullptr, nullptr, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (hdev == INVALID_HANDLE_VALUE) { + return true; + } + + HDevInfoHolder hdevHolder(hdev); + + DWORD i = 0; + SP_DEVICE_INTERFACE_DATA did = {0}; + did.cbSize = sizeof(did); + + while (::SetupDiEnumDeviceInterfaces(hdev, nullptr, &GUID_DEVCLASS_BATTERY, i, + &did)) { + DWORD bufferSize = 0; + ::SetupDiGetDeviceInterfaceDetail(hdev, &did, nullptr, 0, &bufferSize, + nullptr); + if (::GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return true; + } + + UniquePtr buffer(new (std::nothrow) uint8_t[bufferSize]); + if (!buffer) { + return true; + } + + PSP_DEVICE_INTERFACE_DETAIL_DATA pdidd = + reinterpret_cast(buffer.get()); + pdidd->cbSize = sizeof(*pdidd); + if (!::SetupDiGetDeviceInterfaceDetail(hdev, &did, pdidd, bufferSize, + &bufferSize, nullptr)) { + return true; + } + + HANDLE hbat = ::CreateFile(pdidd->DevicePath, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hbat == INVALID_HANDLE_VALUE) { + return true; + } + + HandleHolder hbatHolder(hbat); + + BATTERY_QUERY_INFORMATION bqi = {0}; + DWORD dwWait = 0; + DWORD dwOut; + + // We need the tag to query the information below. + if (!::DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_TAG, &dwWait, + sizeof(dwWait), &bqi.BatteryTag, + sizeof(bqi.BatteryTag), &dwOut, nullptr) || + !bqi.BatteryTag) { + return true; + } + + BATTERY_INFORMATION bi = {0}; + bqi.InformationLevel = BatteryInformation; + + if (!::DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_INFORMATION, &bqi, + sizeof(bqi), &bi, sizeof(bi), &dwOut, nullptr)) { + return true; + } + + // If a battery intended for general use (i.e. system use) is not a UPS + // (i.e. short term), then we know for certain we have a battery. + if ((bi.Capabilities & BATTERY_SYSTEM_BATTERY) && + !(bi.Capabilities & BATTERY_IS_SHORT_TERM)) { + return true; + } + + // Otherwise we check the next battery. + ++i; + } + + // If we fail to enumerate because there are no more batteries to check, then + // we can safely say there are indeed no system batteries. + return ::GetLastError() != ERROR_NO_MORE_ITEMS; +} + +/* Other interesting places for info: + * IDXGIAdapter::GetDesc() + * IDirectDraw7::GetAvailableVidMem() + * e->GetAvailableTextureMem() + * */ + +#define DEVICE_KEY_PREFIX L"\\Registry\\Machine\\" +nsresult GfxInfo::Init() { + nsresult rv = GfxInfoBase::Init(); + + // If we are locked down in a content process, we can't call any of the + // Win32k APIs below. Any method that accesses members of this class should + // assert that it's not used in content + if (IsWin32kLockedDown()) { + return rv; + } + + mHasBattery = HasBattery(); + + DISPLAY_DEVICEW displayDevice; + displayDevice.cb = sizeof(displayDevice); + int deviceIndex = 0; + + const char* spoofedWindowsVersion = + PR_GetEnv("MOZ_GFX_SPOOF_WINDOWS_VERSION"); + if (spoofedWindowsVersion) { + PR_sscanf(spoofedWindowsVersion, "%x,%u", &mWindowsVersion, + &mWindowsBuildNumber); + } else { + OSVERSIONINFO vinfo; + vinfo.dwOSVersionInfoSize = sizeof(vinfo); +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable : 4996) +#endif + if (!GetVersionEx(&vinfo)) { +#ifdef _MSC_VER +# pragma warning(pop) +#endif + mWindowsVersion = kWindowsUnknown; + } else { + mWindowsVersion = + int32_t(vinfo.dwMajorVersion << 16) + vinfo.dwMinorVersion; + mWindowsBuildNumber = vinfo.dwBuildNumber; + } + } + + mDeviceKeyDebug = u"PrimarySearch"_ns; + + while (EnumDisplayDevicesW(nullptr, deviceIndex, &displayDevice, 0)) { + if (displayDevice.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) { + mDeviceKeyDebug = u"NullSearch"_ns; + break; + } + deviceIndex++; + } + + // make sure the string is nullptr terminated + if (wcsnlen(displayDevice.DeviceKey, ArrayLength(displayDevice.DeviceKey)) == + ArrayLength(displayDevice.DeviceKey)) { + // we did not find a nullptr + return rv; + } + + mDeviceKeyDebug = displayDevice.DeviceKey; + + /* DeviceKey is "reserved" according to MSDN so we'll be careful with it */ + /* check that DeviceKey begins with DEVICE_KEY_PREFIX */ + /* some systems have a DeviceKey starting with \REGISTRY\Machine\ so we need + * to compare case insenstively */ + /* If the device key is empty, we are most likely in a remote desktop + * environment. In this case we set the devicekey to an empty string so + * it can be handled later. + */ + if (displayDevice.DeviceKey[0] != '\0') { + if (_wcsnicmp(displayDevice.DeviceKey, DEVICE_KEY_PREFIX, + ArrayLength(DEVICE_KEY_PREFIX) - 1) != 0) { + return rv; + } + + // chop off DEVICE_KEY_PREFIX + mDeviceKey[0] = + displayDevice.DeviceKey + ArrayLength(DEVICE_KEY_PREFIX) - 1; + } else { + mDeviceKey[0].Truncate(); + } + + mDeviceID[0] = displayDevice.DeviceID; + mDeviceString[0] = displayDevice.DeviceString; + + // On Windows 8 and Server 2012 hosts, we want to not block RDP + // sessions from attempting hardware acceleration. RemoteFX + // provides features and functionaltiy that can give a good D3D10 + + // D2D + DirectWrite experience emulated via a software GPU. + // + // Unfortunately, the Device ID is nullptr, and we can't enumerate + // it using the setup infrastructure (SetupDiGetClassDevsW below + // will return INVALID_HANDLE_VALUE). + UINT flags = DIGCF_PRESENT | DIGCF_PROFILE | DIGCF_ALLCLASSES; + if (mWindowsVersion >= kWindows8 && mDeviceID[0].Length() == 0 && + mDeviceString[0].EqualsLiteral("RDPUDD Chained DD")) { + WCHAR sysdir[255]; + UINT len = GetSystemDirectory(sysdir, sizeof(sysdir)); + if (len < sizeof(sysdir)) { + nsString rdpudd(sysdir); + rdpudd.AppendLiteral("\\rdpudd.dll"); + gfxWindowsPlatform::GetDLLVersion(rdpudd.BeginReading(), + mDriverVersion[0]); + mDriverDate[0].AssignLiteral("01-01-1970"); + + // 0x1414 is Microsoft; 0xfefe is an invented (and unused) code + mDeviceID[0].AssignLiteral("PCI\\VEN_1414&DEV_FEFE&SUBSYS_00000000"); + flags |= DIGCF_DEVICEINTERFACE; + } + } + + /* create a device information set composed of the current display device */ + HDEVINFO devinfo = + SetupDiGetClassDevsW(nullptr, mDeviceID[0].get(), nullptr, flags); + + if (devinfo != INVALID_HANDLE_VALUE) { + HKEY key; + LONG result; + WCHAR value[255]; + DWORD dwcbData; + SP_DEVINFO_DATA devinfoData; + DWORD memberIndex = 0; + + devinfoData.cbSize = sizeof(devinfoData); + constexpr auto driverKeyPre = + u"System\\CurrentControlSet\\Control\\Class\\"_ns; + /* enumerate device information elements in the device information set */ + while (SetupDiEnumDeviceInfo(devinfo, memberIndex++, &devinfoData)) { + /* get a string that identifies the device's driver key */ + if (SetupDiGetDeviceRegistryPropertyW(devinfo, &devinfoData, SPDRP_DRIVER, + nullptr, (PBYTE)value, + sizeof(value), nullptr)) { + nsAutoString driverKey(driverKeyPre); + driverKey += value; + result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, driverKey.get(), 0, + KEY_QUERY_VALUE, &key); + if (result == ERROR_SUCCESS) { + /* we've found the driver we're looking for */ + dwcbData = sizeof(value); + result = RegQueryValueExW(key, L"DriverVersion", nullptr, nullptr, + (LPBYTE)value, &dwcbData); + if (result == ERROR_SUCCESS) { + mDriverVersion[0] = value; + } else { + // If the entry wasn't found, assume the worst (0.0.0.0). + mDriverVersion[0].AssignLiteral("0.0.0.0"); + } + dwcbData = sizeof(value); + result = RegQueryValueExW(key, L"DriverDate", nullptr, nullptr, + (LPBYTE)value, &dwcbData); + if (result == ERROR_SUCCESS) { + mDriverDate[0] = value; + } else { + // Again, assume the worst + mDriverDate[0].AssignLiteral("01-01-1970"); + } + RegCloseKey(key); + break; + } + } + } + + SetupDiDestroyDeviceInfoList(devinfo); + } + + // It is convenient to have these as integers + uint32_t adapterVendorID[2] = {0, 0}; + uint32_t adapterDeviceID[2] = {0, 0}; + uint32_t adapterSubsysID[2] = {0, 0}; + + adapterVendorID[0] = ParseIDFromDeviceID(mDeviceID[0], u"VEN_"_ns, 4); + adapterDeviceID[0] = ParseIDFromDeviceID(mDeviceID[0], u"&DEV_"_ns, 4); + adapterSubsysID[0] = ParseIDFromDeviceID(mDeviceID[0], u"&SUBSYS_"_ns, 8); + + // Sometimes we don't get the valid device using this method. For now, + // allow zero vendor or device as valid, as long as the other value is + // non-zero. + bool foundValidDevice = (adapterVendorID[0] != 0 || adapterDeviceID[0] != 0); + + // We now check for second display adapter. If we didn't find the valid + // device using the original approach, we will try the alternative. + + // Device interface class for display adapters. + CLSID GUID_DISPLAY_DEVICE_ARRIVAL; + HRESULT hresult = CLSIDFromString(L"{1CA05180-A699-450A-9A0C-DE4FBE3DDD89}", + &GUID_DISPLAY_DEVICE_ARRIVAL); + if (hresult == NOERROR) { + devinfo = + SetupDiGetClassDevsW(&GUID_DISPLAY_DEVICE_ARRIVAL, nullptr, nullptr, + DIGCF_PRESENT | DIGCF_INTERFACEDEVICE); + + if (devinfo != INVALID_HANDLE_VALUE) { + HKEY key; + LONG result; + WCHAR value[255]; + DWORD dwcbData; + SP_DEVINFO_DATA devinfoData; + DWORD memberIndex = 0; + devinfoData.cbSize = sizeof(devinfoData); + + nsAutoString adapterDriver2; + nsAutoString deviceID2; + nsAutoString driverVersion2; + nsAutoString driverDate2; + + constexpr auto driverKeyPre = + u"System\\CurrentControlSet\\Control\\Class\\"_ns; + /* enumerate device information elements in the device information set */ + while (SetupDiEnumDeviceInfo(devinfo, memberIndex++, &devinfoData)) { + /* get a string that identifies the device's driver key */ + if (SetupDiGetDeviceRegistryPropertyW( + devinfo, &devinfoData, SPDRP_DRIVER, nullptr, (PBYTE)value, + sizeof(value), nullptr)) { + nsAutoString driverKey2(driverKeyPre); + driverKey2 += value; + result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, driverKey2.get(), 0, + KEY_QUERY_VALUE, &key); + if (result == ERROR_SUCCESS) { + dwcbData = sizeof(value); + result = RegQueryValueExW(key, L"MatchingDeviceId", nullptr, + nullptr, (LPBYTE)value, &dwcbData); + if (result != ERROR_SUCCESS) { + continue; + } + deviceID2 = value; + adapterVendorID[1] = ParseIDFromDeviceID(deviceID2, u"VEN_"_ns, 4); + adapterDeviceID[1] = ParseIDFromDeviceID(deviceID2, u"&DEV_"_ns, 4); + // Skip the devices we already considered, as well as any + // "zero" ones. + if ((adapterVendorID[0] == adapterVendorID[1] && + adapterDeviceID[0] == adapterDeviceID[1]) || + (adapterVendorID[1] == 0 && adapterDeviceID[1] == 0)) { + RegCloseKey(key); + continue; + } + + // If this device is missing driver information, it is unlikely to + // be a real display adapter. + if (NS_FAILED(GetKeyValue(driverKey2.get(), + L"InstalledDisplayDrivers", + adapterDriver2, REG_MULTI_SZ))) { + RegCloseKey(key); + continue; + } + dwcbData = sizeof(value); + result = RegQueryValueExW(key, L"DriverVersion", nullptr, nullptr, + (LPBYTE)value, &dwcbData); + if (result != ERROR_SUCCESS) { + RegCloseKey(key); + continue; + } + driverVersion2 = value; + dwcbData = sizeof(value); + result = RegQueryValueExW(key, L"DriverDate", nullptr, nullptr, + (LPBYTE)value, &dwcbData); + if (result != ERROR_SUCCESS) { + RegCloseKey(key); + continue; + } + driverDate2 = value; + dwcbData = sizeof(value); + result = RegQueryValueExW(key, L"Device Description", nullptr, + nullptr, (LPBYTE)value, &dwcbData); + if (result != ERROR_SUCCESS) { + dwcbData = sizeof(value); + result = RegQueryValueExW(key, L"DriverDesc", nullptr, nullptr, + (LPBYTE)value, &dwcbData); + } + RegCloseKey(key); + if (result == ERROR_SUCCESS) { + // If we didn't find a valid device with the original method + // take this one, and continue looking for the second GPU. + if (!foundValidDevice) { + foundValidDevice = true; + adapterVendorID[0] = adapterVendorID[1]; + adapterDeviceID[0] = adapterDeviceID[1]; + mDeviceString[0] = value; + mDeviceID[0] = deviceID2; + mDeviceKey[0] = driverKey2; + mDriverVersion[0] = driverVersion2; + mDriverDate[0] = driverDate2; + adapterSubsysID[0] = + ParseIDFromDeviceID(mDeviceID[0], u"&SUBSYS_"_ns, 8); + continue; + } + + mHasDualGPU = true; + mDeviceString[1] = value; + mDeviceID[1] = deviceID2; + mDeviceKey[1] = driverKey2; + mDriverVersion[1] = driverVersion2; + mDriverDate[1] = driverDate2; + adapterSubsysID[1] = + ParseIDFromDeviceID(mDeviceID[1], u"&SUBSYS_"_ns, 8); + mAdapterVendorID[1].AppendPrintf("0x%04x", adapterVendorID[1]); + mAdapterDeviceID[1].AppendPrintf("0x%04x", adapterDeviceID[1]); + mAdapterSubsysID[1].AppendPrintf("%08x", adapterSubsysID[1]); + break; + } + } + } + } + + SetupDiDestroyDeviceInfoList(devinfo); + } + } + + mAdapterVendorID[0].AppendPrintf("0x%04x", adapterVendorID[0]); + mAdapterDeviceID[0].AppendPrintf("0x%04x", adapterDeviceID[0]); + mAdapterSubsysID[0].AppendPrintf("%08x", adapterSubsysID[0]); + + // Sometimes, the enumeration is not quite right and the two adapters + // end up being swapped. Actually enumerate the adapters that come + // back from the DXGI factory to check, and tag the second as active + // if found. + if (mHasDualGPU) { + nsModuleHandle dxgiModule(LoadLibrarySystem32(L"dxgi.dll")); + decltype(CreateDXGIFactory)* createDXGIFactory = + (decltype(CreateDXGIFactory)*)GetProcAddress(dxgiModule, + "CreateDXGIFactory"); + + if (createDXGIFactory) { + RefPtr factory = nullptr; + createDXGIFactory(__uuidof(IDXGIFactory), (void**)(&factory)); + if (factory) { + RefPtr adapter; + if (SUCCEEDED(factory->EnumAdapters(0, getter_AddRefs(adapter)))) { + DXGI_ADAPTER_DESC desc; + PodZero(&desc); + if (SUCCEEDED(adapter->GetDesc(&desc))) { + if (desc.VendorId != adapterVendorID[0] && + desc.DeviceId != adapterDeviceID[0] && + desc.VendorId == adapterVendorID[1] && + desc.DeviceId == adapterDeviceID[1]) { + mActiveGPUIndex = 1; + } + } + } + } + } + } + + mHasDriverVersionMismatch = false; + if (mAdapterVendorID[mActiveGPUIndex] == + GfxDriverInfo::GetDeviceVendor(DeviceVendor::Intel)) { + // we've had big crashers (bugs 590373 and 595364) apparently correlated + // with bad Intel driver installations where the DriverVersion reported + // by the registry was not the version of the DLL. + + // Note that these start without the .dll extension but eventually gain it. + bool is64bitApp = sizeof(void*) == 8; + nsAutoString dllFileName(is64bitApp ? u"igd10umd64" : u"igd10umd32"); + nsAutoString dllFileName2(is64bitApp ? u"igd10iumd64" : u"igd10iumd32"); + + nsString dllVersion, dllVersion2; + uint64_t dllNumericVersion = 0, dllNumericVersion2 = 0, + driverNumericVersion = 0, knownSafeMismatchVersion = 0; + + // Only parse the DLL version for those found in the driver list + nsAutoString eligibleDLLs; + if (NS_SUCCEEDED(GetAdapterDriver(eligibleDLLs))) { + if (FindInReadable(dllFileName, eligibleDLLs)) { + dllFileName += u".dll"_ns; + gfxWindowsPlatform::GetDLLVersion(dllFileName.get(), dllVersion); + ParseDriverVersion(dllVersion, &dllNumericVersion); + } + if (FindInReadable(dllFileName2, eligibleDLLs)) { + dllFileName2 += u".dll"_ns; + gfxWindowsPlatform::GetDLLVersion(dllFileName2.get(), dllVersion2); + ParseDriverVersion(dllVersion2, &dllNumericVersion2); + } + } + + // Sometimes the DLL is not in the System32 nor SysWOW64 directories. But + // UserModeDriverName (or UserModeDriverNameWow, if available) might provide + // the full path to the DLL in some DriverStore FileRepository. + if (dllNumericVersion == 0 && dllNumericVersion2 == 0) { + nsTArray eligibleDLLpaths; + const WCHAR* keyLocation = mDeviceKey[mActiveGPUIndex].get(); + GetKeyValues(keyLocation, L"UserModeDriverName", eligibleDLLpaths); + GetKeyValues(keyLocation, L"UserModeDriverNameWow", eligibleDLLpaths); + size_t length = eligibleDLLpaths.Length(); + for (size_t i = 0; + i < length && dllNumericVersion == 0 && dllNumericVersion2 == 0; + ++i) { + if (FindInReadable(dllFileName, eligibleDLLpaths[i])) { + gfxWindowsPlatform::GetDLLVersion(eligibleDLLpaths[i].get(), + dllVersion); + ParseDriverVersion(dllVersion, &dllNumericVersion); + } else if (FindInReadable(dllFileName2, eligibleDLLpaths[i])) { + gfxWindowsPlatform::GetDLLVersion(eligibleDLLpaths[i].get(), + dllVersion2); + ParseDriverVersion(dllVersion2, &dllNumericVersion2); + } + } + } + + ParseDriverVersion(mDriverVersion[mActiveGPUIndex], &driverNumericVersion); + ParseDriverVersion(u"9.17.10.0"_ns, &knownSafeMismatchVersion); + + // If there's a driver version mismatch, consider this harmful only when + // the driver version is less than knownSafeMismatchVersion. See the + // above comment about crashes with old mismatches. If the GetDllVersion + // call fails, we are not calling it a mismatch. + if ((dllNumericVersion != 0 && dllNumericVersion != driverNumericVersion) || + (dllNumericVersion2 != 0 && + dllNumericVersion2 != driverNumericVersion)) { + if (driverNumericVersion < knownSafeMismatchVersion || + std::max(dllNumericVersion, dllNumericVersion2) < + knownSafeMismatchVersion) { + mHasDriverVersionMismatch = true; + gfxCriticalNoteOnce + << "Mismatched driver versions between the registry " + << NS_ConvertUTF16toUTF8(mDriverVersion[mActiveGPUIndex]).get() + << " and DLL(s) " << NS_ConvertUTF16toUTF8(dllVersion).get() << ", " + << NS_ConvertUTF16toUTF8(dllVersion2).get() << " reported."; + } + } else if (dllNumericVersion == 0 && dllNumericVersion2 == 0) { + // Leave it as an asserting error for now, to see if we can find + // a system that exhibits this kind of a problem internally. + gfxCriticalErrorOnce() + << "Potential driver version mismatch ignored due to missing DLLs " + << NS_ConvertUTF16toUTF8(dllFileName).get() + << " v=" << NS_ConvertUTF16toUTF8(dllVersion).get() << " and " + << NS_ConvertUTF16toUTF8(dllFileName2).get() + << " v=" << NS_ConvertUTF16toUTF8(dllVersion2).get(); + } + } + + const char* spoofedDriverVersionString = + PR_GetEnv("MOZ_GFX_SPOOF_DRIVER_VERSION"); + if (spoofedDriverVersionString) { + mDriverVersion[mActiveGPUIndex].AssignASCII(spoofedDriverVersionString); + } + + const char* spoofedVendor = PR_GetEnv("MOZ_GFX_SPOOF_VENDOR_ID"); + if (spoofedVendor) { + mAdapterVendorID[mActiveGPUIndex].AssignASCII(spoofedVendor); + } + + const char* spoofedDevice = PR_GetEnv("MOZ_GFX_SPOOF_DEVICE_ID"); + if (spoofedDevice) { + mAdapterDeviceID[mActiveGPUIndex].AssignASCII(spoofedDevice); + } + + AddCrashReportAnnotations(); + + return rv; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDescription(nsAString& aAdapterDescription) { + AssertNotWin32kLockdown(); + + aAdapterDescription = mDeviceString[mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDescription2(nsAString& aAdapterDescription) { + AssertNotWin32kLockdown(); + + aAdapterDescription = mDeviceString[1 - mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterRAM(uint32_t* aAdapterRAM) { + AssertNotWin32kLockdown(); + + uint32_t result = 0; + if (NS_FAILED(GetKeyValue(mDeviceKey[mActiveGPUIndex].get(), + L"HardwareInformation.qwMemorySize", result, + REG_QWORD)) || + result == 0) { + if (NS_FAILED(GetKeyValue(mDeviceKey[mActiveGPUIndex].get(), + L"HardwareInformation.MemorySize", result, + REG_DWORD))) { + result = 0; + } + } + *aAdapterRAM = result; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterRAM2(uint32_t* aAdapterRAM) { + AssertNotWin32kLockdown(); + + uint32_t result = 0; + if (mHasDualGPU) { + if (NS_FAILED(GetKeyValue(mDeviceKey[1 - mActiveGPUIndex].get(), + L"HardwareInformation.qwMemorySize", result, + REG_QWORD)) || + result == 0) { + if (NS_FAILED(GetKeyValue(mDeviceKey[1 - mActiveGPUIndex].get(), + L"HardwareInformation.MemorySize", result, + REG_DWORD))) { + result = 0; + } + } + } + *aAdapterRAM = result; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriver(nsAString& aAdapterDriver) { + AssertNotWin32kLockdown(); + + if (NS_FAILED(GetKeyValue(mDeviceKey[mActiveGPUIndex].get(), + L"InstalledDisplayDrivers", aAdapterDriver, + REG_MULTI_SZ))) + aAdapterDriver = L"Unknown"; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriver2(nsAString& aAdapterDriver) { + AssertNotWin32kLockdown(); + + if (!mHasDualGPU) { + aAdapterDriver.Truncate(); + } else if (NS_FAILED(GetKeyValue(mDeviceKey[1 - mActiveGPUIndex].get(), + L"InstalledDisplayDrivers", aAdapterDriver, + REG_MULTI_SZ))) { + aAdapterDriver = L"Unknown"; + } + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) { + aAdapterDriverVendor.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) { + AssertNotWin32kLockdown(); + + aAdapterDriverVersion = mDriverVersion[mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverDate(nsAString& aAdapterDriverDate) { + AssertNotWin32kLockdown(); + + aAdapterDriverDate = mDriverDate[mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) { + aAdapterDriverVendor.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVersion2(nsAString& aAdapterDriverVersion) { + AssertNotWin32kLockdown(); + + aAdapterDriverVersion = mDriverVersion[1 - mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverDate2(nsAString& aAdapterDriverDate) { + AssertNotWin32kLockdown(); + + aAdapterDriverDate = mDriverDate[1 - mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterVendorID(nsAString& aAdapterVendorID) { + AssertNotWin32kLockdown(); + + aAdapterVendorID = mAdapterVendorID[mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterVendorID2(nsAString& aAdapterVendorID) { + AssertNotWin32kLockdown(); + + aAdapterVendorID = mAdapterVendorID[1 - mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDeviceID(nsAString& aAdapterDeviceID) { + AssertNotWin32kLockdown(); + + aAdapterDeviceID = mAdapterDeviceID[mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDeviceID2(nsAString& aAdapterDeviceID) { + AssertNotWin32kLockdown(); + + aAdapterDeviceID = mAdapterDeviceID[1 - mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterSubsysID(nsAString& aAdapterSubsysID) { + AssertNotWin32kLockdown(); + + aAdapterSubsysID = mAdapterSubsysID[mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterSubsysID2(nsAString& aAdapterSubsysID) { + AssertNotWin32kLockdown(); + + aAdapterSubsysID = mAdapterSubsysID[1 - mActiveGPUIndex]; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) { + // This is never the case, as the active GPU ends up being + // the first one. It should probably be removed. + *aIsGPU2Active = false; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetDrmRenderDevice(nsACString& aDrmRenderDevice) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* Cisco's VPN software can cause corruption of the floating point state. + * Make a note of this in our crash reports so that some weird crashes + * make more sense */ +static void CheckForCiscoVPN() { + LONG result; + HKEY key; + /* This will give false positives, but hopefully no false negatives */ + result = + RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Cisco Systems\\VPN Client", + 0, KEY_QUERY_VALUE, &key); + if (result == ERROR_SUCCESS) { + RegCloseKey(key); + CrashReporter::AppendAppNotesToCrashReport("Cisco VPN\n"_ns); + } +} + +void GfxInfo::AddCrashReportAnnotations() { + AssertNotWin32kLockdown(); + + CheckForCiscoVPN(); + + if (mHasDriverVersionMismatch) { + CrashReporter::AppendAppNotesToCrashReport("DriverVersionMismatch\n"_ns); + } + + nsString deviceID, vendorID, driverVersion, subsysID; + nsCString narrowDeviceID, narrowVendorID, narrowDriverVersion, narrowSubsysID; + + GetAdapterDeviceID(deviceID); + CopyUTF16toUTF8(deviceID, narrowDeviceID); + GetAdapterVendorID(vendorID); + CopyUTF16toUTF8(vendorID, narrowVendorID); + GetAdapterDriverVersion(driverVersion); + CopyUTF16toUTF8(driverVersion, narrowDriverVersion); + GetAdapterSubsysID(subsysID); + CopyUTF16toUTF8(subsysID, narrowSubsysID); + + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterVendorID, + narrowVendorID); + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterDeviceID, + narrowDeviceID); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::AdapterDriverVersion, narrowDriverVersion); + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterSubsysID, + narrowSubsysID); + + /* Add an App Note, this contains extra information. */ + nsAutoCString note; + + // TODO: We should probably convert this into a proper annotation + if (vendorID == GfxDriverInfo::GetDeviceVendor(DeviceVendor::All)) { + /* if we didn't find a valid vendorID lets append the mDeviceID string to + * try to find out why */ + LossyAppendUTF16toASCII(mDeviceID[mActiveGPUIndex], note); + note.AppendLiteral(", "); + LossyAppendUTF16toASCII(mDeviceKeyDebug, note); + } + note.AppendLiteral("\n"); + + if (mHasDualGPU) { + nsString deviceID2, vendorID2, subsysID2; + nsAutoString adapterDriverVersionString2; + nsCString narrowDeviceID2, narrowVendorID2, narrowSubsysID2; + + // Make a slight difference between the two cases so that we + // can see it in the crash reports. It may come in handy. + if (mActiveGPUIndex == 1) { + note.AppendLiteral("Has dual GPUs. GPU-#2: "); + } else { + note.AppendLiteral("Has dual GPUs. GPU #2: "); + } + GetAdapterDeviceID2(deviceID2); + CopyUTF16toUTF8(deviceID2, narrowDeviceID2); + GetAdapterVendorID2(vendorID2); + CopyUTF16toUTF8(vendorID2, narrowVendorID2); + GetAdapterDriverVersion2(adapterDriverVersionString2); + GetAdapterSubsysID(subsysID2); + CopyUTF16toUTF8(subsysID2, narrowSubsysID2); + note.AppendLiteral("AdapterVendorID2: "); + note.Append(narrowVendorID2); + note.AppendLiteral(", AdapterDeviceID2: "); + note.Append(narrowDeviceID2); + note.AppendLiteral(", AdapterSubsysID2: "); + note.Append(narrowSubsysID2); + note.AppendLiteral(", AdapterDriverVersion2: "); + note.Append(NS_LossyConvertUTF16toASCII(adapterDriverVersionString2)); + } + CrashReporter::AppendAppNotesToCrashReport(note); +} + +static OperatingSystem WindowsVersionToOperatingSystem( + int32_t aWindowsVersion) { + switch (aWindowsVersion) { + case kWindows7: + return OperatingSystem::Windows7; + case kWindows8: + return OperatingSystem::Windows8; + case kWindows8_1: + return OperatingSystem::Windows8_1; + case kWindows10: + return OperatingSystem::Windows10; + case kWindowsUnknown: + default: + return OperatingSystem::Unknown; + } +} + +static bool OnlyAllowFeatureOnWhitelistedVendor(int32_t aFeature) { + switch (aFeature) { + // The GPU process doesn't need hardware acceleration and can run on + // devices that we normally block from not being on our whitelist. + case nsIGfxInfo::FEATURE_GPU_PROCESS: + // We can mostly assume that ANGLE will work + case nsIGfxInfo::FEATURE_DIRECT3D_11_ANGLE: + // Remote WebGL is needed for Win32k Lockdown, so it should be enabled + // regardless of HW support or not + case nsIGfxInfo::FEATURE_ALLOW_WEBGL_OUT_OF_PROCESS: + // Backdrop filter should generally work, especially if we fall back to + // Software WebRender because of an unknown vendor. + case nsIGfxInfo::FEATURE_BACKDROP_FILTER: + return false; + default: + return true; + } +} + +// Return true if the CPU supports AVX, but the operating system does not. +#if defined(_M_X64) +static inline bool DetectBrokenAVX() { + int regs[4]; + __cpuid(regs, 0); + if (regs[0] == 0) { + // Level not supported. + return false; + } + + __cpuid(regs, 1); + + const unsigned AVX = 1u << 28; + const unsigned XSAVE = 1u << 26; + if ((regs[2] & (AVX | XSAVE)) != (AVX | XSAVE)) { + // AVX is not supported on this CPU. + return false; + } + + const unsigned OSXSAVE = 1u << 27; + if ((regs[2] & OSXSAVE) != OSXSAVE) { + // AVX is supported, but the OS didn't enable it. + // This can be forced via bcdedit /set xsavedisable 1. + return true; + } + + const unsigned AVX_CTRL_BITS = (1 << 1) | (1 << 2); + return (xgetbv(0) & AVX_CTRL_BITS) != AVX_CTRL_BITS; +} +#endif + +const nsTArray& GfxInfo::GetGfxDriverInfo() { + if (!sDriverInfo->Length()) { + /* + * It should be noted here that more specialized rules on certain features + * should be inserted -before- more generalized restriction. As the first + * match for feature/OS/device found in the list will be used for the final + * blocklisting call. + */ + + /* + * NVIDIA entries + */ + /* + * The last 5 digit of the NVIDIA driver version maps to the version that + * NVIDIA uses. The minor version (15, 16, 17) corresponds roughtly to the + * OS (Vista, Win7, Win7) but they show up in smaller numbers across all + * OS versions (perhaps due to OS upgrades). So we want to support + * October 2009+ drivers across all these minor versions. + * + * 187.45 (late October 2009) and earlier contain a bug which can cause us + * to crash on shutdown. + */ + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::NvidiaAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN_OR_EQUAL, V(8, 15, 11, 8745), + "FEATURE_FAILURE_NV_W7_15", "nVidia driver > 187.45"); + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows7, DeviceFamily::NvidiaAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BETWEEN_INCLUSIVE_START, V(8, 16, 10, 0000), V(8, 16, 11, 8745), + "FEATURE_FAILURE_NV_W7_16", "nVidia driver > 187.45"); + // Telemetry doesn't show any driver in this range so it might not even be + // required. + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows7, DeviceFamily::NvidiaAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BETWEEN_INCLUSIVE_START, V(8, 17, 10, 0000), V(8, 17, 11, 8745), + "FEATURE_FAILURE_NV_W7_17", "nVidia driver > 187.45"); + + /* + * AMD/ATI entries. 8.56.1.15 is the driver that shipped with Windows 7 RTM + */ + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Windows, DeviceFamily::AtiAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(8, 56, 1, 15), "FEATURE_FAILURE_AMD1", "8.56.1.15"); + + // Bug 1099252 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows7, DeviceFamily::AtiAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_EQUAL, V(8, 832, 0, 0), "FEATURE_FAILURE_BUG_1099252"); + + // Bug 1118695 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows7, DeviceFamily::AtiAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_EQUAL, V(8, 783, 2, 2000), "FEATURE_FAILURE_BUG_1118695"); + + // Bug 1587155 + // + // There are a several reports of strange rendering corruptions with this + // driver version, with and without webrender. We weren't able to + // reproduce these problems, but the users were able to update their + // drivers and it went away. So just to be safe, let's blocklist all + // gpu use with this particular (very old) driver, restricted + // to Win10 since we only have reports from that platform. + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows10, DeviceFamily::AtiAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_EQUAL, V(22, 19, 162, 4), "FEATURE_FAILURE_BUG_1587155"); + + // Bug 1198815 + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(15, 200, 0, 0), V(15, 200, 1062, 1004), "FEATURE_FAILURE_BUG_1198815", + "15.200.0.0-15.200.1062.1004"); + + // Bug 1267970 + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows10, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(15, 200, 0, 0), V(15, 301, 2301, 1002), "FEATURE_FAILURE_BUG_1267970", + "15.200.0.0-15.301.2301.1002"); + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows10, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(16, 100, 0, 0), V(16, 300, 2311, 0), "FEATURE_FAILURE_BUG_1267970", + "16.100.0.0-16.300.2311.0"); + + /* + * Bug 783517 - crashes in AMD driver on Windows 8 + */ + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows8, DeviceFamily::AtiAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BETWEEN_INCLUSIVE_START, V(8, 982, 0, 0), V(8, 983, 0, 0), + "FEATURE_FAILURE_BUG_783517_AMD", "!= 8.982.*.*"); + + /* + * Bug 1599981 - crashes in AMD driver on Windows 10 + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows10, DeviceFamily::RadeonCaicos, + nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(15, 301, 1901, 0), "FEATURE_FAILURE_BUG_1599981"); + + /* OpenGL on any ATI/AMD hardware is discouraged + * See: + * bug 619773 - WebGL: Crash with blue screen : "NMI: Parity Check / Memory + * Parity Error" bugs 584403, 584404, 620924 - crashes in atioglxx + * + many complaints about incorrect rendering + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_OPENGL_LAYERS, nsIGfxInfo::FEATURE_DISCOURAGED, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "FEATURE_FAILURE_OGL_ATI_DIS"); + +/* + * Intel entries + */ + +/* The driver versions used here come from bug 594877. They might not + * be particularly relevant anymore. + */ +#define IMPLEMENT_INTEL_DRIVER_BLOCKLIST(winVer, devFamily, driverVer, ruleId) \ + APPEND_TO_DRIVER_BLOCKLIST2(winVer, devFamily, GfxDriverInfo::allFeatures, \ + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, \ + DRIVER_LESS_THAN, driverVer, ruleId) + +#define IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(winVer, devFamily, driverVer, \ + ruleId) \ + APPEND_TO_DRIVER_BLOCKLIST2(winVer, devFamily, nsIGfxInfo::FEATURE_DIRECT2D, \ + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, \ + DRIVER_BUILD_ID_LESS_THAN, driverVer, ruleId) + + IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(OperatingSystem::Windows7, + DeviceFamily::IntelGMA500, 2026, + "FEATURE_FAILURE_594877_7"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D( + OperatingSystem::Windows7, DeviceFamily::IntelGMA900, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_594877_8"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(OperatingSystem::Windows7, + DeviceFamily::IntelGMA950, 1930, + "FEATURE_FAILURE_594877_9"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(OperatingSystem::Windows7, + DeviceFamily::IntelGMA3150, 2117, + "FEATURE_FAILURE_594877_10"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D(OperatingSystem::Windows7, + DeviceFamily::IntelGMAX3000, 1930, + "FEATURE_FAILURE_594877_11"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST_D2D( + OperatingSystem::Windows7, DeviceFamily::IntelHDGraphicsToSandyBridge, + 2202, "FEATURE_FAILURE_594877_12"); + + /* Disable Direct2D on Intel GMAX4500 devices because of rendering + * corruption discovered in bug 1180379. These seems to affect even the most + * recent drivers. We're black listing all of the devices to be safe even + * though we've only confirmed the issue on the G45 + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelGMAX4500HD, + nsIGfxInfo::FEATURE_DIRECT2D, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "FEATURE_FAILURE_1180379"); + + IMPLEMENT_INTEL_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::IntelGMA500, V(5, 0, 0, 2026), + "FEATURE_FAILURE_INTEL_16"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::IntelGMA900, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_INTEL_17"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::IntelGMA950, + V(8, 15, 10, 1930), "FEATURE_FAILURE_INTEL_18"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::IntelGMA3150, + V(8, 14, 10, 1972), "FEATURE_FAILURE_INTEL_19"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::IntelGMAX3000, + V(7, 15, 10, 1666), "FEATURE_FAILURE_INTEL_20"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::IntelGMAX4500HD, + V(7, 15, 10, 1666), "FEATURE_FAILURE_INTEL_21"); + IMPLEMENT_INTEL_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::IntelHDGraphicsToSandyBridge, + V(7, 15, 10, 1666), "FEATURE_FAILURE_INTEL_22"); + + // Bug 1074378 + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::IntelGMAX4500HD, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_EQUAL, V(8, 15, 10, 1749), "FEATURE_FAILURE_BUG_1074378_1", + "8.15.10.2342"); + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Windows7, DeviceFamily::IntelHDGraphicsToSandyBridge, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_EQUAL, V(8, 15, 10, 1749), "FEATURE_FAILURE_BUG_1074378_2", + "8.15.10.2342"); + + /* OpenGL on any Intel hardware is discouraged */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_OPENGL_LAYERS, nsIGfxInfo::FEATURE_DISCOURAGED, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "FEATURE_FAILURE_INTEL_OGL_DIS"); + + /** + * Disable acceleration on Intel HD 3000 for graphics drivers + * <= 8.15.10.2321. See bug 1018278 and bug 1060736. + */ + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Windows, DeviceFamily::IntelSandyBridge, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL, 2321, "FEATURE_FAILURE_BUG_1018278", + "X.X.X.2342"); + + /** + * Disable D2D on Win7 on Intel Haswell for graphics drivers build id <= + * 4578. See bug 1432610 + */ + APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows7, + DeviceFamily::IntelHaswell, + nsIGfxInfo::FEATURE_DIRECT2D, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL, 4578, + "FEATURE_FAILURE_BUG_1432610"); + /** + * Disable VP8 HW decoding on Windows 8.1 on Intel Haswel and a certain + * driver version. See bug 1760464 comment 6 and bug 1761332. + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows8_1, DeviceFamily::IntelHaswell, + nsIGfxInfo::FEATURE_VP8_HW_DECODE, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "FEATURE_FAILURE_BUG_1760464"); + + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows8_1, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_VP8_HW_DECODE, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_EQUAL, V(10, 18, 14, 4264), "FEATURE_FAILURE_BUG_1761332"); + + /* Disable D2D on Win7 on Intel HD Graphics on driver <= 8.15.10.2302 + * See bug 806786 + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows7, DeviceFamily::IntelMobileHDGraphics, + nsIGfxInfo::FEATURE_DIRECT2D, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN_OR_EQUAL, + V(8, 15, 10, 2302), "FEATURE_FAILURE_BUG_806786"); + + /* Disable D2D on Win8 on Intel HD Graphics on driver <= 8.15.10.2302 + * See bug 804144 and 863683 + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows8, DeviceFamily::IntelMobileHDGraphics, + nsIGfxInfo::FEATURE_DIRECT2D, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN_OR_EQUAL, + V(8, 15, 10, 2302), "FEATURE_FAILURE_BUG_804144"); + + /* Disable D2D on Win7 on Intel HD Graphics on driver == 8.15.10.2418 + * See bug 1433790 + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows7, DeviceFamily::IntelHDGraphicsToSandyBridge, + nsIGfxInfo::FEATURE_DIRECT2D, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL, + V(8, 15, 10, 2418), "FEATURE_FAILURE_BUG_1433790"); + + /* Disable D3D11 layers on Intel G41 express graphics and Intel GM965, Intel + * X3100, for causing device resets. See bug 1116812. + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::Bug1116812, + nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1116812"); + + /* Disable D3D11 layers on Intel GMA 3150 for failing to allocate a shared + * handle for textures. See bug 1207665. Additionally block D2D so we don't + * accidentally use WARP. + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::Bug1207665, + nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1207665_1"); + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::Bug1207665, + nsIGfxInfo::FEATURE_DIRECT2D, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "FEATURE_FAILURE_BUG_1207665_2"); + + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows10, DeviceFamily::QualcommAll, + nsIGfxInfo::FEATURE_DIRECT2D, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_QUALCOMM"); + + // Bug 1548410. Disable hardware accelerated video decoding on + // Qualcomm drivers used on Windows on ARM64 which are known to + // cause BSOD's and output suprious green frames while decoding video. + // Bug 1592826 expands the blocklist. + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows10, DeviceFamily::QualcommAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN_OR_EQUAL, + V(25, 18, 10440, 0), "FEATURE_FAILURE_BUG_1592826"); + + /* Disable D2D on AMD Catalyst 14.4 until 14.6 + * See bug 984488 + */ + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_DIRECT2D, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BETWEEN_INCLUSIVE_START, V(14, 1, 0, 0), V(14, 2, 0, 0), + "FEATURE_FAILURE_BUG_984488_1", "ATI Catalyst 14.6+"); + + /* Disable D3D9 layers on NVIDIA 6100/6150/6200 series due to glitches + * whilst scrolling. See bugs: 612007, 644787 & 645872. + */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::NvidiaBlockD3D9Layers, + nsIGfxInfo::FEATURE_DIRECT3D_9_LAYERS, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_612007"); + + /* Microsoft RemoteFX; blocked less than 6.2.0.0 */ + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Windows, DeviceFamily::MicrosoftAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(6, 2, 0, 0), "< 6.2.0.0", + "FEATURE_FAILURE_REMOTE_FX"); + + /* Bug 1008759: Optimus (NVidia) crash. Disable D2D on NV 310M. */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::Nvidia310M, + nsIGfxInfo::FEATURE_DIRECT2D, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "FEATURE_FAILURE_BUG_1008759"); + + /* Bug 1139503: DXVA crashes with ATI cards on windows 10. */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows10, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL, + V(15, 200, 1006, 0), "FEATURE_FAILURE_BUG_1139503"); + + /* Bug 1213107: D3D9 crashes with ATI cards on Windows 7. */ + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows7, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(8, 861, 0, 0), V(8, 862, 6, 5000), "FEATURE_FAILURE_BUG_1213107_1", + "Radeon driver > 8.862.6.5000"); + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows7, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_WEBGL_ANGLE, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(8, 861, 0, 0), V(8, 862, 6, 5000), "FEATURE_FAILURE_BUG_1213107_2", + "Radeon driver > 8.862.6.5000"); + + /* This may not be needed at all */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows7, DeviceFamily::Bug1155608, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(8, 15, 10, 2869), "FEATURE_FAILURE_INTEL_W7_HW_DECODING"); + + /* Bug 1203199/1092166: DXVA startup crashes on some intel drivers. */ + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(9, 17, 10, 0), V(9, 17, 10, 2849), "FEATURE_FAILURE_BUG_1203199_1", + "Intel driver > 9.17.10.2849"); + + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::Nvidia8800GTS, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL, + V(9, 18, 13, 4052), "FEATURE_FAILURE_BUG_1203199_2"); + + /* Bug 1137716: XXX this should really check for the matching Intel piece as + * well. Unfortunately, we don't have the infrastructure to do that */ + APPEND_TO_DRIVER_BLOCKLIST_RANGE_GPU2( + OperatingSystem::Windows7, DeviceFamily::Bug1137716, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BETWEEN_INCLUSIVE, V(8, 17, 12, 5730), V(8, 17, 12, 6901), + "FEATURE_FAILURE_BUG_1137716", "Nvidia driver > 8.17.12.6901"); + + /* Bug 1336710: Crash in rx::Blit9::initialize. */ + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::WindowsXP, DeviceFamily::IntelGMAX4500HD, + nsIGfxInfo::FEATURE_WEBGL_ANGLE, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "FEATURE_FAILURE_BUG_1336710"); + + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::WindowsXP, DeviceFamily::IntelHDGraphicsToSandyBridge, + nsIGfxInfo::FEATURE_WEBGL_ANGLE, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "FEATURE_FAILURE_BUG_1336710"); + + /* Bug 1304360: Graphical artifacts with D3D9 on Windows 7. */ + APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows7, + DeviceFamily::IntelGMAX3000, + nsIGfxInfo::FEATURE_DIRECT3D_9_LAYERS, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL, 1749, + "FEATURE_FAILURE_INTEL_W7_D3D9_LAYERS"); + + /* Bug 1717519/1717911: Crashes while drawing with swgl. + * Reproducible but not investigated yet.*/ + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(8, 15, 10, 2125), V(8, 15, 10, 2141), "FEATURE_FAILURE_BUG_1717911", + "Intel driver > 8.15.10.2141"); + +#if defined(_M_X64) + if (DetectBrokenAVX()) { + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows7, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1403353"); + } +#endif + + //////////////////////////////////// + // WebGL + + // Older than 5-15-2016 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_WEBGL_OPENGL, nsIGfxInfo::FEATURE_DISCOURAGED, + DRIVER_LESS_THAN, V(16, 200, 1010, 1002), "WEBGL_NATIVE_GL_OLD_AMD"); + + // Older than 11-18-2015 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_WEBGL_OPENGL, nsIGfxInfo::FEATURE_DISCOURAGED, + DRIVER_BUILD_ID_LESS_THAN, 4331, "WEBGL_NATIVE_GL_OLD_INTEL"); + + // Older than 2-23-2016 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_WEBGL_OPENGL, nsIGfxInfo::FEATURE_DISCOURAGED, + DRIVER_LESS_THAN, V(10, 18, 13, 6200), "WEBGL_NATIVE_GL_OLD_NVIDIA"); + + //////////////////////////////////// + // FEATURE_DX_INTEROP2 + + // All AMD. + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_DX_INTEROP2, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + GfxDriverInfo::allDriverVersions, "DX_INTEROP2_AMD_CRASH"); + + //////////////////////////////////// + // FEATURE_D3D11_KEYED_MUTEX + + // bug 1359416 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelHDGraphicsToSandyBridge, + nsIGfxInfo::FEATURE_D3D11_KEYED_MUTEX, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1359416"); + + // Bug 1447141, for causing device creation crashes. + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows7, DeviceFamily::Bug1447141, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_EQUAL, V(15, 201, 2201, 0), "FEATURE_FAILURE_BUG_1447141_1"); + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows7, DeviceFamily::Bug1447141, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_EQUAL, V(15, 201, 1701, 0), "FEATURE_FAILURE_BUG_1447141_1"); + + // bug 1457758 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::NvidiaAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_EQUAL, V(24, 21, 13, 9731), "FEATURE_FAILURE_BUG_1457758"); + + //////////////////////////////////// + // FEATURE_DX_NV12 + + // Bug 1437334 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelHDGraphicsToSandyBridge, + nsIGfxInfo::FEATURE_DX_NV12, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BUILD_ID_LESS_THAN_OR_EQUAL, 4459, + "FEATURE_BLOCKED_DRIVER_VERSION"); + + //////////////////////////////////// + // FEATURE_DX_P010 + + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_DX_P010, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "FEATURE_UNQUALIFIED_P010_NVIDIA"); + + //////////////////////////////////// + // FEATURE_VIDEO_OVERLAY - ALLOWLIST +#ifdef EARLY_BETA_OR_EARLIER + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::All, + nsIGfxInfo::FEATURE_VIDEO_OVERLAY, nsIGfxInfo::FEATURE_ALLOW_ALWAYS, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_ROLLOUT_ALL"); +#else + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_VIDEO_OVERLAY, nsIGfxInfo::FEATURE_ALLOW_ALWAYS, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_ROLLOUT_INTEL"); +#endif + + //////////////////////////////////// + // FEATURE_HW_DECODED_VIDEO_ZERO_COPY + + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows10, DeviceFamily::IntelSkylake, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(20, 19, 15, 4285), V(20, 19, 15, 4380), "FEATURE_FAILURE_BUG_1763280", + "Intel driver 20.19.15.*"); + + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows10, DeviceFamily::IntelSkylake, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(10, 18, 15, 4256), V(10, 18, 15, 4293), "FEATURE_FAILURE_BUG_1763280", + "Intel driver 10.18.15.*"); + + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(21, 21, 13, 7576), "FEATURE_FAILURE_BUG_1767212"); + + APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(23, 20, 826, 5120), + "FEATURE_FAILURE_BUG_1767212"); + + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::RadeonBlockZeroVideoCopy, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(26, 20, 15000, 37), "FEATURE_FAILURE_BUG_1767212"); + + //////////////////////////////////// + // FEATURE_HW_DECODED_VIDEO_ZERO_COPY - ALLOWLIST + + // XXX ZeroCopyNV12Texture is disabled with non-intel GPUs for now. + // See Bug 1798242 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_ALLOW_ALWAYS, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_ROLLOUT_ALL"); + + //////////////////////////////////// + // FEATURE_WEBRENDER + // Block 8.56.1.15/16 + APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN_OR_EQUAL, V(8, 56, 1, 16), + "CRASHY_DRIVERS_BUG_1678808"); + + // Shader compilation startup crashes with WebRender on Windows 7. + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows7, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(8, 17, 12, 8019), V(8, 17, 12, 8026), "FEATURE_FAILURE_BUG_1709629", + "nVidia driver > 280.26"); + + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelWebRenderBlocked, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, + "INTEL_DEVICE_GEN5_OR_OLDER"); + + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::NvidiaWebRenderBlocked, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, GfxDriverInfo::allDriverVersions, "EARLY_NVIDIA"); + + //////////////////////////////////// + // FEATURE_WEBRENDER_COMPOSITOR + +#ifndef EARLY_BETA_OR_EARLIER + // See also bug 1616874 + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_WEBRENDER_COMPOSITOR, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_EQUAL, V(24, 20, 100, 6293), + "FEATURE_FAILURE_BUG_1602511"); + + APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Windows, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_WEBRENDER_COMPOSITOR, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN_OR_EQUAL, V(8, 17, 10, 1129), + "FEATURE_FAILURE_CHROME_BUG_800950"); +#endif + + // WebRender is unable to use scissored clears in some cases + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_WEBRENDER_SCISSORED_CACHE_CLEARS, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_BUG_1603515"); + + //////////////////////////////////// + // FEATURE_BACKDROP_FILTER + + // Backdrop filter crashes the driver. See bug 1785366 and bug 1784093. + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows, DeviceFamily::IntelHDGraphicsToIvyBridge, + nsIGfxInfo::FEATURE_BACKDROP_FILTER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_EXCLUSIVE, + V(8, 15, 10, 2879), V(10, 18, 10, 4425), "FEATURE_FAILURE_BUG_1785366", + "Intel driver >= 10.18.10.4425"); + } + return *sDriverInfo; +} + +OperatingSystem GfxInfo::GetOperatingSystem() { + return WindowsVersionToOperatingSystem(mWindowsVersion); +} + +nsresult GfxInfo::GetFeatureStatusImpl( + int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion, + const nsTArray& aDriverInfo, nsACString& aFailureId, + OperatingSystem* aOS /* = nullptr */) { + AssertNotWin32kLockdown(); + + NS_ENSURE_ARG_POINTER(aStatus); + aSuggestedDriverVersion.SetIsVoid(true); + OperatingSystem os = WindowsVersionToOperatingSystem(mWindowsVersion); + *aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN; + if (aOS) *aOS = os; + + if (sShutdownOccurred) { + return NS_OK; + } + + // Don't evaluate special cases if we're checking the downloaded blocklist. + if (!aDriverInfo.Length()) { + nsAutoString adapterVendorID; + nsAutoString adapterDeviceID; + nsAutoString adapterDriverVersionString; + if (NS_FAILED(GetAdapterVendorID(adapterVendorID)) || + NS_FAILED(GetAdapterDeviceID(adapterDeviceID)) || + NS_FAILED(GetAdapterDriverVersion(adapterDriverVersionString))) { + aFailureId = "FEATURE_FAILURE_GET_ADAPTER"; + *aStatus = FEATURE_BLOCKED_DEVICE; + return NS_OK; + } + + if (OnlyAllowFeatureOnWhitelistedVendor(aFeature) && + !adapterVendorID.Equals( + GfxDriverInfo::GetDeviceVendor(DeviceVendor::Intel), + nsCaseInsensitiveStringComparator) && + !adapterVendorID.Equals( + GfxDriverInfo::GetDeviceVendor(DeviceVendor::NVIDIA), + nsCaseInsensitiveStringComparator) && + !adapterVendorID.Equals( + GfxDriverInfo::GetDeviceVendor(DeviceVendor::ATI), + nsCaseInsensitiveStringComparator) && + !adapterVendorID.Equals( + GfxDriverInfo::GetDeviceVendor(DeviceVendor::Microsoft), + nsCaseInsensitiveStringComparator) && + !adapterVendorID.Equals( + GfxDriverInfo::GetDeviceVendor(DeviceVendor::Parallels), + nsCaseInsensitiveStringComparator) && + !adapterVendorID.Equals( + GfxDriverInfo::GetDeviceVendor(DeviceVendor::Qualcomm), + nsCaseInsensitiveStringComparator) && + // FIXME - these special hex values are currently used in xpcshell tests + // introduced by bug 625160 patch 8/8. Maybe these tests need to be + // adjusted now that we're only whitelisting intel/ati/nvidia. + !adapterVendorID.LowerCaseEqualsLiteral("0xabcd") && + !adapterVendorID.LowerCaseEqualsLiteral("0xdcba") && + !adapterVendorID.LowerCaseEqualsLiteral("0xabab") && + !adapterVendorID.LowerCaseEqualsLiteral("0xdcdc")) { + if (adapterVendorID.Equals( + GfxDriverInfo::GetDeviceVendor(DeviceVendor::MicrosoftHyperV), + nsCaseInsensitiveStringComparator) || + adapterVendorID.Equals( + GfxDriverInfo::GetDeviceVendor(DeviceVendor::VMWare), + nsCaseInsensitiveStringComparator) || + adapterVendorID.Equals( + GfxDriverInfo::GetDeviceVendor(DeviceVendor::VirtualBox), + nsCaseInsensitiveStringComparator)) { + aFailureId = "FEATURE_FAILURE_VM_VENDOR"; + } else if (adapterVendorID.Equals(GfxDriverInfo::GetDeviceVendor( + DeviceVendor::MicrosoftBasic), + nsCaseInsensitiveStringComparator)) { + aFailureId = "FEATURE_FAILURE_MICROSOFT_BASIC_VENDOR"; + } else if (adapterVendorID.IsEmpty()) { + aFailureId = "FEATURE_FAILURE_EMPTY_DEVICE_VENDOR"; + } else { + aFailureId = "FEATURE_FAILURE_UNKNOWN_DEVICE_VENDOR"; + } + *aStatus = FEATURE_BLOCKED_DEVICE; + return NS_OK; + } + + uint64_t driverVersion; + if (!ParseDriverVersion(adapterDriverVersionString, &driverVersion)) { + aFailureId = "FEATURE_FAILURE_PARSE_DRIVER"; + *aStatus = FEATURE_BLOCKED_DRIVER_VERSION; + return NS_OK; + } + + if (mHasDriverVersionMismatch) { + aFailureId = "FEATURE_FAILURE_MISMATCHED_VERSION"; + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_MISMATCHED_VERSION; + return NS_OK; + } + } + + return GfxInfoBase::GetFeatureStatusImpl( + aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aFailureId, &os); +} + +void GfxInfo::DescribeFeatures(JSContext* aCx, JS::Handle aObj) { + // Add the platform neutral features + GfxInfoBase::DescribeFeatures(aCx, aObj); + + JS::Rooted obj(aCx); + + gfx::FeatureState& d3d11 = gfxConfig::GetFeature(Feature::D3D11_COMPOSITING); + if (!InitFeatureObject(aCx, aObj, "d3d11", d3d11, &obj)) { + return; + } + if (d3d11.GetValue() == gfx::FeatureStatus::Available) { + DeviceManagerDx* dm = DeviceManagerDx::Get(); + JS::Rooted val(aCx, + JS::Int32Value(dm->GetCompositorFeatureLevel())); + JS_SetProperty(aCx, obj, "version", val); + + val = JS::BooleanValue(dm->IsWARP()); + JS_SetProperty(aCx, obj, "warp", val); + + val = JS::BooleanValue(dm->TextureSharingWorks()); + JS_SetProperty(aCx, obj, "textureSharing", val); + + bool blocklisted = false; + if (nsCOMPtr gfxInfo = components::GfxInfo::Service()) { + int32_t status; + nsCString discardFailureId; + if (SUCCEEDED( + gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_DIRECT3D_11_LAYERS, + discardFailureId, &status))) { + blocklisted = (status != nsIGfxInfo::FEATURE_STATUS_OK); + } + } + + val = JS::BooleanValue(blocklisted); + JS_SetProperty(aCx, obj, "blocklisted", val); + } + + gfx::FeatureState& d2d = gfxConfig::GetFeature(Feature::DIRECT2D); + if (!InitFeatureObject(aCx, aObj, "d2d", d2d, &obj)) { + return; + } + { + const char* version = "1.1"; + JS::Rooted str(aCx, JS_NewStringCopyZ(aCx, version)); + JS::Rooted val(aCx, JS::StringValue(str)); + JS_SetProperty(aCx, obj, "version", val); + } +} + +#ifdef DEBUG + +// Implement nsIGfxInfoDebug + +NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString& aVendorID) { + mAdapterVendorID[mActiveGPUIndex] = aVendorID; + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString& aDeviceID) { + mAdapterDeviceID[mActiveGPUIndex] = aDeviceID; + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString& aDriverVersion) { + mDriverVersion[mActiveGPUIndex] = aDriverVersion; + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) { + mWindowsVersion = aVersion; + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::FireTestProcess() { return NS_OK; } + +#endif diff --git a/widget/windows/GfxInfo.h b/widget/windows/GfxInfo.h new file mode 100644 index 0000000000..faf8f0d4aa --- /dev/null +++ b/widget/windows/GfxInfo.h @@ -0,0 +1,106 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WIDGET_WINDOWS_GFXINFO_H_ +#define WIDGET_WINDOWS_GFXINFO_H_ + +#include "GfxInfoBase.h" + +namespace mozilla::widget { + +class GfxInfo : public GfxInfoBase { + public: + using GfxInfoBase::GetFeatureStatus; + using GfxInfoBase::GetFeatureSuggestedDriverVersion; + + GfxInfo() = default; + nsresult Init() override; + + // We only declare the subset of nsIGfxInfo that we actually implement. The + // rest is brought forward from GfxInfoBase. + NS_IMETHOD GetD2DEnabled(bool* aD2DEnabled) override; + NS_IMETHOD GetDWriteEnabled(bool* aDWriteEnabled) override; + NS_IMETHOD GetDWriteVersion(nsAString& aDwriteVersion) override; + NS_IMETHOD GetEmbeddedInFirefoxReality( + bool* aEmbeddedInFirefoxReality) override; + NS_IMETHOD GetHasBattery(bool* aHasBattery) override; + NS_IMETHOD GetCleartypeParameters(nsAString& aCleartypeParams) override; + NS_IMETHOD GetWindowProtocol(nsAString& aWindowProtocol) override; + NS_IMETHOD GetDesktopEnvironment(nsAString& aDesktopEnvironment) override; + NS_IMETHOD GetTestType(nsAString& aTestType) override; + NS_IMETHOD GetAdapterDescription(nsAString& aAdapterDescription) override; + NS_IMETHOD GetAdapterDriver(nsAString& aAdapterDriver) override; + NS_IMETHOD GetAdapterVendorID(nsAString& aAdapterVendorID) override; + NS_IMETHOD GetAdapterDeviceID(nsAString& aAdapterDeviceID) override; + NS_IMETHOD GetAdapterSubsysID(nsAString& aAdapterSubsysID) override; + NS_IMETHOD GetAdapterRAM(uint32_t* aAdapterRAM) override; + NS_IMETHOD GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) override; + NS_IMETHOD GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) override; + NS_IMETHOD GetAdapterDriverDate(nsAString& aAdapterDriverDate) override; + NS_IMETHOD GetAdapterDescription2(nsAString& aAdapterDescription) override; + NS_IMETHOD GetAdapterDriver2(nsAString& aAdapterDriver) override; + NS_IMETHOD GetAdapterVendorID2(nsAString& aAdapterVendorID) override; + NS_IMETHOD GetAdapterDeviceID2(nsAString& aAdapterDeviceID) override; + NS_IMETHOD GetAdapterSubsysID2(nsAString& aAdapterSubsysID) override; + NS_IMETHOD GetAdapterRAM2(uint32_t* aAdapterRAM) override; + NS_IMETHOD GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) override; + NS_IMETHOD GetAdapterDriverVersion2( + nsAString& aAdapterDriverVersion) override; + NS_IMETHOD GetAdapterDriverDate2(nsAString& aAdapterDriverDate) override; + NS_IMETHOD GetIsGPU2Active(bool* aIsGPU2Active) override; + NS_IMETHOD GetDrmRenderDevice(nsACString& aDrmRenderDevice) override; + + uint32_t OperatingSystemVersion() override { return mWindowsVersion; } + uint32_t OperatingSystemBuild() override { return mWindowsBuildNumber; } + +#ifdef DEBUG + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIGFXINFODEBUG +#endif + + private: + ~GfxInfo() = default; + + // Disallow copy/move + GfxInfo(const GfxInfo&) = delete; + GfxInfo& operator=(const GfxInfo&) = delete; + GfxInfo(GfxInfo&&) = delete; + GfxInfo& operator=(GfxInfo&&) = delete; + + OperatingSystem GetOperatingSystem() override; + + nsresult GetFeatureStatusImpl(int32_t aFeature, int32_t* aStatus, + nsAString& aSuggestedDriverVersion, + const nsTArray& aDriverInfo, + nsACString& aFailureId, + OperatingSystem* aOS = nullptr) override; + const nsTArray& GetGfxDriverInfo() override; + + void DescribeFeatures(JSContext* cx, JS::Handle aOut) override; + + void AddCrashReportAnnotations(); + + nsString mDeviceString[2]; + nsString mDeviceID[2]; + nsString mDriverVersion[2]; + nsString mDriverDate[2]; + nsString mDeviceKey[2]; + nsString mDeviceKeyDebug; + nsString mAdapterVendorID[2]; + nsString mAdapterDeviceID[2]; + nsString mAdapterSubsysID[2]; + uint32_t mWindowsVersion = 0; + uint32_t mWindowsBuildNumber = 0; + uint32_t mActiveGPUIndex = 0; // This must be 0 or 1 + bool mHasDualGPU = false; + bool mHasDriverVersionMismatch = false; + bool mHasBattery = false; +}; + +} // namespace mozilla::widget + +#endif // WIDGET_WINDOWS_GFXINFO_H_ diff --git a/widget/windows/IEnumFE.cpp b/widget/windows/IEnumFE.cpp new file mode 100644 index 0000000000..8fe7138609 --- /dev/null +++ b/widget/windows/IEnumFE.cpp @@ -0,0 +1,139 @@ +/* -*- 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 "IEnumFE.h" +#include "nsAlgorithm.h" +#include + +CEnumFormatEtc::CEnumFormatEtc() : mRefCnt(0), mCurrentIdx(0) {} + +// Constructor used by Clone() +CEnumFormatEtc::CEnumFormatEtc(nsTArray& aArray) + : mRefCnt(0), mCurrentIdx(0) { + // a deep copy, calls FormatEtc's copy constructor on each + mFormatList.AppendElements(aArray); +} + +CEnumFormatEtc::~CEnumFormatEtc() {} + +/* IUnknown impl. */ + +STDMETHODIMP +CEnumFormatEtc::QueryInterface(REFIID riid, LPVOID* ppv) { + *ppv = nullptr; + + if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IEnumFORMATETC)) + *ppv = (LPVOID)this; + + if (*ppv == nullptr) return E_NOINTERFACE; + + // AddRef any interface we'll return. + ((LPUNKNOWN)*ppv)->AddRef(); + return S_OK; +} + +STDMETHODIMP_(ULONG) +CEnumFormatEtc::AddRef() { + ++mRefCnt; + NS_LOG_ADDREF(this, mRefCnt, "CEnumFormatEtc", sizeof(*this)); + return mRefCnt; +} + +STDMETHODIMP_(ULONG) +CEnumFormatEtc::Release() { + uint32_t refReturn; + + refReturn = --mRefCnt; + NS_LOG_RELEASE(this, mRefCnt, "CEnumFormatEtc"); + + if (mRefCnt == 0) delete this; + + return refReturn; +} + +/* IEnumFORMATETC impl. */ + +STDMETHODIMP +CEnumFormatEtc::Next(ULONG aMaxToFetch, FORMATETC* aResult, + ULONG* aNumFetched) { + // If the method retrieves the number of items requested, the return + // value is S_OK. Otherwise, it is S_FALSE. + + if (aNumFetched) *aNumFetched = 0; + + // aNumFetched can be null if aMaxToFetch is 1 + if (!aNumFetched && aMaxToFetch > 1) return S_FALSE; + + if (!aResult) return S_FALSE; + + // We're done walking the list + if (mCurrentIdx >= mFormatList.Length()) return S_FALSE; + + uint32_t left = mFormatList.Length() - mCurrentIdx; + + if (!aMaxToFetch) return S_FALSE; + + uint32_t count = std::min(static_cast(aMaxToFetch), left); + + uint32_t idx = 0; + while (count > 0) { + // Copy out to aResult + mFormatList[mCurrentIdx++].CopyOut(&aResult[idx++]); + count--; + } + + if (aNumFetched) *aNumFetched = idx; + + return S_OK; +} + +STDMETHODIMP +CEnumFormatEtc::Skip(ULONG aSkipNum) { + // If the method skips the number of items requested, the return value is + // S_OK. Otherwise, it is S_FALSE. + + if ((mCurrentIdx + aSkipNum) >= mFormatList.Length()) return S_FALSE; + + mCurrentIdx += aSkipNum; + + return S_OK; +} + +STDMETHODIMP +CEnumFormatEtc::Reset(void) { + mCurrentIdx = 0; + return S_OK; +} + +STDMETHODIMP +CEnumFormatEtc::Clone(LPENUMFORMATETC* aResult) { + // Must return a new IEnumFORMATETC interface with the same iterative state. + + if (!aResult) return E_INVALIDARG; + + CEnumFormatEtc* pEnumObj = new CEnumFormatEtc(mFormatList); + + if (!pEnumObj) return E_OUTOFMEMORY; + + pEnumObj->AddRef(); + pEnumObj->SetIndex(mCurrentIdx); + + *aResult = pEnumObj; + + return S_OK; +} + +/* utils */ + +void CEnumFormatEtc::AddFormatEtc(LPFORMATETC aFormat) { + if (!aFormat) return; + FormatEtc* etc = mFormatList.AppendElement(); + // Make a copy of aFormat + if (etc) etc->CopyIn(aFormat); +} + +/* private */ + +void CEnumFormatEtc::SetIndex(uint32_t aIdx) { mCurrentIdx = aIdx; } diff --git a/widget/windows/IEnumFE.h b/widget/windows/IEnumFE.h new file mode 100644 index 0000000000..b8cb6ad9d0 --- /dev/null +++ b/widget/windows/IEnumFE.h @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef IEnumeFE_h__ +#define IEnumeFE_h__ + +/* + * CEnumFormatEtc - implements IEnumFORMATETC + */ + +#include + +#include "nsTArray.h" +#include "mozilla/Attributes.h" + +// FORMATETC container +class FormatEtc { + public: + FormatEtc() { memset(&mFormat, 0, sizeof(FORMATETC)); } + FormatEtc(const FormatEtc& copy) { CopyIn(©.mFormat); } + ~FormatEtc() { + if (mFormat.ptd) CoTaskMemFree(mFormat.ptd); + } + + void CopyIn(const FORMATETC* aSrc) { + if (!aSrc) { + memset(&mFormat, 0, sizeof(FORMATETC)); + return; + } + mFormat = *aSrc; + if (aSrc->ptd) { + mFormat.ptd = (DVTARGETDEVICE*)CoTaskMemAlloc(sizeof(DVTARGETDEVICE)); + *(mFormat.ptd) = *(aSrc->ptd); + } + } + + void CopyOut(LPFORMATETC aDest) { + if (!aDest) return; + *aDest = mFormat; + if (mFormat.ptd) { + aDest->ptd = (DVTARGETDEVICE*)CoTaskMemAlloc(sizeof(DVTARGETDEVICE)); + *(aDest->ptd) = *(mFormat.ptd); + } + } + + private: + FORMATETC mFormat; +}; + +/* + * CEnumFormatEtc is created within IDataObject::EnumFormatEtc. This object + * lives on its own, that is, QueryInterface only knows IUnknown and + * IEnumFormatEtc, nothing more. We still use an outer unknown for reference + * counting, because as long as this enumerator lives, the data object should + * live, thereby keeping the application up. + */ + +class CEnumFormatEtc final : public IEnumFORMATETC { + public: + explicit CEnumFormatEtc(nsTArray& aArray); + CEnumFormatEtc(); + ~CEnumFormatEtc(); + + // IUnknown impl. + STDMETHODIMP QueryInterface(REFIID riid, LPVOID* ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IEnumFORMATETC impl. + STDMETHODIMP Next(ULONG aMaxToFetch, FORMATETC* aResult, ULONG* aNumFetched); + STDMETHODIMP Skip(ULONG aSkipNum); + STDMETHODIMP Reset(); + STDMETHODIMP Clone(LPENUMFORMATETC* aResult); // Addrefs + + // Utils + void AddFormatEtc(LPFORMATETC aFormat); + + private: + nsTArray mFormatList; // Formats + ULONG mRefCnt; // Object reference count + ULONG mCurrentIdx; // Current element + + void SetIndex(uint32_t aIdx); +}; + +#endif //_IENUMFE_H_ diff --git a/widget/windows/IMMHandler.cpp b/widget/windows/IMMHandler.cpp new file mode 100644 index 0000000000..0fd33bdba2 --- /dev/null +++ b/widget/windows/IMMHandler.cpp @@ -0,0 +1,2410 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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 "mozilla/Logging.h" + +#include "IMMHandler.h" +#include "nsWindow.h" +#include "nsWindowDefs.h" +#include "WinIMEHandler.h" +#include "WinUtils.h" +#include "KeyboardLayout.h" +#include + +#include "mozilla/CheckedInt.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/TextEvents.h" +#include "mozilla/ToString.h" +#include "mozilla/WindowsVersion.h" + +#ifndef IME_PROP_ACCEPT_WIDE_VKEY +# define IME_PROP_ACCEPT_WIDE_VKEY 0x20 +#endif + +//------------------------------------------------------------------------- +// +// from +// http://download.microsoft.com/download/6/0/9/60908e9e-d2c1-47db-98f6-216af76a235f/msime.h +// The document for this has been removed from MSDN... +// +//------------------------------------------------------------------------- + +#define RWM_MOUSE TEXT("MSIMEMouseOperation") + +#define IMEMOUSE_NONE 0x00 // no mouse button was pushed +#define IMEMOUSE_LDOWN 0x01 +#define IMEMOUSE_RDOWN 0x02 +#define IMEMOUSE_MDOWN 0x04 +#define IMEMOUSE_WUP 0x10 // wheel up +#define IMEMOUSE_WDOWN 0x20 // wheel down + +// For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync` +// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too +// big file. +// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior. +extern mozilla::LazyLogModule gIMELog; + +static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; } + +static void HandleSeparator(nsACString& aDesc) { + if (!aDesc.IsEmpty()) { + aDesc.AppendLiteral(" | "); + } +} + +class GetIMEGeneralPropertyName : public nsAutoCString { + public: + explicit GetIMEGeneralPropertyName(DWORD aFlags) { + if (!aFlags) { + AppendLiteral("no flags"); + return; + } + if (aFlags & IME_PROP_AT_CARET) { + AppendLiteral("IME_PROP_AT_CARET"); + } + if (aFlags & IME_PROP_SPECIAL_UI) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_SPECIAL_UI"); + } + if (aFlags & IME_PROP_CANDLIST_START_FROM_1) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_CANDLIST_START_FROM_1"); + } + if (aFlags & IME_PROP_UNICODE) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_UNICODE"); + } + if (aFlags & IME_PROP_COMPLETE_ON_UNSELECT) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_COMPLETE_ON_UNSELECT"); + } + if (aFlags & IME_PROP_ACCEPT_WIDE_VKEY) { + HandleSeparator(*this); + AppendLiteral("IME_PROP_ACCEPT_WIDE_VKEY"); + } + } + virtual ~GetIMEGeneralPropertyName() {} +}; + +class GetIMEUIPropertyName : public nsAutoCString { + public: + explicit GetIMEUIPropertyName(DWORD aFlags) { + if (!aFlags) { + AppendLiteral("no flags"); + return; + } + if (aFlags & UI_CAP_2700) { + AppendLiteral("UI_CAP_2700"); + } + if (aFlags & UI_CAP_ROT90) { + HandleSeparator(*this); + AppendLiteral("UI_CAP_ROT90"); + } + if (aFlags & UI_CAP_ROTANY) { + HandleSeparator(*this); + AppendLiteral("UI_CAP_ROTANY"); + } + } + virtual ~GetIMEUIPropertyName() {} +}; + +class GetReconvertStringLog : public nsAutoCString { + public: + explicit GetReconvertStringLog(RECONVERTSTRING* aReconv) { + AssignLiteral("{ dwSize="); + AppendInt(static_cast(aReconv->dwSize)); + AppendLiteral(", dwVersion="); + AppendInt(static_cast(aReconv->dwVersion)); + AppendLiteral(", dwStrLen="); + AppendInt(static_cast(aReconv->dwStrLen)); + AppendLiteral(", dwStrOffset="); + AppendInt(static_cast(aReconv->dwStrOffset)); + AppendLiteral(", dwCompStrLen="); + AppendInt(static_cast(aReconv->dwCompStrLen)); + AppendLiteral(", dwCompStrOffset="); + AppendInt(static_cast(aReconv->dwCompStrOffset)); + AppendLiteral(", dwTargetStrLen="); + AppendInt(static_cast(aReconv->dwTargetStrLen)); + AppendLiteral(", dwTargetStrOffset="); + AppendInt(static_cast(aReconv->dwTargetStrOffset)); + AppendLiteral(", result str=\""); + if (aReconv->dwStrLen) { + char16_t* strStart = reinterpret_cast( + reinterpret_cast(aReconv) + aReconv->dwStrOffset); + nsDependentString str(strStart, aReconv->dwStrLen); + Append(NS_ConvertUTF16toUTF8(str)); + } + AppendLiteral("\" }"); + } + virtual ~GetReconvertStringLog() {} +}; + +namespace mozilla { +namespace widget { + +static IMMHandler* gIMMHandler = nullptr; + +/****************************************************************************** + * IMEContext + ******************************************************************************/ + +IMEContext::IMEContext(HWND aWnd) : mWnd(aWnd), mIMC(::ImmGetContext(aWnd)) {} + +IMEContext::IMEContext(nsWindow* aWindowBase) + : mWnd(aWindowBase->GetWindowHandle()), + mIMC(::ImmGetContext(aWindowBase->GetWindowHandle())) {} + +void IMEContext::Init(HWND aWnd) { + Clear(); + mWnd = aWnd; + mIMC = ::ImmGetContext(mWnd); +} + +void IMEContext::Init(nsWindow* aWindowBase) { + Init(aWindowBase->GetWindowHandle()); +} + +void IMEContext::Clear() { + if (mWnd && mIMC) { + ::ImmReleaseContext(mWnd, mIMC); + } + mWnd = nullptr; + mIMC = nullptr; +} + +/****************************************************************************** + * IMMHandler + ******************************************************************************/ + +static UINT sWM_MSIME_MOUSE = 0; // mouse message for MSIME 98/2000 + +WritingMode IMMHandler::sWritingModeOfCompositionFont; +nsString IMMHandler::sIMEName; +UINT IMMHandler::sCodePage = 0; +DWORD IMMHandler::sIMEProperty = 0; +DWORD IMMHandler::sIMEUIProperty = 0; +bool IMMHandler::sAssumeVerticalWritingModeNotSupported = false; +bool IMMHandler::sHasFocus = false; + +#define IMPL_IS_IME_ACTIVE(aReadableName, aActualName) \ + bool IMMHandler::Is##aReadableName##Active() { \ + return sIMEName.Equals(aActualName); \ + } + +IMPL_IS_IME_ACTIVE(ATOK2006, u"ATOK 2006") +IMPL_IS_IME_ACTIVE(ATOK2007, u"ATOK 2007") +IMPL_IS_IME_ACTIVE(ATOK2008, u"ATOK 2008") +IMPL_IS_IME_ACTIVE(ATOK2009, u"ATOK 2009") +IMPL_IS_IME_ACTIVE(ATOK2010, u"ATOK 2010") +// NOTE: Even on Windows for en-US, the name of Google Japanese Input is +// written in Japanese. +IMPL_IS_IME_ACTIVE(GoogleJapaneseInput, + u"Google \x65E5\x672C\x8A9E\x5165\x529B " + u"IMM32 \x30E2\x30B8\x30E5\x30FC\x30EB") +IMPL_IS_IME_ACTIVE(Japanist2003, u"Japanist 2003") + +#undef IMPL_IS_IME_ACTIVE + +// static +bool IMMHandler::IsActiveIMEInBlockList() { + if (sIMEName.IsEmpty()) { + return false; + } +#ifdef _WIN64 + // ATOK started to be TIP of TSF since 2011. Older than it, i.e., ATOK 2010 + // and earlier have a lot of problems even for daily use. Perhaps, the + // reason is Win 8 has a lot of changes around IMM-IME support and TSF, + // and ATOK 2010 is released earlier than Win 8. + // ATOK 2006 crashes while converting a word with candidate window. + // ATOK 2007 doesn't paint and resize suggest window and candidate window + // correctly (showing white window or too big window). + // ATOK 2008 and ATOK 2009 crash when user just opens their open state. + // ATOK 2010 isn't installable newly on Win 7 or later, but we have a lot of + // crash reports. + if (IsWin8OrLater() && + (IsATOK2006Active() || IsATOK2007Active() || IsATOK2008Active() || + IsATOK2009Active() || IsATOK2010Active())) { + return true; + } +#endif // #ifdef _WIN64 + return false; +} + +// static +void IMMHandler::EnsureHandlerInstance() { + if (!gIMMHandler) { + gIMMHandler = new IMMHandler(); + } +} + +// static +void IMMHandler::Initialize() { + if (!sWM_MSIME_MOUSE) { + sWM_MSIME_MOUSE = ::RegisterWindowMessage(RWM_MOUSE); + } + sAssumeVerticalWritingModeNotSupported = Preferences::GetBool( + "intl.imm.vertical_writing.always_assume_not_supported", false); + InitKeyboardLayout(nullptr, ::GetKeyboardLayout(0)); +} + +// static +void IMMHandler::Terminate() { + if (!gIMMHandler) return; + delete gIMMHandler; + gIMMHandler = nullptr; +} + +// static +bool IMMHandler::IsComposingOnOurEditor() { + return gIMMHandler && gIMMHandler->mIsComposing; +} + +// static +bool IMMHandler::IsComposingWindow(nsWindow* aWindow) { + return gIMMHandler && gIMMHandler->mComposingWindow == aWindow; +} + +// static +bool IMMHandler::IsTopLevelWindowOfComposition(nsWindow* aWindow) { + if (!gIMMHandler || !gIMMHandler->mComposingWindow) { + return false; + } + HWND wnd = gIMMHandler->mComposingWindow->GetWindowHandle(); + return WinUtils::GetTopLevelHWND(wnd, true) == aWindow->GetWindowHandle(); +} + +// static +bool IMMHandler::ShouldDrawCompositionStringOurselves() { + // If current IME has special UI or its composition window should not + // positioned to caret position, we should now draw composition string + // ourselves. + return !(sIMEProperty & IME_PROP_SPECIAL_UI) && + (sIMEProperty & IME_PROP_AT_CARET); +} + +// static +bool IMMHandler::IsVerticalWritingSupported() { + // Even if IME claims that they support vertical writing mode but it may not + // support vertical writing mode for its candidate window. + if (sAssumeVerticalWritingModeNotSupported) { + return false; + } + // Google Japanese Input doesn't support vertical writing mode. We should + // return false if it's active IME. + if (IsGoogleJapaneseInputActive()) { + return false; + } + return !!(sIMEUIProperty & (UI_CAP_2700 | UI_CAP_ROT90 | UI_CAP_ROTANY)); +} + +// static +void IMMHandler::InitKeyboardLayout(nsWindow* aWindow, HKL aKeyboardLayout) { + UINT IMENameLength = ::ImmGetDescriptionW(aKeyboardLayout, nullptr, 0); + if (IMENameLength) { + // Add room for the terminating null character + sIMEName.SetLength(++IMENameLength); + IMENameLength = + ::ImmGetDescriptionW(aKeyboardLayout, sIMEName.get(), IMENameLength); + // Adjust the length to ignore the terminating null character + sIMEName.SetLength(IMENameLength); + } else { + sIMEName.Truncate(); + } + + WORD langID = LOWORD(aKeyboardLayout); + ::GetLocaleInfoW(MAKELCID(langID, SORT_DEFAULT), + LOCALE_IDEFAULTANSICODEPAGE | LOCALE_RETURN_NUMBER, + (PWSTR)&sCodePage, sizeof(sCodePage) / sizeof(WCHAR)); + sIMEProperty = ::ImmGetProperty(aKeyboardLayout, IGP_PROPERTY); + sIMEUIProperty = ::ImmGetProperty(aKeyboardLayout, IGP_UI); + + // If active IME is a TIP of TSF, we cannot retrieve the name with IMM32 API. + // For hacking some bugs of some TIP, we should set an IME name from the + // pref. + if (sCodePage == 932 && sIMEName.IsEmpty()) { + Preferences::GetString("intl.imm.japanese.assume_active_tip_name_as", + sIMEName); + } + + // Whether the IME supports vertical writing mode might be changed or + // some IMEs may need specific font for their UI. Therefore, we should + // update composition font forcibly here. + if (aWindow) { + MaybeAdjustCompositionFont(aWindow, sWritingModeOfCompositionFont, true); + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::InitKeyboardLayout, aKeyboardLayout=%p (\"%s\"), " + "sCodePage=%u, sIMEProperty=%s, sIMEUIProperty=%s", + aKeyboardLayout, NS_ConvertUTF16toUTF8(sIMEName).get(), sCodePage, + GetIMEGeneralPropertyName(sIMEProperty).get(), + GetIMEUIPropertyName(sIMEUIProperty).get())); +} + +// static +UINT IMMHandler::GetKeyboardCodePage() { return sCodePage; } + +// static +IMENotificationRequests IMMHandler::GetIMENotificationRequests() { + return IMENotificationRequests( + IMENotificationRequests::NOTIFY_POSITION_CHANGE | + IMENotificationRequests::NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR); +} + +// used for checking the lParam of WM_IME_COMPOSITION +#define IS_COMPOSING_LPARAM(lParam) \ + ((lParam) & (GCS_COMPSTR | GCS_COMPATTR | GCS_COMPCLAUSE | GCS_CURSORPOS)) +#define IS_COMMITTING_LPARAM(lParam) ((lParam)&GCS_RESULTSTR) +// Some IMEs (e.g., the standard IME for Korean) don't have caret position, +// then, we should not set caret position to compositionchange event. +#define NO_IME_CARET -1 + +IMMHandler::IMMHandler() + : mComposingWindow(nullptr), + mCursorPosition(NO_IME_CARET), + mCompositionStart(0), + mIsComposing(false) { + MOZ_LOG(gIMELog, LogLevel::Debug, ("IMMHandler::IMMHandler is created")); +} + +IMMHandler::~IMMHandler() { + if (mIsComposing) { + MOZ_LOG( + gIMELog, LogLevel::Error, + (" IMMHandler::~IMMHandler, ERROR, the instance is still composing")); + } + MOZ_LOG(gIMELog, LogLevel::Debug, ("IMMHandler::IMMHandler is destroyed")); +} + +nsresult IMMHandler::EnsureClauseArray(int32_t aCount) { + NS_ENSURE_ARG_MIN(aCount, 0); + mClauseArray.SetCapacity(aCount + 32); + return NS_OK; +} + +nsresult IMMHandler::EnsureAttributeArray(int32_t aCount) { + NS_ENSURE_ARG_MIN(aCount, 0); + mAttributeArray.SetCapacity(aCount + 64); + return NS_OK; +} + +// static +void IMMHandler::CommitComposition(nsWindow* aWindow, bool aForce) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::CommitComposition, aForce=%s, aWindow=%p, hWnd=%p, " + "mComposingWindow=%p%s", + GetBoolName(aForce), aWindow, aWindow->GetWindowHandle(), + gIMMHandler ? gIMMHandler->mComposingWindow : nullptr, + gIMMHandler && gIMMHandler->mComposingWindow + ? IsComposingOnOurEditor() ? " (composing on editor)" + : " (composing on plug-in)" + : "")); + if (!aForce && !IsComposingWindow(aWindow)) { + return; + } + + IMEContext context(aWindow); + bool associated = context.AssociateDefaultContext(); + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::CommitComposition, associated=%s", + GetBoolName(associated))); + + if (context.IsValid()) { + ::ImmNotifyIME(context.get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0); + ::ImmNotifyIME(context.get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0); + } + + if (associated) { + context.Disassociate(); + } +} + +// static +void IMMHandler::CancelComposition(nsWindow* aWindow, bool aForce) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::CancelComposition, aForce=%s, aWindow=%p, hWnd=%p, " + "mComposingWindow=%p%s", + GetBoolName(aForce), aWindow, aWindow->GetWindowHandle(), + gIMMHandler ? gIMMHandler->mComposingWindow : nullptr, + gIMMHandler && gIMMHandler->mComposingWindow + ? IsComposingOnOurEditor() ? " (composing on editor)" + : " (composing on plug-in)" + : "")); + if (!aForce && !IsComposingWindow(aWindow)) { + return; + } + + IMEContext context(aWindow); + bool associated = context.AssociateDefaultContext(); + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::CancelComposition, associated=%s", + GetBoolName(associated))); + + if (context.IsValid()) { + ::ImmNotifyIME(context.get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0); + } + + if (associated) { + context.Disassociate(); + } +} + +// static +void IMMHandler::OnFocusChange(bool aFocus, nsWindow* aWindow) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnFocusChange(aFocus=%s, aWindow=%p), sHasFocus=%s, " + "IsComposingWindow(aWindow)=%s, aWindow->Destroyed()=%s", + GetBoolName(aFocus), aWindow, GetBoolName(sHasFocus), + GetBoolName(IsComposingWindow(aWindow)), + GetBoolName(aWindow->Destroyed()))); + + if (!aFocus) { + IMEHandler::MaybeDestroyNativeCaret(); + if (IsComposingWindow(aWindow) && aWindow->Destroyed()) { + CancelComposition(aWindow); + } + } + if (gIMMHandler) { + gIMMHandler->mContentSelection.reset(); + } + sHasFocus = aFocus; +} + +// static +void IMMHandler::OnUpdateComposition(nsWindow* aWindow) { + if (!gIMMHandler) { + return; + } + + IMEContext context(aWindow); + gIMMHandler->SetIMERelatedWindowsPos(aWindow, context); +} + +// static +void IMMHandler::OnSelectionChange(nsWindow* aWindow, + const IMENotification& aIMENotification, + bool aIsIMMActive) { + if (!aIMENotification.mSelectionChangeData.mCausedByComposition && + aIsIMMActive) { + MaybeAdjustCompositionFont( + aWindow, aIMENotification.mSelectionChangeData.GetWritingMode()); + } + // MaybeAdjustCompositionFont() may create gIMMHandler. So, check it + // after a call of MaybeAdjustCompositionFont(). + if (gIMMHandler) { + gIMMHandler->mContentSelection = + Some(ContentSelection(aIMENotification.mSelectionChangeData)); + } +} + +// static +void IMMHandler::MaybeAdjustCompositionFont(nsWindow* aWindow, + const WritingMode& aWritingMode, + bool aForceUpdate) { + switch (sCodePage) { + case 932: // Japanese Shift-JIS + case 936: // Simlified Chinese GBK + case 949: // Korean + case 950: // Traditional Chinese Big5 + EnsureHandlerInstance(); + break; + default: + // If there is no instance of nsIMM32Hander, we shouldn't waste footprint. + if (!gIMMHandler) { + return; + } + } + + // Like Navi-Bar of ATOK, some IMEs may require proper composition font even + // before sending WM_IME_STARTCOMPOSITION. + IMEContext context(aWindow); + gIMMHandler->AdjustCompositionFont(aWindow, context, aWritingMode, + aForceUpdate); +} + +// static +bool IMMHandler::ProcessInputLangChangeMessage(nsWindow* aWindow, WPARAM wParam, + LPARAM lParam, + MSGResult& aResult) { + aResult.mResult = 0; + aResult.mConsumed = false; + // We don't need to create the instance of the handler here. + if (gIMMHandler) { + gIMMHandler->OnInputLangChange(aWindow, wParam, lParam, aResult); + } + InitKeyboardLayout(aWindow, reinterpret_cast(lParam)); + // We can release the instance here, because the instance may be never + // used. E.g., the new keyboard layout may not use IME, or it may use TSF. + Terminate(); + // Don't return as "processed", the messages should be processed on nsWindow + // too. + return false; +} + +// static +bool IMMHandler::ProcessMessage(nsWindow* aWindow, UINT msg, WPARAM& wParam, + LPARAM& lParam, MSGResult& aResult) { + // XXX We store the composing window in mComposingWindow. If IME messages are + // sent to different window, we should commit the old transaction. And also + // if the new window handle is not focused, probably, we should not start + // the composition, however, such case should not be, it's just bad scenario. + + aResult.mResult = 0; + switch (msg) { + case WM_INPUTLANGCHANGE: + return ProcessInputLangChangeMessage(aWindow, wParam, lParam, aResult); + case WM_IME_STARTCOMPOSITION: + EnsureHandlerInstance(); + return gIMMHandler->OnIMEStartComposition(aWindow, aResult); + case WM_IME_COMPOSITION: + EnsureHandlerInstance(); + return gIMMHandler->OnIMEComposition(aWindow, wParam, lParam, aResult); + case WM_IME_ENDCOMPOSITION: + EnsureHandlerInstance(); + return gIMMHandler->OnIMEEndComposition(aWindow, aResult); + case WM_IME_CHAR: + return OnIMEChar(aWindow, wParam, lParam, aResult); + case WM_IME_NOTIFY: + return OnIMENotify(aWindow, wParam, lParam, aResult); + case WM_IME_REQUEST: + EnsureHandlerInstance(); + return gIMMHandler->OnIMERequest(aWindow, wParam, lParam, aResult); + case WM_IME_SELECT: + return OnIMESelect(aWindow, wParam, lParam, aResult); + case WM_IME_SETCONTEXT: + return OnIMESetContext(aWindow, wParam, lParam, aResult); + case WM_KEYDOWN: + return OnKeyDownEvent(aWindow, wParam, lParam, aResult); + case WM_CHAR: + if (!gIMMHandler) { + return false; + } + return gIMMHandler->OnChar(aWindow, wParam, lParam, aResult); + default: + return false; + }; +} + +/**************************************************************************** + * message handlers + ****************************************************************************/ + +void IMMHandler::OnInputLangChange(nsWindow* aWindow, WPARAM wParam, + LPARAM lParam, MSGResult& aResult) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnInputLangChange, hWnd=%p, wParam=%08zx, " + "lParam=%08" PRIxLPTR, + aWindow->GetWindowHandle(), wParam, lParam)); + + aWindow->NotifyIME(REQUEST_TO_COMMIT_COMPOSITION); + NS_ASSERTION(!mIsComposing, "ResetInputState failed"); + + if (mIsComposing) { + HandleEndComposition(aWindow); + } + + aResult.mConsumed = false; +} + +bool IMMHandler::OnIMEStartComposition(nsWindow* aWindow, MSGResult& aResult) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMEStartComposition, hWnd=%p, mIsComposing=%s", + aWindow->GetWindowHandle(), GetBoolName(mIsComposing))); + aResult.mConsumed = ShouldDrawCompositionStringOurselves(); + if (mIsComposing) { + NS_WARNING("Composition has been already started"); + return true; + } + + IMEContext context(aWindow); + HandleStartComposition(aWindow, context); + return true; +} + +bool IMMHandler::OnIMEComposition(nsWindow* aWindow, WPARAM wParam, + LPARAM lParam, MSGResult& aResult) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("IMMHandler::OnIMEComposition, hWnd=%p, lParam=%08" PRIxLPTR + ", mIsComposing=%s, " + "GCS_RESULTSTR=%s, GCS_COMPSTR=%s, GCS_COMPATTR=%s, GCS_COMPCLAUSE=%s, " + "GCS_CURSORPOS=%s,", + aWindow->GetWindowHandle(), lParam, GetBoolName(mIsComposing), + GetBoolName(lParam & GCS_RESULTSTR), GetBoolName(lParam & GCS_COMPSTR), + GetBoolName(lParam & GCS_COMPATTR), GetBoolName(lParam & GCS_COMPCLAUSE), + GetBoolName(lParam & GCS_CURSORPOS))); + + IMEContext context(aWindow); + aResult.mConsumed = HandleComposition(aWindow, context, lParam); + return true; +} + +bool IMMHandler::OnIMEEndComposition(nsWindow* aWindow, MSGResult& aResult) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMEEndComposition, hWnd=%p, mIsComposing=%s", + aWindow->GetWindowHandle(), GetBoolName(mIsComposing))); + + aResult.mConsumed = ShouldDrawCompositionStringOurselves(); + if (!mIsComposing) { + return true; + } + + // Korean IME posts WM_IME_ENDCOMPOSITION first when we hit space during + // composition. Then, we should ignore the message and commit the composition + // string at following WM_IME_COMPOSITION. + MSG compositionMsg; + if (WinUtils::PeekMessage(&compositionMsg, aWindow->GetWindowHandle(), + WM_IME_STARTCOMPOSITION, WM_IME_COMPOSITION, + PM_NOREMOVE) && + compositionMsg.message == WM_IME_COMPOSITION && + IS_COMMITTING_LPARAM(compositionMsg.lParam)) { + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::OnIMEEndComposition, WM_IME_ENDCOMPOSITION is " + "followed by WM_IME_COMPOSITION, ignoring the message...")); + return true; + } + + // Otherwise, e.g., ChangJie doesn't post WM_IME_COMPOSITION before + // WM_IME_ENDCOMPOSITION when composition string becomes empty. + // Then, we should dispatch a compositionupdate event, a compositionchange + // event and a compositionend event. + // XXX Shouldn't we dispatch the compositionchange event with actual or + // latest composition string? + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::OnIMEEndComposition, mCompositionString=\"%s\"%s", + NS_ConvertUTF16toUTF8(mCompositionString).get(), + mCompositionString.IsEmpty() ? "" : ", but canceling it...")); + + HandleEndComposition(aWindow, &EmptyString()); + + return true; +} + +// static +bool IMMHandler::OnIMEChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMEChar, hWnd=%p, char=%08zx", + aWindow->GetWindowHandle(), wParam)); + + // We don't need to fire any compositionchange events from here. This method + // will be called when the composition string of the current IME is not drawn + // by us and some characters are committed. In that case, the committed + // string was processed in nsWindow::OnIMEComposition already. + + // We need to consume the message so that Windows don't send two WM_CHAR msgs + aResult.mConsumed = true; + return true; +} + +// static +bool IMMHandler::OnIMECompositionFull(nsWindow* aWindow, MSGResult& aResult) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMECompositionFull, hWnd=%p", + aWindow->GetWindowHandle())); + + // not implement yet + aResult.mConsumed = false; + return true; +} + +// static +bool IMMHandler::OnIMENotify(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + switch (wParam) { + case IMN_CHANGECANDIDATE: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_CHANGECANDIDATE, " + "lParam=%08" PRIxLPTR, + aWindow->GetWindowHandle(), lParam)); + break; + case IMN_CLOSECANDIDATE: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_CLOSECANDIDATE, " + "lParam=%08" PRIxLPTR, + aWindow->GetWindowHandle(), lParam)); + break; + case IMN_CLOSESTATUSWINDOW: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_CLOSESTATUSWINDOW", + aWindow->GetWindowHandle())); + break; + case IMN_GUIDELINE: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_GUIDELINE", + aWindow->GetWindowHandle())); + break; + case IMN_OPENCANDIDATE: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_OPENCANDIDATE, " + "lParam=%08" PRIxLPTR, + aWindow->GetWindowHandle(), lParam)); + break; + case IMN_OPENSTATUSWINDOW: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_OPENSTATUSWINDOW", + aWindow->GetWindowHandle())); + break; + case IMN_SETCANDIDATEPOS: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCANDIDATEPOS, " + "lParam=%08" PRIxLPTR, + aWindow->GetWindowHandle(), lParam)); + break; + case IMN_SETCOMPOSITIONFONT: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCOMPOSITIONFONT", + aWindow->GetWindowHandle())); + break; + case IMN_SETCOMPOSITIONWINDOW: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCOMPOSITIONWINDOW", + aWindow->GetWindowHandle())); + break; + case IMN_SETCONVERSIONMODE: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETCONVERSIONMODE", + aWindow->GetWindowHandle())); + break; + case IMN_SETOPENSTATUS: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETOPENSTATUS", + aWindow->GetWindowHandle())); + break; + case IMN_SETSENTENCEMODE: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETSENTENCEMODE", + aWindow->GetWindowHandle())); + break; + case IMN_SETSTATUSWINDOWPOS: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_SETSTATUSWINDOWPOS", + aWindow->GetWindowHandle())); + break; + case IMN_PRIVATE: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMENotify, hWnd=%p, IMN_PRIVATE", + aWindow->GetWindowHandle())); + break; + } + + // not implement yet + aResult.mConsumed = false; + return true; +} + +bool IMMHandler::OnIMERequest(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + switch (wParam) { + case IMR_RECONVERTSTRING: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMERequest, hWnd=%p, IMR_RECONVERTSTRING", + aWindow->GetWindowHandle())); + aResult.mConsumed = HandleReconvert(aWindow, lParam, &aResult.mResult); + return true; + case IMR_QUERYCHARPOSITION: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMERequest, hWnd=%p, IMR_QUERYCHARPOSITION", + aWindow->GetWindowHandle())); + aResult.mConsumed = + HandleQueryCharPosition(aWindow, lParam, &aResult.mResult); + return true; + case IMR_DOCUMENTFEED: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMERequest, hWnd=%p, IMR_DOCUMENTFEED", + aWindow->GetWindowHandle())); + aResult.mConsumed = HandleDocumentFeed(aWindow, lParam, &aResult.mResult); + return true; + default: + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMERequest, hWnd=%p, wParam=%08zx", + aWindow->GetWindowHandle(), wParam)); + aResult.mConsumed = false; + return true; + } +} + +// static +bool IMMHandler::OnIMESelect(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("IMMHandler::OnIMESelect, hWnd=%p, wParam=%08zx, lParam=%08" PRIxLPTR, + aWindow->GetWindowHandle(), wParam, lParam)); + + // not implement yet + aResult.mConsumed = false; + return true; +} + +// static +bool IMMHandler::OnIMESetContext(nsWindow* aWindow, WPARAM wParam, + LPARAM lParam, MSGResult& aResult) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnIMESetContext, hWnd=%p, %s, lParam=%08" PRIxLPTR, + aWindow->GetWindowHandle(), wParam ? "Active" : "Deactive", lParam)); + + aResult.mConsumed = false; + + // NOTE: If the aWindow is top level window of the composing window because + // when a window on deactive window gets focus, WM_IME_SETCONTEXT (wParam is + // TRUE) is sent to the top level window first. After that, + // WM_IME_SETCONTEXT (wParam is FALSE) is sent to the top level window. + // Finally, WM_IME_SETCONTEXT (wParam is TRUE) is sent to the focused window. + // The top level window never becomes composing window, so, we can ignore + // the WM_IME_SETCONTEXT on the top level window. + if (IsTopLevelWindowOfComposition(aWindow)) { + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::OnIMESetContext, hWnd=%p is top level window", + aWindow->GetWindowHandle())); + return true; + } + + // When IME context is activating on another window, + // we should commit the old composition on the old window. + bool cancelComposition = false; + if (wParam && gIMMHandler) { + cancelComposition = gIMMHandler->CommitCompositionOnPreviousWindow(aWindow); + } + + if (wParam && (lParam & ISC_SHOWUICOMPOSITIONWINDOW) && + ShouldDrawCompositionStringOurselves()) { + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::OnIMESetContext, ISC_SHOWUICOMPOSITIONWINDOW is " + "removed")); + lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW; + } + + // We should sent WM_IME_SETCONTEXT to the DefWndProc here because the + // ancestor windows shouldn't receive this message. If they receive the + // message, we cannot know whether which window is the target of the message. + aResult.mResult = ::DefWindowProc(aWindow->GetWindowHandle(), + WM_IME_SETCONTEXT, wParam, lParam); + + // Cancel composition on the new window if we committed our composition on + // another window. + if (cancelComposition) { + CancelComposition(aWindow, true); + } + + aResult.mConsumed = true; + return true; +} + +bool IMMHandler::OnChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + // The return value must be same as aResult.mConsumed because only when we + // consume the message, the caller shouldn't do anything anymore but + // otherwise, the caller should handle the message. + aResult.mConsumed = false; + if (IsIMECharRecordsEmpty()) { + return aResult.mConsumed; + } + WPARAM recWParam; + LPARAM recLParam; + DequeueIMECharRecords(recWParam, recLParam); + MOZ_LOG( + gIMELog, LogLevel::Info, + ("IMMHandler::OnChar, aWindow=%p, wParam=%08zx, lParam=%08" PRIxLPTR ", " + "recorded: wParam=%08zx, lParam=%08" PRIxLPTR, + aWindow->GetWindowHandle(), wParam, lParam, recWParam, recLParam)); + // If an unexpected char message comes, we should reset the records, + // of course, this shouldn't happen. + if (recWParam != wParam || recLParam != lParam) { + ResetIMECharRecords(); + return aResult.mConsumed; + } + // Eat the char message which is caused by WM_IME_CHAR because we should + // have processed the IME messages, so, this message could be come from + // a windowless plug-in. + aResult.mConsumed = true; + return aResult.mConsumed; +} + +/**************************************************************************** + * others + ****************************************************************************/ + +TextEventDispatcher* IMMHandler::GetTextEventDispatcherFor(nsWindow* aWindow) { + return aWindow == mComposingWindow && mDispatcher + ? mDispatcher.get() + : aWindow->GetTextEventDispatcher(); +} + +void IMMHandler::HandleStartComposition(nsWindow* aWindow, + const IMEContext& aContext) { + MOZ_ASSERT(!mIsComposing, + "HandleStartComposition is called but mIsComposing is TRUE"); + + const Maybe& contentSelection = + GetContentSelectionWithQueryIfNothing(aWindow); + if (contentSelection.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::HandleStartComposition, FAILED, due to " + "Selection::GetContentSelectionWithQueryIfNothing() failure")); + return; + } + if (!contentSelection->HasRange()) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::HandleStartComposition, FAILED, due to " + "there is no selection")); + return; + } + + AdjustCompositionFont(aWindow, aContext, contentSelection->WritingModeRef()); + + mCompositionStart = contentSelection->OffsetAndDataRef().StartOffset(); + mCursorPosition = NO_IME_CARET; + + RefPtr dispatcher = GetTextEventDispatcherFor(aWindow); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::HandleStartComposition, FAILED due to " + "TextEventDispatcher::BeginNativeInputTransaction() failure")); + return; + } + WidgetEventTime eventTime = aWindow->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = dispatcher->StartComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::HandleStartComposition, FAILED, due to " + "TextEventDispatcher::StartComposition() failure")); + return; + } + + mIsComposing = true; + mComposingWindow = aWindow; + mDispatcher = dispatcher; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::HandleStartComposition, START composition, " + "mCompositionStart=%u", + mCompositionStart)); +} + +bool IMMHandler::HandleComposition(nsWindow* aWindow, + const IMEContext& aContext, LPARAM lParam) { + // for bug #60050 + // MS-IME 95/97/98/2000 may send WM_IME_COMPOSITION with non-conversion + // mode before it send WM_IME_STARTCOMPOSITION. + // However, ATOK sends a WM_IME_COMPOSITION before WM_IME_STARTCOMPOSITION, + // and if we access ATOK via some APIs, ATOK will sometimes fail to + // initialize its state. If WM_IME_STARTCOMPOSITION is already in the + // message queue, we should ignore the strange WM_IME_COMPOSITION message and + // skip to the next. So, we should look for next composition message + // (WM_IME_STARTCOMPOSITION or WM_IME_ENDCOMPOSITION or WM_IME_COMPOSITION), + // and if it's WM_IME_STARTCOMPOSITION, and one more next composition message + // is WM_IME_COMPOSITION, current IME is ATOK, probably. Otherwise, we + // should start composition forcibly. + if (!mIsComposing) { + MSG msg1, msg2; + HWND wnd = aWindow->GetWindowHandle(); + if (WinUtils::PeekMessage(&msg1, wnd, WM_IME_STARTCOMPOSITION, + WM_IME_COMPOSITION, PM_NOREMOVE) && + msg1.message == WM_IME_STARTCOMPOSITION && + WinUtils::PeekMessage(&msg2, wnd, WM_IME_ENDCOMPOSITION, + WM_IME_COMPOSITION, PM_NOREMOVE) && + msg2.message == WM_IME_COMPOSITION) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::HandleComposition, Ignores due to find a " + "WM_IME_STARTCOMPOSITION")); + return ShouldDrawCompositionStringOurselves(); + } + } + + bool startCompositionMessageHasBeenSent = mIsComposing; + + // + // This catches a fixed result + // + if (IS_COMMITTING_LPARAM(lParam)) { + if (!mIsComposing) { + HandleStartComposition(aWindow, aContext); + } + + GetCompositionString(aContext, GCS_RESULTSTR, mCompositionString); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::HandleComposition, GCS_RESULTSTR")); + + HandleEndComposition(aWindow, &mCompositionString); + + if (!IS_COMPOSING_LPARAM(lParam)) { + return ShouldDrawCompositionStringOurselves(); + } + } + + // + // This provides us with a composition string + // + if (!mIsComposing) { + HandleStartComposition(aWindow, aContext); + } + + //-------------------------------------------------------- + // 1. Get GCS_COMPSTR + //-------------------------------------------------------- + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::HandleComposition, GCS_COMPSTR")); + + nsAutoString previousCompositionString(mCompositionString); + GetCompositionString(aContext, GCS_COMPSTR, mCompositionString); + + if (!IS_COMPOSING_LPARAM(lParam)) { + MOZ_LOG( + gIMELog, LogLevel::Info, + (" IMMHandler::HandleComposition, lParam doesn't indicate composing, " + "mCompositionString=\"%s\", previousCompositionString=\"%s\"", + NS_ConvertUTF16toUTF8(mCompositionString).get(), + NS_ConvertUTF16toUTF8(previousCompositionString).get())); + + // If composition string isn't changed, we can trust the lParam. + // So, we need to do nothing. + if (previousCompositionString == mCompositionString) { + return ShouldDrawCompositionStringOurselves(); + } + + // IME may send WM_IME_COMPOSITION without composing lParam values + // when composition string becomes empty (e.g., using Backspace key). + // If composition string is empty, we should dispatch a compositionchange + // event with empty string and clear the clause information. + if (mCompositionString.IsEmpty()) { + mClauseArray.Clear(); + mAttributeArray.Clear(); + mCursorPosition = 0; + DispatchCompositionChangeEvent(aWindow, aContext); + return ShouldDrawCompositionStringOurselves(); + } + + // Otherwise, we cannot trust the lParam value. We might need to + // dispatch compositionchange event with the latest composition string + // information. + } + + // See https://bugzilla.mozilla.org/show_bug.cgi?id=296339 + if (mCompositionString.IsEmpty() && !startCompositionMessageHasBeenSent) { + // In this case, maybe, the sender is MSPinYin. That sends *only* + // WM_IME_COMPOSITION with GCS_COMP* and GCS_RESULT* when + // user inputted the Chinese full stop. So, that doesn't send + // WM_IME_STARTCOMPOSITION and WM_IME_ENDCOMPOSITION. + // If WM_IME_STARTCOMPOSITION was not sent and the composition + // string is null (it indicates the composition transaction ended), + // WM_IME_ENDCOMPOSITION may not be sent. If so, we cannot run + // HandleEndComposition() in other place. + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::HandleComposition, Aborting GCS_COMPSTR")); + HandleEndComposition(aWindow); + return IS_COMMITTING_LPARAM(lParam); + } + + //-------------------------------------------------------- + // 2. Get GCS_COMPCLAUSE + //-------------------------------------------------------- + long clauseArrayLength = + ::ImmGetCompositionStringW(aContext.get(), GCS_COMPCLAUSE, nullptr, 0); + clauseArrayLength /= sizeof(uint32_t); + + if (clauseArrayLength > 0) { + nsresult rv = EnsureClauseArray(clauseArrayLength); + NS_ENSURE_SUCCESS(rv, false); + + // Intelligent ABC IME (Simplified Chinese IME, the code page is 936) + // will crash in ImmGetCompositionStringW for GCS_COMPCLAUSE (bug 424663). + // See comment 35 of the bug for the detail. Therefore, we should use A + // API for it, however, we should not kill Unicode support on all IMEs. + bool useA_API = !(sIMEProperty & IME_PROP_UNICODE); + + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::HandleComposition, GCS_COMPCLAUSE, useA_API=%s", + useA_API ? "TRUE" : "FALSE")); + + long clauseArrayLength2 = + useA_API ? ::ImmGetCompositionStringA( + aContext.get(), GCS_COMPCLAUSE, mClauseArray.Elements(), + mClauseArray.Capacity() * sizeof(uint32_t)) + : ::ImmGetCompositionStringW( + aContext.get(), GCS_COMPCLAUSE, mClauseArray.Elements(), + mClauseArray.Capacity() * sizeof(uint32_t)); + clauseArrayLength2 /= sizeof(uint32_t); + + if (clauseArrayLength != clauseArrayLength2) { + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::HandleComposition, GCS_COMPCLAUSE, " + "clauseArrayLength=%ld but clauseArrayLength2=%ld", + clauseArrayLength, clauseArrayLength2)); + if (clauseArrayLength > clauseArrayLength2) + clauseArrayLength = clauseArrayLength2; + } + + if (useA_API && clauseArrayLength > 0) { + // Convert each values of sIMECompClauseArray. The values mean offset of + // the clauses in ANSI string. But we need the values in Unicode string. + nsAutoCString compANSIStr; + if (ConvertToANSIString(mCompositionString, GetKeyboardCodePage(), + compANSIStr)) { + uint32_t maxlen = compANSIStr.Length(); + mClauseArray.SetLength(clauseArrayLength); + mClauseArray[0] = 0; // first value must be 0 + for (int32_t i = 1; i < clauseArrayLength; i++) { + uint32_t len = std::min(mClauseArray[i], maxlen); + mClauseArray[i] = + ::MultiByteToWideChar(GetKeyboardCodePage(), MB_PRECOMPOSED, + (LPCSTR)compANSIStr.get(), len, nullptr, 0); + } + } + } + } + // compClauseArrayLength may be negative. I.e., ImmGetCompositionStringW + // may return an error code. + mClauseArray.SetLength(std::max(0, clauseArrayLength)); + + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::HandleComposition, GCS_COMPCLAUSE, mClauseLength=%zu", + mClauseArray.Length())); + + //-------------------------------------------------------- + // 3. Get GCS_COMPATTR + //-------------------------------------------------------- + // This provides us with the attribute string necessary + // for doing hiliting + long attrArrayLength = + ::ImmGetCompositionStringW(aContext.get(), GCS_COMPATTR, nullptr, 0); + attrArrayLength /= sizeof(uint8_t); + + if (attrArrayLength > 0) { + nsresult rv = EnsureAttributeArray(attrArrayLength); + NS_ENSURE_SUCCESS(rv, false); + attrArrayLength = ::ImmGetCompositionStringW( + aContext.get(), GCS_COMPATTR, mAttributeArray.Elements(), + mAttributeArray.Capacity() * sizeof(uint8_t)); + } + + // attrStrLen may be negative. I.e., ImmGetCompositionStringW may return an + // error code. + mAttributeArray.SetLength(std::max(0, attrArrayLength)); + + MOZ_LOG( + gIMELog, LogLevel::Info, + (" IMMHandler::HandleComposition, GCS_COMPATTR, mAttributeLength=%zu", + mAttributeArray.Length())); + + //-------------------------------------------------------- + // 4. Get GCS_CURSOPOS + //-------------------------------------------------------- + // Some IMEs (e.g., the standard IME for Korean) don't have caret position. + if (lParam & GCS_CURSORPOS) { + mCursorPosition = + ::ImmGetCompositionStringW(aContext.get(), GCS_CURSORPOS, nullptr, 0); + if (mCursorPosition < 0) { + mCursorPosition = NO_IME_CARET; // The result is error + } + } else { + mCursorPosition = NO_IME_CARET; + } + + NS_ASSERTION(mCursorPosition <= (long)mCompositionString.Length(), + "illegal pos"); + + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::HandleComposition, GCS_CURSORPOS, mCursorPosition=%d", + mCursorPosition)); + + //-------------------------------------------------------- + // 5. Send the compositionchange event + //-------------------------------------------------------- + DispatchCompositionChangeEvent(aWindow, aContext); + + return ShouldDrawCompositionStringOurselves(); +} + +void IMMHandler::HandleEndComposition(nsWindow* aWindow, + const nsAString* aCommitString) { + MOZ_ASSERT(mIsComposing, + "HandleEndComposition is called but mIsComposing is FALSE"); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::HandleEndComposition(aWindow=0x%p, aCommitString=0x%p " + "(\"%s\"))", + aWindow, aCommitString, + aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "")); + + IMEHandler::MaybeDestroyNativeCaret(); + + RefPtr dispatcher = GetTextEventDispatcherFor(aWindow); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::HandleEndComposition, FAILED due to " + "TextEventDispatcher::BeginNativeInputTransaction() failure")); + return; + } + WidgetEventTime eventTime = aWindow->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = dispatcher->CommitComposition(status, aCommitString, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::HandleStartComposition, FAILED, due to " + "TextEventDispatcher::CommitComposition() failure")); + return; + } + mIsComposing = false; + // XXX aWindow and mComposingWindow are always same?? + mComposingWindow = nullptr; + mDispatcher = nullptr; +} + +bool IMMHandler::HandleReconvert(nsWindow* aWindow, LPARAM lParam, + LRESULT* oResult) { + *oResult = 0; + RECONVERTSTRING* pReconv = reinterpret_cast(lParam); + + const Maybe& contentSelection = + GetContentSelectionWithQueryIfNothing(aWindow); + if (contentSelection.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleReconvert, FAILED, due to " + "Selection::GetContentSelectionWithQueryIfNothing() failure")); + return false; + } + + const uint32_t len = contentSelection->HasRange() + ? contentSelection->OffsetAndDataRef().Length() + : 0u; + uint32_t needSize = sizeof(RECONVERTSTRING) + len * sizeof(WCHAR); + + if (!pReconv) { + // Return need size to reconvert. + if (len == 0) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleReconvert, There are not selected text")); + return false; + } + *oResult = needSize; + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::HandleReconvert, succeeded, result=%" PRIdLPTR, + *oResult)); + return true; + } + + if (pReconv->dwSize < needSize) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::HandleReconvert, FAILED, pReconv->dwSize=%ld, " + "needSize=%u", + pReconv->dwSize, needSize)); + return false; + } + + *oResult = needSize; + + // Fill reconvert struct + pReconv->dwVersion = 0; + pReconv->dwStrLen = len; + pReconv->dwStrOffset = sizeof(RECONVERTSTRING); + pReconv->dwCompStrLen = len; + pReconv->dwCompStrOffset = 0; + pReconv->dwTargetStrLen = len; + pReconv->dwTargetStrOffset = 0; + + if (len) { + ::CopyMemory(reinterpret_cast(lParam + sizeof(RECONVERTSTRING)), + contentSelection->OffsetAndDataRef().DataRef().get(), + len * sizeof(WCHAR)); + } + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("IMMHandler::HandleReconvert, SUCCEEDED, pReconv=%s, result=%" PRIdLPTR, + GetReconvertStringLog(pReconv).get(), *oResult)); + + return true; +} + +bool IMMHandler::HandleQueryCharPosition(nsWindow* aWindow, LPARAM lParam, + LRESULT* oResult) { + uint32_t len = mIsComposing ? mCompositionString.Length() : 0; + *oResult = false; + IMECHARPOSITION* pCharPosition = reinterpret_cast(lParam); + if (!pCharPosition) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleQueryCharPosition, FAILED, due to " + "pCharPosition is null")); + return false; + } + if (pCharPosition->dwSize < sizeof(IMECHARPOSITION)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleReconvert, FAILED, pCharPosition->dwSize=%lu, " + "sizeof(IMECHARPOSITION)=%zu", + pCharPosition->dwSize, sizeof(IMECHARPOSITION))); + return false; + } + if (::GetFocus() != aWindow->GetWindowHandle()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleReconvert, FAILED, ::GetFocus()=%p, " + "OurWindowHandle=%p", + ::GetFocus(), aWindow->GetWindowHandle())); + return false; + } + if (pCharPosition->dwCharPos > len) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleQueryCharPosition, FAILED, " + "pCharPosition->dwCharPos=%ld, len=%u", + pCharPosition->dwCharPos, len)); + return false; + } + + LayoutDeviceIntRect r; + bool ret = + GetCharacterRectOfSelectedTextAt(aWindow, pCharPosition->dwCharPos, r); + NS_ENSURE_TRUE(ret, false); + + LayoutDeviceIntRect screenRect; + // We always need top level window that is owner window of the popup window + // even if the content of the popup window has focus. + ResolveIMECaretPos(aWindow->GetTopLevelWindow(false), r, nullptr, screenRect); + + // XXX This might need to check writing mode. However, MSDN doesn't explain + // how to set the values in vertical writing mode. Additionally, IME + // doesn't work well with top-left of the character (this is explicitly + // documented) and its horizontal width. So, it might be better to set + // top-right corner of the character and horizontal width, but we're not + // sure if it doesn't cause any problems with a lot of IMEs... + pCharPosition->pt.x = screenRect.X(); + pCharPosition->pt.y = screenRect.Y(); + + pCharPosition->cLineHeight = r.Height(); + + WidgetQueryContentEvent queryEditorRectEvent(true, eQueryEditorRect, aWindow); + aWindow->InitEvent(queryEditorRectEvent); + DispatchEvent(aWindow, queryEditorRectEvent); + if (NS_WARN_IF(queryEditorRectEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::HandleQueryCharPosition, eQueryEditorRect failed")); + ::GetWindowRect(aWindow->GetWindowHandle(), &pCharPosition->rcDocument); + } else { + LayoutDeviceIntRect editorRectInWindow = queryEditorRectEvent.mReply->mRect; + nsWindow* window = !!queryEditorRectEvent.mReply->mFocusedWidget + ? static_cast( + queryEditorRectEvent.mReply->mFocusedWidget) + : aWindow; + LayoutDeviceIntRect editorRectInScreen; + ResolveIMECaretPos(window, editorRectInWindow, nullptr, editorRectInScreen); + ::SetRect(&pCharPosition->rcDocument, editorRectInScreen.X(), + editorRectInScreen.Y(), editorRectInScreen.XMost(), + editorRectInScreen.YMost()); + } + + *oResult = TRUE; + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("IMMHandler::HandleQueryCharPosition, SUCCEEDED, pCharPosition={ " + "pt={ x=%ld, y=%ld }, cLineHeight=%d, rcDocument={ left=%ld, top=%ld, " + "right=%ld, bottom=%ld } }", + pCharPosition->pt.x, pCharPosition->pt.y, pCharPosition->cLineHeight, + pCharPosition->rcDocument.left, pCharPosition->rcDocument.top, + pCharPosition->rcDocument.right, pCharPosition->rcDocument.bottom)); + return true; +} + +bool IMMHandler::HandleDocumentFeed(nsWindow* aWindow, LPARAM lParam, + LRESULT* oResult) { + *oResult = 0; + RECONVERTSTRING* pReconv = reinterpret_cast(lParam); + + LayoutDeviceIntPoint point(0, 0); + + bool hasCompositionString = + mIsComposing && ShouldDrawCompositionStringOurselves(); + + int32_t targetOffset, targetLength; + if (!hasCompositionString) { + const Maybe& contentSelection = + GetContentSelectionWithQueryIfNothing(aWindow); + if (contentSelection.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleDocumentFeed, FAILED, due to " + "Selection::GetContentSelectionWithQueryIfNothing() failure")); + return false; + } + if (contentSelection->HasRange()) { + targetOffset = static_cast( + contentSelection->OffsetAndDataRef().StartOffset()); + targetLength = + static_cast(contentSelection->OffsetAndDataRef().Length()); + } else { + // If there is no selection range, let's return all text in the editor. + targetOffset = 0; + targetLength = INT32_MAX; + } + } else { + targetOffset = int32_t(mCompositionStart); + targetLength = int32_t(mCompositionString.Length()); + } + + // XXX nsString::Find and nsString::RFind take int32_t for offset, so, + // we cannot support this message when the current offset is larger than + // INT32_MAX. + if (targetOffset < 0 || targetLength < 0 || targetOffset + targetLength < 0) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleDocumentFeed, FAILED, " + "due to the selection is out of range")); + return false; + } + + // Get all contents of the focused editor. + WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, + aWindow); + queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX); + aWindow->InitEvent(queryTextContentEvent, &point); + DispatchEvent(aWindow, queryTextContentEvent); + if (NS_WARN_IF(queryTextContentEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleDocumentFeed, FAILED, " + "due to eQueryTextContent failure")); + return false; + } + + nsAutoString str(queryTextContentEvent.mReply->DataRef()); + if (targetOffset > static_cast(str.Length())) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::HandleDocumentFeed, FAILED, " + "due to the caret offset is invalid")); + return false; + } + + // Get the focused paragraph, we decide that it starts from the previous CRLF + // (or start of the editor) to the next one (or the end of the editor). + int32_t paragraphStart = 0; + if (targetOffset > 0) { + paragraphStart = Substring(str, 0, targetOffset).RFind(u"\n") + 1; + } + int32_t paragraphEnd = str.Find(u"\r", targetOffset + targetLength); + if (paragraphEnd < 0) { + paragraphEnd = str.Length(); + } + nsDependentSubstring paragraph(str, paragraphStart, + paragraphEnd - paragraphStart); + + uint32_t len = paragraph.Length(); + uint32_t needSize = sizeof(RECONVERTSTRING) + len * sizeof(WCHAR); + + if (!pReconv) { + *oResult = needSize; + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::HandleDocumentFeed, succeeded, result=%" PRIdLPTR, + *oResult)); + return true; + } + + if (pReconv->dwSize < needSize) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleDocumentFeed, FAILED, " + "pReconv->dwSize=%ld, needSize=%u", + pReconv->dwSize, needSize)); + return false; + } + + // Fill reconvert struct + pReconv->dwVersion = 0; + pReconv->dwStrLen = len; + pReconv->dwStrOffset = sizeof(RECONVERTSTRING); + if (hasCompositionString) { + pReconv->dwCompStrLen = targetLength; + pReconv->dwCompStrOffset = (targetOffset - paragraphStart) * sizeof(WCHAR); + // Set composition target clause information + uint32_t offset, length; + if (!GetTargetClauseRange(&offset, &length)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::HandleDocumentFeed, FAILED, " + "due to IMMHandler::GetTargetClauseRange() failure")); + return false; + } + pReconv->dwTargetStrLen = length; + pReconv->dwTargetStrOffset = (offset - paragraphStart) * sizeof(WCHAR); + } else { + pReconv->dwTargetStrLen = targetLength; + pReconv->dwTargetStrOffset = + (targetOffset - paragraphStart) * sizeof(WCHAR); + // There is no composition string, so, the length is zero but we should + // set the cursor offset to the composition str offset. + pReconv->dwCompStrLen = 0; + pReconv->dwCompStrOffset = pReconv->dwTargetStrOffset; + } + + *oResult = needSize; + ::CopyMemory(reinterpret_cast(lParam + sizeof(RECONVERTSTRING)), + paragraph.BeginReading(), len * sizeof(WCHAR)); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::HandleDocumentFeed, SUCCEEDED, pReconv=%s, " + "result=%" PRIdLPTR, + GetReconvertStringLog(pReconv).get(), *oResult)); + + return true; +} + +bool IMMHandler::CommitCompositionOnPreviousWindow(nsWindow* aWindow) { + if (!mComposingWindow || mComposingWindow == aWindow) { + return false; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::CommitCompositionOnPreviousWindow, mIsComposing=%s", + GetBoolName(mIsComposing))); + + // If we have composition, we should dispatch composition events internally. + if (mIsComposing) { + IMEContext context(mComposingWindow); + NS_ASSERTION(context.IsValid(), "IME context must be valid"); + + HandleEndComposition(mComposingWindow); + return true; + } + + return false; +} + +static TextRangeType PlatformToNSAttr(uint8_t aAttr) { + switch (aAttr) { + case ATTR_INPUT_ERROR: + // case ATTR_FIXEDCONVERTED: + case ATTR_INPUT: + return TextRangeType::eRawClause; + case ATTR_CONVERTED: + return TextRangeType::eConvertedClause; + case ATTR_TARGET_NOTCONVERTED: + return TextRangeType::eSelectedRawClause; + case ATTR_TARGET_CONVERTED: + return TextRangeType::eSelectedClause; + default: + NS_ASSERTION(false, "unknown attribute"); + return TextRangeType::eCaret; + } +} + +// static +void IMMHandler::DispatchEvent(nsWindow* aWindow, WidgetGUIEvent& aEvent) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("IMMHandler::DispatchEvent(aWindow=0x%p, aEvent={ mMessage=%s }, " + "aWindow->Destroyed()=%s", + aWindow, ToChar(aEvent.mMessage), GetBoolName(aWindow->Destroyed()))); + + if (aWindow->Destroyed()) { + return; + } + + aWindow->DispatchWindowEvent(aEvent); +} + +void IMMHandler::DispatchCompositionChangeEvent(nsWindow* aWindow, + const IMEContext& aContext) { + NS_ASSERTION(mIsComposing, "conflict state"); + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::DispatchCompositionChangeEvent")); + + // If we don't need to draw composition string ourselves, we don't need to + // fire compositionchange event during composing. + if (!ShouldDrawCompositionStringOurselves()) { + // But we need to adjust composition window pos and native caret pos, here. + SetIMERelatedWindowsPos(aWindow, aContext); + return; + } + + RefPtr kungFuDeathGrip(aWindow); + RefPtr dispatcher = GetTextEventDispatcherFor(aWindow); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to " + "TextEventDispatcher::BeginNativeInputTransaction() failure")); + return; + } + + // NOTE: Calling SetIMERelatedWindowsPos() from this method will be failure + // in e10s mode. compositionchange event will notify this of + // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED, then + // SetIMERelatedWindowsPos() will be called. + + // XXX Sogou (Simplified Chinese IME) returns contradictory values: + // The cursor position is actual cursor position. However, other values + // (composition string and attributes) are empty. + + if (mCompositionString.IsEmpty()) { + // Don't append clause information if composition string is empty. + } else if (mClauseArray.IsEmpty()) { + // Some IMEs don't return clause array information, then, we assume that + // all characters in the composition string are in one clause. + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::DispatchCompositionChangeEvent, " + "mClauseArray.Length()=0")); + rv = dispatcher->SetPendingComposition(mCompositionString, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::SetPendingComposition() failure")); + return; + } + } else { + // iterate over the attributes + rv = dispatcher->SetPendingCompositionString(mCompositionString); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::SetPendingCompositionString() failure")); + return; + } + uint32_t lastOffset = 0; + for (uint32_t i = 0; i < mClauseArray.Length() - 1; i++) { + uint32_t current = mClauseArray[i + 1]; + if (current > mCompositionString.Length()) { + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::DispatchCompositionChangeEvent, " + "mClauseArray[%u]=%u. " + "This is larger than mCompositionString.Length()=%zu", + i + 1, current, mCompositionString.Length())); + current = int32_t(mCompositionString.Length()); + } + + uint32_t length = current - lastOffset; + if (NS_WARN_IF(lastOffset >= mAttributeArray.Length())) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to " + "invalid data of mClauseArray or mAttributeArray")); + return; + } + TextRangeType textRangeType = + PlatformToNSAttr(mAttributeArray[lastOffset]); + rv = dispatcher->AppendClauseToPendingComposition(length, textRangeType); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::AppendClauseToPendingComposition() " + "failure")); + return; + } + + lastOffset = current; + + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::DispatchCompositionChangeEvent, index=%u, " + "rangeType=%s, range length=%u", + i, ToChar(textRangeType), length)); + } + } + + if (mCursorPosition == NO_IME_CARET) { + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::DispatchCompositionChangeEvent, no caret")); + } else { + uint32_t cursor = static_cast(mCursorPosition); + if (cursor > mCompositionString.Length()) { + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::CreateTextRangeArray, mCursorPosition=%d. " + "This is larger than mCompositionString.Length()=%zu", + mCursorPosition, mCompositionString.Length())); + cursor = mCompositionString.Length(); + } + + // If caret is in the target clause, the target clause will be painted as + // normal selection range. Since caret shouldn't be in selection range on + // Windows, we shouldn't append caret range in such case. + const TextRangeArray* clauses = dispatcher->GetPendingCompositionClauses(); + const TextRange* targetClause = + clauses ? clauses->GetTargetClause() : nullptr; + if (targetClause && cursor >= targetClause->mStartOffset && + cursor <= targetClause->mEndOffset) { + // Forget the caret position specified by IME since Gecko's caret position + // will be at the end of composition string. + mCursorPosition = NO_IME_CARET; + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::CreateTextRangeArray, no caret due to it's in " + "the target clause, now, mCursorPosition is NO_IME_CARET")); + } + + if (mCursorPosition != NO_IME_CARET) { + rv = dispatcher->SetCaretInPendingComposition(cursor, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG( + gIMELog, LogLevel::Error, + (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::SetCaretInPendingComposition() failure")); + return; + } + } + } + + WidgetEventTime eventTime = aWindow->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = dispatcher->FlushPendingComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::DispatchCompositionChangeEvent, FAILED due to" + "TextEventDispatcher::FlushPendingComposition() failure")); + return; + } +} + +void IMMHandler::GetCompositionString(const IMEContext& aContext, DWORD aIndex, + nsAString& aCompositionString) const { + aCompositionString.Truncate(); + + // Retrieve the size of the required output buffer. + long lRtn = ::ImmGetCompositionStringW(aContext.get(), aIndex, nullptr, 0); + if (lRtn < 0 || !aCompositionString.SetLength((lRtn / sizeof(WCHAR)) + 1, + mozilla::fallible)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::GetCompositionString, FAILED, due to OOM")); + return; // Error or out of memory. + } + + // Actually retrieve the composition string information. + lRtn = ::ImmGetCompositionStringW(aContext.get(), aIndex, + (LPVOID)aCompositionString.BeginWriting(), + lRtn + sizeof(WCHAR)); + aCompositionString.SetLength(lRtn / sizeof(WCHAR)); + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("IMMHandler::GetCompositionString, succeeded, aCompositionString=\"%s\"", + NS_ConvertUTF16toUTF8(aCompositionString).get())); +} + +bool IMMHandler::GetTargetClauseRange(uint32_t* aOffset, uint32_t* aLength) { + NS_ENSURE_TRUE(aOffset, false); + NS_ENSURE_TRUE(mIsComposing, false); + NS_ENSURE_TRUE(ShouldDrawCompositionStringOurselves(), false); + + bool found = false; + *aOffset = mCompositionStart; + for (uint32_t i = 0; i < mAttributeArray.Length(); i++) { + if (mAttributeArray[i] == ATTR_TARGET_NOTCONVERTED || + mAttributeArray[i] == ATTR_TARGET_CONVERTED) { + *aOffset = mCompositionStart + i; + found = true; + break; + } + } + + if (!aLength) { + return true; + } + + if (!found) { + // The all composition string is targetted when there is no ATTR_TARGET_* + // clause. E.g., there is only ATTR_INPUT + *aLength = mCompositionString.Length(); + return true; + } + + uint32_t offsetInComposition = *aOffset - mCompositionStart; + *aLength = mCompositionString.Length() - offsetInComposition; + for (uint32_t i = offsetInComposition; i < mAttributeArray.Length(); i++) { + if (mAttributeArray[i] != ATTR_TARGET_NOTCONVERTED && + mAttributeArray[i] != ATTR_TARGET_CONVERTED) { + *aLength = i - offsetInComposition; + break; + } + } + return true; +} + +bool IMMHandler::ConvertToANSIString(const nsString& aStr, UINT aCodePage, + nsACString& aANSIStr) { + int len = ::WideCharToMultiByte(aCodePage, 0, (LPCWSTR)aStr.get(), + aStr.Length(), nullptr, 0, nullptr, nullptr); + NS_ENSURE_TRUE(len >= 0, false); + + if (!aANSIStr.SetLength(len, mozilla::fallible)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::ConvertToANSIString, FAILED, due to OOM")); + return false; + } + ::WideCharToMultiByte(aCodePage, 0, (LPCWSTR)aStr.get(), aStr.Length(), + (LPSTR)aANSIStr.BeginWriting(), len, nullptr, nullptr); + return true; +} + +bool IMMHandler::GetCharacterRectOfSelectedTextAt( + nsWindow* aWindow, uint32_t aOffset, LayoutDeviceIntRect& aCharRect, + WritingMode* aWritingMode) { + LayoutDeviceIntPoint point(0, 0); + + const Maybe& contentSelection = + GetContentSelectionWithQueryIfNothing(aWindow); + if (contentSelection.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("IMMHandler::GetCharacterRectOfSelectedTextAt, FAILED, due to " + "Selection::GetContentSelectionWithQueryIfNothing() failure")); + return false; + } + + // If there is neither a selection range nor composition string, cannot return + // character rect, of course. + if (!contentSelection->HasRange() && !mIsComposing) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("IMMHandler::GetCharacterRectOfSelectedTextAt, FAILED, due to " + "there is neither a selection range nor composition string")); + return false; + } + + // If the offset is larger than the end of composition string or selected + // string, we should return false since such case must be a bug of the caller + // or the active IME. If it's an IME's bug, we need to set targetLength to + // aOffset. + const uint32_t targetLength = + mIsComposing ? mCompositionString.Length() + : contentSelection->OffsetAndDataRef().Length(); + if (NS_WARN_IF(aOffset > targetLength)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("IMMHandler::GetCharacterRectOfSelectedTextAt, FAILED, due to " + "aOffset is too large (aOffset=%u, targetLength=%u, mIsComposing=%s)", + aOffset, targetLength, GetBoolName(mIsComposing))); + return false; + } + + // If there is caret, we might be able to use caret rect. + uint32_t caretOffset = UINT32_MAX; + // There is a caret only when the normal selection is collapsed. + if (contentSelection.isNothing() || + contentSelection->OffsetAndDataRef().IsDataEmpty()) { + if (mIsComposing) { + // If it's composing, mCursorPosition is the offset to caret in + // the composition string. + if (mCursorPosition != NO_IME_CARET) { + MOZ_ASSERT(mCursorPosition >= 0); + caretOffset = mCursorPosition; + } else if (!ShouldDrawCompositionStringOurselves() || + mCompositionString.IsEmpty()) { + // Otherwise, if there is no composition string, we should assume that + // there is a caret at the start of composition string. + caretOffset = 0; + } + } else { + // If there is no composition, the selection offset is the caret offset. + caretOffset = 0; + } + } + + // If there is a caret and retrieving offset is same as the caret offset, + // we should use the caret rect. + if (aOffset != caretOffset) { + WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, aWindow); + WidgetQueryContentEvent::Options options; + options.mRelativeToInsertionPoint = true; + queryTextRectEvent.InitForQueryTextRect(aOffset, 1, options); + aWindow->InitEvent(queryTextRectEvent, &point); + DispatchEvent(aWindow, queryTextRectEvent); + if (queryTextRectEvent.Succeeded()) { + aCharRect = queryTextRectEvent.mReply->mRect; + if (aWritingMode) { + *aWritingMode = queryTextRectEvent.mReply->WritingModeRef(); + } + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("IMMHandler::GetCharacterRectOfSelectedTextAt, Succeeded, " + "aOffset=%u, aCharRect={ x: %d, y: %d, width: %d, height: %d }, " + "queryTextRectEvent={ mReply=%s }", + aOffset, aCharRect.X(), aCharRect.Y(), aCharRect.Width(), + aCharRect.Height(), ToString(queryTextRectEvent.mReply).c_str())); + return true; + } + } + + return GetCaretRect(aWindow, aCharRect, aWritingMode); +} + +bool IMMHandler::GetCaretRect(nsWindow* aWindow, + LayoutDeviceIntRect& aCaretRect, + WritingMode* aWritingMode) { + LayoutDeviceIntPoint point(0, 0); + + WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, aWindow); + WidgetQueryContentEvent::Options options; + options.mRelativeToInsertionPoint = true; + queryCaretRectEvent.InitForQueryCaretRect(0, options); + aWindow->InitEvent(queryCaretRectEvent, &point); + DispatchEvent(aWindow, queryCaretRectEvent); + if (queryCaretRectEvent.Failed()) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("IMMHandler::GetCaretRect, FAILED, due to eQueryCaretRect failure")); + return false; + } + aCaretRect = queryCaretRectEvent.mReply->mRect; + if (aWritingMode) { + *aWritingMode = queryCaretRectEvent.mReply->WritingModeRef(); + } + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::GetCaretRect, SUCCEEDED, " + "aCaretRect={ x: %d, y: %d, width: %d, height: %d }, " + "queryCaretRectEvent={ mReply=%s }", + aCaretRect.X(), aCaretRect.Y(), aCaretRect.Width(), + aCaretRect.Height(), ToString(queryCaretRectEvent.mReply).c_str())); + return true; +} + +bool IMMHandler::SetIMERelatedWindowsPos(nsWindow* aWindow, + const IMEContext& aContext) { + // Get first character rect of current a normal selected text or a composing + // string. + WritingMode writingMode; + LayoutDeviceIntRect firstSelectedCharRectRelativeToWindow; + bool ret = GetCharacterRectOfSelectedTextAt( + aWindow, 0, firstSelectedCharRectRelativeToWindow, &writingMode); + NS_ENSURE_TRUE(ret, false); + nsWindow* toplevelWindow = aWindow->GetTopLevelWindow(false); + LayoutDeviceIntRect firstSelectedCharRect; + ResolveIMECaretPos(toplevelWindow, firstSelectedCharRectRelativeToWindow, + aWindow, firstSelectedCharRect); + + // Set native caret size/position to our caret. Some IMEs honor it. E.g., + // "Intelligent ABC" (Simplified Chinese) and "MS PinYin 3.0" (Simplified + // Chinese) on XP. But if a11y module is handling native caret, we shouldn't + // touch it. + if (!IMEHandler::IsA11yHandlingNativeCaret()) { + LayoutDeviceIntRect caretRect(firstSelectedCharRect), + caretRectRelativeToWindow; + if (GetCaretRect(aWindow, caretRectRelativeToWindow)) { + ResolveIMECaretPos(toplevelWindow, caretRectRelativeToWindow, aWindow, + caretRect); + } else { + NS_WARNING("failed to get caret rect"); + caretRect.SetWidth(1); + } + IMEHandler::CreateNativeCaret(aWindow, caretRect); + } + + if (ShouldDrawCompositionStringOurselves()) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::SetIMERelatedWindowsPos, Set candidate window")); + + // Get a rect of first character in current target in composition string. + LayoutDeviceIntRect firstTargetCharRect, lastTargetCharRect; + if (mIsComposing && !mCompositionString.IsEmpty()) { + // If there are no targetted selection, we should use it's first character + // rect instead. + uint32_t offset, length; + if (!GetTargetClauseRange(&offset, &length)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::SetIMERelatedWindowsPos, FAILED, due to " + "GetTargetClauseRange() failure")); + return false; + } + ret = + GetCharacterRectOfSelectedTextAt(aWindow, offset - mCompositionStart, + firstTargetCharRect, &writingMode); + NS_ENSURE_TRUE(ret, false); + if (length) { + ret = GetCharacterRectOfSelectedTextAt( + aWindow, offset + length - 1 - mCompositionStart, + lastTargetCharRect); + NS_ENSURE_TRUE(ret, false); + } else { + lastTargetCharRect = firstTargetCharRect; + } + } else { + // If there are no composition string, we should use a first character + // rect. + ret = GetCharacterRectOfSelectedTextAt(aWindow, 0, firstTargetCharRect, + &writingMode); + NS_ENSURE_TRUE(ret, false); + lastTargetCharRect = firstTargetCharRect; + } + ResolveIMECaretPos(toplevelWindow, firstTargetCharRect, aWindow, + firstTargetCharRect); + ResolveIMECaretPos(toplevelWindow, lastTargetCharRect, aWindow, + lastTargetCharRect); + LayoutDeviceIntRect targetClauseRect; + targetClauseRect.UnionRect(firstTargetCharRect, lastTargetCharRect); + + // Move the candidate window to proper position from the target clause as + // far as possible. + CANDIDATEFORM candForm; + candForm.dwIndex = 0; + if (!writingMode.IsVertical() || IsVerticalWritingSupported()) { + candForm.dwStyle = CFS_EXCLUDE; + // Candidate window shouldn't overlap the target clause in any writing + // mode. + candForm.rcArea.left = targetClauseRect.X(); + candForm.rcArea.right = targetClauseRect.XMost(); + candForm.rcArea.top = targetClauseRect.Y(); + candForm.rcArea.bottom = targetClauseRect.YMost(); + if (!writingMode.IsVertical()) { + // In horizontal layout, current point of interest should be top-left + // of the first character. + candForm.ptCurrentPos.x = firstTargetCharRect.X(); + candForm.ptCurrentPos.y = firstTargetCharRect.Y(); + } else if (writingMode.IsVerticalRL()) { + // In vertical layout (RL), candidate window should be positioned right + // side of target clause. However, we don't set vertical writing font + // to the IME. Therefore, the candidate window may be positioned + // bottom-left of target clause rect with these information. + candForm.ptCurrentPos.x = targetClauseRect.X(); + candForm.ptCurrentPos.y = targetClauseRect.Y(); + } else { + MOZ_ASSERT(writingMode.IsVerticalLR(), "Did we miss some causes?"); + // In vertical layout (LR), candidate window should be poisitioned left + // side of target clause. Although, we don't set vertical writing font + // to the IME, the candidate window may be positioned bottom-right of + // the target clause rect with these information. + candForm.ptCurrentPos.x = targetClauseRect.XMost(); + candForm.ptCurrentPos.y = targetClauseRect.Y(); + } + } else { + // If vertical writing is not supported by IME, let's set candidate + // window position to the bottom-left of the target clause because + // the position must be the safest position to prevent the candidate + // window to overlap with the target clause. + candForm.dwStyle = CFS_CANDIDATEPOS; + candForm.ptCurrentPos.x = targetClauseRect.X(); + candForm.ptCurrentPos.y = targetClauseRect.YMost(); + } + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::SetIMERelatedWindowsPos, Calling " + "ImmSetCandidateWindow()... ptCurrentPos={ x=%ld, y=%ld }, " + "rcArea={ left=%ld, top=%ld, right=%ld, bottom=%ld }, " + "writingMode=%s", + candForm.ptCurrentPos.x, candForm.ptCurrentPos.y, + candForm.rcArea.left, candForm.rcArea.top, candForm.rcArea.right, + candForm.rcArea.bottom, ToString(writingMode).c_str())); + ::ImmSetCandidateWindow(aContext.get(), &candForm); + } else { + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::SetIMERelatedWindowsPos, Set composition window")); + + // Move the composition window to caret position (if selected some + // characters, we should use first character rect of them). + // And in this mode, IME adjusts the candidate window position + // automatically. So, we don't need to set it. + COMPOSITIONFORM compForm; + compForm.dwStyle = CFS_POINT; + compForm.ptCurrentPos.x = !writingMode.IsVerticalLR() + ? firstSelectedCharRect.X() + : firstSelectedCharRect.XMost(); + compForm.ptCurrentPos.y = firstSelectedCharRect.Y(); + ::ImmSetCompositionWindow(aContext.get(), &compForm); + } + + return true; +} + +void IMMHandler::ResolveIMECaretPos(nsIWidget* aReferenceWidget, + LayoutDeviceIntRect& aCursorRect, + nsIWidget* aNewOriginWidget, + LayoutDeviceIntRect& aOutRect) { + aOutRect = aCursorRect; + + if (aReferenceWidget == aNewOriginWidget) return; + + if (aReferenceWidget) + aOutRect.MoveBy(aReferenceWidget->WidgetToScreenOffset()); + + if (aNewOriginWidget) + aOutRect.MoveBy(-aNewOriginWidget->WidgetToScreenOffset()); +} + +static void SetHorizontalFontToLogFont(const nsAString& aFontFace, + LOGFONTW& aLogFont) { + aLogFont.lfEscapement = aLogFont.lfOrientation = 0; + if (NS_WARN_IF(aFontFace.Length() > LF_FACESIZE - 1)) { + memcpy(aLogFont.lfFaceName, L"System", sizeof(L"System")); + return; + } + memcpy(aLogFont.lfFaceName, aFontFace.BeginReading(), + aFontFace.Length() * sizeof(wchar_t)); + aLogFont.lfFaceName[aFontFace.Length()] = 0; +} + +static void SetVerticalFontToLogFont(const nsAString& aFontFace, + LOGFONTW& aLogFont) { + aLogFont.lfEscapement = aLogFont.lfOrientation = 2700; + if (NS_WARN_IF(aFontFace.Length() > LF_FACESIZE - 2)) { + memcpy(aLogFont.lfFaceName, L"@System", sizeof(L"@System")); + return; + } + aLogFont.lfFaceName[0] = '@'; + memcpy(&aLogFont.lfFaceName[1], aFontFace.BeginReading(), + aFontFace.Length() * sizeof(wchar_t)); + aLogFont.lfFaceName[aFontFace.Length() + 1] = 0; +} + +void IMMHandler::AdjustCompositionFont(nsWindow* aWindow, + const IMEContext& aContext, + const WritingMode& aWritingMode, + bool aForceUpdate) { + // An instance of IMMHandler is destroyed when active IME is changed. + // Therefore, we need to store the information which are set to the IM + // context to static variables since IM context is never recreated. + static bool sCompositionFontsInitialized = false; + static nsString sCompositionFont; + static bool sCompositionFontPrefDone = false; + if (!sCompositionFontPrefDone) { + sCompositionFontPrefDone = true; + Preferences::GetString("intl.imm.composition_font", sCompositionFont); + } + + // If composition font is customized by pref, we need to modify the + // composition font of the IME context at first time even if the writing mode + // is horizontal. + bool setCompositionFontForcibly = + aForceUpdate || + (!sCompositionFontsInitialized && !sCompositionFont.IsEmpty()); + + static WritingMode sCurrentWritingMode; + static nsString sCurrentIMEName; + if (!setCompositionFontForcibly && + sWritingModeOfCompositionFont == aWritingMode && + sCurrentIMEName == sIMEName) { + // Nothing to do if writing mode isn't being changed. + return; + } + + // Decide composition fonts for both horizontal writing mode and vertical + // writing mode. If the font isn't specified by the pref, use default + // font which is already set to the IM context. And also in vertical writing + // mode, insert '@' to the start of the font. + if (!sCompositionFontsInitialized) { + sCompositionFontsInitialized = true; + // sCompositionFontH must not start with '@' and its length is less than + // LF_FACESIZE since it needs to end with null terminating character. + if (sCompositionFont.IsEmpty() || + sCompositionFont.Length() > LF_FACESIZE - 1 || + sCompositionFont[0] == '@') { + LOGFONTW defaultLogFont; + if (NS_WARN_IF( + !::ImmGetCompositionFont(aContext.get(), &defaultLogFont))) { + MOZ_LOG( + gIMELog, LogLevel::Error, + (" IMMHandler::AdjustCompositionFont, ::ImmGetCompositionFont() " + "failed")); + sCompositionFont.AssignLiteral("System"); + } else { + // The font face is typically, "System". + sCompositionFont.Assign(defaultLogFont.lfFaceName); + } + } + + MOZ_LOG(gIMELog, LogLevel::Info, + (" IMMHandler::AdjustCompositionFont, sCompositionFont=\"%s\" is " + "initialized", + NS_ConvertUTF16toUTF8(sCompositionFont).get())); + } + + static nsString sCompositionFontForJapanist2003; + if (IsJapanist2003Active() && sCompositionFontForJapanist2003.IsEmpty()) { + const char* kCompositionFontForJapanist2003 = + "intl.imm.composition_font.japanist_2003"; + Preferences::GetString(kCompositionFontForJapanist2003, + sCompositionFontForJapanist2003); + // If the font name is not specified properly, let's use + // "MS PGothic" instead. + if (sCompositionFontForJapanist2003.IsEmpty() || + sCompositionFontForJapanist2003.Length() > LF_FACESIZE - 2 || + sCompositionFontForJapanist2003[0] == '@') { + sCompositionFontForJapanist2003.AssignLiteral("MS PGothic"); + } + } + + sWritingModeOfCompositionFont = aWritingMode; + sCurrentIMEName = sIMEName; + + LOGFONTW logFont; + memset(&logFont, 0, sizeof(logFont)); + if (!::ImmGetCompositionFont(aContext.get(), &logFont)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::AdjustCompositionFont, ::ImmGetCompositionFont() " + "failed")); + logFont.lfFaceName[0] = 0; + } + // Need to reset some information which should be recomputed with new font. + logFont.lfWidth = 0; + logFont.lfWeight = FW_DONTCARE; + logFont.lfOutPrecision = OUT_DEFAULT_PRECIS; + logFont.lfClipPrecision = CLIP_DEFAULT_PRECIS; + logFont.lfPitchAndFamily = DEFAULT_PITCH; + + if (aWritingMode.IsVertical() && IsVerticalWritingSupported()) { + SetVerticalFontToLogFont(IsJapanist2003Active() + ? sCompositionFontForJapanist2003 + : sCompositionFont, + logFont); + } else { + SetHorizontalFontToLogFont(IsJapanist2003Active() + ? sCompositionFontForJapanist2003 + : sCompositionFont, + logFont); + } + MOZ_LOG(gIMELog, LogLevel::Warning, + (" IMMHandler::AdjustCompositionFont, calling " + "::ImmSetCompositionFont(\"%s\")", + NS_ConvertUTF16toUTF8(nsDependentString(logFont.lfFaceName)).get())); + ::ImmSetCompositionFontW(aContext.get(), &logFont); +} + +// static +nsresult IMMHandler::OnMouseButtonEvent( + nsWindow* aWindow, const IMENotification& aIMENotification) { + // We don't need to create the instance of the handler here. + if (!gIMMHandler) { + return NS_OK; + } + + if (!sWM_MSIME_MOUSE || !IsComposingOnOurEditor() || + !ShouldDrawCompositionStringOurselves()) { + return NS_OK; + } + + // We need to handle only mousedown event. + if (aIMENotification.mMouseButtonEventData.mEventMessage != eMouseDown) { + return NS_OK; + } + + // If the character under the cursor is not in the composition string, + // we don't need to notify IME of it. + uint32_t compositionStart = gIMMHandler->mCompositionStart; + uint32_t compositionEnd = + compositionStart + gIMMHandler->mCompositionString.Length(); + if (aIMENotification.mMouseButtonEventData.mOffset < compositionStart || + aIMENotification.mMouseButtonEventData.mOffset >= compositionEnd) { + return NS_OK; + } + + BYTE button; + switch (aIMENotification.mMouseButtonEventData.mButton) { + case MouseButton::ePrimary: + button = IMEMOUSE_LDOWN; + break; + case MouseButton::eMiddle: + button = IMEMOUSE_MDOWN; + break; + case MouseButton::eSecondary: + button = IMEMOUSE_RDOWN; + break; + default: + return NS_OK; + } + + // calcurate positioning and offset + // char : JCH1|JCH2|JCH3 + // offset: 0011 1122 2233 + // positioning: 2301 2301 2301 + LayoutDeviceIntPoint cursorPos = + aIMENotification.mMouseButtonEventData.mCursorPos; + LayoutDeviceIntRect charRect = + aIMENotification.mMouseButtonEventData.mCharRect; + int32_t cursorXInChar = cursorPos.x - charRect.X(); + // The event might hit to zero-width character, see bug 694913. + // The reason might be: + // * There are some zero-width characters are actually. + // * font-size is specified zero. + // But nobody reproduced this bug actually... + // We should assume that user clicked on right most of the zero-width + // character in such case. + int positioning = 1; + if (charRect.Width() > 0) { + positioning = cursorXInChar * 4 / charRect.Width(); + positioning = (positioning + 2) % 4; + } + + int offset = + aIMENotification.mMouseButtonEventData.mOffset - compositionStart; + if (positioning < 2) { + offset++; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::OnMouseButtonEvent, x,y=%d,%d, offset=%d, " + "positioning=%d", + cursorPos.x.value, cursorPos.y.value, offset, positioning)); + + // send MS_MSIME_MOUSE message to default IME window. + HWND imeWnd = ::ImmGetDefaultIMEWnd(aWindow->GetWindowHandle()); + IMEContext context(aWindow); + if (::SendMessageW(imeWnd, sWM_MSIME_MOUSE, + MAKELONG(MAKEWORD(button, positioning), offset), + (LPARAM)context.get()) == 1) { + return NS_SUCCESS_EVENT_CONSUMED; + } + return NS_OK; +} + +// static +bool IMMHandler::OnKeyDownEvent(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("IMMHandler::OnKeyDownEvent, hWnd=%p, wParam=%08zx, lParam=%08" PRIxLPTR, + aWindow->GetWindowHandle(), wParam, lParam)); + aResult.mConsumed = false; + switch (wParam) { + case VK_TAB: + case VK_PRIOR: + case VK_NEXT: + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_RETURN: + // If IME didn't process the key message (the virtual key code wasn't + // converted to VK_PROCESSKEY), and the virtual key code event causes + // moving caret or editing text with keeping composing state, we should + // cancel the composition here because we cannot support moving + // composition string with DOM events (IE also cancels the composition + // in same cases). Then, this event will be dispatched. + if (IsComposingOnOurEditor()) { + // NOTE: We don't need to cancel the composition on another window. + CancelComposition(aWindow, false); + } + return false; + default: + return false; + } +} + +Maybe IMMHandler::QueryContentSelection(nsWindow* aWindow) { + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + aWindow); + LayoutDeviceIntPoint point(0, 0); + aWindow->InitEvent(querySelectedTextEvent, &point); + DispatchEvent(aWindow, querySelectedTextEvent); + if (NS_WARN_IF(querySelectedTextEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::Selection::Init, FAILED, due to eQuerySelectedText " + "failure")); + return Nothing(); + } + // If the window is destroyed during querying selected text, we shouldn't + // do anymore. + if (aWindow->Destroyed()) { + MOZ_LOG( + gIMELog, LogLevel::Error, + (" IMMHandler::Selection::Init, FAILED, due to the widget destroyed")); + return Nothing(); + } + + ContentSelection contentSelection(querySelectedTextEvent); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("IMMHandler::Selection::Init, querySelectedTextEvent={ mReply=%s }", + ToString(querySelectedTextEvent.mReply).c_str())); + + if (contentSelection.HasRange() && + !contentSelection.OffsetAndDataRef().IsValid()) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" IMMHandler::Selection::Init, FAILED, due to invalid range")); + return Nothing(); + } + return Some(contentSelection); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/IMMHandler.h b/widget/windows/IMMHandler.h new file mode 100644 index 0000000000..3da53387b2 --- /dev/null +++ b/widget/windows/IMMHandler.h @@ -0,0 +1,424 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef IMMHandler_h_ +#define IMMHandler_h_ + +#include "mozilla/ContentData.h" +#include "mozilla/EventForwards.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/WritingModes.h" + +#include "npapi.h" +#include "nsCOMPtr.h" +#include "nsIWidget.h" +#include "nsRect.h" +#include "nsString.h" +#include "nsTArray.h" + +#include + +class nsWindow; + +namespace mozilla { +namespace widget { + +struct MSGResult; + +class IMEContext final { + public: + IMEContext() : mWnd(nullptr), mIMC(nullptr) {} + + explicit IMEContext(HWND aWnd); + explicit IMEContext(nsWindow* aWindowBase); + + ~IMEContext() { Clear(); } + + HIMC get() const { return mIMC; } + + void Init(HWND aWnd); + void Init(nsWindow* aWindowBase); + void Clear(); + + bool IsValid() const { return !!mIMC; } + + void SetOpenState(bool aOpen) const { + if (!mIMC) { + return; + } + ::ImmSetOpenStatus(mIMC, aOpen); + } + + bool GetOpenState() const { + if (!mIMC) { + return false; + } + return (::ImmGetOpenStatus(mIMC) != FALSE); + } + + bool AssociateDefaultContext() { + // We assume that there is only default IMC, no new IMC has been created. + if (mIMC) { + return false; + } + if (!::ImmAssociateContextEx(mWnd, nullptr, IACE_DEFAULT)) { + return false; + } + mIMC = ::ImmGetContext(mWnd); + return (mIMC != nullptr); + } + + bool Disassociate() { + if (!mIMC) { + return false; + } + if (!::ImmAssociateContextEx(mWnd, nullptr, 0)) { + return false; + } + ::ImmReleaseContext(mWnd, mIMC); + mIMC = nullptr; + return true; + } + + protected: + IMEContext(const IMEContext& aOther) { MOZ_CRASH("Don't copy IMEContext"); } + + HWND mWnd; + HIMC mIMC; +}; + +class IMMHandler final { + public: + static void Initialize(); + static void Terminate(); + + // If Process*() returns true, the caller shouldn't do anything anymore. + static bool ProcessMessage(nsWindow* aWindow, UINT msg, WPARAM& wParam, + LPARAM& lParam, MSGResult& aResult); + static bool IsComposing() { return IsComposingOnOurEditor(); } + static bool IsComposingOn(nsWindow* aWindow) { + return IsComposing() && IsComposingWindow(aWindow); + } + +#ifdef DEBUG + /** + * IsIMEAvailable() returns TRUE when current keyboard layout has IME. + * Otherwise, FALSE. + */ + static bool IsIMEAvailable() { return !!::ImmIsIME(::GetKeyboardLayout(0)); } +#endif + + // If aForce is TRUE, these methods doesn't check whether we have composition + // or not. If you don't set it to TRUE, these method doesn't commit/cancel + // the composition on uexpected window. + static void CommitComposition(nsWindow* aWindow, bool aForce = false); + static void CancelComposition(nsWindow* aWindow, bool aForce = false); + static void OnFocusChange(bool aFocus, nsWindow* aWindow); + static void OnUpdateComposition(nsWindow* aWindow); + static void OnSelectionChange(nsWindow* aWindow, + const IMENotification& aIMENotification, + bool aIsIMMActive); + + static IMENotificationRequests GetIMENotificationRequests(); + + // Returns NS_SUCCESS_EVENT_CONSUMED if the mouse button event is consumed by + // IME. Otherwise, NS_OK. + static nsresult OnMouseButtonEvent(nsWindow* aWindow, + const IMENotification& aIMENotification); + +#define DECL_IS_IME_ACTIVE(aReadableName) \ + static bool Is##aReadableName##Active(); + + // Japanese IMEs + DECL_IS_IME_ACTIVE(ATOK2006) + DECL_IS_IME_ACTIVE(ATOK2007) + DECL_IS_IME_ACTIVE(ATOK2008) + DECL_IS_IME_ACTIVE(ATOK2009) + DECL_IS_IME_ACTIVE(ATOK2010) + DECL_IS_IME_ACTIVE(GoogleJapaneseInput) + DECL_IS_IME_ACTIVE(Japanist2003) + +#undef DECL_IS_IME_ACTIVE + + /** + * IsActiveIMEInBlockList() returns true if we know active keyboard layout's + * IME has some crash bugs or something which make some damage to us. When + * this returns true, IMC shouldn't be associated with any windows. + */ + static bool IsActiveIMEInBlockList(); + + protected: + static void EnsureHandlerInstance(); + + static bool IsComposingOnOurEditor(); + static bool IsComposingWindow(nsWindow* aWindow); + + static bool ShouldDrawCompositionStringOurselves(); + static bool IsVerticalWritingSupported(); + // aWindow can be nullptr if it's called without receiving WM_INPUTLANGCHANGE. + static void InitKeyboardLayout(nsWindow* aWindow, HKL aKeyboardLayout); + static UINT GetKeyboardCodePage(); + + /** + * Checks whether the window is top level window of the composing window. + * In this method, the top level window means in all windows, not only in all + * OUR windows. I.e., if the aWindow is embedded, this always returns FALSE. + */ + static bool IsTopLevelWindowOfComposition(nsWindow* aWindow); + + static bool ProcessInputLangChangeMessage(nsWindow* aWindow, WPARAM wParam, + LPARAM lParam, MSGResult& aResult); + + IMMHandler(); + ~IMMHandler(); + + // On*() methods return true if the caller of message handler shouldn't do + // anything anymore. Otherwise, false. + static bool OnKeyDownEvent(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + + bool OnIMEStartComposition(nsWindow* aWindow, MSGResult& aResult); + bool OnIMEComposition(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + bool OnIMEEndComposition(nsWindow* aWindow, MSGResult& aResult); + bool OnIMERequest(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + bool OnChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + void OnInputLangChange(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + + // These message handlers don't use instance members, we should not create + // the instance by the messages. So, they should be static. + static bool OnIMEChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + static bool OnIMESetContext(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + static bool OnIMECompositionFull(nsWindow* aWindow, MSGResult& aResult); + static bool OnIMENotify(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + static bool OnIMESelect(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + + // The result of Handle* method mean "Processed" when it's TRUE. + void HandleStartComposition(nsWindow* aWindow, const IMEContext& aContext); + bool HandleComposition(nsWindow* aWindow, const IMEContext& aContext, + LPARAM lParam); + // If aCommitString is null, this commits composition with the latest + // dispatched data. Otherwise, commits composition with the value. + void HandleEndComposition(nsWindow* aWindow, + const nsAString* aCommitString = nullptr); + bool HandleReconvert(nsWindow* aWindow, LPARAM lParam, LRESULT* oResult); + bool HandleQueryCharPosition(nsWindow* aWindow, LPARAM lParam, + LRESULT* oResult); + bool HandleDocumentFeed(nsWindow* aWindow, LPARAM lParam, LRESULT* oResult); + + /** + * When a window's IME context is activating but we have composition on + * another window, we should commit our composition because IME context is + * shared by all our windows (including plug-ins). + * @param aWindow is a new activated window. + * If aWindow is our composing window, this method does nothing. + * Otherwise, this commits the composition on the previous window. + * If this method did commit a composition, this returns TRUE. + */ + bool CommitCompositionOnPreviousWindow(nsWindow* aWindow); + + /** + * ResolveIMECaretPos + * Convert the caret rect of a composition event to another widget's + * coordinate system. + * + * @param aReferenceWidget The origin widget of aCursorRect. + * Typically, this is mReferenceWidget of the + * composing events. If the aCursorRect is in screen + * coordinates, set nullptr. + * @param aCursorRect The cursor rect. + * @param aNewOriginWidget aOutRect will be in this widget's coordinates. If + * this is nullptr, aOutRect will be in screen + * coordinates. + * @param aOutRect The converted cursor rect. + */ + void ResolveIMECaretPos(nsIWidget* aReferenceWidget, + mozilla::LayoutDeviceIntRect& aCursorRect, + nsIWidget* aNewOriginWidget, + mozilla::LayoutDeviceIntRect& aOutRect); + + bool ConvertToANSIString(const nsString& aStr, UINT aCodePage, + nsACString& aANSIStr); + + bool SetIMERelatedWindowsPos(nsWindow* aWindow, const IMEContext& aContext); + /** + * GetCharacterRectOfSelectedTextAt() returns character rect of the offset + * from the selection start or the start of composition string if there is + * a composition. + * + * @param aWindow The window which has focus. + * @param aOffset Offset from the selection start or the start of + * composition string when there is a composition. + * This must be in the selection range or + * the composition string. + * @param aCharRect The result. + * @param aWritingMode The writing mode of current selection. When this + * is nullptr, this assumes that the selection is in + * horizontal writing mode. + * @return true if this succeeded to retrieve the rect. + * Otherwise, false. + */ + bool GetCharacterRectOfSelectedTextAt( + nsWindow* aWindow, uint32_t aOffset, + mozilla::LayoutDeviceIntRect& aCharRect, + mozilla::WritingMode* aWritingMode = nullptr); + /** + * GetCaretRect() returns caret rect at current selection start. + * + * @param aWindow The window which has focus. + * @param aCaretRect The result. + * @param aWritingMode The writing mode of current selection. When this + * is nullptr, this assumes that the selection is in + * horizontal writing mode. + * @return true if this succeeded to retrieve the rect. + * Otherwise, false. + */ + bool GetCaretRect(nsWindow* aWindow, mozilla::LayoutDeviceIntRect& aCaretRect, + mozilla::WritingMode* aWritingMode = nullptr); + void GetCompositionString(const IMEContext& aContext, DWORD aIndex, + nsAString& aCompositionString) const; + + /** + * AdjustCompositionFont() makes IME vertical writing mode if it's supported. + * If aForceUpdate is true, it will update composition font even if writing + * mode isn't being changed. + */ + void AdjustCompositionFont(nsWindow* aWindow, const IMEContext& aContext, + const mozilla::WritingMode& aWritingMode, + bool aForceUpdate = false); + + /** + * MaybeAdjustCompositionFont() calls AdjustCompositionFont() when the + * locale of active IME is CJK. Note that this creates an instance even + * when there is no composition but the locale is CJK. + */ + static void MaybeAdjustCompositionFont( + nsWindow* aWindow, const mozilla::WritingMode& aWritingMode, + bool aForceUpdate = false); + + /** + * Get the current target clause of composition string. + * If there are one or more characters whose attribute is ATTR_TARGET_*, + * this returns the first character's offset and its length. + * Otherwise, e.g., the all characters are ATTR_INPUT, this returns + * the composition string range because the all is the current target. + * + * aLength can be null (default), but aOffset must not be null. + * + * The aOffset value is offset in the contents. So, when you need offset + * in the composition string, you need to subtract mCompositionStart from it. + */ + bool GetTargetClauseRange(uint32_t* aOffset, uint32_t* aLength = nullptr); + + /** + * DispatchEvent() dispatches aEvent if aWidget hasn't been destroyed yet. + */ + static void DispatchEvent(nsWindow* aWindow, WidgetGUIEvent& aEvent); + + /** + * DispatchCompositionChangeEvent() dispatches eCompositionChange event + * with clause information (it'll be retrieved by CreateTextRangeArray()). + * I.e., this should be called only during composing. If a composition is + * being committed, only HandleCompositionEnd() should be called. + * + * @param aWindow The window which has the composition. + * @param aContext Native IME context which has the composition. + */ + void DispatchCompositionChangeEvent(nsWindow* aWindow, + const IMEContext& aContext); + + nsresult EnsureClauseArray(int32_t aCount); + nsresult EnsureAttributeArray(int32_t aCount); + + /** + * When WM_IME_CHAR is received and passed to DefWindowProc, we need to + * record the messages. In other words, we should record the messages + * when we receive WM_IME_CHAR on windowless plug-in (if we have focus, + * we always eat them). When focus is moved from a windowless plug-in to + * our window during composition, WM_IME_CHAR messages were received when + * the plug-in has focus. However, WM_CHAR messages are received after the + * plug-in lost focus. So, we need to ignore the WM_CHAR messages because + * they make unexpected text input events on us. + */ + nsTArray mPassedIMEChar; + + bool IsIMECharRecordsEmpty() { return mPassedIMEChar.IsEmpty(); } + void ResetIMECharRecords() { mPassedIMEChar.Clear(); } + void DequeueIMECharRecords(WPARAM& wParam, LPARAM& lParam) { + MSG msg = mPassedIMEChar.ElementAt(0); + wParam = msg.wParam; + lParam = msg.lParam; + mPassedIMEChar.RemoveElementAt(0); + } + void EnqueueIMECharRecords(WPARAM wParam, LPARAM lParam) { + MSG msg; + msg.wParam = wParam; + msg.lParam = lParam; + mPassedIMEChar.AppendElement(msg); + } + + TextEventDispatcher* GetTextEventDispatcherFor(nsWindow* aWindow); + + nsWindow* mComposingWindow; + RefPtr mDispatcher; + nsString mCompositionString; + nsTArray mClauseArray; + nsTArray mAttributeArray; + + int32_t mCursorPosition; + uint32_t mCompositionStart; + + // mContentSelection stores the latest selection data only when sHasFocus is + // true. Don't access mContentSelection directly. You should use + // GetContentSelectionWithQueryIfNothing() for getting proper state. + Maybe mContentSelection; + + const Maybe& GetContentSelectionWithQueryIfNothing( + nsWindow* aWindow) { + // When IME has focus, mContentSelection is automatically updated by + // NOTIFY_IME_OF_SELECTION_CHANGE. + if (sHasFocus) { + if (mContentSelection.isNothing()) { + // But if this is the first access of mContentSelection, we need to + // query selection now. + mContentSelection = QueryContentSelection(aWindow); + } + return mContentSelection; + } + // Otherwise, i.e., While IME doesn't have focus, we cannot observe + // selection changes. So, in such case, we need to query selection + // when it's necessary. + static Maybe sTempContentSelection; + sTempContentSelection = QueryContentSelection(aWindow); + return sTempContentSelection; + } + + /** + * Query content selection on aWindow with WidgetQueryContent event. + */ + static Maybe QueryContentSelection(nsWindow* aWindow); + + bool mIsComposing; + + static mozilla::WritingMode sWritingModeOfCompositionFont; + static nsString sIMEName; + static UINT sCodePage; + static DWORD sIMEProperty; + static DWORD sIMEUIProperty; + static bool sAssumeVerticalWritingModeNotSupported; + static bool sHasFocus; +}; + +} // namespace widget +} // namespace mozilla + +#endif // IMMHandler_h_ diff --git a/widget/windows/InProcessWinCompositorWidget.cpp b/widget/windows/InProcessWinCompositorWidget.cpp new file mode 100644 index 0000000000..b65abf225e --- /dev/null +++ b/widget/windows/InProcessWinCompositorWidget.cpp @@ -0,0 +1,363 @@ +/* -*- 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 "InProcessWinCompositorWidget.h" + +#include "mozilla/StaticPrefs_layers.h" +#include "mozilla/gfx/DeviceManagerDx.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/layers/Compositor.h" +#include "mozilla/layers/CompositorThread.h" +#include "mozilla/webrender/RenderThread.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "gfxPlatform.h" +#include "HeadlessCompositorWidget.h" +#include "HeadlessWidget.h" +#include "nsWindow.h" +#include "VsyncDispatcher.h" +#include "WinCompositorWindowThread.h" +#include "VRShMem.h" + +#include + +namespace mozilla { +namespace widget { + +using namespace mozilla::gfx; +using namespace mozilla; + +/* static */ +RefPtr CompositorWidget::CreateLocal( + const CompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, nsIWidget* aWidget) { + if (aInitData.type() == + CompositorWidgetInitData::THeadlessCompositorWidgetInitData) { + return new HeadlessCompositorWidget( + aInitData.get_HeadlessCompositorWidgetInitData(), aOptions, + static_cast(aWidget)); + } else { + return new InProcessWinCompositorWidget( + aInitData.get_WinCompositorWidgetInitData(), aOptions, + static_cast(aWidget)); + } +} + +InProcessWinCompositorWidget::InProcessWinCompositorWidget( + const WinCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, nsWindow* aWindow) + : WinCompositorWidget(aInitData, aOptions), + mWindow(aWindow), + mWnd(reinterpret_cast(aInitData.hWnd())), + mTransparentSurfaceLock("mTransparentSurfaceLock"), + mTransparencyMode(aInitData.transparencyMode()), + mMemoryDC(nullptr), + mCompositeDC(nullptr), + mLockedBackBufferData(nullptr) { + MOZ_ASSERT(mWindow); + MOZ_ASSERT(mWnd && ::IsWindow(mWnd)); + + // mNotDeferEndRemoteDrawing is set on the main thread during init, + // but is only accessed after on the compositor thread. + mNotDeferEndRemoteDrawing = + StaticPrefs::layers_offmainthreadcomposition_frame_rate() == 0 || + gfxPlatform::IsInLayoutAsapMode() || gfxPlatform::ForceSoftwareVsync(); +} + +void InProcessWinCompositorWidget::OnDestroyWindow() { + gfx::CriticalSectionAutoEnter presentLock(&mPresentLock); + MutexAutoLock lock(mTransparentSurfaceLock); + mTransparentSurface = nullptr; + mMemoryDC = nullptr; +} + +bool InProcessWinCompositorWidget::OnWindowResize( + const LayoutDeviceIntSize& aSize) { + return true; +} + +void InProcessWinCompositorWidget::OnWindowModeChange(nsSizeMode aSizeMode) {} + +bool InProcessWinCompositorWidget::PreRender(WidgetRenderingContext* aContext) { + // This can block waiting for WM_SETTEXT to finish + // Using PreRender is unnecessarily pessimistic because + // we technically only need to block during the present call + // not all of compositor rendering + mPresentLock.Enter(); + return true; +} + +void InProcessWinCompositorWidget::PostRender( + WidgetRenderingContext* aContext) { + mPresentLock.Leave(); +} + +LayoutDeviceIntSize InProcessWinCompositorWidget::GetClientSize() { + RECT r; + if (!::GetClientRect(mWnd, &r)) { + return LayoutDeviceIntSize(); + } + return LayoutDeviceIntSize(r.right - r.left, r.bottom - r.top); +} + +already_AddRefed +InProcessWinCompositorWidget::StartRemoteDrawing() { + MutexAutoLock lock(mTransparentSurfaceLock); + + MOZ_ASSERT(!mCompositeDC); + + RefPtr surf; + if (mTransparencyMode == eTransparencyTransparent) { + surf = EnsureTransparentSurface(); + } + + // Must call this after EnsureTransparentSurface(), since it could update + // the DC. + HDC dc = GetWindowSurface(); + if (!surf) { + if (!dc) { + return nullptr; + } + uint32_t flags = (mTransparencyMode == eTransparencyOpaque) + ? 0 + : gfxWindowsSurface::FLAG_IS_TRANSPARENT; + surf = new gfxWindowsSurface(dc, flags); + } + + IntSize size = surf->GetSize(); + if (size.width <= 0 || size.height <= 0) { + if (dc) { + FreeWindowSurface(dc); + } + return nullptr; + } + + RefPtr dt = + mozilla::gfx::Factory::CreateDrawTargetForCairoSurface( + surf->CairoSurface(), size); + if (dt) { + mCompositeDC = dc; + } else { + FreeWindowSurface(dc); + } + + return dt.forget(); +} + +void InProcessWinCompositorWidget::EndRemoteDrawing() { + MOZ_ASSERT(!mLockedBackBufferData); + + if (mTransparencyMode == eTransparencyTransparent) { + MOZ_ASSERT(mTransparentSurface); + RedrawTransparentWindow(); + } + if (mCompositeDC) { + FreeWindowSurface(mCompositeDC); + } + mCompositeDC = nullptr; +} + +bool InProcessWinCompositorWidget::NeedsToDeferEndRemoteDrawing() { + if (mNotDeferEndRemoteDrawing) { + return false; + } + + IDirectDraw7* ddraw = DeviceManagerDx::Get()->GetDirectDraw(); + if (!ddraw) { + return false; + } + + DWORD scanLine = 0; + int height = ::GetSystemMetrics(SM_CYSCREEN); + HRESULT ret = ddraw->GetScanLine(&scanLine); + if (ret == DDERR_VERTICALBLANKINPROGRESS) { + scanLine = 0; + } else if (ret != DD_OK) { + return false; + } + + // Check if there is a risk of tearing with GDI. + if (static_cast(scanLine) > height / 2) { + // No need to defer. + return false; + } + + return true; +} + +already_AddRefed +InProcessWinCompositorWidget::GetBackBufferDrawTarget( + gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect, + bool* aOutIsCleared) { + MOZ_ASSERT(!mLockedBackBufferData); + + RefPtr target = CompositorWidget::GetBackBufferDrawTarget( + aScreenTarget, aRect, aOutIsCleared); + if (!target) { + return nullptr; + } + + MOZ_ASSERT(target->GetBackendType() == BackendType::CAIRO); + + uint8_t* destData; + IntSize destSize; + int32_t destStride; + SurfaceFormat destFormat; + if (!target->LockBits(&destData, &destSize, &destStride, &destFormat)) { + // LockBits is not supported. Use original DrawTarget. + return target.forget(); + } + + RefPtr dataTarget = Factory::CreateDrawTargetForData( + BackendType::CAIRO, destData, destSize, destStride, destFormat); + mLockedBackBufferData = destData; + + return dataTarget.forget(); +} + +already_AddRefed +InProcessWinCompositorWidget::EndBackBufferDrawing() { + if (mLockedBackBufferData) { + MOZ_ASSERT(mLastBackBuffer); + mLastBackBuffer->ReleaseBits(mLockedBackBufferData); + mLockedBackBufferData = nullptr; + } + return CompositorWidget::EndBackBufferDrawing(); +} + +bool InProcessWinCompositorWidget::InitCompositor( + layers::Compositor* aCompositor) { + return true; +} + +void InProcessWinCompositorWidget::EnterPresentLock() { mPresentLock.Enter(); } + +void InProcessWinCompositorWidget::LeavePresentLock() { mPresentLock.Leave(); } + +RefPtr InProcessWinCompositorWidget::EnsureTransparentSurface() { + mTransparentSurfaceLock.AssertCurrentThreadOwns(); + MOZ_ASSERT(mTransparencyMode == eTransparencyTransparent); + + IntSize size = GetClientSize().ToUnknownSize(); + if (!mTransparentSurface || mTransparentSurface->GetSize() != size) { + mTransparentSurface = nullptr; + mMemoryDC = nullptr; + CreateTransparentSurface(size); + } + + RefPtr surface = mTransparentSurface; + return surface.forget(); +} + +void InProcessWinCompositorWidget::CreateTransparentSurface( + const gfx::IntSize& aSize) { + mTransparentSurfaceLock.AssertCurrentThreadOwns(); + MOZ_ASSERT(!mTransparentSurface && !mMemoryDC); + RefPtr surface = + new gfxWindowsSurface(aSize, SurfaceFormat::A8R8G8B8_UINT32); + mTransparentSurface = surface; + mMemoryDC = surface->GetDC(); +} + +void InProcessWinCompositorWidget::UpdateTransparency( + nsTransparencyMode aMode) { + gfx::CriticalSectionAutoEnter presentLock(&mPresentLock); + MutexAutoLock lock(mTransparentSurfaceLock); + if (mTransparencyMode == aMode) { + return; + } + + mTransparencyMode = aMode; + mTransparentSurface = nullptr; + mMemoryDC = nullptr; + + if (mTransparencyMode == eTransparencyTransparent) { + EnsureTransparentSurface(); + } +} + +void InProcessWinCompositorWidget::NotifyVisibilityUpdated( + nsSizeMode aSizeMode, bool aIsFullyOccluded) { + mSizeMode = aSizeMode; + mIsFullyOccluded = aIsFullyOccluded; +} + +nsSizeMode InProcessWinCompositorWidget::GetWindowSizeMode() const { + nsSizeMode sizeMode = mSizeMode; + return sizeMode; +} + +bool InProcessWinCompositorWidget::GetWindowIsFullyOccluded() const { + bool isFullyOccluded = mIsFullyOccluded; + return isFullyOccluded; +} + +bool InProcessWinCompositorWidget::HasGlass() const { + MOZ_ASSERT(layers::CompositorThreadHolder::IsInCompositorThread() || + wr::RenderThread::IsInRenderThread()); + return mTransparencyMode == eTransparencyBorderlessGlass; +} + +void InProcessWinCompositorWidget::ClearTransparentWindow() { + gfx::CriticalSectionAutoEnter presentLock(&mPresentLock); + MutexAutoLock lock(mTransparentSurfaceLock); + if (!mTransparentSurface) { + return; + } + + EnsureTransparentSurface(); + + IntSize size = mTransparentSurface->GetSize(); + if (!size.IsEmpty()) { + RefPtr drawTarget = + gfxPlatform::CreateDrawTargetForSurface(mTransparentSurface, size); + if (!drawTarget) { + return; + } + drawTarget->ClearRect(Rect(0, 0, size.width, size.height)); + RedrawTransparentWindow(); + } +} + +bool InProcessWinCompositorWidget::RedrawTransparentWindow() { + MOZ_ASSERT(mTransparencyMode == eTransparencyTransparent); + + LayoutDeviceIntSize size = GetClientSize(); + + ::GdiFlush(); + + BLENDFUNCTION bf = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA}; + SIZE winSize = {size.width, size.height}; + POINT srcPos = {0, 0}; + HWND hWnd = WinUtils::GetTopLevelHWND(mWnd, true); + RECT winRect; + ::GetWindowRect(hWnd, &winRect); + + // perform the alpha blend + return !!::UpdateLayeredWindow(hWnd, nullptr, (POINT*)&winRect, &winSize, + mMemoryDC, &srcPos, 0, &bf, ULW_ALPHA); +} + +HDC InProcessWinCompositorWidget::GetWindowSurface() { + return eTransparencyTransparent == mTransparencyMode ? mMemoryDC + : ::GetDC(mWnd); +} + +void InProcessWinCompositorWidget::FreeWindowSurface(HDC dc) { + if (eTransparencyTransparent != mTransparencyMode) ::ReleaseDC(mWnd, dc); +} + +bool InProcessWinCompositorWidget::IsHidden() const { return ::IsIconic(mWnd); } + +nsIWidget* InProcessWinCompositorWidget::RealWidget() { return mWindow; } + +void InProcessWinCompositorWidget::ObserveVsync(VsyncObserver* aObserver) { + if (RefPtr cvd = + mWindow->GetCompositorVsyncDispatcher()) { + cvd->SetCompositorVsyncObserver(aObserver); + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/InProcessWinCompositorWidget.h b/widget/windows/InProcessWinCompositorWidget.h new file mode 100644 index 0000000000..8ef79bef71 --- /dev/null +++ b/widget/windows/InProcessWinCompositorWidget.h @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_windows_InProcessWinCompositorWidget_h +#define widget_windows_InProcessWinCompositorWidget_h + +#include "WinCompositorWidget.h" + +class nsWindow; +class gfxASurface; + +namespace mozilla { +namespace widget { + +// This is the Windows-specific implementation of CompositorWidget. For +// the most part it only requires an HWND, however it maintains extra state +// for transparent windows, as well as for synchronizing WM_SETTEXT messages +// with the compositor. +class InProcessWinCompositorWidget final + : public WinCompositorWidget, + public PlatformCompositorWidgetDelegate { + public: + InProcessWinCompositorWidget(const WinCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, + nsWindow* aWindow); + + bool PreRender(WidgetRenderingContext*) override; + void PostRender(WidgetRenderingContext*) override; + already_AddRefed StartRemoteDrawing() override; + void EndRemoteDrawing() override; + bool NeedsToDeferEndRemoteDrawing() override; + LayoutDeviceIntSize GetClientSize() override; + already_AddRefed GetBackBufferDrawTarget( + gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect, + bool* aOutIsCleared) override; + already_AddRefed EndBackBufferDrawing() override; + bool InitCompositor(layers::Compositor* aCompositor) override; + CompositorWidgetDelegate* AsDelegate() override { return this; } + bool IsHidden() const override; + + // PlatformCompositorWidgetDelegate Overrides + + void EnterPresentLock() override; + void LeavePresentLock() override; + void OnDestroyWindow() override; + bool OnWindowResize(const LayoutDeviceIntSize& aSize) override; + void OnWindowModeChange(nsSizeMode aSizeMode) override; + void UpdateTransparency(nsTransparencyMode aMode) override; + void NotifyVisibilityUpdated(nsSizeMode aSizeMode, + bool aIsFullyOccluded) override; + void ClearTransparentWindow() override; + + bool RedrawTransparentWindow(); + + // Ensure that a transparent surface exists, then return it. + RefPtr EnsureTransparentSurface(); + + HDC GetTransparentDC() const { return mMemoryDC; } + + mozilla::Mutex& GetTransparentSurfaceLock() { + return mTransparentSurfaceLock; + } + + bool HasGlass() const override; + + nsSizeMode GetWindowSizeMode() const override; + bool GetWindowIsFullyOccluded() const override; + + void ObserveVsync(VsyncObserver* aObserver) override; + nsIWidget* RealWidget() override; + + void UpdateCompositorWnd(const HWND aCompositorWnd, + const HWND aParentWnd) override {} + void SetRootLayerTreeID(const layers::LayersId& aRootLayerTreeId) override {} + + private: + HDC GetWindowSurface(); + void FreeWindowSurface(HDC dc); + + void CreateTransparentSurface(const gfx::IntSize& aSize); + + nsWindow* mWindow; + + HWND mWnd; + + gfx::CriticalSection mPresentLock; + + // Transparency handling. + mozilla::Mutex mTransparentSurfaceLock MOZ_UNANNOTATED; + mozilla::Atomic + mTransparencyMode; + + // Visibility handling. + mozilla::Atomic mSizeMode; + mozilla::Atomic mIsFullyOccluded; + + RefPtr mTransparentSurface; + HDC mMemoryDC; + HDC mCompositeDC; + + // Locked back buffer of BasicCompositor + uint8_t* mLockedBackBufferData; + + bool mNotDeferEndRemoteDrawing; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_windows_InProcessWinCompositorWidget_h diff --git a/widget/windows/InkCollector.cpp b/widget/windows/InkCollector.cpp new file mode 100644 index 0000000000..4c2dfbf387 --- /dev/null +++ b/widget/windows/InkCollector.cpp @@ -0,0 +1,245 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=2 sw=2 et tw=78: + * 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 "InkCollector.h" + +// Msinkaut_i.c and Msinkaut.h should both be included +// https://msdn.microsoft.com/en-us/library/windows/desktop/ms695519.aspx +#include + +StaticAutoPtr InkCollector::sInkCollector; + +InkCollector::~InkCollector() { + Shutdown(); + MOZ_ASSERT(!mCookie && !mEnabled && !mComInitialized && !mMarshaller && + !mInkCollector && !mConnectionPoint && !mInkCollectorEvent); +} + +void InkCollector::Initialize() { + // Possibly, we can use mConnectionPoint for checking, + // But if errors exist (perhaps COM object is unavailable), + // Initialize() will be called more times. + static bool sInkCollectorCreated = false; + if (sInkCollectorCreated) { + return; + } + sInkCollectorCreated = true; + + // COM could get uninitialized due to previous initialization. + mComInitialized = SUCCEEDED(::CoInitialize(nullptr)); + + // Set up instance of InkCollectorEvent. + mInkCollectorEvent = new InkCollectorEvent(); + + // Set up a free threaded marshaler. + if (FAILED(::CoCreateFreeThreadedMarshaler(mInkCollectorEvent, + getter_AddRefs(mMarshaller)))) { + return; + } + + // Create the ink collector. + if (FAILED(::CoCreateInstance(CLSID_InkCollector, NULL, CLSCTX_INPROC_SERVER, + IID_IInkCollector, + getter_AddRefs(mInkCollector)))) { + return; + } + + // Set up connection between sink and InkCollector. + RefPtr connPointContainer; + + // Get the connection point container. + if (SUCCEEDED(mInkCollector->QueryInterface( + IID_IConnectionPointContainer, getter_AddRefs(connPointContainer)))) { + // Find the connection point for Ink Collector events. + if (SUCCEEDED(connPointContainer->FindConnectionPoint( + __uuidof(_IInkCollectorEvents), + getter_AddRefs(mConnectionPoint)))) { + // Hook up sink to connection point. + if (SUCCEEDED(mConnectionPoint->Advise(mInkCollectorEvent, &mCookie))) { + OnInitialize(); + } + } + } +} + +void InkCollector::Shutdown() { + Enable(false); + if (mConnectionPoint) { + // Remove the connection of the sink to the Ink Collector. + mConnectionPoint->Unadvise(mCookie); + mCookie = 0; + mConnectionPoint = nullptr; + } + mInkCollector = nullptr; + mMarshaller = nullptr; + mInkCollectorEvent = nullptr; + + // Let uninitialization get handled in a place where it got inited. + if (mComInitialized) { + CoUninitialize(); + mComInitialized = false; + } +} + +void InkCollector::OnInitialize() { + // Suppress all events to do not allow performance decreasing. + // https://msdn.microsoft.com/en-us/library/ms820347.aspx + mInkCollector->SetEventInterest(InkCollectorEventInterest::ICEI_AllEvents, + VARIANT_FALSE); + + // Sets a value that indicates whether an object or control has interest in a + // specified event. + mInkCollector->SetEventInterest( + InkCollectorEventInterest::ICEI_CursorOutOfRange, VARIANT_TRUE); + + // If the MousePointer property is set to IMP_Custom and the MouseIcon + // property is NULL, Then the ink collector no longer handles mouse cursor + // settings. + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms700686.aspx + mInkCollector->put_MouseIcon(nullptr); + mInkCollector->put_MousePointer(InkMousePointer::IMP_Custom); + + // This mode allows an ink collector to collect ink from any tablet attached + // to the Tablet PC. The Boolean value that indicates whether to use the mouse + // as an input device. If TRUE, the mouse is used for input. + // https://msdn.microsoft.com/en-us/library/ms820346.aspx + mInkCollector->SetAllTabletsMode(VARIANT_FALSE); + + // Sets the value that specifies whether ink is rendered as it is drawn. + // VARIANT_TRUE to render ink as it is drawn on the display. + // VARIANT_FALSE to not have the ink appear on the display as strokes are + // made. + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd314598.aspx + mInkCollector->put_DynamicRendering(VARIANT_FALSE); + + // Set AutoRedraw to false to prevent repainting the ink when the window is + // invalidated. + mInkCollector->put_AutoRedraw(VARIANT_FALSE); +} + +// Sets a value that specifies whether the InkCollector object collects pen +// input. This property must be set to FALSE before setting or calling specific +// properties and methods of the object. +// https://msdn.microsoft.com/en-us/library/windows/desktop/ms701721.aspx +void InkCollector::Enable(bool aNewState) { + if (aNewState != mEnabled) { + if (mInkCollector) { + if (SUCCEEDED(mInkCollector->put_Enabled(aNewState ? VARIANT_TRUE + : VARIANT_FALSE))) { + mEnabled = aNewState; + } else { + NS_WARNING("InkCollector did not change status successfully"); + } + } else { + NS_WARNING("InkCollector should be exist"); + } + } +} + +HWND InkCollector::GetTarget() { return mTargetWindow; } + +void InkCollector::SetTarget(HWND aTargetWindow) { + NS_ASSERTION(aTargetWindow, "aTargetWindow should be exist"); + if (aTargetWindow && (aTargetWindow != mTargetWindow)) { + Initialize(); + if (mInkCollector) { + Enable(false); + if (SUCCEEDED(mInkCollector->put_hWnd((LONG_PTR)aTargetWindow))) { + mTargetWindow = aTargetWindow; + } else { + NS_WARNING("InkCollector did not change window property successfully"); + } + Enable(true); + } + } +} + +void InkCollector::ClearTarget() { + if (mTargetWindow && mInkCollector) { + Enable(false); + if (SUCCEEDED(mInkCollector->put_hWnd(0))) { + mTargetWindow = 0; + } else { + NS_WARNING("InkCollector did not clear window property successfully"); + } + } +} + +uint16_t InkCollector::GetPointerId() { return mPointerId; } + +void InkCollector::SetPointerId(uint16_t aPointerId) { + mPointerId = aPointerId; +} + +void InkCollector::ClearPointerId() { mPointerId = 0; } + +// The display and the digitizer have quite different properties. +// The display has CursorMustTouch, the mouse pointer alway touches the display +// surface. The digitizer lists Integrated and HardProximity. When the stylus is +// in the proximity of the tablet its movements are also detected. An external +// tablet will only list HardProximity. +bool InkCollectorEvent::IsHardProximityTablet(IInkTablet* aTablet) const { + if (aTablet) { + TabletHardwareCapabilities caps; + if (SUCCEEDED(aTablet->get_HardwareCapabilities(&caps))) { + return (TabletHardwareCapabilities::THWC_HardProximity & caps); + } + } + return false; +} + +HRESULT __stdcall InkCollectorEvent::QueryInterface(REFIID aRiid, + void** aObject) { + // Validate the input + if (!aObject) { + return E_POINTER; + } + HRESULT result = E_NOINTERFACE; + // This object supports IUnknown/IDispatch/IInkCollectorEvents + if ((IID_IUnknown == aRiid) || (IID_IDispatch == aRiid) || + (DIID__IInkCollectorEvents == aRiid)) { + *aObject = this; + // AddRef should be called when we give info about interface + NS_ADDREF_THIS(); + result = S_OK; + } + return result; +} + +HRESULT InkCollectorEvent::Invoke(DISPID aDispIdMember, REFIID /*aRiid*/, + LCID /*aId*/, WORD /*wFlags*/, + DISPPARAMS* aDispParams, + VARIANT* /*aVarResult*/, + EXCEPINFO* /*aExcepInfo*/, + UINT* /*aArgErr*/) { + switch (aDispIdMember) { + case DISPID_ICECursorOutOfRange: { + if (aDispParams && aDispParams->cArgs) { + CursorOutOfRange( + static_cast(aDispParams->rgvarg[0].pdispVal)); + } + break; + } + } + return S_OK; +} + +void InkCollectorEvent::CursorOutOfRange(IInkCursor* aCursor) const { + IInkTablet* curTablet = nullptr; + if (FAILED(aCursor->get_Tablet(&curTablet))) { + return; + } + // All events should be suppressed except + // from tablets with hard proximity. + if (!IsHardProximityTablet(curTablet)) { + return; + } + // Notify current target window. + if (HWND targetWindow = InkCollector::sInkCollector->GetTarget()) { + ::SendMessage(targetWindow, MOZ_WM_PEN_LEAVES_HOVER_OF_DIGITIZER, 0, 0); + } +} diff --git a/widget/windows/InkCollector.h b/widget/windows/InkCollector.h new file mode 100644 index 0000000000..f13bfbf570 --- /dev/null +++ b/widget/windows/InkCollector.h @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=2 sw=2 et tw=78: + * 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 InkCollector_h__ +#define InkCollector_h__ + +#include +#include "mozilla/StaticPtr.h" + +#define MOZ_WM_PEN_LEAVES_HOVER_OF_DIGITIZER WM_USER + 0x83 + +class InkCollectorEvent final : public _IInkCollectorEvents { + public: + // IUnknown + HRESULT __stdcall QueryInterface(REFIID aRiid, void** aObject); + virtual ULONG STDMETHODCALLTYPE AddRef() { return ++mRefCount; } + virtual ULONG STDMETHODCALLTYPE Release() { + MOZ_ASSERT(mRefCount); + if (!--mRefCount) { + delete this; + return 0; + } + return mRefCount; + } + + protected: + // IDispatch + STDMETHOD(GetTypeInfoCount)(UINT* aInfo) { return E_NOTIMPL; } + STDMETHOD(GetTypeInfo)(UINT aInfo, LCID aId, ITypeInfo** aTInfo) { + return E_NOTIMPL; + } + STDMETHOD(GetIDsOfNames) + (REFIID aRiid, LPOLESTR* aStrNames, UINT aNames, LCID aId, DISPID* aDispId) { + return E_NOTIMPL; + } + STDMETHOD(Invoke) + (DISPID aDispIdMember, REFIID aRiid, LCID aId, WORD wFlags, + DISPPARAMS* aDispParams, VARIANT* aVarResult, EXCEPINFO* aExcepInfo, + UINT* aArgErr); + + // InkCollectorEvent + void CursorOutOfRange(IInkCursor* aCursor) const; + bool IsHardProximityTablet(IInkTablet* aTablet) const; + + private: + uint32_t mRefCount = 0; + + ~InkCollectorEvent() = default; +}; + +class InkCollector { + public: + ~InkCollector(); + void Shutdown(); + + HWND GetTarget(); + void SetTarget(HWND aTargetWindow); + void ClearTarget(); + + uint16_t GetPointerId(); // 0 shows that there is no existing pen. + void SetPointerId(uint16_t aPointerId); + void ClearPointerId(); + + static mozilla::StaticAutoPtr sInkCollector; + + protected: + void Initialize(); + void OnInitialize(); + void Enable(bool aNewState); + + private: + RefPtr mMarshaller; + RefPtr mInkCollector; + RefPtr mConnectionPoint; + RefPtr mInkCollectorEvent; + + HWND mTargetWindow = 0; + DWORD mCookie = 0; + bool mComInitialized = false; + bool mEnabled = false; + + // This value holds the previous pointerId of the pen, and is used by the + // nsWindow when processing a MOZ_WM_PEN_LEAVES_HOVER_OF_DIGITIZER which + // indicates that a pen leaves the digitizer. + + // TODO: If we move our implementation to window pointer input messages, then + // we no longer need this value, since the pointerId can be retrieved from the + // window message, please refer to + // https://msdn.microsoft.com/en-us/library/windows/desktop/hh454916(v=vs.85).aspx + + // NOTE: The pointerId of a pen shouldn't be 0 on a Windows platform, since 0 + // is reserved of the mouse, please refer to + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms703320(v=vs.85).aspx + uint16_t mPointerId = 0; +}; + +#endif // InkCollector_h__ diff --git a/widget/windows/InputDeviceUtils.cpp b/widget/windows/InputDeviceUtils.cpp new file mode 100644 index 0000000000..3636b93d6a --- /dev/null +++ b/widget/windows/InputDeviceUtils.cpp @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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 "InputDeviceUtils.h" + +#define INITGUID +#include +#include +#include +#include + +namespace mozilla { +namespace widget { + +HDEVNOTIFY +InputDeviceUtils::RegisterNotification(HWND aHwnd) { + DEV_BROADCAST_DEVICEINTERFACE filter = {}; + + filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE); + filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; + // Some touchsreen devices are not GUID_DEVINTERFACE_MOUSE, so here we use + // GUID_DEVINTERFACE_HID instead. + filter.dbcc_classguid = GUID_DEVINTERFACE_HID; + return RegisterDeviceNotification(aHwnd, &filter, + DEVICE_NOTIFY_WINDOW_HANDLE); +} + +void InputDeviceUtils::UnregisterNotification(HDEVNOTIFY aHandle) { + if (!aHandle) { + return; + } + UnregisterDeviceNotification(aHandle); +} + +DWORD +InputDeviceUtils::CountMouseDevices() { + HDEVINFO hdev = + SetupDiGetClassDevs(&GUID_DEVINTERFACE_MOUSE, nullptr, nullptr, + DIGCF_DEVICEINTERFACE | DIGCF_PRESENT); + if (hdev == INVALID_HANDLE_VALUE) { + return 0; + } + + DWORD count = 0; + SP_INTERFACE_DEVICE_DATA info = {}; + info.cbSize = sizeof(SP_INTERFACE_DEVICE_DATA); + while (SetupDiEnumDeviceInterfaces(hdev, nullptr, &GUID_DEVINTERFACE_MOUSE, + count, &info)) { + if (info.Flags & SPINT_ACTIVE) { + count++; + } + } + SetupDiDestroyDeviceInfoList(hdev); + return count; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/InputDeviceUtils.h b/widget/windows/InputDeviceUtils.h new file mode 100644 index 0000000000..d16698a03a --- /dev/null +++ b/widget/windows/InputDeviceUtils.h @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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_widget_InputDeviceUtils_h__ +#define mozilla_widget_InputDeviceUtils_h__ + +#include + +namespace mozilla { +namespace widget { + +class InputDeviceUtils { + public: + static HDEVNOTIFY RegisterNotification(HWND aHwnd); + static void UnregisterNotification(HDEVNOTIFY aHandle); + + // Returns the number of mouse type devices connected to this system. + static DWORD CountMouseDevices(); +}; + +} // namespace widget +} // namespace mozilla +#endif // mozilla_widget_InputDeviceUtils_h__ diff --git a/widget/windows/JumpListBuilder.cpp b/widget/windows/JumpListBuilder.cpp new file mode 100644 index 0000000000..b74c3f1529 --- /dev/null +++ b/widget/windows/JumpListBuilder.cpp @@ -0,0 +1,647 @@ +/* -*- 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 "JumpListBuilder.h" + +#include "nsError.h" +#include "nsCOMPtr.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsArrayUtils.h" +#include "nsWidgetsCID.h" +#include "WinTaskbar.h" +#include "nsDirectoryServiceUtils.h" +#include "mozilla/Preferences.h" +#include "nsStringStream.h" +#include "nsThreadUtils.h" +#include "mozilla/LazyIdleThread.h" +#include "nsIObserverService.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/mscom/ApartmentRegion.h" +#include "mozilla/mscom/EnsureMTA.h" + +#include +#include "WinUtils.h" + +using mozilla::dom::Promise; + +// The amount of time, in milliseconds, that our IO thread will stay alive after +// the last event it processes. +#define DEFAULT_THREAD_TIMEOUT_MS 30000 + +namespace mozilla { +namespace widget { + +// defined in WinTaskbar.cpp +extern const wchar_t* gMozillaJumpListIDGeneric; + +Atomic JumpListBuilder::sBuildingList(false); +const char kPrefTaskbarEnabled[] = "browser.taskbar.lists.enabled"; + +NS_IMPL_ISUPPORTS(JumpListBuilder, nsIJumpListBuilder, nsIObserver) +#define TOPIC_PROFILE_BEFORE_CHANGE "profile-before-change" +#define TOPIC_CLEAR_PRIVATE_DATA "clear-private-data" + +namespace detail { + +class DoneCommitListBuildCallback final : public nsIRunnable { + NS_DECL_THREADSAFE_ISUPPORTS + + public: + DoneCommitListBuildCallback(nsIJumpListCommittedCallback* aCallback, + JumpListBuilder* aBuilder) + : mCallback(aCallback), mBuilder(aBuilder), mResult(false) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + if (mCallback) { + Unused << mCallback->Done(mResult); + } + // Ensure we are releasing on the main thread. + Destroy(); + return NS_OK; + } + + void SetResult(bool aResult) { mResult = aResult; } + + private: + ~DoneCommitListBuildCallback() { + // Destructor does not always call on the main thread. + MOZ_ASSERT(!mCallback); + MOZ_ASSERT(!mBuilder); + } + + void Destroy() { + MOZ_ASSERT(NS_IsMainThread()); + mCallback = nullptr; + mBuilder = nullptr; + } + + // These two references MUST be released on the main thread. + RefPtr mCallback; + RefPtr mBuilder; + bool mResult; +}; + +NS_IMPL_ISUPPORTS(DoneCommitListBuildCallback, nsIRunnable); + +} // namespace detail + +JumpListBuilder::JumpListBuilder() + : mMaxItems(0), mHasCommit(false), mMonitor("JumpListBuilderMonitor") { + MOZ_ASSERT(NS_IsMainThread()); + + // Instantiate mJumpListMgr in the multithreaded apartment so that proxied + // calls on that object do not need to interact with the main thread's message + // pump. + mscom::EnsureMTA([&]() { + RefPtr jumpListMgr; + HRESULT hr = ::CoCreateInstance( + CLSID_DestinationList, nullptr, CLSCTX_INPROC_SERVER, + IID_ICustomDestinationList, getter_AddRefs(jumpListMgr)); + if (FAILED(hr)) { + return; + } + + ReentrantMonitorAutoEnter lock(mMonitor); + // Since we are accessing mJumpListMgr across different threads + // (ie, different apartments), mJumpListMgr must be an agile reference. + mJumpListMgr = jumpListMgr; + }); + + if (!mJumpListMgr) { + return; + } + + // Make a lazy thread for any IO + mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, "Jump List"_ns, + LazyIdleThread::ManualShutdown); + Preferences::AddStrongObserver(this, kPrefTaskbarEnabled); + + nsCOMPtr observerService = + do_GetService("@mozilla.org/observer-service;1"); + if (observerService) { + observerService->AddObserver(this, TOPIC_PROFILE_BEFORE_CHANGE, false); + observerService->AddObserver(this, TOPIC_CLEAR_PRIVATE_DATA, false); + } + + RefPtr jumpListMgr = mJumpListMgr; + if (!jumpListMgr) { + return; + } +} + +JumpListBuilder::~JumpListBuilder() { + Preferences::RemoveObserver(this, kPrefTaskbarEnabled); +} + +NS_IMETHODIMP JumpListBuilder::SetAppUserModelID( + const nsAString& aAppUserModelId) { + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + RefPtr jumpListMgr = mJumpListMgr; + if (!jumpListMgr) { + return NS_ERROR_NOT_AVAILABLE; + } + + mAppUserModelId.Assign(aAppUserModelId); + // MSIX packages explicitly do not support setting the appid from within + // the app, as it is set in the package manifest instead. + if (!mozilla::widget::WinUtils::HasPackageIdentity()) { + jumpListMgr->SetAppID(mAppUserModelId.get()); + } + + return NS_OK; +} + +NS_IMETHODIMP JumpListBuilder::GetAvailable(int16_t* aAvailable) { + *aAvailable = false; + + ReentrantMonitorAutoEnter lock(mMonitor); + if (mJumpListMgr) *aAvailable = true; + + return NS_OK; +} + +NS_IMETHODIMP JumpListBuilder::GetIsListCommitted(bool* aCommit) { + *aCommit = mHasCommit; + + return NS_OK; +} + +NS_IMETHODIMP JumpListBuilder::GetMaxListItems(int16_t* aMaxItems) { + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + *aMaxItems = 0; + + if (sBuildingList) { + *aMaxItems = mMaxItems; + return NS_OK; + } + + RefPtr jumpListMgr = mJumpListMgr; + if (!jumpListMgr) { + return NS_ERROR_UNEXPECTED; + } + + IObjectArray* objArray; + if (SUCCEEDED(jumpListMgr->BeginList(&mMaxItems, IID_PPV_ARGS(&objArray)))) { + *aMaxItems = mMaxItems; + + if (objArray) objArray->Release(); + + jumpListMgr->AbortList(); + } + + return NS_OK; +} + +NS_IMETHODIMP JumpListBuilder::InitListBuild(JSContext* aCx, + Promise** aPromise) { + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_FAILURE; + } + + ErrorResult result; + RefPtr promise = Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + nsCOMPtr runnable = + NewRunnableMethod>>( + "InitListBuild", this, &JumpListBuilder::DoInitListBuild, promise); + nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + promise.forget(aPromise); + return NS_OK; +} + +void JumpListBuilder::DoInitListBuild(RefPtr&& aPromise) { + // Since we're invoking COM interfaces to talk to the shell on a background + // thread, we need to be running inside a multithreaded apartment. + mscom::MTARegion mta; + MOZ_ASSERT(mta.IsValid()); + + ReentrantMonitorAutoEnter lock(mMonitor); + MOZ_ASSERT(mJumpListMgr); + + if (sBuildingList) { + AbortListBuild(); + } + + HRESULT hr = E_UNEXPECTED; + auto errorHandler = MakeScopeExit([&aPromise, &hr]() { + if (SUCCEEDED(hr)) { + return; + } + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "InitListBuildReject", [promise = std::move(aPromise)]() { + promise->MaybeReject(NS_ERROR_FAILURE); + })); + }); + + RefPtr jumpListMgr = mJumpListMgr; + if (!jumpListMgr) { + return; + } + + nsTArray urisToRemove; + RefPtr objArray; + hr = jumpListMgr->BeginList( + &mMaxItems, + IID_PPV_ARGS(static_cast(getter_AddRefs(objArray)))); + if (FAILED(hr)) { + return; + } + + // The returned objArray of removed items are for manually removed items. + // This does not return items which are removed because they were previously + // part of the jump list but are no longer part of the jump list. + sBuildingList = true; + RemoveIconCacheAndGetJumplistShortcutURIs(objArray, urisToRemove); + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "InitListBuildResolve", [urisToRemove = std::move(urisToRemove), + promise = std::move(aPromise)]() { + promise->MaybeResolve(urisToRemove); + })); +} + +// Ensures that we have no old ICO files left in the jump list cache +nsresult JumpListBuilder::RemoveIconCacheForAllItems() { + // Construct the path of our jump list cache + nsCOMPtr jumpListCacheDir; + nsresult rv = + NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(jumpListCacheDir)); + NS_ENSURE_SUCCESS(rv, rv); + rv = jumpListCacheDir->AppendNative( + nsDependentCString(mozilla::widget::FaviconHelper::kJumpListCacheDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr entries; + rv = jumpListCacheDir->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ENSURE_SUCCESS(rv, rv); + + // Loop through each directory entry and remove all ICO files found + do { + nsCOMPtr currFile; + if (NS_FAILED(entries->GetNextFile(getter_AddRefs(currFile))) || !currFile) + break; + + nsAutoString path; + if (NS_FAILED(currFile->GetPath(path))) continue; + + if (StringTail(path, 4).LowerCaseEqualsASCII(".ico")) { + // Check if the cached ICO file exists + bool exists; + if (NS_FAILED(currFile->Exists(&exists)) || !exists) continue; + + // We found an ICO file that exists, so we should remove it + currFile->Remove(false); + } + } while (true); + + return NS_OK; +} + +NS_IMETHODIMP JumpListBuilder::AddListToBuild(int16_t aCatType, nsIArray* items, + const nsAString& catName, + bool* _retval) { + nsresult rv; + + *_retval = false; + + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + RefPtr jumpListMgr = mJumpListMgr; + if (!jumpListMgr) { + return NS_ERROR_UNEXPECTED; + } + + switch (aCatType) { + case nsIJumpListBuilder::JUMPLIST_CATEGORY_TASKS: { + NS_ENSURE_ARG_POINTER(items); + + HRESULT hr; + RefPtr collection; + hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr, + CLSCTX_INPROC_SERVER, IID_IObjectCollection, + getter_AddRefs(collection)); + if (FAILED(hr)) return NS_ERROR_UNEXPECTED; + + // Build the list + uint32_t length; + items->GetLength(&length); + for (uint32_t i = 0; i < length; ++i) { + nsCOMPtr item = do_QueryElementAt(items, i); + if (!item) continue; + // Check for separators + if (IsSeparator(item)) { + RefPtr link; + rv = JumpListSeparator::GetSeparator(link); + if (NS_FAILED(rv)) return rv; + collection->AddObject(link); + continue; + } + // These should all be ShellLinks + RefPtr link; + rv = JumpListShortcut::GetShellLink(item, link, mIOThread); + if (NS_FAILED(rv)) return rv; + collection->AddObject(link); + } + + // We need IObjectArray to submit + RefPtr pArray; + hr = collection->QueryInterface(IID_IObjectArray, getter_AddRefs(pArray)); + if (FAILED(hr)) return NS_ERROR_UNEXPECTED; + + // Add the tasks + hr = jumpListMgr->AddUserTasks(pArray); + if (SUCCEEDED(hr)) *_retval = true; + return NS_OK; + } break; + case nsIJumpListBuilder::JUMPLIST_CATEGORY_RECENT: { + if (SUCCEEDED(jumpListMgr->AppendKnownCategory(KDC_RECENT))) + *_retval = true; + return NS_OK; + } break; + case nsIJumpListBuilder::JUMPLIST_CATEGORY_FREQUENT: { + if (SUCCEEDED(jumpListMgr->AppendKnownCategory(KDC_FREQUENT))) + *_retval = true; + return NS_OK; + } break; + case nsIJumpListBuilder::JUMPLIST_CATEGORY_CUSTOMLIST: { + NS_ENSURE_ARG_POINTER(items); + + if (catName.IsEmpty()) return NS_ERROR_INVALID_ARG; + + HRESULT hr; + RefPtr collection; + hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr, + CLSCTX_INPROC_SERVER, IID_IObjectCollection, + getter_AddRefs(collection)); + if (FAILED(hr)) return NS_ERROR_UNEXPECTED; + + uint32_t length; + items->GetLength(&length); + for (uint32_t i = 0; i < length; ++i) { + nsCOMPtr item = do_QueryElementAt(items, i); + if (!item) continue; + int16_t type; + if (NS_FAILED(item->GetType(&type))) continue; + switch (type) { + case nsIJumpListItem::JUMPLIST_ITEM_SEPARATOR: { + RefPtr shellItem; + rv = JumpListSeparator::GetSeparator(shellItem); + if (NS_FAILED(rv)) return rv; + collection->AddObject(shellItem); + } break; + case nsIJumpListItem::JUMPLIST_ITEM_LINK: { + RefPtr shellItem; + rv = JumpListLink::GetShellItem(item, shellItem); + if (NS_FAILED(rv)) return rv; + collection->AddObject(shellItem); + } break; + case nsIJumpListItem::JUMPLIST_ITEM_SHORTCUT: { + RefPtr shellItem; + rv = JumpListShortcut::GetShellLink(item, shellItem, mIOThread); + if (NS_FAILED(rv)) return rv; + collection->AddObject(shellItem); + } break; + } + } + + // We need IObjectArray to submit + RefPtr pArray; + hr = collection->QueryInterface(IID_IObjectArray, (LPVOID*)&pArray); + if (FAILED(hr)) return NS_ERROR_UNEXPECTED; + + // Add the tasks + hr = jumpListMgr->AppendCategory( + reinterpret_cast(catName.BeginReading()), pArray); + if (SUCCEEDED(hr)) *_retval = true; + + // Get rid of the old icons + nsCOMPtr event = + new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(true); + mIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + + return NS_OK; + } break; + } + return NS_OK; +} + +NS_IMETHODIMP JumpListBuilder::AbortListBuild() { + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + RefPtr jumpListMgr = mJumpListMgr; + if (!jumpListMgr) { + return NS_ERROR_UNEXPECTED; + } + + jumpListMgr->AbortList(); + sBuildingList = false; + + return NS_OK; +} + +NS_IMETHODIMP JumpListBuilder::CommitListBuild( + nsIJumpListCommittedCallback* aCallback) { + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + // Also holds a strong reference to this to prevent use-after-free. + RefPtr callback = + new detail::DoneCommitListBuildCallback(aCallback, this); + + // The builder has a strong reference in the callback already, so we do not + // need to do it for this runnable again. + RefPtr event = + NewNonOwningRunnableMethod>( + "JumpListBuilder::DoCommitListBuild", this, + &JumpListBuilder::DoCommitListBuild, std::move(callback)); + Unused << mIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + + return NS_OK; +} + +void JumpListBuilder::DoCommitListBuild( + RefPtr aCallback) { + // Since we're invoking COM interfaces to talk to the shell on a background + // thread, we need to be running inside a multithreaded apartment. + mscom::MTARegion mta; + MOZ_ASSERT(mta.IsValid()); + + ReentrantMonitorAutoEnter lock(mMonitor); + MOZ_ASSERT(mJumpListMgr); + MOZ_ASSERT(aCallback); + + HRESULT hr = E_UNEXPECTED; + auto onExit = MakeScopeExit([&hr, &aCallback]() { + // XXX We might want some specific error data here. + aCallback->SetResult(SUCCEEDED(hr)); + Unused << NS_DispatchToMainThread(aCallback); + }); + + RefPtr jumpListMgr = mJumpListMgr; + if (!jumpListMgr) { + return; + } + + hr = jumpListMgr->CommitList(); + sBuildingList = false; + + if (SUCCEEDED(hr)) { + mHasCommit = true; + } +} + +NS_IMETHODIMP JumpListBuilder::DeleteActiveList(bool* _retval) { + *_retval = false; + + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + if (sBuildingList) { + AbortListBuild(); + } + + RefPtr jumpListMgr = mJumpListMgr; + if (!jumpListMgr) { + return NS_ERROR_UNEXPECTED; + } + + if (SUCCEEDED(jumpListMgr->DeleteList(mAppUserModelId.get()))) { + *_retval = true; + } + + return NS_OK; +} + +/* internal */ + +bool JumpListBuilder::IsSeparator(nsCOMPtr& item) { + int16_t type; + item->GetType(&type); + if (NS_FAILED(item->GetType(&type))) return false; + + if (type == nsIJumpListItem::JUMPLIST_ITEM_SEPARATOR) return true; + return false; +} + +// RemoveIconCacheAndGetJumplistShortcutURIs - does multiple things to +// avoid unnecessary extra XPCOM incantations. For each object in the input +// array, if it's an IShellLinkW, this deletes the cached icon and adds the +// url param to a list of URLs to be removed from the places database. +void JumpListBuilder::RemoveIconCacheAndGetJumplistShortcutURIs( + IObjectArray* aObjArray, nsTArray& aURISpecs) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Early return here just in case some versions of Windows don't populate this + if (!aObjArray) { + return; + } + + uint32_t count = 0; + aObjArray->GetCount(&count); + + for (uint32_t idx = 0; idx < count; idx++) { + RefPtr pLink; + + if (FAILED(aObjArray->GetAt(idx, IID_IShellLinkW, + static_cast(getter_AddRefs(pLink))))) { + continue; + } + + wchar_t buf[MAX_PATH]; + HRESULT hres = pLink->GetArguments(buf, MAX_PATH); + if (SUCCEEDED(hres)) { + LPWSTR* arglist; + int32_t numArgs; + + arglist = ::CommandLineToArgvW(buf, &numArgs); + if (arglist && numArgs > 0) { + nsString spec(arglist[0]); + aURISpecs.AppendElement(std::move(spec)); + ::LocalFree(arglist); + } + } + + int iconIdx = 0; + hres = pLink->GetIconLocation(buf, MAX_PATH, &iconIdx); + if (SUCCEEDED(hres)) { + nsDependentString spec(buf); + DeleteIconFromDisk(spec); + } + } +} + +void JumpListBuilder::DeleteIconFromDisk(const nsAString& aPath) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Check that we aren't deleting some arbitrary file that is not an icon + if (StringTail(aPath, 4).LowerCaseEqualsASCII(".ico")) { + // Construct the parent path of the passed in path + nsCOMPtr icoFile; + nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(icoFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + icoFile->Remove(false); + } +} + +NS_IMETHODIMP JumpListBuilder::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + NS_ENSURE_ARG_POINTER(aTopic); + if (strcmp(aTopic, TOPIC_PROFILE_BEFORE_CHANGE) == 0) { + nsCOMPtr observerService = + do_GetService("@mozilla.org/observer-service;1"); + if (observerService) { + observerService->RemoveObserver(this, TOPIC_PROFILE_BEFORE_CHANGE); + } + mIOThread->Shutdown(); + // Clear out mJumpListMgr, as MSCOM services won't be available soon. + ReentrantMonitorAutoEnter lock(mMonitor); + mJumpListMgr = nullptr; + } else if (strcmp(aTopic, "nsPref:changed") == 0 && + nsDependentString(aData).EqualsASCII(kPrefTaskbarEnabled)) { + bool enabled = Preferences::GetBool(kPrefTaskbarEnabled, true); + if (!enabled) { + nsCOMPtr event = + new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(); + mIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + } + } else if (strcmp(aTopic, TOPIC_CLEAR_PRIVATE_DATA) == 0) { + // Delete JumpListCache icons from Disk, if any. + nsCOMPtr event = + new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(false); + mIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + } + return NS_OK; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/JumpListBuilder.h b/widget/windows/JumpListBuilder.h new file mode 100644 index 0000000000..6c957c033a --- /dev/null +++ b/widget/windows/JumpListBuilder.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __JumpListBuilder_h__ +#define __JumpListBuilder_h__ + +#include + +#undef NTDDI_VERSION +#define NTDDI_VERSION NTDDI_WIN7 +// Needed for various com interfaces +#include +#undef LogSeverity // SetupAPI.h #defines this as DWORD + +#include "nsString.h" + +#include "nsIJumpListBuilder.h" +#include "nsIJumpListItem.h" +#include "JumpListItem.h" +#include "nsIObserver.h" +#include "nsTArray.h" +#include "mozilla/Attributes.h" +#include "mozilla/mscom/AgileReference.h" +#include "mozilla/ReentrantMonitor.h" + +namespace mozilla { +namespace widget { + +namespace detail { +class DoneCommitListBuildCallback; +} // namespace detail + +class JumpListBuilder : public nsIJumpListBuilder, public nsIObserver { + virtual ~JumpListBuilder(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIJUMPLISTBUILDER + NS_DECL_NSIOBSERVER + + JumpListBuilder(); + + protected: + static Atomic sBuildingList; + + private: + mscom::AgileReference mJumpListMgr MOZ_GUARDED_BY(mMonitor); + uint32_t mMaxItems MOZ_GUARDED_BY(mMonitor); + bool mHasCommit; + nsCOMPtr mIOThread; + ReentrantMonitor mMonitor; + nsString mAppUserModelId; + + bool IsSeparator(nsCOMPtr& item); + void RemoveIconCacheAndGetJumplistShortcutURIs(IObjectArray* aObjArray, + nsTArray& aURISpecs); + void DeleteIconFromDisk(const nsAString& aPath); + nsresult RemoveIconCacheForAllItems(); + void DoCommitListBuild(RefPtr aCallback); + void DoInitListBuild(RefPtr&& aPromise); + + friend class WinTaskbar; +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __JumpListBuilder_h__ */ diff --git a/widget/windows/JumpListItem.cpp b/widget/windows/JumpListItem.cpp new file mode 100644 index 0000000000..941a479203 --- /dev/null +++ b/widget/windows/JumpListItem.cpp @@ -0,0 +1,575 @@ +/* -*- 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 "JumpListItem.h" + +#include +#include +#include + +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsCRT.h" +#include "nsNetCID.h" +#include "nsCExternalHandlerService.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/Preferences.h" +#include "JumpListBuilder.h" +#include "WinUtils.h" + +namespace mozilla { +namespace widget { + +// ISUPPORTS Impl's +NS_IMPL_ISUPPORTS(JumpListItem, nsIJumpListItem) + +NS_INTERFACE_MAP_BEGIN(JumpListSeparator) + NS_INTERFACE_MAP_ENTRY(nsIJumpListSeparator) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIJumpListItem, JumpListItemBase) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, JumpListItemBase) +NS_INTERFACE_MAP_END +NS_IMPL_ADDREF(JumpListSeparator) +NS_IMPL_RELEASE(JumpListSeparator) + +NS_INTERFACE_MAP_BEGIN(JumpListLink) + NS_INTERFACE_MAP_ENTRY(nsIJumpListLink) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIJumpListItem, JumpListItemBase) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, JumpListItemBase) +NS_INTERFACE_MAP_END +NS_IMPL_ADDREF(JumpListLink) +NS_IMPL_RELEASE(JumpListLink) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JumpListShortcut) + NS_INTERFACE_MAP_ENTRY(nsIJumpListShortcut) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIJumpListItem, JumpListItemBase) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIJumpListShortcut) +NS_INTERFACE_MAP_END +NS_IMPL_CYCLE_COLLECTING_ADDREF(JumpListShortcut) +NS_IMPL_CYCLE_COLLECTING_RELEASE(JumpListShortcut) +NS_IMPL_CYCLE_COLLECTION(JumpListShortcut, mHandlerApp) + +NS_IMETHODIMP JumpListItemBase::GetType(int16_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + + *aType = mItemType; + + return NS_OK; +} + +NS_IMETHODIMP JumpListItemBase::Equals(nsIJumpListItem* aItem, bool* aResult) { + NS_ENSURE_ARG_POINTER(aItem); + + *aResult = false; + + int16_t theType = nsIJumpListItem::JUMPLIST_ITEM_EMPTY; + if (NS_FAILED(aItem->GetType(&theType))) return NS_OK; + + // Make sure the types match. + if (Type() != theType) return NS_OK; + + *aResult = true; + + return NS_OK; +} + +/* link impl. */ + +NS_IMETHODIMP JumpListLink::GetUri(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = mURI); + + return NS_OK; +} + +NS_IMETHODIMP JumpListLink::SetUri(nsIURI* aURI) { + mURI = aURI; + + return NS_OK; +} + +NS_IMETHODIMP JumpListLink::SetUriTitle(const nsAString& aUriTitle) { + mUriTitle.Assign(aUriTitle); + + return NS_OK; +} + +NS_IMETHODIMP JumpListLink::GetUriTitle(nsAString& aUriTitle) { + aUriTitle.Assign(mUriTitle); + + return NS_OK; +} + +NS_IMETHODIMP JumpListLink::GetUriHash(nsACString& aUriHash) { + if (!mURI) return NS_ERROR_NOT_AVAILABLE; + + return mozilla::widget::FaviconHelper::HashURI(mCryptoHash, mURI, aUriHash); +} + +NS_IMETHODIMP JumpListLink::CompareHash(nsIURI* aUri, bool* aResult) { + nsresult rv; + + if (!mURI) { + *aResult = !aUri; + return NS_OK; + } + + NS_ENSURE_ARG_POINTER(aUri); + + nsAutoCString hash1, hash2; + + rv = mozilla::widget::FaviconHelper::HashURI(mCryptoHash, mURI, hash1); + NS_ENSURE_SUCCESS(rv, rv); + rv = mozilla::widget::FaviconHelper::HashURI(mCryptoHash, aUri, hash2); + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = hash1.Equals(hash2); + + return NS_OK; +} + +NS_IMETHODIMP JumpListLink::Equals(nsIJumpListItem* aItem, bool* aResult) { + NS_ENSURE_ARG_POINTER(aItem); + + nsresult rv; + + *aResult = false; + + int16_t theType = nsIJumpListItem::JUMPLIST_ITEM_EMPTY; + if (NS_FAILED(aItem->GetType(&theType))) return NS_OK; + + // Make sure the types match. + if (Type() != theType) return NS_OK; + + nsCOMPtr link = do_QueryInterface(aItem, &rv); + if (NS_FAILED(rv)) return rv; + + // Check the titles + nsAutoString title; + link->GetUriTitle(title); + if (!mUriTitle.Equals(title)) return NS_OK; + + // Call the internal object's equals() method to check. + nsCOMPtr theUri; + bool equals = false; + if (NS_SUCCEEDED(link->GetUri(getter_AddRefs(theUri)))) { + if (!theUri) { + if (!mURI) *aResult = true; + return NS_OK; + } + if (NS_SUCCEEDED(theUri->Equals(mURI, &equals)) && equals) { + *aResult = true; + } + } + + return NS_OK; +} + +/* shortcut impl. */ + +NS_IMETHODIMP JumpListShortcut::GetApp(nsILocalHandlerApp** aApp) { + NS_IF_ADDREF(*aApp = mHandlerApp); + + return NS_OK; +} + +NS_IMETHODIMP JumpListShortcut::SetApp(nsILocalHandlerApp* aApp) { + mHandlerApp = aApp; + return NS_OK; +} + +NS_IMETHODIMP JumpListShortcut::GetIconIndex(int32_t* aIconIndex) { + NS_ENSURE_ARG_POINTER(aIconIndex); + + *aIconIndex = mIconIndex; + return NS_OK; +} + +NS_IMETHODIMP JumpListShortcut::SetIconIndex(int32_t aIconIndex) { + mIconIndex = aIconIndex; + return NS_OK; +} + +NS_IMETHODIMP JumpListShortcut::GetFaviconPageUri(nsIURI** aFaviconPageURI) { + NS_IF_ADDREF(*aFaviconPageURI = mFaviconPageURI); + + return NS_OK; +} + +NS_IMETHODIMP JumpListShortcut::SetFaviconPageUri(nsIURI* aFaviconPageURI) { + mFaviconPageURI = aFaviconPageURI; + return NS_OK; +} + +NS_IMETHODIMP JumpListShortcut::Equals(nsIJumpListItem* aItem, bool* aResult) { + NS_ENSURE_ARG_POINTER(aItem); + + nsresult rv; + + *aResult = false; + + int16_t theType = nsIJumpListItem::JUMPLIST_ITEM_EMPTY; + if (NS_FAILED(aItem->GetType(&theType))) return NS_OK; + + // Make sure the types match. + if (Type() != theType) return NS_OK; + + nsCOMPtr shortcut = do_QueryInterface(aItem, &rv); + if (NS_FAILED(rv)) return rv; + + // Check the icon index + // int32_t idx; + // shortcut->GetIconIndex(&idx); + // if (mIconIndex != idx) + // return NS_OK; + // No need to check the icon page URI either + + // Call the internal object's equals() method to check. + nsCOMPtr theApp; + bool equals = false; + if (NS_SUCCEEDED(shortcut->GetApp(getter_AddRefs(theApp)))) { + if (!theApp) { + if (!mHandlerApp) *aResult = true; + return NS_OK; + } + if (NS_SUCCEEDED(theApp->Equals(mHandlerApp, &equals)) && equals) { + *aResult = true; + } + } + + return NS_OK; +} + +/* internal helpers */ + +// (static) Creates a ShellLink that encapsulate a separator. +nsresult JumpListSeparator::GetSeparator(RefPtr& aShellLink) { + HRESULT hr; + IShellLinkW* psl; + + // Create a IShellLink. + hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLinkW, (LPVOID*)&psl); + if (FAILED(hr)) return NS_ERROR_UNEXPECTED; + + IPropertyStore* pPropStore = nullptr; + hr = psl->QueryInterface(IID_IPropertyStore, (LPVOID*)&pPropStore); + if (FAILED(hr)) return NS_ERROR_UNEXPECTED; + + PROPVARIANT pv; + InitPropVariantFromBoolean(TRUE, &pv); + + pPropStore->SetValue(PKEY_AppUserModel_IsDestListSeparator, pv); + pPropStore->Commit(); + pPropStore->Release(); + + PropVariantClear(&pv); + + aShellLink = dont_AddRef(psl); + + return NS_OK; +} + +// (static) Creates a ShellLink that encapsulate a shortcut to local apps. +nsresult JumpListShortcut::GetShellLink(nsCOMPtr& item, + RefPtr& aShellLink, + nsCOMPtr& aIOThread) { + HRESULT hr; + IShellLinkW* psl; + nsresult rv; + + // Shell links: + // http://msdn.microsoft.com/en-us/library/bb776891(VS.85).aspx + // http://msdn.microsoft.com/en-us/library/bb774950(VS.85).aspx + + int16_t type; + if (NS_FAILED(item->GetType(&type))) return NS_ERROR_INVALID_ARG; + + if (type != nsIJumpListItem::JUMPLIST_ITEM_SHORTCUT) + return NS_ERROR_INVALID_ARG; + + nsCOMPtr shortcut = do_QueryInterface(item, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr handlerApp; + rv = shortcut->GetApp(getter_AddRefs(handlerApp)); + NS_ENSURE_SUCCESS(rv, rv); + + // Create a IShellLink + hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLinkW, (LPVOID*)&psl); + if (FAILED(hr)) return NS_ERROR_UNEXPECTED; + + // Retrieve the app path, title, description and optional command line args. + nsAutoString appPath, appTitle, appDescription, appArgs; + int32_t appIconIndex = 0; + + // Path + nsCOMPtr executable; + handlerApp->GetExecutable(getter_AddRefs(executable)); + + rv = executable->GetPath(appPath); + NS_ENSURE_SUCCESS(rv, rv); + + // Command line parameters + uint32_t count = 0; + handlerApp->GetParameterCount(&count); + for (uint32_t idx = 0; idx < count; idx++) { + if (idx > 0) appArgs.Append(' '); + nsAutoString param; + rv = handlerApp->GetParameter(idx, param); + if (NS_FAILED(rv)) return rv; + appArgs.Append(param); + } + + handlerApp->GetName(appTitle); + handlerApp->GetDetailedDescription(appDescription); + + bool useUriIcon = false; // if we want to use the URI icon + bool usedUriIcon = false; // if we did use the URI icon + shortcut->GetIconIndex(&appIconIndex); + + nsCOMPtr iconUri; + rv = shortcut->GetFaviconPageUri(getter_AddRefs(iconUri)); + if (NS_SUCCEEDED(rv) && iconUri) { + useUriIcon = true; + } + + // Store the title of the app + if (appTitle.Length() > 0) { + IPropertyStore* pPropStore = nullptr; + hr = psl->QueryInterface(IID_IPropertyStore, (LPVOID*)&pPropStore); + if (FAILED(hr)) return NS_ERROR_UNEXPECTED; + + PROPVARIANT pv; + InitPropVariantFromString(appTitle.get(), &pv); + + pPropStore->SetValue(PKEY_Title, pv); + pPropStore->Commit(); + pPropStore->Release(); + + PropVariantClear(&pv); + } + + // Store the rest of the params + psl->SetPath(appPath.get()); + psl->SetDescription(appDescription.get()); + psl->SetArguments(appArgs.get()); + + if (useUriIcon) { + nsString icoFilePath; + rv = mozilla::widget::FaviconHelper::ObtainCachedIconFile( + iconUri, icoFilePath, aIOThread, false); + if (NS_SUCCEEDED(rv)) { + // Always use the first icon in the ICO file + // our encoded icon only has 1 resource + psl->SetIconLocation(icoFilePath.get(), 0); + usedUriIcon = true; + } + } + + // We didn't use an ICO via URI so fall back to the app icon + if (!usedUriIcon) { + psl->SetIconLocation(appPath.get(), appIconIndex); + } + + aShellLink = dont_AddRef(psl); + + return NS_OK; +} + +// If successful fills in the aSame parameter +// aSame will be true if the path is in our icon cache +static nsresult IsPathInOurIconCache(nsCOMPtr& aShortcut, + wchar_t* aPath, bool* aSame) { + NS_ENSURE_ARG_POINTER(aPath); + NS_ENSURE_ARG_POINTER(aSame); + + *aSame = false; + + // Construct the path of our jump list cache + nsCOMPtr jumpListCache; + nsresult rv = + NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(jumpListCache)); + NS_ENSURE_SUCCESS(rv, rv); + rv = jumpListCache->AppendNative( + nsDependentCString(FaviconHelper::kJumpListCacheDir)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString jumpListCachePath; + rv = jumpListCache->GetPath(jumpListCachePath); + NS_ENSURE_SUCCESS(rv, rv); + + // Construct the parent path of the passed in path + nsCOMPtr passedInFile = + do_CreateInstance("@mozilla.org/file/local;1"); + NS_ENSURE_TRUE(passedInFile, NS_ERROR_FAILURE); + nsAutoString passedInPath(aPath); + rv = passedInFile->InitWithPath(passedInPath); + nsCOMPtr passedInParentFile; + passedInFile->GetParent(getter_AddRefs(passedInParentFile)); + nsAutoString passedInParentPath; + rv = jumpListCache->GetPath(passedInParentPath); + NS_ENSURE_SUCCESS(rv, rv); + + *aSame = jumpListCachePath.Equals(passedInParentPath); + return NS_OK; +} + +// (static) For a given IShellLink, create and return a populated +// nsIJumpListShortcut. +nsresult JumpListShortcut::GetJumpListShortcut( + IShellLinkW* pLink, nsCOMPtr& aShortcut) { + NS_ENSURE_ARG_POINTER(pLink); + + nsresult rv; + HRESULT hres; + + nsCOMPtr handlerApp = + do_CreateInstance(NS_LOCALHANDLERAPP_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + wchar_t buf[MAX_PATH]; + + // Path + hres = pLink->GetPath(buf, MAX_PATH, nullptr, SLGP_UNCPRIORITY); + if (FAILED(hres)) return NS_ERROR_INVALID_ARG; + + nsCOMPtr file; + nsDependentString filepath(buf); + rv = NS_NewLocalFile(filepath, false, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = handlerApp->SetExecutable(file); + NS_ENSURE_SUCCESS(rv, rv); + + // Parameters + hres = pLink->GetArguments(buf, MAX_PATH); + if (SUCCEEDED(hres)) { + LPWSTR* arglist; + int32_t numArgs; + int32_t idx; + + arglist = ::CommandLineToArgvW(buf, &numArgs); + if (arglist) { + for (idx = 0; idx < numArgs; idx++) { + // szArglist[i] is null terminated + nsDependentString arg(arglist[idx]); + handlerApp->AppendParameter(arg); + } + ::LocalFree(arglist); + } + } + + rv = aShortcut->SetApp(handlerApp); + NS_ENSURE_SUCCESS(rv, rv); + + // Icon index or file location + int iconIdx = 0; + hres = pLink->GetIconLocation(buf, MAX_PATH, &iconIdx); + if (SUCCEEDED(hres)) { + // XXX How do we handle converting local files to images here? Do we need + // to? + aShortcut->SetIconIndex(iconIdx); + + // Obtain the local profile directory and construct the output icon file + // path We only set the Icon Uri if we're sure it was from our icon cache. + bool isInOurCache; + if (NS_SUCCEEDED(IsPathInOurIconCache(aShortcut, buf, &isInOurCache)) && + isInOurCache) { + nsCOMPtr iconUri; + nsAutoString path(buf); + rv = NS_NewURI(getter_AddRefs(iconUri), path); + if (NS_SUCCEEDED(rv)) { + aShortcut->SetFaviconPageUri(iconUri); + } + } + } + + // Do we need the title and description? Probably not since handler app + // doesn't compare these in equals. + + return NS_OK; +} + +// (static) ShellItems are used to encapsulate links to things. We currently +// only support URI links, but more support could be added, such as local file +// and directory links. +nsresult JumpListLink::GetShellItem(nsCOMPtr& item, + RefPtr& aShellItem) { + IShellItem2* psi = nullptr; + nsresult rv; + + int16_t type; + if (NS_FAILED(item->GetType(&type))) return NS_ERROR_INVALID_ARG; + + if (type != nsIJumpListItem::JUMPLIST_ITEM_LINK) return NS_ERROR_INVALID_ARG; + + nsCOMPtr link = do_QueryInterface(item, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr uri; + rv = link->GetUri(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString spec; + rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the IShellItem + if (FAILED(SHCreateItemFromParsingName(NS_ConvertASCIItoUTF16(spec).get(), + nullptr, IID_PPV_ARGS(&psi)))) { + return NS_ERROR_INVALID_ARG; + } + + // Set the title + nsAutoString linkTitle; + link->GetUriTitle(linkTitle); + + IPropertyStore* pPropStore = nullptr; + HRESULT hres = psi->GetPropertyStore(GPS_DEFAULT, IID_IPropertyStore, + (void**)&pPropStore); + if (FAILED(hres)) return NS_ERROR_UNEXPECTED; + + PROPVARIANT pv; + InitPropVariantFromString(linkTitle.get(), &pv); + + // May fail due to shell item access permissions. + pPropStore->SetValue(PKEY_ItemName, pv); + pPropStore->Commit(); + pPropStore->Release(); + + PropVariantClear(&pv); + + aShellItem = dont_AddRef(psi); + + return NS_OK; +} + +// (static) For a given IShellItem, create and return a populated +// nsIJumpListLink. +nsresult JumpListLink::GetJumpListLink(IShellItem* pItem, + nsCOMPtr& aLink) { + NS_ENSURE_ARG_POINTER(pItem); + + // We assume for now these are URI links, but through properties we could + // query and create other types. + nsresult rv; + LPWSTR lpstrName = nullptr; + + if (SUCCEEDED(pItem->GetDisplayName(SIGDN_URL, &lpstrName))) { + nsCOMPtr uri; + nsAutoString spec(lpstrName); + + rv = NS_NewURI(getter_AddRefs(uri), NS_ConvertUTF16toUTF8(spec)); + if (NS_FAILED(rv)) return NS_ERROR_INVALID_ARG; + + aLink->SetUri(uri); + + ::CoTaskMemFree(lpstrName); + } + + return NS_OK; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/JumpListItem.h b/widget/windows/JumpListItem.h new file mode 100644 index 0000000000..67d9d50d50 --- /dev/null +++ b/widget/windows/JumpListItem.h @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __JumpListItem_h__ +#define __JumpListItem_h__ + +#include +#include +#undef LogSeverity // SetupAPI.h #defines this as DWORD + +#include "mozilla/RefPtr.h" +#include "nsIJumpListItem.h" // defines nsIJumpListItem +#include "nsIMIMEInfo.h" // defines nsILocalHandlerApp +#include "nsTArray.h" +#include "nsCOMPtr.h" +#include "nsIURI.h" +#include "nsICryptoHash.h" +#include "nsString.h" +#include "nsCycleCollectionParticipant.h" + +class nsIThread; + +namespace mozilla { +namespace widget { + +class JumpListItemBase : public nsIJumpListItem { + public: + JumpListItemBase() : mItemType(nsIJumpListItem::JUMPLIST_ITEM_EMPTY) {} + + explicit JumpListItemBase(int32_t type) : mItemType(type) {} + + NS_DECL_NSIJUMPLISTITEM + + static const char kJumpListCacheDir[]; + + protected: + virtual ~JumpListItemBase() {} + + short Type() { return mItemType; } + short mItemType; +}; + +class JumpListItem : public JumpListItemBase { + ~JumpListItem() {} + + public: + using JumpListItemBase::JumpListItemBase; + + NS_DECL_ISUPPORTS +}; + +class JumpListSeparator : public JumpListItemBase, public nsIJumpListSeparator { + ~JumpListSeparator() {} + + public: + JumpListSeparator() + : JumpListItemBase(nsIJumpListItem::JUMPLIST_ITEM_SEPARATOR) {} + + NS_DECL_ISUPPORTS + NS_FORWARD_NSIJUMPLISTITEM(JumpListItemBase::) + + static nsresult GetSeparator(RefPtr& aShellLink); +}; + +class JumpListLink : public JumpListItemBase, public nsIJumpListLink { + ~JumpListLink() {} + + public: + JumpListLink() : JumpListItemBase(nsIJumpListItem::JUMPLIST_ITEM_LINK) {} + + NS_DECL_ISUPPORTS + NS_IMETHOD GetType(int16_t* aType) override { + return JumpListItemBase::GetType(aType); + } + NS_IMETHOD Equals(nsIJumpListItem* item, bool* _retval) override; + NS_DECL_NSIJUMPLISTLINK + + static nsresult GetShellItem(nsCOMPtr& item, + RefPtr& aShellItem); + static nsresult GetJumpListLink(IShellItem* pItem, + nsCOMPtr& aLink); + + protected: + nsString mUriTitle; + nsCOMPtr mURI; + nsCOMPtr mCryptoHash; +}; + +class JumpListShortcut : public JumpListItemBase, public nsIJumpListShortcut { + ~JumpListShortcut() {} + + public: + JumpListShortcut() + : JumpListItemBase(nsIJumpListItem::JUMPLIST_ITEM_SHORTCUT) {} + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(JumpListShortcut, JumpListItemBase) + NS_IMETHOD GetType(int16_t* aType) override { + return JumpListItemBase::GetType(aType); + } + NS_IMETHOD Equals(nsIJumpListItem* item, bool* _retval) override; + NS_DECL_NSIJUMPLISTSHORTCUT + + static nsresult GetShellLink(nsCOMPtr& item, + RefPtr& aShellLink, + nsCOMPtr& aIOThread); + static nsresult GetJumpListShortcut(IShellLinkW* pLink, + nsCOMPtr& aShortcut); + static nsresult GetOutputIconPath(nsCOMPtr aFaviconPageURI, + nsCOMPtr& aICOFile); + + protected: + int32_t mIconIndex; + nsCOMPtr mFaviconPageURI; + nsCOMPtr mHandlerApp; + + bool ExecutableExists(nsCOMPtr& handlerApp); + static nsresult ObtainCachedIconFile(nsCOMPtr aFaviconPageURI, + nsString& aICOFilePath, + nsCOMPtr& aIOThread); + static nsresult CacheIconFileFromFaviconURIAsync( + nsCOMPtr aFaviconPageURI, nsCOMPtr aICOFile, + nsCOMPtr& aIOThread); +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __JumpListItem_h__ */ diff --git a/widget/windows/KeyboardLayout.cpp b/widget/windows/KeyboardLayout.cpp new file mode 100644 index 0000000000..aaa9b6747b --- /dev/null +++ b/widget/windows/KeyboardLayout.cpp @@ -0,0 +1,5383 @@ +/* -*- 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 "mozilla/Logging.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEvents.h" + +#include "nsAlgorithm.h" +#include "nsExceptionHandler.h" +#include "nsGkAtoms.h" +#include "nsIUserIdleServiceInternal.h" +#include "nsIWindowsRegKey.h" +#include "nsPrintfCString.h" +#include "nsQuickSort.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsToolkit.h" +#include "nsUnicharUtils.h" +#include "nsWindowDbg.h" + +#include "KeyboardLayout.h" +#include "WidgetUtils.h" +#include "WinUtils.h" + +#include "npapi.h" + +#include +#include +#include + +#ifndef WINABLEAPI +# include +#endif + +// In WinUser.h, MAPVK_VK_TO_VSC_EX is defined only when WINVER >= 0x0600 +#ifndef MAPVK_VK_TO_VSC_EX +# define MAPVK_VK_TO_VSC_EX (4) +#endif + +// For collecting other people's log, tell them `MOZ_LOG=KeyboardHandler:4,sync` +// rather than `MOZ_LOG=KeyboardHandler:5,sync` since using `5` may create too +// big file. +// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior. +mozilla::LazyLogModule gKeyLog("KeyboardHandler"); + +namespace mozilla { +namespace widget { + +static const char* const kVirtualKeyName[] = { + "NULL", + "VK_LBUTTON", + "VK_RBUTTON", + "VK_CANCEL", + "VK_MBUTTON", + "VK_XBUTTON1", + "VK_XBUTTON2", + "0x07", + "VK_BACK", + "VK_TAB", + "0x0A", + "0x0B", + "VK_CLEAR", + "VK_RETURN", + "0x0E", + "0x0F", + + "VK_SHIFT", + "VK_CONTROL", + "VK_MENU", + "VK_PAUSE", + "VK_CAPITAL", + "VK_KANA, VK_HANGUL", + "0x16", + "VK_JUNJA", + "VK_FINAL", + "VK_HANJA, VK_KANJI", + "0x1A", + "VK_ESCAPE", + "VK_CONVERT", + "VK_NONCONVERT", + "VK_ACCEPT", + "VK_MODECHANGE", + + "VK_SPACE", + "VK_PRIOR", + "VK_NEXT", + "VK_END", + "VK_HOME", + "VK_LEFT", + "VK_UP", + "VK_RIGHT", + "VK_DOWN", + "VK_SELECT", + "VK_PRINT", + "VK_EXECUTE", + "VK_SNAPSHOT", + "VK_INSERT", + "VK_DELETE", + "VK_HELP", + + "VK_0", + "VK_1", + "VK_2", + "VK_3", + "VK_4", + "VK_5", + "VK_6", + "VK_7", + "VK_8", + "VK_9", + "0x3A", + "0x3B", + "0x3C", + "0x3D", + "0x3E", + "0x3F", + + "0x40", + "VK_A", + "VK_B", + "VK_C", + "VK_D", + "VK_E", + "VK_F", + "VK_G", + "VK_H", + "VK_I", + "VK_J", + "VK_K", + "VK_L", + "VK_M", + "VK_N", + "VK_O", + + "VK_P", + "VK_Q", + "VK_R", + "VK_S", + "VK_T", + "VK_U", + "VK_V", + "VK_W", + "VK_X", + "VK_Y", + "VK_Z", + "VK_LWIN", + "VK_RWIN", + "VK_APPS", + "0x5E", + "VK_SLEEP", + + "VK_NUMPAD0", + "VK_NUMPAD1", + "VK_NUMPAD2", + "VK_NUMPAD3", + "VK_NUMPAD4", + "VK_NUMPAD5", + "VK_NUMPAD6", + "VK_NUMPAD7", + "VK_NUMPAD8", + "VK_NUMPAD9", + "VK_MULTIPLY", + "VK_ADD", + "VK_SEPARATOR", + "VK_SUBTRACT", + "VK_DECIMAL", + "VK_DIVIDE", + + "VK_F1", + "VK_F2", + "VK_F3", + "VK_F4", + "VK_F5", + "VK_F6", + "VK_F7", + "VK_F8", + "VK_F9", + "VK_F10", + "VK_F11", + "VK_F12", + "VK_F13", + "VK_F14", + "VK_F15", + "VK_F16", + + "VK_F17", + "VK_F18", + "VK_F19", + "VK_F20", + "VK_F21", + "VK_F22", + "VK_F23", + "VK_F24", + "0x88", + "0x89", + "0x8A", + "0x8B", + "0x8C", + "0x8D", + "0x8E", + "0x8F", + + "VK_NUMLOCK", + "VK_SCROLL", + "VK_OEM_NEC_EQUAL, VK_OEM_FJ_JISHO", + "VK_OEM_FJ_MASSHOU", + "VK_OEM_FJ_TOUROKU", + "VK_OEM_FJ_LOYA", + "VK_OEM_FJ_ROYA", + "0x97", + "0x98", + "0x99", + "0x9A", + "0x9B", + "0x9C", + "0x9D", + "0x9E", + "0x9F", + + "VK_LSHIFT", + "VK_RSHIFT", + "VK_LCONTROL", + "VK_RCONTROL", + "VK_LMENU", + "VK_RMENU", + "VK_BROWSER_BACK", + "VK_BROWSER_FORWARD", + "VK_BROWSER_REFRESH", + "VK_BROWSER_STOP", + "VK_BROWSER_SEARCH", + "VK_BROWSER_FAVORITES", + "VK_BROWSER_HOME", + "VK_VOLUME_MUTE", + "VK_VOLUME_DOWN", + "VK_VOLUME_UP", + + "VK_MEDIA_NEXT_TRACK", + "VK_MEDIA_PREV_TRACK", + "VK_MEDIA_STOP", + "VK_MEDIA_PLAY_PAUSE", + "VK_LAUNCH_MAIL", + "VK_LAUNCH_MEDIA_SELECT", + "VK_LAUNCH_APP1", + "VK_LAUNCH_APP2", + "0xB8", + "0xB9", + "VK_OEM_1", + "VK_OEM_PLUS", + "VK_OEM_COMMA", + "VK_OEM_MINUS", + "VK_OEM_PERIOD", + "VK_OEM_2", + + "VK_OEM_3", + "VK_ABNT_C1", + "VK_ABNT_C2", + "0xC3", + "0xC4", + "0xC5", + "0xC6", + "0xC7", + "0xC8", + "0xC9", + "0xCA", + "0xCB", + "0xCC", + "0xCD", + "0xCE", + "0xCF", + + "0xD0", + "0xD1", + "0xD2", + "0xD3", + "0xD4", + "0xD5", + "0xD6", + "0xD7", + "0xD8", + "0xD9", + "0xDA", + "VK_OEM_4", + "VK_OEM_5", + "VK_OEM_6", + "VK_OEM_7", + "VK_OEM_8", + + "0xE0", + "VK_OEM_AX", + "VK_OEM_102", + "VK_ICO_HELP", + "VK_ICO_00", + "VK_PROCESSKEY", + "VK_ICO_CLEAR", + "VK_PACKET", + "0xE8", + "VK_OEM_RESET", + "VK_OEM_JUMP", + "VK_OEM_PA1", + "VK_OEM_PA2", + "VK_OEM_PA3", + "VK_OEM_WSCTRL", + "VK_OEM_CUSEL", + + "VK_OEM_ATTN", + "VK_OEM_FINISH", + "VK_OEM_COPY", + "VK_OEM_AUTO", + "VK_OEM_ENLW", + "VK_OEM_BACKTAB", + "VK_ATTN", + "VK_CRSEL", + "VK_EXSEL", + "VK_EREOF", + "VK_PLAY", + "VK_ZOOM", + "VK_NONAME", + "VK_PA1", + "VK_OEM_CLEAR", + "0xFF"}; + +static_assert(sizeof(kVirtualKeyName) / sizeof(const char*) == 0x100, + "The virtual key name must be defined just 256 keys"); + +static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; } + +static const nsCString GetCharacterCodeName(WPARAM aCharCode) { + switch (aCharCode) { + case 0x0000: + return "NULL (0x0000)"_ns; + case 0x0008: + return "BACKSPACE (0x0008)"_ns; + case 0x0009: + return "CHARACTER TABULATION (0x0009)"_ns; + case 0x000A: + return "LINE FEED (0x000A)"_ns; + case 0x000B: + return "LINE TABULATION (0x000B)"_ns; + case 0x000C: + return "FORM FEED (0x000C)"_ns; + case 0x000D: + return "CARRIAGE RETURN (0x000D)"_ns; + case 0x0018: + return "CANCEL (0x0018)"_ns; + case 0x001B: + return "ESCAPE (0x001B)"_ns; + case 0x0020: + return "SPACE (0x0020)"_ns; + case 0x007F: + return "DELETE (0x007F)"_ns; + case 0x00A0: + return "NO-BREAK SPACE (0x00A0)"_ns; + case 0x00AD: + return "SOFT HYPHEN (0x00AD)"_ns; + case 0x2000: + return "EN QUAD (0x2000)"_ns; + case 0x2001: + return "EM QUAD (0x2001)"_ns; + case 0x2002: + return "EN SPACE (0x2002)"_ns; + case 0x2003: + return "EM SPACE (0x2003)"_ns; + case 0x2004: + return "THREE-PER-EM SPACE (0x2004)"_ns; + case 0x2005: + return "FOUR-PER-EM SPACE (0x2005)"_ns; + case 0x2006: + return "SIX-PER-EM SPACE (0x2006)"_ns; + case 0x2007: + return "FIGURE SPACE (0x2007)"_ns; + case 0x2008: + return "PUNCTUATION SPACE (0x2008)"_ns; + case 0x2009: + return "THIN SPACE (0x2009)"_ns; + case 0x200A: + return "HAIR SPACE (0x200A)"_ns; + case 0x200B: + return "ZERO WIDTH SPACE (0x200B)"_ns; + case 0x200C: + return "ZERO WIDTH NON-JOINER (0x200C)"_ns; + case 0x200D: + return "ZERO WIDTH JOINER (0x200D)"_ns; + case 0x200E: + return "LEFT-TO-RIGHT MARK (0x200E)"_ns; + case 0x200F: + return "RIGHT-TO-LEFT MARK (0x200F)"_ns; + case 0x2029: + return "PARAGRAPH SEPARATOR (0x2029)"_ns; + case 0x202A: + return "LEFT-TO-RIGHT EMBEDDING (0x202A)"_ns; + case 0x202B: + return "RIGHT-TO-LEFT EMBEDDING (0x202B)"_ns; + case 0x202D: + return "LEFT-TO-RIGHT OVERRIDE (0x202D)"_ns; + case 0x202E: + return "RIGHT-TO-LEFT OVERRIDE (0x202E)"_ns; + case 0x202F: + return "NARROW NO-BREAK SPACE (0x202F)"_ns; + case 0x205F: + return "MEDIUM MATHEMATICAL SPACE (0x205F)"_ns; + case 0x2060: + return "WORD JOINER (0x2060)"_ns; + case 0x2066: + return "LEFT-TO-RIGHT ISOLATE (0x2066)"_ns; + case 0x2067: + return "RIGHT-TO-LEFT ISOLATE (0x2067)"_ns; + case 0x3000: + return "IDEOGRAPHIC SPACE (0x3000)"_ns; + case 0xFEFF: + return "ZERO WIDTH NO-BREAK SPACE (0xFEFF)"_ns; + default: { + if (aCharCode < ' ' || (aCharCode >= 0x80 && aCharCode < 0xA0)) { + return nsPrintfCString("control (0x%04zX)", aCharCode); + } + if (NS_IS_HIGH_SURROGATE(aCharCode)) { + return nsPrintfCString("high surrogate (0x%04zX)", aCharCode); + } + if (NS_IS_LOW_SURROGATE(aCharCode)) { + return nsPrintfCString("low surrogate (0x%04zX)", aCharCode); + } + return IS_IN_BMP(aCharCode) + ? nsPrintfCString( + "'%s' (0x%04zX)", + NS_ConvertUTF16toUTF8(nsAutoString(aCharCode)).get(), + aCharCode) + : nsPrintfCString( + "'%s' (0x%08zX)", + NS_ConvertUTF16toUTF8(nsAutoString(aCharCode)).get(), + aCharCode); + } + } +} + +static const nsCString GetKeyLocationName(uint32_t aLocation) { + switch (aLocation) { + case eKeyLocationLeft: + return "KEY_LOCATION_LEFT"_ns; + case eKeyLocationRight: + return "KEY_LOCATION_RIGHT"_ns; + case eKeyLocationStandard: + return "KEY_LOCATION_STANDARD"_ns; + case eKeyLocationNumpad: + return "KEY_LOCATION_NUMPAD"_ns; + default: + return nsPrintfCString("Unknown (0x%04X)", aLocation); + } +} + +static const nsCString GetCharacterCodeNames(const char16_t* aChars, + uint32_t aLength) { + if (!aLength) { + return ""_ns; + } + nsCString result; + result.AssignLiteral("\""); + StringJoinAppend(result, ", "_ns, Span{aChars, aLength}, + [](nsACString& dest, const char16_t charValue) { + dest.Append(GetCharacterCodeName(charValue)); + }); + result.AppendLiteral("\""); + return result; +} + +static const nsCString GetCharacterCodeNames( + const UniCharsAndModifiers& aUniCharsAndModifiers) { + if (aUniCharsAndModifiers.IsEmpty()) { + return ""_ns; + } + nsCString result; + result.AssignLiteral("\""); + StringJoinAppend(result, ", "_ns, Span{aUniCharsAndModifiers.ToString()}, + [](nsACString& dest, const char16_t charValue) { + dest.Append(GetCharacterCodeName(charValue)); + }); + result.AppendLiteral("\""); + return result; +} + +class MOZ_STACK_CLASS GetShiftStateName final : public nsAutoCString { + public: + explicit GetShiftStateName(VirtualKey::ShiftState aShiftState) { + if (!aShiftState) { + AssignLiteral("none"); + return; + } + if (aShiftState & VirtualKey::STATE_SHIFT) { + AssignLiteral("Shift"); + aShiftState &= ~VirtualKey::STATE_SHIFT; + } + if (aShiftState & VirtualKey::STATE_CONTROL) { + MaybeAppendSeparator(); + AssignLiteral("Ctrl"); + aShiftState &= ~VirtualKey::STATE_CONTROL; + } + if (aShiftState & VirtualKey::STATE_ALT) { + MaybeAppendSeparator(); + AssignLiteral("Alt"); + aShiftState &= ~VirtualKey::STATE_ALT; + } + if (aShiftState & VirtualKey::STATE_CAPSLOCK) { + MaybeAppendSeparator(); + AssignLiteral("CapsLock"); + aShiftState &= ~VirtualKey::STATE_CAPSLOCK; + } + MOZ_ASSERT(!aShiftState); + } + + private: + void MaybeAppendSeparator() { + if (!IsEmpty()) { + AppendLiteral(" | "); + } + } +}; + +static const nsCString GetMessageName(UINT aMessage) { + switch (aMessage) { + case WM_NULL: + return "WM_NULL"_ns; + case WM_KEYDOWN: + return "WM_KEYDOWN"_ns; + case WM_KEYUP: + return "WM_KEYUP"_ns; + case WM_SYSKEYDOWN: + return "WM_SYSKEYDOWN"_ns; + case WM_SYSKEYUP: + return "WM_SYSKEYUP"_ns; + case WM_CHAR: + return "WM_CHAR"_ns; + case WM_UNICHAR: + return "WM_UNICHAR"_ns; + case WM_SYSCHAR: + return "WM_SYSCHAR"_ns; + case WM_DEADCHAR: + return "WM_DEADCHAR"_ns; + case WM_SYSDEADCHAR: + return "WM_SYSDEADCHAR"_ns; + case WM_APPCOMMAND: + return "WM_APPCOMMAND"_ns; + case WM_QUIT: + return "WM_QUIT"_ns; + default: + return nsPrintfCString("Unknown Message (0x%04X)", aMessage); + } +} + +static const nsCString GetVirtualKeyCodeName(WPARAM aVK) { + if (aVK >= ArrayLength(kVirtualKeyName)) { + return nsPrintfCString("Invalid (0x%08zX)", aVK); + } + return nsCString(kVirtualKeyName[aVK]); +} + +static const nsCString GetAppCommandName(WPARAM aCommand) { + switch (aCommand) { + case APPCOMMAND_BASS_BOOST: + return "APPCOMMAND_BASS_BOOST"_ns; + case APPCOMMAND_BASS_DOWN: + return "APPCOMMAND_BASS_DOWN"_ns; + case APPCOMMAND_BASS_UP: + return "APPCOMMAND_BASS_UP"_ns; + case APPCOMMAND_BROWSER_BACKWARD: + return "APPCOMMAND_BROWSER_BACKWARD"_ns; + case APPCOMMAND_BROWSER_FAVORITES: + return "APPCOMMAND_BROWSER_FAVORITES"_ns; + case APPCOMMAND_BROWSER_FORWARD: + return "APPCOMMAND_BROWSER_FORWARD"_ns; + case APPCOMMAND_BROWSER_HOME: + return "APPCOMMAND_BROWSER_HOME"_ns; + case APPCOMMAND_BROWSER_REFRESH: + return "APPCOMMAND_BROWSER_REFRESH"_ns; + case APPCOMMAND_BROWSER_SEARCH: + return "APPCOMMAND_BROWSER_SEARCH"_ns; + case APPCOMMAND_BROWSER_STOP: + return "APPCOMMAND_BROWSER_STOP"_ns; + case APPCOMMAND_CLOSE: + return "APPCOMMAND_CLOSE"_ns; + case APPCOMMAND_COPY: + return "APPCOMMAND_COPY"_ns; + case APPCOMMAND_CORRECTION_LIST: + return "APPCOMMAND_CORRECTION_LIST"_ns; + case APPCOMMAND_CUT: + return "APPCOMMAND_CUT"_ns; + case APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE: + return "APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE"_ns; + case APPCOMMAND_FIND: + return "APPCOMMAND_FIND"_ns; + case APPCOMMAND_FORWARD_MAIL: + return "APPCOMMAND_FORWARD_MAIL"_ns; + case APPCOMMAND_HELP: + return "APPCOMMAND_HELP"_ns; + case APPCOMMAND_LAUNCH_APP1: + return "APPCOMMAND_LAUNCH_APP1"_ns; + case APPCOMMAND_LAUNCH_APP2: + return "APPCOMMAND_LAUNCH_APP2"_ns; + case APPCOMMAND_LAUNCH_MAIL: + return "APPCOMMAND_LAUNCH_MAIL"_ns; + case APPCOMMAND_LAUNCH_MEDIA_SELECT: + return "APPCOMMAND_LAUNCH_MEDIA_SELECT"_ns; + case APPCOMMAND_MEDIA_CHANNEL_DOWN: + return "APPCOMMAND_MEDIA_CHANNEL_DOWN"_ns; + case APPCOMMAND_MEDIA_CHANNEL_UP: + return "APPCOMMAND_MEDIA_CHANNEL_UP"_ns; + case APPCOMMAND_MEDIA_FAST_FORWARD: + return "APPCOMMAND_MEDIA_FAST_FORWARD"_ns; + case APPCOMMAND_MEDIA_NEXTTRACK: + return "APPCOMMAND_MEDIA_NEXTTRACK"_ns; + case APPCOMMAND_MEDIA_PAUSE: + return "APPCOMMAND_MEDIA_PAUSE"_ns; + case APPCOMMAND_MEDIA_PLAY: + return "APPCOMMAND_MEDIA_PLAY"_ns; + case APPCOMMAND_MEDIA_PLAY_PAUSE: + return "APPCOMMAND_MEDIA_PLAY_PAUSE"_ns; + case APPCOMMAND_MEDIA_PREVIOUSTRACK: + return "APPCOMMAND_MEDIA_PREVIOUSTRACK"_ns; + case APPCOMMAND_MEDIA_RECORD: + return "APPCOMMAND_MEDIA_RECORD"_ns; + case APPCOMMAND_MEDIA_REWIND: + return "APPCOMMAND_MEDIA_REWIND"_ns; + case APPCOMMAND_MEDIA_STOP: + return "APPCOMMAND_MEDIA_STOP"_ns; + case APPCOMMAND_MIC_ON_OFF_TOGGLE: + return "APPCOMMAND_MIC_ON_OFF_TOGGLE"_ns; + case APPCOMMAND_MICROPHONE_VOLUME_DOWN: + return "APPCOMMAND_MICROPHONE_VOLUME_DOWN"_ns; + case APPCOMMAND_MICROPHONE_VOLUME_MUTE: + return "APPCOMMAND_MICROPHONE_VOLUME_MUTE"_ns; + case APPCOMMAND_MICROPHONE_VOLUME_UP: + return "APPCOMMAND_MICROPHONE_VOLUME_UP"_ns; + case APPCOMMAND_NEW: + return "APPCOMMAND_NEW"_ns; + case APPCOMMAND_OPEN: + return "APPCOMMAND_OPEN"_ns; + case APPCOMMAND_PASTE: + return "APPCOMMAND_PASTE"_ns; + case APPCOMMAND_PRINT: + return "APPCOMMAND_PRINT"_ns; + case APPCOMMAND_REDO: + return "APPCOMMAND_REDO"_ns; + case APPCOMMAND_REPLY_TO_MAIL: + return "APPCOMMAND_REPLY_TO_MAIL"_ns; + case APPCOMMAND_SAVE: + return "APPCOMMAND_SAVE"_ns; + case APPCOMMAND_SEND_MAIL: + return "APPCOMMAND_SEND_MAIL"_ns; + case APPCOMMAND_SPELL_CHECK: + return "APPCOMMAND_SPELL_CHECK"_ns; + case APPCOMMAND_TREBLE_DOWN: + return "APPCOMMAND_TREBLE_DOWN"_ns; + case APPCOMMAND_TREBLE_UP: + return "APPCOMMAND_TREBLE_UP"_ns; + case APPCOMMAND_UNDO: + return "APPCOMMAND_UNDO"_ns; + case APPCOMMAND_VOLUME_DOWN: + return "APPCOMMAND_VOLUME_DOWN"_ns; + case APPCOMMAND_VOLUME_MUTE: + return "APPCOMMAND_VOLUME_MUTE"_ns; + case APPCOMMAND_VOLUME_UP: + return "APPCOMMAND_VOLUME_UP"_ns; + default: + return nsPrintfCString("Unknown app command (0x%08zX)", aCommand); + } +} + +static const nsCString GetAppCommandDeviceName(LPARAM aDevice) { + switch (aDevice) { + case FAPPCOMMAND_KEY: + return "FAPPCOMMAND_KEY"_ns; + case FAPPCOMMAND_MOUSE: + return "FAPPCOMMAND_MOUSE"_ns; + case FAPPCOMMAND_OEM: + return "FAPPCOMMAND_OEM"_ns; + default: + return nsPrintfCString("Unknown app command device (0x%04" PRIXLPTR ")", + aDevice); + } +}; + +class MOZ_STACK_CLASS GetAppCommandKeysName final : public nsAutoCString { + public: + explicit GetAppCommandKeysName(WPARAM aKeys) { + if (aKeys & MK_CONTROL) { + AppendLiteral("MK_CONTROL"); + aKeys &= ~MK_CONTROL; + } + if (aKeys & MK_LBUTTON) { + MaybeAppendSeparator(); + AppendLiteral("MK_LBUTTON"); + aKeys &= ~MK_LBUTTON; + } + if (aKeys & MK_MBUTTON) { + MaybeAppendSeparator(); + AppendLiteral("MK_MBUTTON"); + aKeys &= ~MK_MBUTTON; + } + if (aKeys & MK_RBUTTON) { + MaybeAppendSeparator(); + AppendLiteral("MK_RBUTTON"); + aKeys &= ~MK_RBUTTON; + } + if (aKeys & MK_SHIFT) { + MaybeAppendSeparator(); + AppendLiteral("MK_SHIFT"); + aKeys &= ~MK_SHIFT; + } + if (aKeys & MK_XBUTTON1) { + MaybeAppendSeparator(); + AppendLiteral("MK_XBUTTON1"); + aKeys &= ~MK_XBUTTON1; + } + if (aKeys & MK_XBUTTON2) { + MaybeAppendSeparator(); + AppendLiteral("MK_XBUTTON2"); + aKeys &= ~MK_XBUTTON2; + } + if (aKeys) { + MaybeAppendSeparator(); + AppendPrintf("Unknown Flags (0x%04zX)", aKeys); + } + if (IsEmpty()) { + AssignLiteral("none (0x0000)"); + } + } + + private: + void MaybeAppendSeparator() { + if (!IsEmpty()) { + AppendLiteral(" | "); + } + } +}; + +static const nsCString ToString(const MSG& aMSG) { + nsCString result; + result.AssignLiteral("{ message="); + result.Append(GetMessageName(aMSG.message).get()); + result.AppendLiteral(", "); + switch (aMSG.message) { + case WM_KEYDOWN: + case WM_KEYUP: + case WM_SYSKEYDOWN: + case WM_SYSKEYUP: + result.AppendPrintf( + "virtual keycode=%s, repeat count=%" PRIdLPTR + ", " + "scancode=0x%02X, extended key=%s, " + "context code=%s, previous key state=%s, " + "transition state=%s", + GetVirtualKeyCodeName(aMSG.wParam).get(), aMSG.lParam & 0xFFFF, + WinUtils::GetScanCode(aMSG.lParam), + GetBoolName(WinUtils::IsExtendedScanCode(aMSG.lParam)), + GetBoolName((aMSG.lParam & (1 << 29)) != 0), + GetBoolName((aMSG.lParam & (1 << 30)) != 0), + GetBoolName((aMSG.lParam & (1 << 31)) != 0)); + break; + case WM_CHAR: + case WM_DEADCHAR: + case WM_SYSCHAR: + case WM_SYSDEADCHAR: + result.AppendPrintf( + "character code=%s, repeat count=%" PRIdLPTR + ", " + "scancode=0x%02X, extended key=%s, " + "context code=%s, previous key state=%s, " + "transition state=%s", + GetCharacterCodeName(aMSG.wParam).get(), aMSG.lParam & 0xFFFF, + WinUtils::GetScanCode(aMSG.lParam), + GetBoolName(WinUtils::IsExtendedScanCode(aMSG.lParam)), + GetBoolName((aMSG.lParam & (1 << 29)) != 0), + GetBoolName((aMSG.lParam & (1 << 30)) != 0), + GetBoolName((aMSG.lParam & (1 << 31)) != 0)); + break; + case WM_APPCOMMAND: + result.AppendPrintf( + "window handle=0x%zx, app command=%s, device=%s, dwKeys=%s", + aMSG.wParam, + GetAppCommandName(GET_APPCOMMAND_LPARAM(aMSG.lParam)).get(), + GetAppCommandDeviceName(GET_DEVICE_LPARAM(aMSG.lParam)).get(), + GetAppCommandKeysName(GET_KEYSTATE_LPARAM(aMSG.lParam)).get()); + break; + default: + result.AppendPrintf("wParam=%zu, lParam=%" PRIdLPTR, aMSG.wParam, + aMSG.lParam); + break; + } + result.AppendPrintf(", hwnd=0x%p", aMSG.hwnd); + return result; +} + +static const nsCString ToString( + const UniCharsAndModifiers& aUniCharsAndModifiers) { + if (aUniCharsAndModifiers.IsEmpty()) { + return "{}"_ns; + } + nsCString result; + result.AssignLiteral("{ "); + result.Append(GetCharacterCodeName(aUniCharsAndModifiers.CharAt(0))); + for (size_t i = 1; i < aUniCharsAndModifiers.Length(); ++i) { + if (aUniCharsAndModifiers.ModifiersAt(i - 1) != + aUniCharsAndModifiers.ModifiersAt(i)) { + result.AppendLiteral(" ["); + result.Append(GetModifiersName(aUniCharsAndModifiers.ModifiersAt(0))); + result.AppendLiteral("]"); + } + result.AppendLiteral(", "); + result.Append(GetCharacterCodeName(aUniCharsAndModifiers.CharAt(i))); + } + result.AppendLiteral(" ["); + uint32_t lastIndex = aUniCharsAndModifiers.Length() - 1; + result.Append(GetModifiersName(aUniCharsAndModifiers.ModifiersAt(lastIndex))); + result.AppendLiteral("] }"); + return result; +} + +const nsCString ToString(const ModifierKeyState& aModifierKeyState) { + nsCString result; + result.AssignLiteral("{ "); + result.Append(GetModifiersName(aModifierKeyState.GetModifiers()).get()); + result.AppendLiteral(" }"); + return result; +} + +// Unique id counter associated with a keydown / keypress events. Used in +// identifing keypress events for removal from async event dispatch queue +// in metrofx after preventDefault is called on keydown events. +static uint32_t sUniqueKeyEventId = 0; + +/***************************************************************************** + * mozilla::widget::ModifierKeyState + *****************************************************************************/ + +ModifierKeyState::ModifierKeyState() { Update(); } + +ModifierKeyState::ModifierKeyState(Modifiers aModifiers) + : mModifiers(aModifiers) { + MOZ_ASSERT(!(mModifiers & MODIFIER_ALTGRAPH) || (!IsControl() && !IsAlt()), + "Neither MODIFIER_CONTROL nor MODIFIER_ALT should be set " + "if MODIFIER_ALTGRAPH is set"); +} + +void ModifierKeyState::Update() { + mModifiers = 0; + if (IS_VK_DOWN(VK_SHIFT)) { + mModifiers |= MODIFIER_SHIFT; + } + // If AltGr key (i.e., VK_RMENU on some keyboard layout) is pressed, only + // MODIFIER_ALTGRAPH should be set. Otherwise, i.e., if both Ctrl and Alt + // keys are pressed to emulate AltGr key, MODIFIER_CONTROL and MODIFIER_ALT + // keys should be set separately. + if (KeyboardLayout::GetInstance()->HasAltGr() && IS_VK_DOWN(VK_RMENU)) { + mModifiers |= MODIFIER_ALTGRAPH; + } else { + if (IS_VK_DOWN(VK_CONTROL)) { + mModifiers |= MODIFIER_CONTROL; + } + if (IS_VK_DOWN(VK_MENU)) { + mModifiers |= MODIFIER_ALT; + } + } + if (IS_VK_DOWN(VK_LWIN) || IS_VK_DOWN(VK_RWIN)) { + mModifiers |= MODIFIER_OS; + } + if (::GetKeyState(VK_CAPITAL) & 1) { + mModifiers |= MODIFIER_CAPSLOCK; + } + if (::GetKeyState(VK_NUMLOCK) & 1) { + mModifiers |= MODIFIER_NUMLOCK; + } + if (::GetKeyState(VK_SCROLL) & 1) { + mModifiers |= MODIFIER_SCROLLLOCK; + } +} + +void ModifierKeyState::Unset(Modifiers aRemovingModifiers) { + mModifiers &= ~aRemovingModifiers; +} + +void ModifierKeyState::Set(Modifiers aAddingModifiers) { + mModifiers |= aAddingModifiers; + MOZ_ASSERT(!(mModifiers & MODIFIER_ALTGRAPH) || (!IsControl() && !IsAlt()), + "Neither MODIFIER_CONTROL nor MODIFIER_ALT should be set " + "if MODIFIER_ALTGRAPH is set"); +} + +void ModifierKeyState::InitInputEvent(WidgetInputEvent& aInputEvent) const { + aInputEvent.mModifiers = mModifiers; + + switch (aInputEvent.mClass) { + case eMouseEventClass: + case eMouseScrollEventClass: + case eWheelEventClass: + case eDragEventClass: + case eSimpleGestureEventClass: + InitMouseEvent(aInputEvent); + break; + default: + break; + } +} + +void ModifierKeyState::InitMouseEvent(WidgetInputEvent& aMouseEvent) const { + NS_ASSERTION(aMouseEvent.mClass == eMouseEventClass || + aMouseEvent.mClass == eWheelEventClass || + aMouseEvent.mClass == eDragEventClass || + aMouseEvent.mClass == eSimpleGestureEventClass, + "called with non-mouse event"); + + WidgetMouseEventBase& mouseEvent = *aMouseEvent.AsMouseEventBase(); + mouseEvent.mButtons = 0; + if (::GetKeyState(VK_LBUTTON) < 0) { + mouseEvent.mButtons |= MouseButtonsFlag::ePrimaryFlag; + } + if (::GetKeyState(VK_RBUTTON) < 0) { + mouseEvent.mButtons |= MouseButtonsFlag::eSecondaryFlag; + } + if (::GetKeyState(VK_MBUTTON) < 0) { + mouseEvent.mButtons |= MouseButtonsFlag::eMiddleFlag; + } + if (::GetKeyState(VK_XBUTTON1) < 0) { + mouseEvent.mButtons |= MouseButtonsFlag::e4thFlag; + } + if (::GetKeyState(VK_XBUTTON2) < 0) { + mouseEvent.mButtons |= MouseButtonsFlag::e5thFlag; + } +} + +bool ModifierKeyState::IsShift() const { + return (mModifiers & MODIFIER_SHIFT) != 0; +} + +bool ModifierKeyState::IsControl() const { + return (mModifiers & MODIFIER_CONTROL) != 0; +} + +bool ModifierKeyState::IsAlt() const { + return (mModifiers & MODIFIER_ALT) != 0; +} + +bool ModifierKeyState::IsWin() const { return (mModifiers & MODIFIER_OS) != 0; } + +bool ModifierKeyState::MaybeMatchShortcutKey() const { + // If Windows key is pressed, even if both Ctrl key and Alt key are pressed, + // it's possible to match a shortcut key. + if (IsWin()) { + return true; + } + // Otherwise, when both Ctrl key and Alt key are pressed, it shouldn't be + // a shortcut key for Windows since it means pressing AltGr key on + // some keyboard layouts. + if (IsControl() ^ IsAlt()) { + return true; + } + // If no modifier key is active except a lockable modifier nor Shift key, + // the key shouldn't match any shortcut keys (there are Space and + // Shift+Space, though, let's ignore these special case...). + return false; +} + +bool ModifierKeyState::IsCapsLocked() const { + return (mModifiers & MODIFIER_CAPSLOCK) != 0; +} + +bool ModifierKeyState::IsNumLocked() const { + return (mModifiers & MODIFIER_NUMLOCK) != 0; +} + +bool ModifierKeyState::IsScrollLocked() const { + return (mModifiers & MODIFIER_SCROLLLOCK) != 0; +} + +/***************************************************************************** + * mozilla::widget::UniCharsAndModifiers + *****************************************************************************/ + +void UniCharsAndModifiers::Append(char16_t aUniChar, Modifiers aModifiers) { + mChars.Append(aUniChar); + mModifiers.AppendElement(aModifiers); +} + +void UniCharsAndModifiers::FillModifiers(Modifiers aModifiers) { + for (size_t i = 0; i < Length(); i++) { + mModifiers[i] = aModifiers; + } +} + +void UniCharsAndModifiers::OverwriteModifiersIfBeginsWith( + const UniCharsAndModifiers& aOther) { + if (!BeginsWith(aOther)) { + return; + } + for (size_t i = 0; i < aOther.Length(); ++i) { + mModifiers[i] = aOther.mModifiers[i]; + } +} + +bool UniCharsAndModifiers::UniCharsEqual( + const UniCharsAndModifiers& aOther) const { + return mChars.Equals(aOther.mChars); +} + +bool UniCharsAndModifiers::UniCharsCaseInsensitiveEqual( + const UniCharsAndModifiers& aOther) const { + return mChars.Equals(aOther.mChars, nsCaseInsensitiveStringComparator); +} + +bool UniCharsAndModifiers::BeginsWith( + const UniCharsAndModifiers& aOther) const { + return StringBeginsWith(mChars, aOther.mChars); +} + +UniCharsAndModifiers& UniCharsAndModifiers::operator+=( + const UniCharsAndModifiers& aOther) { + mChars.Append(aOther.mChars); + mModifiers.AppendElements(aOther.mModifiers); + return *this; +} + +UniCharsAndModifiers UniCharsAndModifiers::operator+( + const UniCharsAndModifiers& aOther) const { + UniCharsAndModifiers result(*this); + result += aOther; + return result; +} + +/***************************************************************************** + * mozilla::widget::VirtualKey + *****************************************************************************/ + +// static +VirtualKey::ShiftState VirtualKey::ModifiersToShiftState(Modifiers aModifiers) { + ShiftState state = 0; + if (aModifiers & MODIFIER_SHIFT) { + state |= STATE_SHIFT; + } + if (aModifiers & MODIFIER_ALTGRAPH) { + state |= STATE_ALTGRAPH; + } else { + if (aModifiers & MODIFIER_CONTROL) { + state |= STATE_CONTROL; + } + if (aModifiers & MODIFIER_ALT) { + state |= STATE_ALT; + } + } + if (aModifiers & MODIFIER_CAPSLOCK) { + state |= STATE_CAPSLOCK; + } + return state; +} + +// static +Modifiers VirtualKey::ShiftStateToModifiers(ShiftState aShiftState) { + Modifiers modifiers = 0; + if (aShiftState & STATE_SHIFT) { + modifiers |= MODIFIER_SHIFT; + } + if (aShiftState & STATE_ALTGRAPH) { + modifiers |= MODIFIER_ALTGRAPH; + } else { + if (aShiftState & STATE_CONTROL) { + modifiers |= MODIFIER_CONTROL; + } + if (aShiftState & STATE_ALT) { + modifiers |= MODIFIER_ALT; + } + } + if (aShiftState & STATE_CAPSLOCK) { + modifiers |= MODIFIER_CAPSLOCK; + } + return modifiers; +} + +const DeadKeyTable* VirtualKey::MatchingDeadKeyTable( + const DeadKeyEntry* aDeadKeyArray, uint32_t aEntries) const { + if (!mIsDeadKey) { + return nullptr; + } + + for (ShiftState shiftState = 0; shiftState < 16; shiftState++) { + if (!IsDeadKey(shiftState)) { + continue; + } + const DeadKeyTable* dkt = mShiftStates[shiftState].DeadKey.Table; + if (dkt && dkt->IsEqual(aDeadKeyArray, aEntries)) { + return dkt; + } + } + + return nullptr; +} + +void VirtualKey::SetNormalChars(ShiftState aShiftState, const char16_t* aChars, + uint32_t aNumOfChars) { + MOZ_ASSERT(aShiftState == ToShiftStateIndex(aShiftState)); + + SetDeadKey(aShiftState, false); + + for (uint32_t index = 0; index < aNumOfChars; index++) { + // Ignore legacy non-printable control characters + mShiftStates[aShiftState].Normal.Chars[index] = + (aChars[index] >= 0x20) ? aChars[index] : 0; + } + + uint32_t len = ArrayLength(mShiftStates[aShiftState].Normal.Chars); + for (uint32_t index = aNumOfChars; index < len; index++) { + mShiftStates[aShiftState].Normal.Chars[index] = 0; + } +} + +void VirtualKey::SetDeadChar(ShiftState aShiftState, char16_t aDeadChar) { + MOZ_ASSERT(aShiftState == ToShiftStateIndex(aShiftState)); + + SetDeadKey(aShiftState, true); + + mShiftStates[aShiftState].DeadKey.DeadChar = aDeadChar; + mShiftStates[aShiftState].DeadKey.Table = nullptr; +} + +UniCharsAndModifiers VirtualKey::GetUniChars(ShiftState aShiftState) const { + UniCharsAndModifiers result = GetNativeUniChars(aShiftState); + + const uint8_t kShiftStateIndex = ToShiftStateIndex(aShiftState); + if (!(kShiftStateIndex & STATE_CONTROL_ALT)) { + // If neither Alt nor Ctrl key is pressed, just return stored data + // for the key. + return result; + } + + if (result.IsEmpty()) { + // If Alt and/or Control are pressed and the key produces no + // character, return characters which is produced by the key without + // Alt and Control, and return given modifiers as is. + result = GetNativeUniChars(kShiftStateIndex & ~STATE_CONTROL_ALT); + result.FillModifiers(ShiftStateToModifiers(aShiftState)); + return result; + } + + if (IsAltGrIndex(kShiftStateIndex)) { + // If AltGr or both Ctrl and Alt are pressed and the key produces + // character(s), we need to clear MODIFIER_ALT and MODIFIER_CONTROL + // since TextEditor won't handle eKeyPress event whose mModifiers + // has MODIFIER_ALT or MODIFIER_CONTROL. Additionally, we need to + // use MODIFIER_ALTGRAPH when a key produces character(s) with + // AltGr or both Ctrl and Alt on Windows. See following spec issue: + // + Modifiers finalModifiers = ShiftStateToModifiers(aShiftState); + finalModifiers &= ~(MODIFIER_ALT | MODIFIER_CONTROL); + finalModifiers |= MODIFIER_ALTGRAPH; + result.FillModifiers(finalModifiers); + return result; + } + + // Otherwise, i.e., Alt or Ctrl is pressed and it produces character(s), + // check if different character(s) is produced by the key without Alt/Ctrl. + // If it produces different character, we need to consume the Alt and + // Ctrl modifier for TextEditor. Otherwise, the key does not produces the + // character actually. So, keep setting Alt and Ctrl modifiers. + UniCharsAndModifiers unmodifiedReslt = + GetNativeUniChars(kShiftStateIndex & ~STATE_CONTROL_ALT); + if (!result.UniCharsEqual(unmodifiedReslt)) { + Modifiers finalModifiers = ShiftStateToModifiers(aShiftState); + finalModifiers &= ~(MODIFIER_ALT | MODIFIER_CONTROL); + result.FillModifiers(finalModifiers); + } + return result; +} + +UniCharsAndModifiers VirtualKey::GetNativeUniChars( + ShiftState aShiftState) const { + const uint8_t kShiftStateIndex = ToShiftStateIndex(aShiftState); + UniCharsAndModifiers result; + Modifiers modifiers = ShiftStateToModifiers(aShiftState); + if (IsDeadKey(aShiftState)) { + result.Append(mShiftStates[kShiftStateIndex].DeadKey.DeadChar, modifiers); + return result; + } + + uint32_t len = ArrayLength(mShiftStates[kShiftStateIndex].Normal.Chars); + for (uint32_t i = 0; + i < len && mShiftStates[kShiftStateIndex].Normal.Chars[i]; i++) { + result.Append(mShiftStates[kShiftStateIndex].Normal.Chars[i], modifiers); + } + return result; +} + +// static +void VirtualKey::FillKbdState(PBYTE aKbdState, const ShiftState aShiftState) { + if (aShiftState & STATE_SHIFT) { + aKbdState[VK_SHIFT] |= 0x80; + } else { + aKbdState[VK_SHIFT] &= ~0x80; + aKbdState[VK_LSHIFT] &= ~0x80; + aKbdState[VK_RSHIFT] &= ~0x80; + } + + if (aShiftState & STATE_ALTGRAPH) { + aKbdState[VK_CONTROL] |= 0x80; + aKbdState[VK_LCONTROL] |= 0x80; + aKbdState[VK_RCONTROL] &= ~0x80; + aKbdState[VK_MENU] |= 0x80; + aKbdState[VK_LMENU] &= ~0x80; + aKbdState[VK_RMENU] |= 0x80; + } else { + if (aShiftState & STATE_CONTROL) { + aKbdState[VK_CONTROL] |= 0x80; + } else { + aKbdState[VK_CONTROL] &= ~0x80; + aKbdState[VK_LCONTROL] &= ~0x80; + aKbdState[VK_RCONTROL] &= ~0x80; + } + + if (aShiftState & STATE_ALT) { + aKbdState[VK_MENU] |= 0x80; + } else { + aKbdState[VK_MENU] &= ~0x80; + aKbdState[VK_LMENU] &= ~0x80; + aKbdState[VK_RMENU] &= ~0x80; + } + } + + if (aShiftState & STATE_CAPSLOCK) { + aKbdState[VK_CAPITAL] |= 0x01; + } else { + aKbdState[VK_CAPITAL] &= ~0x01; + } +} + +/***************************************************************************** + * mozilla::widget::NativeKey + *****************************************************************************/ + +uint8_t NativeKey::sDispatchedKeyOfAppCommand = 0; +NativeKey* NativeKey::sLatestInstance = nullptr; +const MSG NativeKey::sEmptyMSG = {}; +MSG NativeKey::sLastKeyOrCharMSG = {}; +MSG NativeKey::sLastKeyMSG = {}; + +NativeKey::NativeKey(nsWindow* aWidget, const MSG& aMessage, + const ModifierKeyState& aModKeyState, + HKL aOverrideKeyboardLayout, + nsTArray* aFakeCharMsgs) + : mLastInstance(sLatestInstance), + mRemovingMsg(sEmptyMSG), + mReceivedMsg(sEmptyMSG), + mWidget(aWidget), + mDispatcher(aWidget->GetTextEventDispatcher()), + mMsg(aMessage), + mFocusedWndBeforeDispatch(::GetFocus()), + mDOMKeyCode(0), + mKeyNameIndex(KEY_NAME_INDEX_Unidentified), + mCodeNameIndex(CODE_NAME_INDEX_UNKNOWN), + mModKeyState(aModKeyState), + mVirtualKeyCode(0), + mOriginalVirtualKeyCode(0), + mShiftedLatinChar(0), + mUnshiftedLatinChar(0), + mScanCode(0), + mIsExtended(false), + mIsRepeat(false), + mIsDeadKey(false), + mIsPrintableKey(false), + mIsSkippableInRemoteProcess(false), + mCharMessageHasGone(false), + mCanIgnoreModifierStateAtKeyPress(true), + mFakeCharMsgs(aFakeCharMsgs && aFakeCharMsgs->Length() ? aFakeCharMsgs + : nullptr) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::NativeKey(aWidget=0x%p { GetWindowHandle()=0x%p }, " + "aMessage=%s, aModKeyState=%s), sLatestInstance=0x%p", + this, aWidget, aWidget->GetWindowHandle(), ToString(aMessage).get(), + ToString(aModKeyState).get(), sLatestInstance)); + + MOZ_ASSERT(aWidget); + MOZ_ASSERT(mDispatcher); + sLatestInstance = this; + KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance(); + mKeyboardLayout = keyboardLayout->GetLayout(); + if (aOverrideKeyboardLayout && mKeyboardLayout != aOverrideKeyboardLayout) { + keyboardLayout->OverrideLayout(aOverrideKeyboardLayout); + mKeyboardLayout = keyboardLayout->GetLayout(); + MOZ_ASSERT(mKeyboardLayout == aOverrideKeyboardLayout); + mIsOverridingKeyboardLayout = true; + } else { + mIsOverridingKeyboardLayout = false; + sLastKeyOrCharMSG = aMessage; + } + + if (mMsg.message == WM_APPCOMMAND) { + InitWithAppCommand(); + } else { + InitWithKeyOrChar(); + } + + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::NativeKey(), mKeyboardLayout=0x%p, " + "mFocusedWndBeforeDispatch=0x%p, mDOMKeyCode=%s, " + "mKeyNameIndex=%s, mCodeNameIndex=%s, mModKeyState=%s, " + "mVirtualKeyCode=%s, mOriginalVirtualKeyCode=%s, " + "mCommittedCharsAndModifiers=%s, mInputtingStringAndModifiers=%s, " + "mShiftedString=%s, mUnshiftedString=%s, mShiftedLatinChar=%s, " + "mUnshiftedLatinChar=%s, mScanCode=0x%04X, mIsExtended=%s, " + "mIsRepeat=%s, mIsDeadKey=%s, mIsPrintableKey=%s, " + "mIsSkippableInRemoteProcess=%s, mCharMessageHasGone=%s, " + "mIsOverridingKeyboardLayout=%s", + this, mKeyboardLayout, mFocusedWndBeforeDispatch, + GetDOMKeyCodeName(mDOMKeyCode).get(), ToString(mKeyNameIndex).get(), + ToString(mCodeNameIndex).get(), ToString(mModKeyState).get(), + GetVirtualKeyCodeName(mVirtualKeyCode).get(), + GetVirtualKeyCodeName(mOriginalVirtualKeyCode).get(), + ToString(mCommittedCharsAndModifiers).get(), + ToString(mInputtingStringAndModifiers).get(), + ToString(mShiftedString).get(), ToString(mUnshiftedString).get(), + GetCharacterCodeName(mShiftedLatinChar).get(), + GetCharacterCodeName(mUnshiftedLatinChar).get(), mScanCode, + GetBoolName(mIsExtended), GetBoolName(mIsRepeat), + GetBoolName(mIsDeadKey), GetBoolName(mIsPrintableKey), + GetBoolName(mIsSkippableInRemoteProcess), + GetBoolName(mCharMessageHasGone), + GetBoolName(mIsOverridingKeyboardLayout))); +} + +void NativeKey::InitIsSkippableForKeyOrChar(const MSG& aLastKeyMSG) { + mIsSkippableInRemoteProcess = false; + + if (!mIsRepeat) { + // If the message is not repeated key message, the event should be always + // handled in remote process even if it's too old. + return; + } + + // Keyboard utilities may send us some generated messages and such messages + // may be marked as "repeated", e.g., SendInput() calls with + // KEYEVENTF_UNICODE but without KEYEVENTF_KEYUP. However, key sequence + // comes from such utilities may be really important. For example, utilities + // may send WM_KEYDOWN for VK_BACK to remove previous character and send + // WM_KEYDOWN for VK_PACKET to insert a composite character. Therefore, we + // should check if current message and previous key message are caused by + // same physical key. If not, the message may be generated by such + // utility. + // XXX With this approach, if VK_BACK messages are generated with known + // scancode, we cannot distinguish whether coming VK_BACK message is + // actually repeated by the auto-repeat feature. Currently, we need + // this hack only for "SinhalaTamil IME" and fortunately, it generates + // VK_BACK messages with odd scancode. So, we don't need to handle + // VK_BACK specially at least for now. + + if (mCodeNameIndex == CODE_NAME_INDEX_UNKNOWN) { + // If current event is not caused by physical key operation, it may be + // caused by a keyboard utility. If so, the event shouldn't be ignored by + // BrowserChild since it want to insert the character, delete a character or + // move caret. + return; + } + + if (mOriginalVirtualKeyCode == VK_PACKET) { + // If the message is VK_PACKET, that means that a keyboard utility + // tries to insert a character. + return; + } + + switch (mMsg.message) { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + case WM_CHAR: + case WM_SYSCHAR: + case WM_DEADCHAR: + case WM_SYSDEADCHAR: + // However, some keyboard layouts may send some keyboard messages with + // activating the bit. If we dispatch repeated keyboard events, they + // may be ignored by BrowserChild due to performance reason. So, we need + // to check if actually a physical key is repeated by the auto-repeat + // feature. + switch (aLastKeyMSG.message) { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (aLastKeyMSG.wParam == VK_PACKET) { + // If the last message was VK_PACKET, that means that a keyboard + // utility tried to insert a character. So, current message is + // not repeated key event of the previous event. + return; + } + // Let's check whether current message and previous message are + // caused by same physical key. + mIsSkippableInRemoteProcess = + mScanCode == WinUtils::GetScanCode(aLastKeyMSG.lParam) && + mIsExtended == WinUtils::IsExtendedScanCode(aLastKeyMSG.lParam); + return; + default: + // If previous message is not a keydown, this must not be generated + // by the auto-repeat feature. + return; + } + return; + case WM_APPCOMMAND: + MOZ_ASSERT_UNREACHABLE( + "WM_APPCOMMAND should be handled in " + "InitWithAppCommand()"); + return; + default: + // keyup message shouldn't be repeated by the auto-repeat feature. + return; + } +} + +void NativeKey::InitWithKeyOrChar() { + MSG lastKeyMSG = sLastKeyMSG; + mScanCode = WinUtils::GetScanCode(mMsg.lParam); + mIsExtended = WinUtils::IsExtendedScanCode(mMsg.lParam); + switch (mMsg.message) { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + case WM_KEYUP: + case WM_SYSKEYUP: { + // Modify sLastKeyMSG now since retrieving following char messages may + // cause sending another key message if odd tool hooks GetMessage(), + // PeekMessage(). + sLastKeyMSG = mMsg; + + // Note that we don't need to compute raw virtual keycode here even when + // it's VK_PROCESS (i.e., already handled by IME) because we need to + // export it as DOM_VK_PROCESS and KEY_NAME_INDEX_Process. + mOriginalVirtualKeyCode = static_cast(mMsg.wParam); + + // If the key message is sent from other application like a11y tools, the + // scancode value might not be set proper value. Then, probably the value + // is 0. + // NOTE: If the virtual keycode can be caused by both non-extended key + // and extended key, the API returns the non-extended key's + // scancode. E.g., VK_LEFT causes "4" key on numpad. + if (!mScanCode && mOriginalVirtualKeyCode != VK_PACKET) { + uint16_t scanCodeEx = ComputeScanCodeExFromVirtualKeyCode(mMsg.wParam); + if (scanCodeEx) { + mScanCode = static_cast(scanCodeEx & 0xFF); + uint8_t extended = static_cast((scanCodeEx & 0xFF00) >> 8); + mIsExtended = (extended == 0xE0) || (extended == 0xE1); + } + } + + // Most keys are not distinguished as left or right keys. + bool isLeftRightDistinguishedKey = false; + + // mOriginalVirtualKeyCode must not distinguish left or right of + // Shift, Control or Alt. + switch (mOriginalVirtualKeyCode) { + case VK_SHIFT: + case VK_CONTROL: + case VK_MENU: + isLeftRightDistinguishedKey = true; + break; + case VK_LSHIFT: + case VK_RSHIFT: + mVirtualKeyCode = mOriginalVirtualKeyCode; + mOriginalVirtualKeyCode = VK_SHIFT; + isLeftRightDistinguishedKey = true; + break; + case VK_LCONTROL: + case VK_RCONTROL: + mVirtualKeyCode = mOriginalVirtualKeyCode; + mOriginalVirtualKeyCode = VK_CONTROL; + isLeftRightDistinguishedKey = true; + break; + case VK_LMENU: + case VK_RMENU: + mVirtualKeyCode = mOriginalVirtualKeyCode; + mOriginalVirtualKeyCode = VK_MENU; + isLeftRightDistinguishedKey = true; + break; + } + + // If virtual keycode (left-right distinguished keycode) is already + // computed, we don't need to do anymore. + if (mVirtualKeyCode) { + break; + } + + // If the keycode doesn't have LR distinguished keycode, we just set + // mOriginalVirtualKeyCode to mVirtualKeyCode. Note that don't compute + // it from MapVirtualKeyEx() because the scan code might be wrong if + // the message is sent/posted by other application. Then, we will compute + // unexpected keycode from the scan code. + if (!isLeftRightDistinguishedKey) { + break; + } + + NS_ASSERTION(!mVirtualKeyCode, + "mVirtualKeyCode has been computed already"); + + // Otherwise, compute the virtual keycode with MapVirtualKeyEx(). + mVirtualKeyCode = ComputeVirtualKeyCodeFromScanCodeEx(); + + // Following code shouldn't be used now because we compute scancode value + // if we detect that the sender doesn't set proper scancode. + // However, the detection might fail. Therefore, let's keep using this. + switch (mOriginalVirtualKeyCode) { + case VK_CONTROL: + if (mVirtualKeyCode != VK_LCONTROL && + mVirtualKeyCode != VK_RCONTROL) { + mVirtualKeyCode = mIsExtended ? VK_RCONTROL : VK_LCONTROL; + } + break; + case VK_MENU: + if (mVirtualKeyCode != VK_LMENU && mVirtualKeyCode != VK_RMENU) { + mVirtualKeyCode = mIsExtended ? VK_RMENU : VK_LMENU; + } + break; + case VK_SHIFT: + if (mVirtualKeyCode != VK_LSHIFT && mVirtualKeyCode != VK_RSHIFT) { + // Neither left shift nor right shift is an extended key, + // let's use VK_LSHIFT for unknown mapping. + mVirtualKeyCode = VK_LSHIFT; + } + break; + default: + MOZ_CRASH("Unsupported mOriginalVirtualKeyCode"); + } + break; + } + case WM_CHAR: + case WM_UNICHAR: + case WM_SYSCHAR: + // If there is another instance and it is trying to remove a char message + // from the queue, this message should be handled in the old instance. + if (IsAnotherInstanceRemovingCharMessage()) { + // XXX Do we need to make mReceivedMsg an array? + MOZ_ASSERT(IsEmptyMSG(mLastInstance->mReceivedMsg)); + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::InitWithKeyOrChar(), WARNING, detecting another " + "instance is trying to remove a char message, so, this instance " + "should do nothing, mLastInstance=0x%p, mRemovingMsg=%s, " + "mReceivedMsg=%s", + this, mLastInstance, ToString(mLastInstance->mRemovingMsg).get(), + ToString(mLastInstance->mReceivedMsg).get())); + mLastInstance->mReceivedMsg = mMsg; + return; + } + + // NOTE: If other applications like a11y tools sends WM_*CHAR without + // scancode, we cannot compute virtual keycode. I.e., with such + // applications, we cannot generate proper KeyboardEvent.code value. + + mVirtualKeyCode = mOriginalVirtualKeyCode = + ComputeVirtualKeyCodeFromScanCodeEx(); + NS_ASSERTION(mVirtualKeyCode, "Failed to compute virtual keycode"); + break; + default: { + MOZ_CRASH_UNSAFE_PRINTF("Unsupported message: 0x%04X", mMsg.message); + break; + } + } + + if (!mVirtualKeyCode) { + mVirtualKeyCode = mOriginalVirtualKeyCode; + } + + KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance(); + mDOMKeyCode = + keyboardLayout->ConvertNativeKeyCodeToDOMKeyCode(mVirtualKeyCode); + // Be aware, keyboard utilities can change non-printable keys to printable + // keys. In such case, we should make the key value as a printable key. + // FYI: IsFollowedByPrintableCharMessage() returns true only when it's + // handling a keydown message. + mKeyNameIndex = + IsFollowedByPrintableCharMessage() + ? KEY_NAME_INDEX_USE_STRING + : keyboardLayout->ConvertNativeKeyCodeToKeyNameIndex(mVirtualKeyCode); + mCodeNameIndex = KeyboardLayout::ConvertScanCodeToCodeNameIndex( + GetScanCodeWithExtendedFlag()); + + // If next message of WM_(SYS)KEYDOWN is WM_*CHAR message and the key + // combination is not reserved by the system, let's consume it now. + // TODO: We cannot initialize mCommittedCharsAndModifiers for VK_PACKET + // if the message is WM_KEYUP because we don't have preceding + // WM_CHAR message. + // TODO: Like Edge, we shouldn't dispatch two sets of keyboard events + // for a Unicode character in non-BMP because its key value looks + // broken and not good thing for our editor if only one keydown or + // keypress event's default is prevented. I guess, we should store + // key message information globally and we should wait following + // WM_KEYDOWN if following WM_CHAR is a part of a Unicode character. + if ((mMsg.message == WM_KEYDOWN || mMsg.message == WM_SYSKEYDOWN) && + !IsReservedBySystem()) { + MSG charMsg; + while (GetFollowingCharMessage(charMsg)) { + // Although, got message shouldn't be WM_NULL in desktop apps, + // we should keep checking this. FYI: This was added for Metrofox. + if (charMsg.message == WM_NULL) { + continue; + } + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::InitWithKeyOrChar(), removed char message, %s", + this, ToString(charMsg).get())); + Unused << NS_WARN_IF(charMsg.hwnd != mMsg.hwnd); + mFollowingCharMsgs.AppendElement(charMsg); + } + } + + keyboardLayout->InitNativeKey(*this); + + // Now, we can know if the key produces character(s) or a dead key with + // AltGraph modifier. When user emulates AltGr key press with pressing + // both Ctrl and Alt and the key produces character(s) or a dead key, we + // need to replace Control and Alt state with AltGraph if the keyboard + // layout has AltGr key. + // Note that if Ctrl and/or Alt are pressed (not to emulate to press AltGr), + // we need to set actual modifiers to eKeyDown and eKeyUp. + if (MaybeEmulatingAltGraph() && + (mCommittedCharsAndModifiers.IsProducingCharsWithAltGr() || + mKeyNameIndex == KEY_NAME_INDEX_Dead)) { + mModKeyState.Unset(MODIFIER_CONTROL | MODIFIER_ALT); + mModKeyState.Set(MODIFIER_ALTGRAPH); + } + + mIsDeadKey = + (IsFollowedByDeadCharMessage() || + keyboardLayout->IsDeadKey(mOriginalVirtualKeyCode, mModKeyState)); + mIsPrintableKey = mKeyNameIndex == KEY_NAME_INDEX_USE_STRING || + KeyboardLayout::IsPrintableCharKey(mOriginalVirtualKeyCode); + // The repeat count in mMsg.lParam isn't useful to check whether the event + // is caused by the auto-repeat feature because it's not incremented even + // if it's repeated twice or more (i.e., always 1). Therefore, we need to + // check previous key state (31th bit) instead. If it's 1, the key was down + // before the message was sent. + mIsRepeat = (mMsg.lParam & (1 << 30)) != 0; + InitIsSkippableForKeyOrChar(lastKeyMSG); + + if (IsKeyDownMessage()) { + // Compute some strings which may be inputted by the key with various + // modifier state if this key event won't cause text input actually. + // They will be used for setting mAlternativeCharCodes in the callback + // method which will be called by TextEventDispatcher. + if (!IsFollowedByPrintableCharMessage()) { + ComputeInputtingStringWithKeyboardLayout(); + } + // Remove odd char messages if there are. + RemoveFollowingOddCharMessages(); + } +} + +void NativeKey::InitCommittedCharsAndModifiersWithFollowingCharMessages() { + mCommittedCharsAndModifiers.Clear(); + // This should cause inputting text in focused editor. However, it + // ignores keypress events whose altKey or ctrlKey is true. + // Therefore, we need to remove these modifier state here. + Modifiers modifiers = mModKeyState.GetModifiers(); + if (IsFollowedByPrintableCharMessage()) { + modifiers &= ~(MODIFIER_ALT | MODIFIER_CONTROL); + if (MaybeEmulatingAltGraph()) { + modifiers |= MODIFIER_ALTGRAPH; + } + } + // NOTE: This method assumes that WM_CHAR and WM_SYSCHAR are never retrieved + // at same time. + for (size_t i = 0; i < mFollowingCharMsgs.Length(); ++i) { + // Ignore non-printable char messages. + if (!IsPrintableCharOrSysCharMessage(mFollowingCharMsgs[i])) { + continue; + } + char16_t ch = static_cast(mFollowingCharMsgs[i].wParam); + mCommittedCharsAndModifiers.Append(ch, modifiers); + } +} + +NativeKey::~NativeKey() { + MOZ_LOG(gKeyLog, LogLevel::Debug, + ("%p NativeKey::~NativeKey(), destroyed", this)); + if (mIsOverridingKeyboardLayout) { + KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance(); + keyboardLayout->RestoreLayout(); + } + sLatestInstance = mLastInstance; +} + +void NativeKey::InitWithAppCommand() { + if (GET_DEVICE_LPARAM(mMsg.lParam) != FAPPCOMMAND_KEY) { + return; + } + + uint32_t appCommand = GET_APPCOMMAND_LPARAM(mMsg.lParam); + switch (GET_APPCOMMAND_LPARAM(mMsg.lParam)) { +#undef NS_APPCOMMAND_TO_DOM_KEY_NAME_INDEX +#define NS_APPCOMMAND_TO_DOM_KEY_NAME_INDEX(aAppCommand, aKeyNameIndex) \ + case aAppCommand: \ + mKeyNameIndex = aKeyNameIndex; \ + break; + +#include "NativeKeyToDOMKeyName.h" + +#undef NS_APPCOMMAND_TO_DOM_KEY_NAME_INDEX + + default: + mKeyNameIndex = KEY_NAME_INDEX_Unidentified; + } + + // Guess the virtual keycode which caused this message. + switch (appCommand) { + case APPCOMMAND_BROWSER_BACKWARD: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_BACK; + break; + case APPCOMMAND_BROWSER_FORWARD: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_FORWARD; + break; + case APPCOMMAND_BROWSER_REFRESH: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_REFRESH; + break; + case APPCOMMAND_BROWSER_STOP: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_STOP; + break; + case APPCOMMAND_BROWSER_SEARCH: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_SEARCH; + break; + case APPCOMMAND_BROWSER_FAVORITES: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_FAVORITES; + break; + case APPCOMMAND_BROWSER_HOME: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_BROWSER_HOME; + break; + case APPCOMMAND_VOLUME_MUTE: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_VOLUME_MUTE; + break; + case APPCOMMAND_VOLUME_DOWN: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_VOLUME_DOWN; + break; + case APPCOMMAND_VOLUME_UP: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_VOLUME_UP; + break; + case APPCOMMAND_MEDIA_NEXTTRACK: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_MEDIA_NEXT_TRACK; + break; + case APPCOMMAND_MEDIA_PREVIOUSTRACK: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_MEDIA_PREV_TRACK; + break; + case APPCOMMAND_MEDIA_STOP: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_MEDIA_STOP; + break; + case APPCOMMAND_MEDIA_PLAY_PAUSE: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_MEDIA_PLAY_PAUSE; + break; + case APPCOMMAND_LAUNCH_MAIL: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_LAUNCH_MAIL; + break; + case APPCOMMAND_LAUNCH_MEDIA_SELECT: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_LAUNCH_MEDIA_SELECT; + break; + case APPCOMMAND_LAUNCH_APP1: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_LAUNCH_APP1; + break; + case APPCOMMAND_LAUNCH_APP2: + mVirtualKeyCode = mOriginalVirtualKeyCode = VK_LAUNCH_APP2; + break; + default: + return; + } + + uint16_t scanCodeEx = ComputeScanCodeExFromVirtualKeyCode(mVirtualKeyCode); + mScanCode = static_cast(scanCodeEx & 0xFF); + uint8_t extended = static_cast((scanCodeEx & 0xFF00) >> 8); + mIsExtended = (extended == 0xE0) || (extended == 0xE1); + mDOMKeyCode = KeyboardLayout::GetInstance()->ConvertNativeKeyCodeToDOMKeyCode( + mOriginalVirtualKeyCode); + mCodeNameIndex = KeyboardLayout::ConvertScanCodeToCodeNameIndex( + GetScanCodeWithExtendedFlag()); + // If we can map the WM_APPCOMMAND to a virtual keycode, we can trust + // the result of GetKeyboardState(). Otherwise, we dispatch both + // keydown and keyup events from WM_APPCOMMAND handler. Therefore, + // even if WM_APPCOMMAND is caused by auto key repeat, web apps receive + // a pair of DOM keydown and keyup events. I.e., KeyboardEvent.repeat + // should be never true of such keys. + // XXX Isn't the key state always true? If the key press caused this + // WM_APPCOMMAND, that means it's pressed at that time. + if (mVirtualKeyCode) { + BYTE kbdState[256]; + memset(kbdState, 0, sizeof(kbdState)); + ::GetKeyboardState(kbdState); + mIsSkippableInRemoteProcess = mIsRepeat = !!kbdState[mVirtualKeyCode]; + } +} + +bool NativeKey::MaybeEmulatingAltGraph() const { + return IsControl() && IsAlt() && KeyboardLayout::GetInstance()->HasAltGr(); +} + +// static +bool NativeKey::IsControlChar(char16_t aChar) { + static const char16_t U_SPACE = 0x20; + static const char16_t U_DELETE = 0x7F; + return aChar < U_SPACE || aChar == U_DELETE; +} + +bool NativeKey::IsFollowedByDeadCharMessage() const { + if (mFollowingCharMsgs.IsEmpty()) { + return false; + } + return IsDeadCharMessage(mFollowingCharMsgs[0]); +} + +bool NativeKey::IsFollowedByPrintableCharMessage() const { + for (size_t i = 0; i < mFollowingCharMsgs.Length(); ++i) { + if (IsPrintableCharMessage(mFollowingCharMsgs[i])) { + return true; + } + } + return false; +} + +bool NativeKey::IsFollowedByPrintableCharOrSysCharMessage() const { + for (size_t i = 0; i < mFollowingCharMsgs.Length(); ++i) { + if (IsPrintableCharOrSysCharMessage(mFollowingCharMsgs[i])) { + return true; + } + } + return false; +} + +bool NativeKey::IsReservedBySystem() const { + // Alt+Space key is handled by OS, we shouldn't touch it. + if (mModKeyState.IsAlt() && !mModKeyState.IsControl() && + mVirtualKeyCode == VK_SPACE) { + return true; + } + + // XXX How about Alt+F4? We receive WM_SYSKEYDOWN for F4 before closing the + // window. Although, we don't prevent to close the window but the key + // event shouldn't be exposed to the web. + + return false; +} + +bool NativeKey::IsIMEDoingKakuteiUndo() const { + // Following message pattern is caused by "Kakutei-Undo" of ATOK or WXG: + // --------------------------------------------------------------------------- + // WM_KEYDOWN * n (wParam = VK_BACK, lParam = 0x1) + // WM_KEYUP * 1 (wParam = VK_BACK, lParam = 0xC0000001) # ATOK + // WM_IME_STARTCOMPOSITION * 1 (wParam = 0x0, lParam = 0x0) + // WM_IME_COMPOSITION * 1 (wParam = 0x0, lParam = 0x1BF) + // WM_CHAR * n (wParam = VK_BACK, lParam = 0x1) + // WM_KEYUP * 1 (wParam = VK_BACK, lParam = 0xC00E0001) + // --------------------------------------------------------------------------- + // This doesn't match usual key message pattern such as: + // WM_KEYDOWN -> WM_CHAR -> WM_KEYDOWN -> WM_CHAR -> ... -> WM_KEYUP + // See following bugs for the detail. + // https://bugzilla.mozilla.gr.jp/show_bug.cgi?id=2885 (written in Japanese) + // https://bugzilla.mozilla.org/show_bug.cgi?id=194559 (written in English) + MSG startCompositionMsg, compositionMsg, charMsg; + return WinUtils::PeekMessage(&startCompositionMsg, mMsg.hwnd, + WM_IME_STARTCOMPOSITION, WM_IME_STARTCOMPOSITION, + PM_NOREMOVE | PM_NOYIELD) && + WinUtils::PeekMessage(&compositionMsg, mMsg.hwnd, WM_IME_COMPOSITION, + WM_IME_COMPOSITION, PM_NOREMOVE | PM_NOYIELD) && + WinUtils::PeekMessage(&charMsg, mMsg.hwnd, WM_CHAR, WM_CHAR, + PM_NOREMOVE | PM_NOYIELD) && + startCompositionMsg.wParam == 0x0 && + startCompositionMsg.lParam == 0x0 && compositionMsg.wParam == 0x0 && + compositionMsg.lParam == 0x1BF && charMsg.wParam == VK_BACK && + charMsg.lParam == 0x1 && + startCompositionMsg.time <= compositionMsg.time && + compositionMsg.time <= charMsg.time; +} + +void NativeKey::RemoveFollowingOddCharMessages() { + MOZ_ASSERT(IsKeyDownMessage()); + + // If the keydown message is synthesized for automated tests, there is + // nothing to do here. + if (mFakeCharMsgs) { + return; + } + + // If there are some following char messages before another key message, + // there is nothing to do here. + if (!mFollowingCharMsgs.IsEmpty()) { + return; + } + + // If the handling key isn't Backspace, there is nothing to do here. + if (mOriginalVirtualKeyCode != VK_BACK) { + return; + } + + // If we don't see the odd message pattern, there is nothing to do here. + if (!IsIMEDoingKakuteiUndo()) { + return; + } + + // Otherwise, we need to remove odd WM_CHAR messages for ATOK or WXG (both + // of them are Japanese IME). + MSG msg; + while (WinUtils::PeekMessage(&msg, mMsg.hwnd, WM_CHAR, WM_CHAR, + PM_REMOVE | PM_NOYIELD)) { + if (msg.message != WM_CHAR) { + MOZ_RELEASE_ASSERT(msg.message == WM_NULL, + "Unexpected message was removed"); + continue; + } + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::RemoveFollowingOddCharMessages(), removed odd char " + "message, %s", + this, ToString(msg).get())); + mRemovedOddCharMsgs.AppendElement(msg); + } +} + +UINT NativeKey::GetScanCodeWithExtendedFlag() const { + if (!mIsExtended) { + return mScanCode; + } + return (0xE000 | mScanCode); +} + +uint32_t NativeKey::GetKeyLocation() const { + switch (mVirtualKeyCode) { + case VK_LSHIFT: + case VK_LCONTROL: + case VK_LMENU: + case VK_LWIN: + return eKeyLocationLeft; + + case VK_RSHIFT: + case VK_RCONTROL: + case VK_RMENU: + case VK_RWIN: + return eKeyLocationRight; + + case VK_RETURN: + // XXX This code assumes that all keyboard drivers use same mapping. + return !mIsExtended ? eKeyLocationStandard : eKeyLocationNumpad; + + case VK_INSERT: + case VK_DELETE: + case VK_END: + case VK_DOWN: + case VK_NEXT: + case VK_LEFT: + case VK_CLEAR: + case VK_RIGHT: + case VK_HOME: + case VK_UP: + case VK_PRIOR: + // XXX This code assumes that all keyboard drivers use same mapping. + return mIsExtended ? eKeyLocationStandard : eKeyLocationNumpad; + + // NumLock key isn't included due to IE9's behavior. + case VK_NUMPAD0: + case VK_NUMPAD1: + case VK_NUMPAD2: + case VK_NUMPAD3: + case VK_NUMPAD4: + case VK_NUMPAD5: + case VK_NUMPAD6: + case VK_NUMPAD7: + case VK_NUMPAD8: + case VK_NUMPAD9: + case VK_DECIMAL: + case VK_DIVIDE: + case VK_MULTIPLY: + case VK_SUBTRACT: + case VK_ADD: + // Separator key of Brazilian keyboard or JIS keyboard for Mac + case VK_ABNT_C2: + return eKeyLocationNumpad; + + case VK_SHIFT: + case VK_CONTROL: + case VK_MENU: + NS_WARNING("Failed to decide the key location?"); + [[fallthrough]]; + + default: + return eKeyLocationStandard; + } +} + +uint8_t NativeKey::ComputeVirtualKeyCodeFromScanCode() const { + return static_cast( + ::MapVirtualKeyEx(mScanCode, MAPVK_VSC_TO_VK, mKeyboardLayout)); +} + +uint8_t NativeKey::ComputeVirtualKeyCodeFromScanCodeEx() const { + // MapVirtualKeyEx() has been improved for supporting extended keys since + // Vista. When we call it for mapping a scancode of an extended key and + // a virtual keycode, we need to add 0xE000 to the scancode. + return static_cast(::MapVirtualKeyEx( + GetScanCodeWithExtendedFlag(), MAPVK_VSC_TO_VK_EX, mKeyboardLayout)); +} + +uint16_t NativeKey::ComputeScanCodeExFromVirtualKeyCode( + UINT aVirtualKeyCode) const { + return static_cast( + ::MapVirtualKeyEx(aVirtualKeyCode, MAPVK_VK_TO_VSC_EX, mKeyboardLayout)); +} + +char16_t NativeKey::ComputeUnicharFromScanCode() const { + return static_cast(::MapVirtualKeyEx( + ComputeVirtualKeyCodeFromScanCode(), MAPVK_VK_TO_CHAR, mKeyboardLayout)); +} + +nsEventStatus NativeKey::InitKeyEvent(WidgetKeyboardEvent& aKeyEvent) const { + return InitKeyEvent(aKeyEvent, mModKeyState); +} + +nsEventStatus NativeKey::InitKeyEvent( + WidgetKeyboardEvent& aKeyEvent, + const ModifierKeyState& aModKeyState) const { + if (mWidget->Destroyed()) { + MOZ_CRASH("NativeKey tries to dispatch a key event on destroyed widget"); + } + + LayoutDeviceIntPoint point(0, 0); + mWidget->InitEvent(aKeyEvent, &point); + + switch (aKeyEvent.mMessage) { + case eKeyDown: + // If it was followed by a char message but it was consumed by somebody, + // we should mark it as consumed because somebody must have handled it + // and we should prevent to do "double action" for the key operation. + // However, for compatibility with older version and other browsers, + // we should dispatch the events even in the web content. + if (mCharMessageHasGone) { + aKeyEvent.PreventDefaultBeforeDispatch(CrossProcessForwarding::eAllow); + } + aKeyEvent.mKeyCode = mDOMKeyCode; + // Unique id for this keydown event and its associated keypress. + sUniqueKeyEventId++; + aKeyEvent.mUniqueId = sUniqueKeyEventId; + break; + case eKeyUp: + aKeyEvent.mKeyCode = mDOMKeyCode; + // Set defaultPrevented of the key event if the VK_MENU is not a system + // key release, so that the menu bar does not trigger. This helps avoid + // triggering the menu bar for ALT key accelerators used in assistive + // technologies such as Window-Eyes and ZoomText or for switching open + // state of IME. On the other hand, we should dispatch the events even + // in the web content for compatibility with older version and other + // browsers. + if (mOriginalVirtualKeyCode == VK_MENU && mMsg.message != WM_SYSKEYUP) { + aKeyEvent.PreventDefaultBeforeDispatch(CrossProcessForwarding::eAllow); + } + break; + case eKeyPress: + MOZ_ASSERT(!mCharMessageHasGone, + "If following char message was consumed by somebody, " + "keydown event should be consumed above"); + aKeyEvent.mUniqueId = sUniqueKeyEventId; + break; + default: + MOZ_CRASH("Invalid event message"); + } + + aKeyEvent.mIsRepeat = mIsRepeat; + aKeyEvent.mMaybeSkippableInRemoteProcess = mIsSkippableInRemoteProcess; + aKeyEvent.mKeyNameIndex = mKeyNameIndex; + if (mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) { + aKeyEvent.mKeyValue = mCommittedCharsAndModifiers.ToString(); + } + aKeyEvent.mCodeNameIndex = mCodeNameIndex; + MOZ_ASSERT(mCodeNameIndex != CODE_NAME_INDEX_USE_STRING); + aKeyEvent.mLocation = GetKeyLocation(); + aModKeyState.InitInputEvent(aKeyEvent); + + KeyboardLayout::NotifyIdleServiceOfUserActivity(); + + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::InitKeyEvent(), initialized, aKeyEvent={ " + "mMessage=%s, mKeyNameIndex=%s, mKeyValue=\"%s\", mCodeNameIndex=%s, " + "mKeyCode=%s, mLocation=%s, mModifiers=%s, DefaultPrevented()=%s }", + this, ToChar(aKeyEvent.mMessage), + ToString(aKeyEvent.mKeyNameIndex).get(), + NS_ConvertUTF16toUTF8(aKeyEvent.mKeyValue).get(), + ToString(aKeyEvent.mCodeNameIndex).get(), + GetDOMKeyCodeName(aKeyEvent.mKeyCode).get(), + GetKeyLocationName(aKeyEvent.mLocation).get(), + GetModifiersName(aKeyEvent.mModifiers).get(), + GetBoolName(aKeyEvent.DefaultPrevented()))); + + return aKeyEvent.DefaultPrevented() ? nsEventStatus_eConsumeNoDefault + : nsEventStatus_eIgnore; +} + +bool NativeKey::DispatchCommandEvent(uint32_t aEventCommand) const { + RefPtr command; + switch (aEventCommand) { + case APPCOMMAND_BROWSER_BACKWARD: + command = nsGkAtoms::Back; + break; + case APPCOMMAND_BROWSER_FORWARD: + command = nsGkAtoms::Forward; + break; + case APPCOMMAND_BROWSER_REFRESH: + command = nsGkAtoms::Reload; + break; + case APPCOMMAND_BROWSER_STOP: + command = nsGkAtoms::Stop; + break; + case APPCOMMAND_BROWSER_SEARCH: + command = nsGkAtoms::Search; + break; + case APPCOMMAND_BROWSER_FAVORITES: + command = nsGkAtoms::Bookmarks; + break; + case APPCOMMAND_BROWSER_HOME: + command = nsGkAtoms::Home; + break; + case APPCOMMAND_CLOSE: + command = nsGkAtoms::Close; + break; + case APPCOMMAND_FIND: + command = nsGkAtoms::Find; + break; + case APPCOMMAND_HELP: + command = nsGkAtoms::Help; + break; + case APPCOMMAND_NEW: + command = nsGkAtoms::New; + break; + case APPCOMMAND_OPEN: + command = nsGkAtoms::Open; + break; + case APPCOMMAND_PRINT: + command = nsGkAtoms::Print; + break; + case APPCOMMAND_SAVE: + command = nsGkAtoms::Save; + break; + case APPCOMMAND_FORWARD_MAIL: + command = nsGkAtoms::ForwardMail; + break; + case APPCOMMAND_REPLY_TO_MAIL: + command = nsGkAtoms::ReplyToMail; + break; + case APPCOMMAND_SEND_MAIL: + command = nsGkAtoms::SendMail; + break; + case APPCOMMAND_MEDIA_NEXTTRACK: + command = nsGkAtoms::NextTrack; + break; + case APPCOMMAND_MEDIA_PREVIOUSTRACK: + command = nsGkAtoms::PreviousTrack; + break; + case APPCOMMAND_MEDIA_STOP: + command = nsGkAtoms::MediaStop; + break; + case APPCOMMAND_MEDIA_PLAY_PAUSE: + command = nsGkAtoms::PlayPause; + break; + default: + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::DispatchCommandEvent(), doesn't dispatch command " + "event", + this)); + return false; + } + WidgetCommandEvent appCommandEvent(true, command, mWidget); + + mWidget->InitEvent(appCommandEvent); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::DispatchCommandEvent(), dispatching " + "%s app command event...", + this, nsAtomCString(command).get())); + bool ok = + mWidget->DispatchWindowEvent(appCommandEvent) || mWidget->Destroyed(); + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::DispatchCommandEvent(), dispatched app command event, " + "result=%s, mWidget->Destroyed()=%s", + this, GetBoolName(ok), GetBoolName(mWidget->Destroyed()))); + return ok; +} + +bool NativeKey::HandleAppCommandMessage() const { + // If the widget has gone, we should do nothing. + if (mWidget->Destroyed()) { + MOZ_LOG(gKeyLog, LogLevel::Warning, + ("%p NativeKey::HandleAppCommandMessage(), WARNING, not handled " + "due to " + "destroyed the widget", + this)); + return false; + } + + // NOTE: Typical behavior of WM_APPCOMMAND caused by key is, WM_APPCOMMAND + // message is _sent_ first. Then, the DefaultWndProc will _post_ + // WM_KEYDOWN message and WM_KEYUP message if the keycode for the + // command is available (i.e., mVirtualKeyCode is not 0). + + // NOTE: IntelliType (Microsoft's keyboard utility software) always consumes + // WM_KEYDOWN and WM_KEYUP. + + // Let's dispatch keydown message before our chrome handles the command + // when the message is caused by a keypress. This behavior makes handling + // WM_APPCOMMAND be a default action of the keydown event. This means that + // web applications can handle multimedia keys and prevent our default action. + // This allow web applications to provide better UX for multimedia keyboard + // users. + bool dispatchKeyEvent = (GET_DEVICE_LPARAM(mMsg.lParam) == FAPPCOMMAND_KEY); + if (dispatchKeyEvent) { + // If a plug-in window has focus but it didn't consume the message, our + // window receive WM_APPCOMMAND message. In this case, we shouldn't + // dispatch KeyboardEvents because an event handler may access the + // plug-in process synchronously. + dispatchKeyEvent = + WinUtils::IsOurProcessWindow(reinterpret_cast(mMsg.wParam)); + } + + bool consumed = false; + + if (dispatchKeyEvent) { + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gKeyLog, LogLevel::Error, + ("%p NativeKey::HandleAppCommandMessage(), FAILED due to " + "BeginNativeInputTransaction() failure", + this)); + return true; + } + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), initializing keydown " + "event...", + this)); + WidgetKeyboardEvent keydownEvent(true, eKeyDown, mWidget); + nsEventStatus status = InitKeyEvent(keydownEvent, mModKeyState); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), tries to dispatch " + "keydown event...", + this)); + // NOTE: If the keydown event is consumed by web contents, we shouldn't + // continue to handle the command. + if (!mDispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, status, + const_cast(this))) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), keydown event isn't " + "dispatched", + this)); + // If keyboard event wasn't fired, there must be composition. + // So, we don't need to dispatch a command event. + return true; + } + consumed = status == nsEventStatus_eConsumeNoDefault; + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), keydown event was " + "dispatched, consumed=%s", + this, GetBoolName(consumed))); + sDispatchedKeyOfAppCommand = mVirtualKeyCode; + if (mWidget->Destroyed()) { + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), keydown event caused " + "destroying the widget", + this)); + return true; + } + } + + // Dispatch a command event or a content command event if the command is + // supported. + if (!consumed) { + uint32_t appCommand = GET_APPCOMMAND_LPARAM(mMsg.lParam); + EventMessage contentCommandMessage = eVoidEvent; + switch (appCommand) { + case APPCOMMAND_BROWSER_BACKWARD: + case APPCOMMAND_BROWSER_FORWARD: + case APPCOMMAND_BROWSER_REFRESH: + case APPCOMMAND_BROWSER_STOP: + case APPCOMMAND_BROWSER_SEARCH: + case APPCOMMAND_BROWSER_FAVORITES: + case APPCOMMAND_BROWSER_HOME: + case APPCOMMAND_CLOSE: + case APPCOMMAND_FIND: + case APPCOMMAND_HELP: + case APPCOMMAND_NEW: + case APPCOMMAND_OPEN: + case APPCOMMAND_PRINT: + case APPCOMMAND_SAVE: + case APPCOMMAND_FORWARD_MAIL: + case APPCOMMAND_REPLY_TO_MAIL: + case APPCOMMAND_SEND_MAIL: + case APPCOMMAND_MEDIA_NEXTTRACK: + case APPCOMMAND_MEDIA_PREVIOUSTRACK: + case APPCOMMAND_MEDIA_STOP: + case APPCOMMAND_MEDIA_PLAY_PAUSE: + // We shouldn't consume the message always because if we don't handle + // the message, the sender (typically, utility of keyboard or mouse) + // may send other key messages which indicate well known shortcut key. + consumed = DispatchCommandEvent(appCommand); + break; + + // Use content command for following commands: + case APPCOMMAND_COPY: + contentCommandMessage = eContentCommandCopy; + break; + case APPCOMMAND_CUT: + contentCommandMessage = eContentCommandCut; + break; + case APPCOMMAND_PASTE: + contentCommandMessage = eContentCommandPaste; + break; + case APPCOMMAND_REDO: + contentCommandMessage = eContentCommandRedo; + break; + case APPCOMMAND_UNDO: + contentCommandMessage = eContentCommandUndo; + break; + } + + if (contentCommandMessage) { + MOZ_ASSERT(!mWidget->Destroyed()); + WidgetContentCommandEvent contentCommandEvent(true, contentCommandMessage, + mWidget); + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), dispatching %s event...", + this, ToChar(contentCommandMessage))); + mWidget->DispatchWindowEvent(contentCommandEvent); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), dispatched %s event", + this, ToChar(contentCommandMessage))); + consumed = true; + + if (mWidget->Destroyed()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), %s event caused " + "destroying the widget", + this, ToChar(contentCommandMessage))); + return true; + } + } else { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), doesn't dispatch " + "content " + "command event", + this)); + } + } + + // Dispatch a keyup event if the command is caused by pressing a key and + // the key isn't mapped to a virtual keycode. + if (dispatchKeyEvent && !mVirtualKeyCode) { + MOZ_ASSERT(!mWidget->Destroyed()); + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gKeyLog, LogLevel::Error, + ("%p NativeKey::HandleAppCommandMessage(), FAILED due to " + "BeginNativeInputTransaction() failure", + this)); + return true; + } + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), initializing keyup " + "event...", + this)); + WidgetKeyboardEvent keyupEvent(true, eKeyUp, mWidget); + nsEventStatus status = InitKeyEvent(keyupEvent, mModKeyState); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), dispatching keyup " + "event...", + this)); + // NOTE: Ignore if the keyup event is consumed because keyup event + // represents just a physical key event state change. + mDispatcher->DispatchKeyboardEvent(eKeyUp, keyupEvent, status, + const_cast(this)); + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), dispatched keyup event", + this)); + if (mWidget->Destroyed()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleAppCommandMessage(), keyup event caused " + "destroying the widget", + this)); + return true; + } + } + + return consumed; +} + +bool NativeKey::HandleKeyDownMessage(bool* aEventDispatched) const { + MOZ_ASSERT(IsKeyDownMessage()); + + if (aEventDispatched) { + *aEventDispatched = false; + } + + if (sDispatchedKeyOfAppCommand && + sDispatchedKeyOfAppCommand == mOriginalVirtualKeyCode) { + // The multimedia key event has already been dispatch from + // HandleAppCommandMessage(). + sDispatchedKeyOfAppCommand = 0; + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), doesn't dispatch keydown " + "event due to already dispatched from HandleAppCommandMessage(), ", + this)); + if (RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) { + RedirectedKeyDownMessageManager::Forget(); + } + return true; + } + + if (IsReservedBySystem()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), doesn't dispatch keydown " + "event because the key combination is reserved by the system", + this)); + if (RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) { + RedirectedKeyDownMessageManager::Forget(); + } + return false; + } + + // If the widget has gone, we should do nothing. + if (mWidget->Destroyed()) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::HandleKeyDownMessage(), WARNING, not handled due to " + "destroyed the widget", + this)); + if (RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) { + RedirectedKeyDownMessageManager::Forget(); + } + return false; + } + + bool defaultPrevented = false; + if (mFakeCharMsgs || + !RedirectedKeyDownMessageManager::IsRedirectedMessage(mMsg)) { + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gKeyLog, LogLevel::Error, + ("%p NativeKey::HandleKeyDownMessage(), FAILED due to " + "BeginNativeInputTransaction() failure", + this)); + return true; + } + + bool isIMEEnabled = WinUtils::IsIMEEnabled(mWidget->GetInputContext()); + + MOZ_LOG(gKeyLog, LogLevel::Debug, + ("%p NativeKey::HandleKeyDownMessage(), initializing keydown " + "event...", + this)); + + WidgetKeyboardEvent keydownEvent(true, eKeyDown, mWidget); + nsEventStatus status = InitKeyEvent(keydownEvent, mModKeyState); + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), dispatching keydown event...", + this)); + bool dispatched = mDispatcher->DispatchKeyboardEvent( + eKeyDown, keydownEvent, status, const_cast(this)); + if (aEventDispatched) { + *aEventDispatched = dispatched; + } + if (!dispatched) { + // If the keydown event wasn't fired, there must be composition. + // we don't need to do anything anymore. + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), doesn't dispatch keypress " + "event(s) because keydown event isn't dispatched actually", + this)); + return false; + } + defaultPrevented = status == nsEventStatus_eConsumeNoDefault; + + if (mWidget->Destroyed() || IsFocusedWindowChanged()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), keydown event caused " + "destroying the widget", + this)); + return true; + } + + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), dispatched keydown event, " + "dispatched=%s, defaultPrevented=%s", + this, GetBoolName(dispatched), GetBoolName(defaultPrevented))); + + // If IMC wasn't associated to the window but is associated it now (i.e., + // focus is moved from a non-editable editor to an editor by keydown + // event handler), WM_CHAR and WM_SYSCHAR shouldn't cause first character + // inputting if IME is opened. But then, we should redirect the native + // keydown message to IME. + // However, note that if focus has been already moved to another + // application, we shouldn't redirect the message to it because the keydown + // message is processed by us, so, nobody shouldn't process it. + HWND focusedWnd = ::GetFocus(); + if (!defaultPrevented && !mFakeCharMsgs && focusedWnd && !isIMEEnabled && + WinUtils::IsIMEEnabled(mWidget->GetInputContext())) { + RedirectedKeyDownMessageManager::RemoveNextCharMessage(focusedWnd); + + INPUT keyinput; + keyinput.type = INPUT_KEYBOARD; + keyinput.ki.wVk = mOriginalVirtualKeyCode; + keyinput.ki.wScan = mScanCode; + keyinput.ki.dwFlags = KEYEVENTF_SCANCODE; + if (mIsExtended) { + keyinput.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; + } + keyinput.ki.time = 0; + keyinput.ki.dwExtraInfo = 0; + + RedirectedKeyDownMessageManager::WillRedirect(mMsg, defaultPrevented); + + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), redirecting %s...", + this, ToString(mMsg).get())); + + ::SendInput(1, &keyinput, sizeof(keyinput)); + + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), redirected %s", this, + ToString(mMsg).get())); + + // Return here. We shouldn't dispatch keypress event for this WM_KEYDOWN. + // If it's needed, it will be dispatched after next (redirected) + // WM_KEYDOWN. + return true; + } + } else { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), received a redirected %s", + this, ToString(mMsg).get())); + + defaultPrevented = RedirectedKeyDownMessageManager::DefaultPrevented(); + // If this is redirected keydown message, we have dispatched the keydown + // event already. + if (aEventDispatched) { + *aEventDispatched = true; + } + } + + RedirectedKeyDownMessageManager::Forget(); + + MOZ_ASSERT(!mWidget->Destroyed()); + + // If the key was processed by IME and didn't cause WM_(SYS)CHAR messages, we + // shouldn't dispatch keypress event. + if (mOriginalVirtualKeyCode == VK_PROCESSKEY && + !IsFollowedByPrintableCharOrSysCharMessage()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress " + "event because the key was already handled by IME, " + "defaultPrevented=%s", + this, GetBoolName(defaultPrevented))); + return defaultPrevented; + } + + if (defaultPrevented) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress " + "event because preceding keydown event was consumed", + this)); + return true; + } + + MOZ_ASSERT(!mCharMessageHasGone, + "If following char message was consumed by somebody, " + "keydown event should have been consumed before dispatch"); + + // If mCommittedCharsAndModifiers was initialized with following char + // messages, we should dispatch keypress events with its information. + if (IsFollowedByPrintableCharOrSysCharMessage()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), tries to be dispatching " + "keypress events with retrieved char messages...", + this)); + return DispatchKeyPressEventsWithRetrievedCharMessages(); + } + + // If we won't be getting a WM_CHAR, WM_SYSCHAR or WM_DEADCHAR, synthesize a + // keypress for almost all keys + if (NeedsToHandleWithoutFollowingCharMessages()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), tries to be dispatching " + "keypress events...", + this)); + return DispatchKeyPressEventsWithoutCharMessage(); + } + + // If WM_KEYDOWN of VK_PACKET isn't followed by WM_CHAR, we don't need to + // dispatch keypress events. + if (mVirtualKeyCode == VK_PACKET) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress " + "event " + "because the key is VK_PACKET and there are no char messages", + this)); + return false; + } + + if (!mModKeyState.IsControl() && !mModKeyState.IsAlt() && + !mModKeyState.IsWin() && mIsPrintableKey) { + // If this is simple KeyDown event but next message is not WM_CHAR, + // this event may not input text, so we should ignore this event. + // See bug 314130. + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress " + "event " + "because the key event is simple printable key's event but not " + "followed " + "by char messages", + this)); + return false; + } + + if (mIsDeadKey) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), not dispatching keypress " + "event " + "because the key is a dead key and not followed by char messages", + this)); + return false; + } + + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), tries to be dispatching " + "keypress events due to no following char messages...", + this)); + return DispatchKeyPressEventsWithoutCharMessage(); +} + +bool NativeKey::HandleCharMessage(bool* aEventDispatched) const { + MOZ_ASSERT(IsCharOrSysCharMessage(mMsg)); + return HandleCharMessage(mMsg, aEventDispatched); +} + +bool NativeKey::HandleCharMessage(const MSG& aCharMsg, + bool* aEventDispatched) const { + MOZ_ASSERT(IsKeyDownMessage() || IsCharOrSysCharMessage(mMsg)); + MOZ_ASSERT(IsCharOrSysCharMessage(aCharMsg.message)); + + if (aEventDispatched) { + *aEventDispatched = false; + } + + if ((IsCharOrSysCharMessage(mMsg) || IsEnterKeyPressCharMessage(mMsg)) && + IsAnotherInstanceRemovingCharMessage()) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::HandleCharMessage(), WARNING, does nothing because " + "the message should be handled in another instance removing this " + "message", + this)); + // Consume this for now because it will be handled by another instance. + return true; + } + + // If the key combinations is reserved by the system, we shouldn't dispatch + // eKeyPress event for it and passes the message to next wndproc. + if (IsReservedBySystem()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleCharMessage(), doesn't dispatch keypress " + "event because the key combination is reserved by the system", + this)); + return false; + } + + // If the widget has gone, we should do nothing. + if (mWidget->Destroyed()) { + MOZ_LOG(gKeyLog, LogLevel::Warning, + ("%p NativeKey::HandleCharMessage(), WARNING, not handled due to " + "destroyed the widget", + this)); + return false; + } + + // When a control key is inputted by a key, it should be handled without + // WM_*CHAR messages at receiving WM_*KEYDOWN message. So, when we receive + // WM_*CHAR message directly, we see a control character here. + // Note that when the char is '\r', it means that the char message should + // cause "Enter" keypress event for inserting a line break. + if (IsControlCharMessage(aCharMsg) && !IsEnterKeyPressCharMessage(aCharMsg)) { + // In this case, we don't need to dispatch eKeyPress event because: + // 1. We're the only browser which dispatches "keypress" event for + // non-printable characters (Although, both Chrome and Edge dispatch + // "keypress" event for some keys accidentally. For example, "IntlRo" + // key with Ctrl of Japanese keyboard layout). + // 2. Currently, we may handle shortcut keys with "keydown" event if + // it's reserved or something. So, we shouldn't dispatch "keypress" + // event without it. + // Note that this does NOT mean we stop dispatching eKeyPress event for + // key presses causes a control character when Ctrl is pressed. In such + // case, DispatchKeyPressEventsWithoutCharMessage() dispatches eKeyPress + // instead of this method. + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleCharMessage(), doesn't dispatch keypress " + "event because received a control character input without WM_KEYDOWN", + this)); + return false; + } + + // XXXmnakano I think that if mMsg is WM_CHAR, i.e., it comes without + // preceding WM_KEYDOWN, we should should dispatch composition + // events instead of eKeyPress because they are not caused by + // actual keyboard operation. + + // First, handle normal text input or non-printable key case here. + WidgetKeyboardEvent keypressEvent(true, eKeyPress, mWidget); + if (IsEnterKeyPressCharMessage(aCharMsg)) { + keypressEvent.mKeyCode = NS_VK_RETURN; + } else { + keypressEvent.mCharCode = static_cast(aCharMsg.wParam); + } + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gKeyLog, LogLevel::Error, + ("%p NativeKey::HandleCharMessage(), FAILED due to " + "BeginNativeInputTransaction() failure", + this)); + return true; + } + + MOZ_LOG(gKeyLog, LogLevel::Debug, + ("%p NativeKey::HandleCharMessage(), initializing keypress " + "event...", + this)); + + ModifierKeyState modKeyState(mModKeyState); + // When AltGr is pressed, both Alt and Ctrl are active. However, when they + // are active, TextEditor won't treat the keypress event as inputting a + // character. Therefore, when AltGr is pressed and the key tries to input + // a character, let's set them to false. + if (modKeyState.IsControl() && modKeyState.IsAlt() && + IsPrintableCharMessage(aCharMsg)) { + modKeyState.Unset(MODIFIER_ALT | MODIFIER_CONTROL); + } + nsEventStatus status = InitKeyEvent(keypressEvent, modKeyState); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleCharMessage(), dispatching keypress event...", + this)); + bool dispatched = mDispatcher->MaybeDispatchKeypressEvents( + keypressEvent, status, const_cast(this)); + if (aEventDispatched) { + *aEventDispatched = dispatched; + } + if (mWidget->Destroyed()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleCharMessage(), keypress event caused " + "destroying the widget", + this)); + return true; + } + bool consumed = status == nsEventStatus_eConsumeNoDefault; + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleCharMessage(), dispatched keypress event, " + "dispatched=%s, consumed=%s", + this, GetBoolName(dispatched), GetBoolName(consumed))); + return consumed; +} + +bool NativeKey::HandleKeyUpMessage(bool* aEventDispatched) const { + MOZ_ASSERT(IsKeyUpMessage()); + + if (aEventDispatched) { + *aEventDispatched = false; + } + + // If the key combinations is reserved by the system, we shouldn't dispatch + // eKeyUp event for it and passes the message to next wndproc. + if (IsReservedBySystem()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyUpMessage(), doesn't dispatch keyup " + "event because the key combination is reserved by the system", + this)); + return false; + } + + // If the widget has gone, we should do nothing. + if (mWidget->Destroyed()) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::HandleKeyUpMessage(), WARNING, not handled due to " + "destroyed the widget", + this)); + return false; + } + + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gKeyLog, LogLevel::Error, + ("%p NativeKey::HandleKeyUpMessage(), FAILED due to " + "BeginNativeInputTransaction() failure", + this)); + return true; + } + + MOZ_LOG(gKeyLog, LogLevel::Debug, + ("%p NativeKey::HandleKeyUpMessage(), initializing keyup event...", + this)); + WidgetKeyboardEvent keyupEvent(true, eKeyUp, mWidget); + nsEventStatus status = InitKeyEvent(keyupEvent, mModKeyState); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyUpMessage(), dispatching keyup event...", + this)); + bool dispatched = mDispatcher->DispatchKeyboardEvent( + eKeyUp, keyupEvent, status, const_cast(this)); + if (aEventDispatched) { + *aEventDispatched = dispatched; + } + if (mWidget->Destroyed()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyUpMessage(), keyup event caused " + "destroying the widget", + this)); + return true; + } + bool consumed = status == nsEventStatus_eConsumeNoDefault; + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyUpMessage(), dispatched keyup event, " + "dispatched=%s, consumed=%s", + this, GetBoolName(dispatched), GetBoolName(consumed))); + return consumed; +} + +bool NativeKey::NeedsToHandleWithoutFollowingCharMessages() const { + MOZ_ASSERT(IsKeyDownMessage()); + + // If the key combination is reserved by the system, the caller shouldn't + // do anything with following WM_*CHAR messages. So, let's return true here. + if (IsReservedBySystem()) { + return true; + } + + // If the keydown message is generated for inputting some Unicode characters + // via SendInput() API, we need to handle it only with WM_*CHAR messages. + if (mVirtualKeyCode == VK_PACKET) { + return false; + } + + // If following char message is for a control character, it should be handled + // without WM_CHAR message. This is typically Ctrl + [a-z]. + if (mFollowingCharMsgs.Length() == 1 && + IsControlCharMessage(mFollowingCharMsgs[0])) { + return true; + } + + // If keydown message is followed by WM_CHAR or WM_SYSCHAR whose wParam isn't + // a control character, we should dispatch keypress event with the char + // message even with any modifier state. + if (IsFollowedByPrintableCharOrSysCharMessage()) { + return false; + } + + // If any modifier keys which may cause printable keys becoming non-printable + // are not pressed, we don't need special handling for the key. + // Note that if the key does not produce a character with AltGr and when + // AltGr key is pressed, we don't need to dispatch eKeyPress event for it + // because AltGr shouldn't be used for a modifier for a shortcut without + // Ctrl, Alt or Win. That means that we should treat it in same path for + // Shift key. + if (!mModKeyState.IsControl() && !mModKeyState.IsAlt() && + !mModKeyState.IsWin()) { + return false; + } + + // If the key event causes dead key event, we don't need to dispatch keypress + // event. + if (mIsDeadKey && mCommittedCharsAndModifiers.IsEmpty()) { + return false; + } + + // Even if the key is a printable key, it might cause non-printable character + // input with modifier key(s). + return mIsPrintableKey; +} + +static nsCString GetResultOfInSendMessageEx() { + DWORD ret = ::InSendMessageEx(nullptr); + if (!ret) { + return "ISMEX_NOSEND"_ns; + } + nsCString result; + if (ret & ISMEX_CALLBACK) { + result = "ISMEX_CALLBACK"; + } + if (ret & ISMEX_NOTIFY) { + if (!result.IsEmpty()) { + result += " | "; + } + result += "ISMEX_NOTIFY"; + } + if (ret & ISMEX_REPLIED) { + if (!result.IsEmpty()) { + result += " | "; + } + result += "ISMEX_REPLIED"; + } + if (ret & ISMEX_SEND) { + if (!result.IsEmpty()) { + result += " | "; + } + result += "ISMEX_SEND"; + } + return result; +} + +bool NativeKey::MayBeSameCharMessage(const MSG& aCharMsg1, + const MSG& aCharMsg2) const { + // NOTE: Although, we don't know when this case occurs, the scan code value + // in lParam may be changed from 0 to something. The changed value + // is different from the scan code of handling keydown message. + static const LPARAM kScanCodeMask = 0x00FF0000; + return aCharMsg1.message == aCharMsg2.message && + aCharMsg1.wParam == aCharMsg2.wParam && + (aCharMsg1.lParam & ~kScanCodeMask) == + (aCharMsg2.lParam & ~kScanCodeMask); +} + +bool NativeKey::IsSamePhysicalKeyMessage(const MSG& aKeyOrCharMsg1, + const MSG& aKeyOrCharMsg2) const { + if (NS_WARN_IF(aKeyOrCharMsg1.message < WM_KEYFIRST) || + NS_WARN_IF(aKeyOrCharMsg1.message > WM_KEYLAST) || + NS_WARN_IF(aKeyOrCharMsg2.message < WM_KEYFIRST) || + NS_WARN_IF(aKeyOrCharMsg2.message > WM_KEYLAST)) { + return false; + } + return WinUtils::GetScanCode(aKeyOrCharMsg1.lParam) == + WinUtils::GetScanCode(aKeyOrCharMsg2.lParam) && + WinUtils::IsExtendedScanCode(aKeyOrCharMsg1.lParam) == + WinUtils::IsExtendedScanCode(aKeyOrCharMsg2.lParam); +} + +bool NativeKey::GetFollowingCharMessage(MSG& aCharMsg) { + MOZ_ASSERT(IsKeyDownMessage()); + + aCharMsg.message = WM_NULL; + + if (mFakeCharMsgs) { + for (size_t i = 0; i < mFakeCharMsgs->Length(); i++) { + FakeCharMsg& fakeCharMsg = mFakeCharMsgs->ElementAt(i); + if (fakeCharMsg.mConsumed) { + continue; + } + MSG charMsg = fakeCharMsg.GetCharMsg(mMsg.hwnd); + fakeCharMsg.mConsumed = true; + if (!IsCharMessage(charMsg)) { + return false; + } + aCharMsg = charMsg; + return true; + } + return false; + } + + // If next key message is not char message, we should give up to find a + // related char message for the handling keydown event for now. + // Note that it's possible other applications may send other key message + // after we call TranslateMessage(). That may cause PeekMessage() failing + // to get char message for the handling keydown message. + MSG nextKeyMsg; + if (!WinUtils::PeekMessage(&nextKeyMsg, mMsg.hwnd, WM_KEYFIRST, WM_KEYLAST, + PM_NOREMOVE | PM_NOYIELD) || + !IsCharMessage(nextKeyMsg)) { + MOZ_LOG(gKeyLog, LogLevel::Debug, + ("%p NativeKey::GetFollowingCharMessage(), there are no char " + "messages", + this)); + return false; + } + const MSG kFoundCharMsg = nextKeyMsg; + + AutoRestore saveLastRemovingMsg(mRemovingMsg); + mRemovingMsg = nextKeyMsg; + + mReceivedMsg = sEmptyMSG; + AutoRestore ensureToClearRecivedMsg(mReceivedMsg); + + // On Metrofox, PeekMessage() sometimes returns WM_NULL even if we specify + // the message range. So, if it returns WM_NULL, we should retry to get + // the following char message it was found above. + for (uint32_t i = 0; i < 50; i++) { + MSG removedMsg, nextKeyMsgInAllWindows; + bool doCrash = false; + if (!WinUtils::PeekMessage(&removedMsg, mMsg.hwnd, nextKeyMsg.message, + nextKeyMsg.message, PM_REMOVE | PM_NOYIELD)) { + // We meets unexpected case. We should collect the message queue state + // and crash for reporting the bug. + doCrash = true; + + // If another instance was created for the removing message during trying + // to remove a char message, the instance didn't handle it for preventing + // recursive handling. So, let's handle it in this instance. + if (!IsEmptyMSG(mReceivedMsg)) { + // If focus is moved to different window, we shouldn't handle it on + // the widget. Let's discard it for now. + if (mReceivedMsg.hwnd != nextKeyMsg.hwnd) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, received a " + "char message during removing it from the queue, but it's for " + "different window, mReceivedMsg=%s, nextKeyMsg=%s, " + "kFoundCharMsg=%s", + this, ToString(mReceivedMsg).get(), ToString(nextKeyMsg).get(), + ToString(kFoundCharMsg).get())); + // There might still exist char messages, the loop of calling + // this method should be continued. + aCharMsg.message = WM_NULL; + return true; + } + // Even if the received message is different from what we tried to + // remove from the queue, let's take the received message as a part of + // the result of this key sequence. + if (mReceivedMsg.message != nextKeyMsg.message || + mReceivedMsg.wParam != nextKeyMsg.wParam || + mReceivedMsg.lParam != nextKeyMsg.lParam) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, received a " + "char message during removing it from the queue, but it's " + "differnt from what trying to remove from the queue, " + "aCharMsg=%s, nextKeyMsg=%s, kFoundCharMsg=%s", + this, ToString(mReceivedMsg).get(), ToString(nextKeyMsg).get(), + ToString(kFoundCharMsg).get())); + } else { + MOZ_LOG( + gKeyLog, LogLevel::Debug, + ("%p NativeKey::GetFollowingCharMessage(), succeeded to " + "retrieve next char message via another instance, aCharMsg=%s, " + "kFoundCharMsg=%s", + this, ToString(mReceivedMsg).get(), + ToString(kFoundCharMsg).get())); + } + aCharMsg = mReceivedMsg; + return true; + } + + // The char message is redirected to different thread's window by focus + // move or something or just cancelled by external application. + if (!WinUtils::PeekMessage(&nextKeyMsgInAllWindows, 0, WM_KEYFIRST, + WM_KEYLAST, PM_NOREMOVE | PM_NOYIELD)) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message, but it's already gone from all message " + "queues, nextKeyMsg=%s, kFoundCharMsg=%s", + this, ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get())); + return true; // XXX should return false in this case + } + // The next key message is redirected to different window created by our + // thread, we should do nothing because we must not have focus. + if (nextKeyMsgInAllWindows.hwnd != mMsg.hwnd) { + aCharMsg = nextKeyMsgInAllWindows; + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message, but found in another message queue, " + "nextKeyMsgInAllWindows=%s, nextKeyMsg=%s, kFoundCharMsg=%s", + this, ToString(nextKeyMsgInAllWindows).get(), + ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get())); + return true; + } + // If next key message becomes non-char message, this key operation + // may have already been consumed or canceled. + if (!IsCharMessage(nextKeyMsgInAllWindows)) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message and next key message becomes non-char " + "message, nextKeyMsgInAllWindows=%s, nextKeyMsg=%s, " + "kFoundCharMsg=%s", + this, ToString(nextKeyMsgInAllWindows).get(), + ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get())); + MOZ_ASSERT(!mCharMessageHasGone); + mFollowingCharMsgs.Clear(); + mCharMessageHasGone = true; + return false; + } + // If next key message is still a char message but different key message, + // we should treat current key operation is consumed or canceled and + // next char message should be handled as an orphan char message later. + if (!IsSamePhysicalKeyMessage(nextKeyMsgInAllWindows, kFoundCharMsg)) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message and next key message becomes differnt " + "key's " + "char message, nextKeyMsgInAllWindows=%s, nextKeyMsg=%s, " + "kFoundCharMsg=%s", + this, ToString(nextKeyMsgInAllWindows).get(), + ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get())); + MOZ_ASSERT(!mCharMessageHasGone); + mFollowingCharMsgs.Clear(); + mCharMessageHasGone = true; + return false; + } + // If next key message is still a char message but the message is changed, + // we should retry to remove the new message with PeekMessage() again. + if (nextKeyMsgInAllWindows.message != nextKeyMsg.message) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message due to message change, let's retry to " + "remove the message with newly found char message, " + "nextKeyMsgInAllWindows=%s, nextKeyMsg=%s, kFoundCharMsg=%s", + this, ToString(nextKeyMsgInAllWindows).get(), + ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get())); + nextKeyMsg = nextKeyMsgInAllWindows; + continue; + } + // If there is still existing a char message caused by same physical key + // in the queue but PeekMessage(PM_REMOVE) failed to remove it from the + // queue, it might be possible that the odd keyboard layout or utility + // hooks only PeekMessage(PM_NOREMOVE) and GetMessage(). So, let's try + // remove the char message with GetMessage() again. + // FYI: The wParam might be different from the found message, but it's + // okay because we assume that odd keyboard layouts return actual + // inputting character at removing the char message. + if (WinUtils::GetMessage(&removedMsg, mMsg.hwnd, nextKeyMsg.message, + nextKeyMsg.message)) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message, but succeeded with GetMessage(), " + "removedMsg=%s, kFoundCharMsg=%s", + this, ToString(removedMsg).get(), ToString(kFoundCharMsg).get())); + // Cancel to crash, but we need to check the removed message value. + doCrash = false; + } + // If we've already removed some WM_NULL messages from the queue and + // the found message has already gone from the queue, let's treat the key + // as inputting no characters and already consumed. + else if (i > 0) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message, but removed %d WM_NULL messages", + this, i)); + // If the key is a printable key or a control key but tried to input + // a character, mark mCharMessageHasGone true for handling the keydown + // event as inputting empty string. + MOZ_ASSERT(!mCharMessageHasGone); + mFollowingCharMsgs.Clear(); + mCharMessageHasGone = true; + return false; + } + MOZ_LOG(gKeyLog, LogLevel::Error, + ("%p NativeKey::GetFollowingCharMessage(), FAILED, lost target " + "message to remove, nextKeyMsg=%s", + this, ToString(nextKeyMsg).get())); + } + + if (doCrash) { + nsPrintfCString info( + "\nPeekMessage() failed to remove char message! " + "\nActive keyboard layout=0x%p (%s), " + "\nHandling message: %s, InSendMessageEx()=%s, " + "\nFound message: %s, " + "\nWM_NULL has been removed: %d, " + "\nNext key message in all windows: %s, " + "time=%ld, ", + KeyboardLayout::GetActiveLayout(), + KeyboardLayout::GetActiveLayoutName().get(), ToString(mMsg).get(), + GetResultOfInSendMessageEx().get(), ToString(kFoundCharMsg).get(), i, + ToString(nextKeyMsgInAllWindows).get(), nextKeyMsgInAllWindows.time); + CrashReporter::AppendAppNotesToCrashReport(info); + MSG nextMsg; + if (WinUtils::PeekMessage(&nextMsg, 0, 0, 0, PM_NOREMOVE | PM_NOYIELD)) { + nsPrintfCString info("\nNext message in all windows: %s, time=%ld", + ToString(nextMsg).get(), nextMsg.time); + CrashReporter::AppendAppNotesToCrashReport(info); + } else { + CrashReporter::AppendAppNotesToCrashReport( + "\nThere is no message in any window"_ns); + } + + MOZ_CRASH("We lost the following char message"); + } + + // We're still not sure why ::PeekMessage() may return WM_NULL even with + // its first message and its last message are same message. However, + // at developing Metrofox, we met this case even with usual keyboard + // layouts. So, it might be possible in desktop application or it really + // occurs with some odd keyboard layouts which perhaps hook API. + if (removedMsg.message == WM_NULL) { + MOZ_LOG(gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message, instead, removed WM_NULL message, " + "removedMsg=%s", + this, ToString(removedMsg).get())); + // Check if there is the message which we're trying to remove. + MSG newNextKeyMsg; + if (!WinUtils::PeekMessage(&newNextKeyMsg, mMsg.hwnd, WM_KEYFIRST, + WM_KEYLAST, PM_NOREMOVE | PM_NOYIELD)) { + // If there is no key message, we should mark this keydown as consumed + // because the key operation may have already been handled or canceled. + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message because it's gone during removing it from " + "the queue, nextKeyMsg=%s, kFoundCharMsg=%s", + this, ToString(nextKeyMsg).get(), ToString(kFoundCharMsg).get())); + MOZ_ASSERT(!mCharMessageHasGone); + mFollowingCharMsgs.Clear(); + mCharMessageHasGone = true; + return false; + } + if (!IsCharMessage(newNextKeyMsg)) { + // If next key message becomes a non-char message, we should mark this + // keydown as consumed because the key operation may have already been + // handled or canceled. + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, failed to " + "remove a char message because it's gone during removing it from " + "the queue, nextKeyMsg=%s, newNextKeyMsg=%s, kFoundCharMsg=%s", + this, ToString(nextKeyMsg).get(), ToString(newNextKeyMsg).get(), + ToString(kFoundCharMsg).get())); + MOZ_ASSERT(!mCharMessageHasGone); + mFollowingCharMsgs.Clear(); + mCharMessageHasGone = true; + return false; + } + MOZ_LOG( + gKeyLog, LogLevel::Debug, + ("%p NativeKey::GetFollowingCharMessage(), there is the message " + "which is being tried to be removed from the queue, trying again...", + this)); + continue; + } + + // Typically, this case occurs with WM_DEADCHAR. If the removed message's + // wParam becomes 0, that means that the key event shouldn't cause text + // input. So, let's ignore the strange char message. + if (removedMsg.message == nextKeyMsg.message && !removedMsg.wParam) { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, succeeded to " + "remove a char message, but the removed message's wParam is 0, " + "removedMsg=%s", + this, ToString(removedMsg).get())); + return false; + } + + // This is normal case. + if (MayBeSameCharMessage(removedMsg, nextKeyMsg)) { + aCharMsg = removedMsg; + MOZ_LOG( + gKeyLog, LogLevel::Debug, + ("%p NativeKey::GetFollowingCharMessage(), succeeded to retrieve " + "next char message, aCharMsg=%s", + this, ToString(aCharMsg).get())); + return true; + } + + // Even if removed message is different char message from the found char + // message, when the scan code is same, we can assume that the message + // is overwritten by somebody who hooks API. See bug 1336028 comment 0 for + // the possible scenarios. + if (IsCharMessage(removedMsg) && + IsSamePhysicalKeyMessage(removedMsg, nextKeyMsg)) { + aCharMsg = removedMsg; + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, succeeded to " + "remove a char message, but the removed message was changed from " + "the found message except their scancode, aCharMsg=%s, " + "nextKeyMsg=%s, kFoundCharMsg=%s", + this, ToString(aCharMsg).get(), ToString(nextKeyMsg).get(), + ToString(kFoundCharMsg).get())); + return true; + } + + // When found message's wParam is 0 and its scancode is 0xFF, we may remove + // usual char message actually. In such case, we should use the removed + // char message. + if (IsCharMessage(removedMsg) && !nextKeyMsg.wParam && + WinUtils::GetScanCode(nextKeyMsg.lParam) == 0xFF) { + aCharMsg = removedMsg; + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::GetFollowingCharMessage(), WARNING, succeeded to " + "remove a char message, but the removed message was changed from " + "the found message but the found message was odd, so, ignoring the " + "odd found message and respecting the removed message, aCharMsg=%s, " + "nextKeyMsg=%s, kFoundCharMsg=%s", + this, ToString(aCharMsg).get(), ToString(nextKeyMsg).get(), + ToString(kFoundCharMsg).get())); + return true; + } + + // NOTE: Although, we don't know when this case occurs, the scan code value + // in lParam may be changed from 0 to something. The changed value + // is different from the scan code of handling keydown message. + MOZ_LOG( + gKeyLog, LogLevel::Error, + ("%p NativeKey::GetFollowingCharMessage(), FAILED, removed message " + "is really different from what we have already found, removedMsg=%s, " + "nextKeyMsg=%s, kFoundCharMsg=%s", + this, ToString(removedMsg).get(), ToString(nextKeyMsg).get(), + ToString(kFoundCharMsg).get())); + nsPrintfCString info( + "\nPeekMessage() removed unexpcted char message! " + "\nActive keyboard layout=0x%p (%s), " + "\nHandling message: %s, InSendMessageEx()=%s, " + "\nFound message: %s, " + "\nRemoved message: %s, ", + KeyboardLayout::GetActiveLayout(), + KeyboardLayout::GetActiveLayoutName().get(), ToString(mMsg).get(), + GetResultOfInSendMessageEx().get(), ToString(kFoundCharMsg).get(), + ToString(removedMsg).get()); + CrashReporter::AppendAppNotesToCrashReport(info); + // What's the next key message? + MSG nextKeyMsgAfter; + if (WinUtils::PeekMessage(&nextKeyMsgAfter, mMsg.hwnd, WM_KEYFIRST, + WM_KEYLAST, PM_NOREMOVE | PM_NOYIELD)) { + nsPrintfCString info( + "\nNext key message after unexpected char message " + "removed: %s, ", + ToString(nextKeyMsgAfter).get()); + CrashReporter::AppendAppNotesToCrashReport(info); + } else { + CrashReporter::AppendAppNotesToCrashReport( + nsLiteralCString("\nThere is no key message after unexpected char " + "message removed, ")); + } + // Another window has a key message? + if (WinUtils::PeekMessage(&nextKeyMsgInAllWindows, 0, WM_KEYFIRST, + WM_KEYLAST, PM_NOREMOVE | PM_NOYIELD)) { + nsPrintfCString info("\nNext key message in all windows: %s.", + ToString(nextKeyMsgInAllWindows).get()); + CrashReporter::AppendAppNotesToCrashReport(info); + } else { + CrashReporter::AppendAppNotesToCrashReport( + "\nThere is no key message in any windows."_ns); + } + + MOZ_CRASH("PeekMessage() removed unexpected message"); + } + MOZ_LOG( + gKeyLog, LogLevel::Error, + ("%p NativeKey::GetFollowingCharMessage(), FAILED, removed messages " + "are all WM_NULL, nextKeyMsg=%s", + this, ToString(nextKeyMsg).get())); + nsPrintfCString info( + "\nWe lost following char message! " + "\nActive keyboard layout=0x%p (%s), " + "\nHandling message: %s, InSendMessageEx()=%s, \n" + "Found message: %s, removed a lot of WM_NULL", + KeyboardLayout::GetActiveLayout(), + KeyboardLayout::GetActiveLayoutName().get(), ToString(mMsg).get(), + GetResultOfInSendMessageEx().get(), ToString(kFoundCharMsg).get()); + CrashReporter::AppendAppNotesToCrashReport(info); + MOZ_CRASH("We lost the following char message"); + return false; +} + +void NativeKey::ComputeInputtingStringWithKeyboardLayout() { + KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance(); + + if (KeyboardLayout::IsPrintableCharKey(mVirtualKeyCode) || + mCharMessageHasGone) { + mInputtingStringAndModifiers = mCommittedCharsAndModifiers; + } else { + mInputtingStringAndModifiers.Clear(); + } + mShiftedString.Clear(); + mUnshiftedString.Clear(); + mShiftedLatinChar = mUnshiftedLatinChar = 0; + + // XXX How about when Win key is pressed? + if (mModKeyState.IsControl() == mModKeyState.IsAlt()) { + return; + } + + // If user is inputting a Unicode character with typing Alt + Numpad + // keys, we shouldn't set alternative key codes because the key event + // shouldn't match with a mnemonic. However, we should set only + // mUnshiftedString for keeping traditional behavior at WM_SYSKEYDOWN. + // FYI: I guess that it's okay that mUnshiftedString stays empty. So, + // if its value breaks something, you must be able to just return here. + if (MaybeTypingUnicodeScalarValue()) { + if (!mCommittedCharsAndModifiers.IsEmpty()) { + MOZ_ASSERT(mMsg.message == WM_SYSKEYDOWN); + char16_t num = mCommittedCharsAndModifiers.CharAt(0); + MOZ_ASSERT(num >= '0' && num <= '9'); + mUnshiftedString.Append(num, MODIFIER_NONE); + return; + } + // If user presses a function key without NumLock or 3rd party utility + // synthesizes a function key on numpad, we should handle it as-is because + // the user's intention may be performing `Alt` + foo. + MOZ_ASSERT(!KeyboardLayout::IsPrintableCharKey(mVirtualKeyCode)); + return; + } + + ModifierKeyState capsLockState(mModKeyState.GetModifiers() & + MODIFIER_CAPSLOCK); + + mUnshiftedString = + keyboardLayout->GetUniCharsAndModifiers(mVirtualKeyCode, capsLockState); + capsLockState.Set(MODIFIER_SHIFT); + mShiftedString = + keyboardLayout->GetUniCharsAndModifiers(mVirtualKeyCode, capsLockState); + + // The current keyboard cannot input alphabets or numerics, + // we should append them for Shortcut/Access keys. + // E.g., for Cyrillic keyboard layout. + capsLockState.Unset(MODIFIER_SHIFT); + WidgetUtils::GetLatinCharCodeForKeyCode( + mDOMKeyCode, capsLockState.GetModifiers(), &mUnshiftedLatinChar, + &mShiftedLatinChar); + + // If the mShiftedLatinChar isn't 0, the key code is NS_VK_[A-Z]. + if (mShiftedLatinChar) { + // If the produced characters of the key on current keyboard layout + // are same as computed Latin characters, we shouldn't append the + // Latin characters to alternativeCharCode. + if (mUnshiftedLatinChar == mUnshiftedString.CharAt(0) && + mShiftedLatinChar == mShiftedString.CharAt(0)) { + mShiftedLatinChar = mUnshiftedLatinChar = 0; + } + } else if (mUnshiftedLatinChar) { + // If the mShiftedLatinChar is 0, the mKeyCode doesn't produce + // alphabet character. At that time, the character may be produced + // with Shift key. E.g., on French keyboard layout, NS_VK_PERCENT + // key produces LATIN SMALL LETTER U WITH GRAVE (U+00F9) without + // Shift key but with Shift key, it produces '%'. + // If the mUnshiftedLatinChar is produced by the key on current + // keyboard layout, we shouldn't append it to alternativeCharCode. + if (mUnshiftedLatinChar == mUnshiftedString.CharAt(0) || + mUnshiftedLatinChar == mShiftedString.CharAt(0)) { + mUnshiftedLatinChar = 0; + } + } + + if (!mModKeyState.IsControl()) { + return; + } + + // If the mCharCode is not ASCII character, we should replace the + // mCharCode with ASCII character only when Ctrl is pressed. + // But don't replace the mCharCode when the mCharCode is not same as + // unmodified characters. In such case, Ctrl is sometimes used for a + // part of character inputting key combination like Shift. + uint32_t ch = + mModKeyState.IsShift() ? mShiftedLatinChar : mUnshiftedLatinChar; + if (!ch) { + return; + } + if (mInputtingStringAndModifiers.IsEmpty() || + mInputtingStringAndModifiers.UniCharsCaseInsensitiveEqual( + mModKeyState.IsShift() ? mShiftedString : mUnshiftedString)) { + mInputtingStringAndModifiers.Clear(); + mInputtingStringAndModifiers.Append(ch, mModKeyState.GetModifiers()); + } +} + +bool NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages() const { + MOZ_ASSERT(IsKeyDownMessage()); + MOZ_ASSERT(IsFollowedByPrintableCharOrSysCharMessage()); + MOZ_ASSERT(!mWidget->Destroyed()); + + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG( + gKeyLog, LogLevel::Error, + ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), " + "FAILED due to BeginNativeInputTransaction() failure", + this)); + return true; + } + WidgetKeyboardEvent keypressEvent(true, eKeyPress, mWidget); + MOZ_LOG(gKeyLog, LogLevel::Debug, + ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), " + "initializing keypress event...", + this)); + ModifierKeyState modKeyState(mModKeyState); + if (mCanIgnoreModifierStateAtKeyPress && IsFollowedByPrintableCharMessage()) { + // If eKeyPress event should cause inputting text in focused editor, + // we need to remove Alt and Ctrl state. + modKeyState.Unset(MODIFIER_ALT | MODIFIER_CONTROL); + } + // We don't need to send char message here if there are two or more retrieved + // messages because we need to set each message to each eKeyPress event. + bool needsCallback = mFollowingCharMsgs.Length() > 1; + nsEventStatus status = InitKeyEvent(keypressEvent, modKeyState); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), " + "dispatching keypress event(s)...", + this)); + bool dispatched = mDispatcher->MaybeDispatchKeypressEvents( + keypressEvent, status, const_cast(this), needsCallback); + if (mWidget->Destroyed()) { + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), " + "keypress event(s) caused destroying the widget", + this)); + return true; + } + bool consumed = status == nsEventStatus_eConsumeNoDefault; + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::DispatchKeyPressEventsWithRetrievedCharMessages(), " + "dispatched keypress event(s), dispatched=%s, consumed=%s", + this, GetBoolName(dispatched), GetBoolName(consumed))); + return consumed; +} + +bool NativeKey::DispatchKeyPressEventsWithoutCharMessage() const { + MOZ_ASSERT(IsKeyDownMessage()); + MOZ_ASSERT(!mIsDeadKey || !mCommittedCharsAndModifiers.IsEmpty()); + MOZ_ASSERT(!mWidget->Destroyed()); + + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gKeyLog, LogLevel::Error, + ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), " + "FAILED due " + "to BeginNativeInputTransaction() failure", + this)); + return true; + } + + WidgetKeyboardEvent keypressEvent(true, eKeyPress, mWidget); + if (mInputtingStringAndModifiers.IsEmpty() && mShiftedString.IsEmpty() && + mUnshiftedString.IsEmpty()) { + keypressEvent.mKeyCode = mDOMKeyCode; + } + MOZ_LOG(gKeyLog, LogLevel::Debug, + ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), " + "initializing " + "keypress event...", + this)); + nsEventStatus status = InitKeyEvent(keypressEvent, mModKeyState); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), " + "dispatching " + "keypress event(s)...", + this)); + bool dispatched = mDispatcher->MaybeDispatchKeypressEvents( + keypressEvent, status, const_cast(this)); + if (mWidget->Destroyed()) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), " + "keypress event(s) caused destroying the widget", + this)); + return true; + } + bool consumed = status == nsEventStatus_eConsumeNoDefault; + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::DispatchKeyPressEventsWithoutCharMessage(), dispatched " + "keypress event(s), dispatched=%s, consumed=%s", + this, GetBoolName(dispatched), GetBoolName(consumed))); + return consumed; +} + +void NativeKey::WillDispatchKeyboardEvent(WidgetKeyboardEvent& aKeyboardEvent, + uint32_t aIndex) { + // If it's an eKeyPress event and it's generated from retrieved char message, + // we need to set raw message information for plugins. + if (aKeyboardEvent.mMessage == eKeyPress && + IsFollowedByPrintableCharOrSysCharMessage()) { + MOZ_RELEASE_ASSERT(aIndex < mCommittedCharsAndModifiers.Length()); + uint32_t foundPrintableCharMessages = 0; + for (size_t i = 0; i < mFollowingCharMsgs.Length(); ++i) { + if (!IsPrintableCharOrSysCharMessage(mFollowingCharMsgs[i])) { + // XXX Should we dispatch a plugin event for WM_*DEADCHAR messages and + // WM_CHAR with a control character here? But we're not sure + // how can we create such message queue (i.e., WM_CHAR or + // WM_SYSCHAR with a printable character and such message are + // generated by a keydown). So, let's ignore such case until + // we'd get some bug reports. + MOZ_LOG(gKeyLog, LogLevel::Warning, + ("%p NativeKey::WillDispatchKeyboardEvent(), WARNING, " + "ignoring %zuth message due to non-printable char message, %s", + this, i + 1, ToString(mFollowingCharMsgs[i]).get())); + continue; + } + if (foundPrintableCharMessages++ == aIndex) { + // Found message which caused the eKeyPress event. + break; + } + } + // Set modifier state from mCommittedCharsAndModifiers because some of them + // might be different. For example, Shift key was pressed at inputting + // dead char but Shift key was released before inputting next character. + if (mCanIgnoreModifierStateAtKeyPress) { + ModifierKeyState modKeyState(mModKeyState); + modKeyState.Unset(MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | + MODIFIER_ALTGRAPH | MODIFIER_CAPSLOCK); + modKeyState.Set(mCommittedCharsAndModifiers.ModifiersAt(aIndex)); + modKeyState.InitInputEvent(aKeyboardEvent); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::WillDispatchKeyboardEvent(), " + "setting %uth modifier state to %s", + this, aIndex + 1, ToString(modKeyState).get())); + } + } + size_t longestLength = + std::max(mInputtingStringAndModifiers.Length(), + std::max(mShiftedString.Length(), mUnshiftedString.Length())); + size_t skipUniChars = longestLength - mInputtingStringAndModifiers.Length(); + size_t skipShiftedChars = longestLength - mShiftedString.Length(); + size_t skipUnshiftedChars = longestLength - mUnshiftedString.Length(); + if (aIndex >= longestLength) { + MOZ_LOG( + gKeyLog, LogLevel::Info, + ("%p NativeKey::WillDispatchKeyboardEvent(), does nothing for %uth " + "%s event", + this, aIndex + 1, ToChar(aKeyboardEvent.mMessage))); + return; + } + + // Check if aKeyboardEvent is the last event for a key press. + // So, if it's not an eKeyPress event, it's always the last event. + // Otherwise, check if the index is the last character of + // mCommittedCharsAndModifiers. + bool isLastIndex = aKeyboardEvent.mMessage != eKeyPress || + mCommittedCharsAndModifiers.IsEmpty() || + mCommittedCharsAndModifiers.Length() - 1 == aIndex; + + nsTArray& altArray = + aKeyboardEvent.mAlternativeCharCodes; + + // Set charCode and adjust modifier state for every eKeyPress event. + // This is not necessary for the other keyboard events because the other + // keyboard events shouldn't have non-zero charCode value and should have + // current modifier state. + if (aKeyboardEvent.mMessage == eKeyPress && skipUniChars <= aIndex) { + // XXX Modifying modifier state of aKeyboardEvent is illegal, but no way + // to set different modifier state per keypress event except this + // hack. Note that ideally, dead key should cause composition events + // instead of keypress events, though. + if (aIndex - skipUniChars < mInputtingStringAndModifiers.Length()) { + ModifierKeyState modKeyState(mModKeyState); + // If key in combination with Alt and/or Ctrl produces a different + // character than without them then do not report these flags + // because it is separate keyboard layout shift state. If dead-key + // and base character does not produce a valid composite character + // then both produced dead-key character and following base + // character may have different modifier flags, too. + modKeyState.Unset(MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | + MODIFIER_ALTGRAPH | MODIFIER_CAPSLOCK); + modKeyState.Set( + mInputtingStringAndModifiers.ModifiersAt(aIndex - skipUniChars)); + modKeyState.InitInputEvent(aKeyboardEvent); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::WillDispatchKeyboardEvent(), " + "setting %uth modifier state to %s", + this, aIndex + 1, ToString(modKeyState).get())); + } + uint16_t uniChar = + mInputtingStringAndModifiers.CharAt(aIndex - skipUniChars); + + // The mCharCode was set from mKeyValue. However, for example, when Ctrl key + // is pressed, its value should indicate an ASCII character for backward + // compatibility rather than inputting character without the modifiers. + // Therefore, we need to modify mCharCode value here. + aKeyboardEvent.SetCharCode(uniChar); + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::WillDispatchKeyboardEvent(), " + "setting %uth charCode to %s", + this, aIndex + 1, GetCharacterCodeName(uniChar).get())); + } + + // We need to append alterntaive charCode values: + // - if the event is eKeyPress, we need to append for the index because + // eKeyPress event is dispatched for every character inputted by a + // key press. + // - if the event is not eKeyPress, we need to append for all characters + // inputted by the key press because the other keyboard events (e.g., + // eKeyDown are eKeyUp) are fired only once for a key press. + size_t count; + if (aKeyboardEvent.mMessage == eKeyPress) { + // Basically, append alternative charCode values only for the index. + count = 1; + // However, if it's the last eKeyPress event but different shift state + // can input longer string, the last eKeyPress event should have all + // remaining alternative charCode values. + if (isLastIndex) { + count = longestLength - aIndex; + } + } else { + count = longestLength; + } + for (size_t i = 0; i < count; ++i) { + uint16_t shiftedChar = 0, unshiftedChar = 0; + if (skipShiftedChars <= aIndex + i) { + shiftedChar = mShiftedString.CharAt(aIndex + i - skipShiftedChars); + } + if (skipUnshiftedChars <= aIndex + i) { + unshiftedChar = mUnshiftedString.CharAt(aIndex + i - skipUnshiftedChars); + } + + if (shiftedChar || unshiftedChar) { + AlternativeCharCode chars(unshiftedChar, shiftedChar); + altArray.AppendElement(chars); + } + + if (!isLastIndex) { + continue; + } + + if (mUnshiftedLatinChar || mShiftedLatinChar) { + AlternativeCharCode chars(mUnshiftedLatinChar, mShiftedLatinChar); + altArray.AppendElement(chars); + } + + // Typically, following virtual keycodes are used for a key which can + // input the character. However, these keycodes are also used for + // other keys on some keyboard layout. E.g., in spite of Shift+'1' + // inputs '+' on Thai keyboard layout, a key which is at '=/+' + // key on ANSI keyboard layout is VK_OEM_PLUS. Native applications + // handle it as '+' key if Ctrl key is pressed. + char16_t charForOEMKeyCode = 0; + switch (mVirtualKeyCode) { + case VK_OEM_PLUS: + charForOEMKeyCode = '+'; + break; + case VK_OEM_COMMA: + charForOEMKeyCode = ','; + break; + case VK_OEM_MINUS: + charForOEMKeyCode = '-'; + break; + case VK_OEM_PERIOD: + charForOEMKeyCode = '.'; + break; + } + if (charForOEMKeyCode && charForOEMKeyCode != mUnshiftedString.CharAt(0) && + charForOEMKeyCode != mShiftedString.CharAt(0) && + charForOEMKeyCode != mUnshiftedLatinChar && + charForOEMKeyCode != mShiftedLatinChar) { + AlternativeCharCode OEMChars(charForOEMKeyCode, charForOEMKeyCode); + altArray.AppendElement(OEMChars); + } + } +} + +/***************************************************************************** + * mozilla::widget::KeyboardLayout + *****************************************************************************/ + +KeyboardLayout* KeyboardLayout::sInstance = nullptr; +nsIUserIdleServiceInternal* KeyboardLayout::sIdleService = nullptr; + +// static +KeyboardLayout* KeyboardLayout::GetInstance() { + if (!sInstance) { + sInstance = new KeyboardLayout(); + nsCOMPtr idleService = + do_GetService("@mozilla.org/widget/useridleservice;1"); + // The refcount will be decreased at shut down. + sIdleService = idleService.forget().take(); + } + return sInstance; +} + +// static +void KeyboardLayout::Shutdown() { + delete sInstance; + sInstance = nullptr; + NS_IF_RELEASE(sIdleService); +} + +// static +void KeyboardLayout::NotifyIdleServiceOfUserActivity() { + sIdleService->ResetIdleTimeOut(0); +} + +KeyboardLayout::KeyboardLayout() + : mKeyboardLayout(0), + mIsOverridden(false), + mIsPendingToRestoreKeyboardLayout(false), + mHasAltGr(false) { + mDeadKeyTableListHead = nullptr; + // A dead key sequence should be made from up to 5 keys. Therefore, 4 is + // enough and makes sense because the item is uint8_t. + // (Although, even if it's possible to be 6 keys or more in a sequence, + // this array will be re-allocated). + mActiveDeadKeys.SetCapacity(4); + mDeadKeyShiftStates.SetCapacity(4); + + // NOTE: LoadLayout() should be called via OnLayoutChange(). +} + +KeyboardLayout::~KeyboardLayout() { ReleaseDeadKeyTables(); } + +bool KeyboardLayout::IsPrintableCharKey(uint8_t aVirtualKey) { + return GetKeyIndex(aVirtualKey) >= 0; +} + +WORD KeyboardLayout::ComputeScanCodeForVirtualKeyCode( + uint8_t aVirtualKeyCode) const { + return static_cast( + ::MapVirtualKeyEx(aVirtualKeyCode, MAPVK_VK_TO_VSC, GetLayout())); +} + +bool KeyboardLayout::IsDeadKey(uint8_t aVirtualKey, + const ModifierKeyState& aModKeyState) const { + int32_t virtualKeyIndex = GetKeyIndex(aVirtualKey); + + // XXX KeyboardLayout class doesn't support unusual keyboard layout which + // maps some function keys as dead keys. + if (virtualKeyIndex < 0) { + return false; + } + + return mVirtualKeys[virtualKeyIndex].IsDeadKey( + VirtualKey::ModifiersToShiftState(aModKeyState.GetModifiers())); +} + +bool KeyboardLayout::IsSysKey(uint8_t aVirtualKey, + const ModifierKeyState& aModKeyState) const { + // If Alt key is not pressed, it's never a system key combination. + // Additionally, if Ctrl key is pressed, it's never a system key combination + // too. + // FYI: Windows logo key state won't affect if it's a system key. + if (!aModKeyState.IsAlt() || aModKeyState.IsControl()) { + return false; + } + + int32_t virtualKeyIndex = GetKeyIndex(aVirtualKey); + if (virtualKeyIndex < 0) { + return true; + } + + UniCharsAndModifiers inputCharsAndModifiers = + GetUniCharsAndModifiers(aVirtualKey, aModKeyState); + if (inputCharsAndModifiers.IsEmpty()) { + return true; + } + + // If the Alt key state isn't consumed, that means that the key with Alt + // doesn't cause text input. So, the combination is a system key. + return !!(inputCharsAndModifiers.ModifiersAt(0) & MODIFIER_ALT); +} + +void KeyboardLayout::InitNativeKey(NativeKey& aNativeKey) { + if (mIsPendingToRestoreKeyboardLayout) { + LoadLayout(::GetKeyboardLayout(0)); + } + + // If the aNativeKey is initialized with WM_CHAR, the key information + // should be discarded because mKeyValue should have the string to be + // inputted. + if (aNativeKey.mMsg.message == WM_CHAR) { + char16_t ch = static_cast(aNativeKey.mMsg.wParam); + // But don't set key value as printable key if the character is a control + // character such as 0x0D at pressing Enter key. + if (!NativeKey::IsControlChar(ch)) { + aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING; + Modifiers modifiers = + aNativeKey.GetModifiers() & ~(MODIFIER_ALT | MODIFIER_CONTROL); + aNativeKey.mCommittedCharsAndModifiers.Append(ch, modifiers); + return; + } + } + + // If the aNativeKey is in a sequence to input a Unicode character with + // Alt + numpad keys, we should just set the number as the inputting charcter. + // Note that we should compute the key value from the virtual key code + // because they may be mapped to alphabets, but they should be treated as + // Alt + [0-9] even by web apps. + // However, we shouldn't touch the key value if given virtual key code is + // not a printable key because it may be synthesized by 3rd party utility + // or just NumLock is unlocked and user tries to use shortcut key. In the + // latter case, we cannot solve the conflict issue with Alt+foo shortcut key + // and inputting a Unicode scalar value like reported to bug 1606655, though, + // I have no better idea. Perhaps, `Alt` shouldn't be used for shortcut key + // combination on Windows. + if (aNativeKey.MaybeTypingUnicodeScalarValue() && + KeyboardLayout::IsPrintableCharKey(aNativeKey.mVirtualKeyCode)) { + // If the key code value is mapped to a Numpad key, let's compute the key + // value with it. This is same behavior as Chrome. In strictly speaking, + // I think that the else block's computation is better because it seems + // that Windows does not refer virtual key code value, but we should avoid + // web-compat issues. + char16_t num = '0'; + if (aNativeKey.mVirtualKeyCode >= VK_NUMPAD0 && + aNativeKey.mVirtualKeyCode <= VK_NUMPAD9) { + num = '0' + aNativeKey.mVirtualKeyCode - VK_NUMPAD0; + } + // Otherwise, let's use fake key value for making never match with + // mnemonic. + else { + switch (aNativeKey.mScanCode) { + case 0x0052: // Numpad0 + num = '0'; + break; + case 0x004F: // Numpad1 + num = '1'; + break; + case 0x0050: // Numpad2 + num = '2'; + break; + case 0x0051: // Numpad3 + num = '3'; + break; + case 0x004B: // Numpad4 + num = '4'; + break; + case 0x004C: // Numpad5 + num = '5'; + break; + case 0x004D: // Numpad6 + num = '6'; + break; + case 0x0047: // Numpad7 + num = '7'; + break; + case 0x0048: // Numpad8 + num = '8'; + break; + case 0x0049: // Numpad9 + num = '9'; + break; + default: + MOZ_ASSERT_UNREACHABLE( + "IsTypingUnicodeScalarValue() must have returned true for wrong " + "scancode"); + break; + } + } + aNativeKey.mCommittedCharsAndModifiers.Append(num, + aNativeKey.GetModifiers()); + aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING; + return; + } + + // When it's followed by non-dead char message(s) for printable character(s), + // aNativeKey should dispatch eKeyPress events for them rather than + // information from keyboard layout because respecting WM_(SYS)CHAR messages + // guarantees that we can always input characters which is expected by + // the user even if the user uses odd keyboard layout. + // Or, when it was followed by non-dead char message for a printable character + // but it's gone at removing the message from the queue, let's treat it + // as a key inputting empty string. + if (aNativeKey.IsFollowedByPrintableCharOrSysCharMessage() || + aNativeKey.mCharMessageHasGone) { + MOZ_ASSERT(!aNativeKey.IsCharMessage(aNativeKey.mMsg)); + if (aNativeKey.IsFollowedByPrintableCharOrSysCharMessage()) { + // Initialize mCommittedCharsAndModifiers with following char messages. + aNativeKey.InitCommittedCharsAndModifiersWithFollowingCharMessages(); + MOZ_ASSERT(!aNativeKey.mCommittedCharsAndModifiers.IsEmpty()); + + // Currently, we are doing a ugly hack to keypress events to cause + // inputting character even if Ctrl or Alt key is pressed, that is, we + // remove Ctrl and Alt modifier state from keypress event. However, for + // example, Ctrl+Space which causes ' ' of WM_CHAR message never causes + // keypress event whose ctrlKey is true. For preventing this problem, + // we should mark as not removable if Ctrl or Alt key does not cause + // changing inputting character. + if (IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode) && + (aNativeKey.IsControl() ^ aNativeKey.IsAlt())) { + ModifierKeyState state = aNativeKey.ModifierKeyStateRef(); + state.Unset(MODIFIER_ALT | MODIFIER_CONTROL); + UniCharsAndModifiers charsWithoutModifier = + GetUniCharsAndModifiers(aNativeKey.GenericVirtualKeyCode(), state); + aNativeKey.mCanIgnoreModifierStateAtKeyPress = + !charsWithoutModifier.UniCharsEqual( + aNativeKey.mCommittedCharsAndModifiers); + } + } else { + aNativeKey.mCommittedCharsAndModifiers.Clear(); + } + aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING; + + // If it's not in dead key sequence, we don't need to do anymore here. + if (!IsInDeadKeySequence()) { + return; + } + + // If it's in dead key sequence and dead char is inputted as is, we need to + // set the previous modifier state which is stored when preceding dead key + // is pressed. + UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers(); + aNativeKey.mCommittedCharsAndModifiers.OverwriteModifiersIfBeginsWith( + deadChars); + // Finish the dead key sequence. + DeactivateDeadKeyState(); + return; + } + + // If it's a dead key, aNativeKey will be initialized by + // MaybeInitNativeKeyAsDeadKey(). + if (MaybeInitNativeKeyAsDeadKey(aNativeKey)) { + return; + } + + // If the key is not a usual printable key, KeyboardLayout class assume that + // it's not cause dead char nor printable char. Therefore, there are nothing + // to do here fore such keys (e.g., function keys). + // However, this should keep dead key state even if non-printable key is + // pressed during a dead key sequence. + if (!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode)) { + return; + } + + MOZ_ASSERT(aNativeKey.mOriginalVirtualKeyCode != VK_PACKET, + "At handling VK_PACKET, we shouldn't refer keyboard layout"); + MOZ_ASSERT( + aNativeKey.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING, + "Printable key's key name index must be KEY_NAME_INDEX_USE_STRING"); + + // If it's in dead key handling and the pressed key causes a composite + // character, aNativeKey will be initialized by + // MaybeInitNativeKeyWithCompositeChar(). + if (MaybeInitNativeKeyWithCompositeChar(aNativeKey)) { + return; + } + + UniCharsAndModifiers baseChars = GetUniCharsAndModifiers(aNativeKey); + + // If the key press isn't related to any dead keys, initialize aNativeKey + // with the characters which should be caused by the key. + if (!IsInDeadKeySequence()) { + aNativeKey.mCommittedCharsAndModifiers = baseChars; + return; + } + + // If the key doesn't cause a composite character with preceding dead key, + // initialize aNativeKey with the dead-key character followed by current + // key's character. + UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers(); + aNativeKey.mCommittedCharsAndModifiers = deadChars + baseChars; + if (aNativeKey.IsKeyDownMessage()) { + DeactivateDeadKeyState(); + } +} + +bool KeyboardLayout::MaybeInitNativeKeyAsDeadKey(NativeKey& aNativeKey) { + // Only when it's not in dead key sequence, we can trust IsDeadKey() result. + if (!IsInDeadKeySequence() && !IsDeadKey(aNativeKey)) { + return false; + } + + // When keydown message is followed by a dead char message, it should be + // initialized as dead key. + bool isDeadKeyDownEvent = + aNativeKey.IsKeyDownMessage() && aNativeKey.IsFollowedByDeadCharMessage(); + + // When keyup message is received, let's check if it's one of preceding + // dead keys because keydown message order and keyup message order may be + // different. + bool isDeadKeyUpEvent = + !aNativeKey.IsKeyDownMessage() && + mActiveDeadKeys.Contains(aNativeKey.GenericVirtualKeyCode()); + + if (isDeadKeyDownEvent || isDeadKeyUpEvent) { + ActivateDeadKeyState(aNativeKey); + // Any dead key events don't generate characters. So, a dead key should + // cause only keydown event and keyup event whose KeyboardEvent.key + // values are "Dead". + aNativeKey.mCommittedCharsAndModifiers.Clear(); + aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_Dead; + return true; + } + + // At keydown message handling, we need to forget the first dead key + // because there is no guarantee coming WM_KEYUP for the second dead + // key before next WM_KEYDOWN. E.g., due to auto key repeat or pressing + // another dead key before releasing current key. Therefore, we can + // set only a character for current key for keyup event. + if (!IsInDeadKeySequence()) { + aNativeKey.mCommittedCharsAndModifiers = + GetUniCharsAndModifiers(aNativeKey); + return true; + } + + // When non-printable key event comes during a dead key sequence, that must + // be a modifier key event. So, such events shouldn't be handled as a part + // of the dead key sequence. + if (!IsDeadKey(aNativeKey)) { + return false; + } + + // FYI: Following code may run when the user doesn't input text actually + // but the key sequence is a dead key sequence. For example, + // ` -> Ctrl+` with Spanish keyboard layout. Let's keep using this + // complicated code for now because this runs really rarely. + + // Dead key followed by another dead key may cause a composed character + // (e.g., "Russian - Mnemonic" keyboard layout's 's' -> 'c'). + if (MaybeInitNativeKeyWithCompositeChar(aNativeKey)) { + return true; + } + + // Otherwise, dead key followed by another dead key causes inputting both + // character. + UniCharsAndModifiers prevDeadChars = GetDeadUniCharsAndModifiers(); + UniCharsAndModifiers newChars = GetUniCharsAndModifiers(aNativeKey); + // But keypress events should be fired for each committed character. + aNativeKey.mCommittedCharsAndModifiers = prevDeadChars + newChars; + if (aNativeKey.IsKeyDownMessage()) { + DeactivateDeadKeyState(); + } + return true; +} + +bool KeyboardLayout::MaybeInitNativeKeyWithCompositeChar( + NativeKey& aNativeKey) { + if (!IsInDeadKeySequence()) { + return false; + } + + if (NS_WARN_IF(!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode))) { + return false; + } + + UniCharsAndModifiers baseChars = GetUniCharsAndModifiers(aNativeKey); + if (baseChars.IsEmpty() || !baseChars.CharAt(0)) { + return false; + } + + char16_t compositeChar = GetCompositeChar(baseChars.CharAt(0)); + if (!compositeChar) { + return false; + } + + // Active dead-key and base character does produce exactly one composite + // character. + aNativeKey.mCommittedCharsAndModifiers.Append(compositeChar, + baseChars.ModifiersAt(0)); + if (aNativeKey.IsKeyDownMessage()) { + DeactivateDeadKeyState(); + } + return true; +} + +UniCharsAndModifiers KeyboardLayout::GetUniCharsAndModifiers( + uint8_t aVirtualKey, VirtualKey::ShiftState aShiftState) const { + UniCharsAndModifiers result; + int32_t key = GetKeyIndex(aVirtualKey); + if (key < 0) { + return result; + } + return mVirtualKeys[key].GetUniChars(aShiftState); +} + +UniCharsAndModifiers KeyboardLayout::GetDeadUniCharsAndModifiers() const { + MOZ_RELEASE_ASSERT(mActiveDeadKeys.Length() == mDeadKeyShiftStates.Length()); + + if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) { + return UniCharsAndModifiers(); + } + + UniCharsAndModifiers result; + for (size_t i = 0; i < mActiveDeadKeys.Length(); ++i) { + result += + GetUniCharsAndModifiers(mActiveDeadKeys[i], mDeadKeyShiftStates[i]); + } + return result; +} + +char16_t KeyboardLayout::GetCompositeChar(char16_t aBaseChar) const { + if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) { + return 0; + } + // XXX Currently, we don't support computing a composite character with + // two or more dead keys since it needs big table for supporting + // long chained dead keys. However, this should be a minor bug + // because this runs only when the latest keydown event does not cause + // WM_(SYS)CHAR messages. So, when user wants to input a character, + // this path never runs. + if (mActiveDeadKeys.Length() > 1) { + return 0; + } + int32_t key = GetKeyIndex(mActiveDeadKeys[0]); + if (key < 0) { + return 0; + } + return mVirtualKeys[key].GetCompositeChar(mDeadKeyShiftStates[0], aBaseChar); +} + +// static +HKL KeyboardLayout::GetActiveLayout() { return GetInstance()->mKeyboardLayout; } + +// static +nsCString KeyboardLayout::GetActiveLayoutName() { + return GetInstance()->GetLayoutName(GetActiveLayout()); +} + +static bool IsValidKeyboardLayoutsChild(const nsAString& aChildName) { + if (aChildName.Length() != 8) { + return false; + } + for (size_t i = 0; i < aChildName.Length(); i++) { + if ((aChildName[i] >= '0' && aChildName[i] <= '9') || + (aChildName[i] >= 'a' && aChildName[i] <= 'f') || + (aChildName[i] >= 'A' && aChildName[i] <= 'F')) { + continue; + } + return false; + } + return true; +} + +nsCString KeyboardLayout::GetLayoutName(HKL aLayout) const { + const wchar_t kKeyboardLayouts[] = + L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\"; + uint16_t language = reinterpret_cast(aLayout) & 0xFFFF; + uint16_t layout = (reinterpret_cast(aLayout) >> 16) & 0xFFFF; + // If the layout is less than 0xA000XXXX (normal keyboard layout for the + // language) or 0xEYYYXXXX (IMM-IME), we can retrieve its name simply. + if (layout < 0xA000 || (layout & 0xF000) == 0xE000) { + nsAutoString key(kKeyboardLayouts); + key.AppendPrintf("%08" PRIXPTR, layout < 0xA000 + ? layout + : reinterpret_cast(aLayout)); + wchar_t buf[256]; + if (NS_WARN_IF(!WinUtils::GetRegistryKey( + HKEY_LOCAL_MACHINE, key.get(), L"Layout Text", buf, sizeof(buf)))) { + return "No name or too long name"_ns; + } + return NS_ConvertUTF16toUTF8(buf); + } + + if (NS_WARN_IF((layout & 0xF000) != 0xF000)) { + nsCString result; + result.AppendPrintf("Odd HKL: 0x%08" PRIXPTR, + reinterpret_cast(aLayout)); + return result; + } + + // Otherwise, we need to walk the registry under "Keyboard Layouts". + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (NS_WARN_IF(!regKey)) { + return ""_ns; + } + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, + nsString(kKeyboardLayouts), nsIWindowsRegKey::ACCESS_READ); + if (NS_WARN_IF(NS_FAILED(rv))) { + return ""_ns; + } + uint32_t childCount = 0; + if (NS_WARN_IF(NS_FAILED(regKey->GetChildCount(&childCount))) || + NS_WARN_IF(!childCount)) { + return ""_ns; + } + for (uint32_t i = 0; i < childCount; i++) { + nsAutoString childName; + if (NS_WARN_IF(NS_FAILED(regKey->GetChildName(i, childName))) || + !IsValidKeyboardLayoutsChild(childName)) { + continue; + } + uint32_t childNum = static_cast(childName.ToInteger64(&rv, 16)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + // Ignore normal keyboard layouts for each language. + if (childNum <= 0xFFFF) { + continue; + } + // If it doesn't start with 'A' nor 'a', language should be matched. + if ((childNum & 0xFFFF) != language && + (childNum & 0xF0000000) != 0xA0000000) { + continue; + } + // Then, the child should have "Layout Id" which is "YYY" of 0xFYYYXXXX. + nsAutoString key(kKeyboardLayouts); + key += childName; + wchar_t buf[256]; + if (NS_WARN_IF(!WinUtils::GetRegistryKey(HKEY_LOCAL_MACHINE, key.get(), + L"Layout Id", buf, sizeof(buf)))) { + continue; + } + uint16_t layoutId = wcstol(buf, nullptr, 16); + if (layoutId != (layout & 0x0FFF)) { + continue; + } + if (NS_WARN_IF(!WinUtils::GetRegistryKey( + HKEY_LOCAL_MACHINE, key.get(), L"Layout Text", buf, sizeof(buf)))) { + continue; + } + return NS_ConvertUTF16toUTF8(buf); + } + return ""_ns; +} + +void KeyboardLayout::LoadLayout(HKL aLayout) { + mIsPendingToRestoreKeyboardLayout = false; + + if (mKeyboardLayout == aLayout) { + return; + } + + mKeyboardLayout = aLayout; + mHasAltGr = false; + + MOZ_LOG(gKeyLog, LogLevel::Info, + ("KeyboardLayout::LoadLayout(aLayout=0x%p (%s))", aLayout, + GetLayoutName(aLayout).get())); + + BYTE kbdState[256]; + memset(kbdState, 0, sizeof(kbdState)); + + BYTE originalKbdState[256]; + // Bitfield with all shift states that have at least one dead-key. + uint16_t shiftStatesWithDeadKeys = 0; + // Bitfield with all shift states that produce any possible dead-key base + // characters. + uint16_t shiftStatesWithBaseChars = 0; + + mActiveDeadKeys.Clear(); + mDeadKeyShiftStates.Clear(); + + ReleaseDeadKeyTables(); + + ::GetKeyboardState(originalKbdState); + + // For each shift state gather all printable characters that are produced + // for normal case when no any dead-key is active. + + for (VirtualKey::ShiftState shiftState = 0; shiftState < 16; shiftState++) { + VirtualKey::FillKbdState(kbdState, shiftState); + bool isAltGr = VirtualKey::IsAltGrIndex(shiftState); + for (uint32_t virtualKey = 0; virtualKey < 256; virtualKey++) { + int32_t vki = GetKeyIndex(virtualKey); + if (vki < 0) { + continue; + } + NS_ASSERTION(uint32_t(vki) < ArrayLength(mVirtualKeys), "invalid index"); + char16_t uniChars[5]; + int32_t ret = ::ToUnicodeEx(virtualKey, 0, kbdState, (LPWSTR)uniChars, + ArrayLength(uniChars), 0, mKeyboardLayout); + // dead-key + if (ret < 0) { + shiftStatesWithDeadKeys |= (1 << shiftState); + // Repeat dead-key to deactivate it and get its character + // representation. + char16_t deadChar[2]; + ret = ::ToUnicodeEx(virtualKey, 0, kbdState, (LPWSTR)deadChar, + ArrayLength(deadChar), 0, mKeyboardLayout); + NS_ASSERTION(ret == 2, "Expecting twice repeated dead-key character"); + mVirtualKeys[vki].SetDeadChar(shiftState, deadChar[0]); + + MOZ_LOG(gKeyLog, LogLevel::Verbose, + (" %s (%d): DeadChar(%s, %s) (ret=%d)", + kVirtualKeyName[virtualKey], vki, + GetShiftStateName(shiftState).get(), + GetCharacterCodeNames(deadChar, 1).get(), ret)); + } else { + if (ret == 1) { + // dead-key can pair only with exactly one base character. + shiftStatesWithBaseChars |= (1 << shiftState); + } + mVirtualKeys[vki].SetNormalChars(shiftState, uniChars, ret); + MOZ_LOG(gKeyLog, LogLevel::Verbose, + (" %s (%d): NormalChar(%s, %s) (ret=%d)", + kVirtualKeyName[virtualKey], vki, + GetShiftStateName(shiftState).get(), + GetCharacterCodeNames(uniChars, ret).get(), ret)); + } + + // If the key inputs at least one character with AltGr modifier, + // check if AltGr changes inputting character. If it does, mark + // this keyboard layout has AltGr modifier actually. + if (!mHasAltGr && ret > 0 && isAltGr && + mVirtualKeys[vki].IsChangedByAltGr(shiftState)) { + mHasAltGr = true; + MOZ_LOG(gKeyLog, LogLevel::Info, + (" Found a key (%s) changed by AltGr: %s -> %s (%s) (ret=%d)", + kVirtualKeyName[virtualKey], + GetCharacterCodeNames( + mVirtualKeys[vki].GetNativeUniChars( + shiftState - VirtualKey::ShiftStateIndex::eAltGr)) + .get(), + GetCharacterCodeNames( + mVirtualKeys[vki].GetNativeUniChars(shiftState)) + .get(), + GetShiftStateName(shiftState).get(), ret)); + } + } + } + + // Now process each dead-key to find all its base characters and resulting + // composite characters. + for (VirtualKey::ShiftState shiftState = 0; shiftState < 16; shiftState++) { + if (!(shiftStatesWithDeadKeys & (1 << shiftState))) { + continue; + } + + VirtualKey::FillKbdState(kbdState, shiftState); + + for (uint32_t virtualKey = 0; virtualKey < 256; virtualKey++) { + int32_t vki = GetKeyIndex(virtualKey); + if (vki >= 0 && mVirtualKeys[vki].IsDeadKey(shiftState)) { + DeadKeyEntry deadKeyArray[256]; + int32_t n = GetDeadKeyCombinations( + virtualKey, kbdState, shiftStatesWithBaseChars, deadKeyArray, + ArrayLength(deadKeyArray)); + const DeadKeyTable* dkt = + mVirtualKeys[vki].MatchingDeadKeyTable(deadKeyArray, n); + if (!dkt) { + dkt = AddDeadKeyTable(deadKeyArray, n); + } + mVirtualKeys[vki].AttachDeadKeyTable(shiftState, dkt); + } + } + } + + ::SetKeyboardState(originalKbdState); + + if (MOZ_LOG_TEST(gKeyLog, LogLevel::Verbose)) { + static const UINT kExtendedScanCode[] = {0x0000, 0xE000}; + static const UINT kMapType = MAPVK_VSC_TO_VK_EX; + MOZ_LOG(gKeyLog, LogLevel::Verbose, + ("Logging virtual keycode values for scancode (0x%p)...", + mKeyboardLayout)); + for (uint32_t i = 0; i < ArrayLength(kExtendedScanCode); i++) { + for (uint32_t j = 1; j <= 0xFF; j++) { + UINT scanCode = kExtendedScanCode[i] + j; + UINT virtualKeyCode = + ::MapVirtualKeyEx(scanCode, kMapType, mKeyboardLayout); + MOZ_LOG(gKeyLog, LogLevel::Verbose, + ("0x%04X, %s", scanCode, kVirtualKeyName[virtualKeyCode])); + } + } + } + + MOZ_LOG(gKeyLog, LogLevel::Info, + (" AltGr key is %s in %s", mHasAltGr ? "found" : "not found", + GetLayoutName(aLayout).get())); +} + +inline int32_t KeyboardLayout::GetKeyIndex(uint8_t aVirtualKey) { + // Currently these 68 (NS_NUM_OF_KEYS) virtual keys are assumed + // to produce visible representation: + // 0x20 - VK_SPACE ' ' + // 0x30..0x39 '0'..'9' + // 0x41..0x5A 'A'..'Z' + // 0x60..0x69 '0'..'9' on numpad + // 0x6A - VK_MULTIPLY '*' on numpad + // 0x6B - VK_ADD '+' on numpad + // 0x6D - VK_SUBTRACT '-' on numpad + // 0x6E - VK_DECIMAL '.' on numpad + // 0x6F - VK_DIVIDE '/' on numpad + // 0x6E - VK_DECIMAL '.' + // 0xBA - VK_OEM_1 ';:' for US + // 0xBB - VK_OEM_PLUS '+' any country + // 0xBC - VK_OEM_COMMA ',' any country + // 0xBD - VK_OEM_MINUS '-' any country + // 0xBE - VK_OEM_PERIOD '.' any country + // 0xBF - VK_OEM_2 '/?' for US + // 0xC0 - VK_OEM_3 '`~' for US + // 0xC1 - VK_ABNT_C1 '/?' for Brazilian + // 0xC2 - VK_ABNT_C2 separator key on numpad (Brazilian or JIS for Mac) + // 0xDB - VK_OEM_4 '[{' for US + // 0xDC - VK_OEM_5 '\|' for US + // 0xDD - VK_OEM_6 ']}' for US + // 0xDE - VK_OEM_7 ''"' for US + // 0xDF - VK_OEM_8 + // 0xE1 - no name + // 0xE2 - VK_OEM_102 '\_' for JIS + // 0xE3 - no name + // 0xE4 - no name + + static const int8_t xlat[256] = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + //----------------------------------------------------------------------- + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10 + 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1, -1, -1, -1, -1, -1, // 30 + -1, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // 40 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, -1, -1, -1, -1, -1, // 50 + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, -1, 49, 50, 51, // 60 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 70 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 90 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // A0 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 52, 53, 54, 55, 56, 57, // B0 + 58, 59, 60, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // C0 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 61, 62, 63, 64, 65, // D0 + -1, 66, 67, 68, 69, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // E0 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 // F0 + }; + + return xlat[aVirtualKey]; +} + +int KeyboardLayout::CompareDeadKeyEntries(const void* aArg1, const void* aArg2, + void*) { + const DeadKeyEntry* arg1 = static_cast(aArg1); + const DeadKeyEntry* arg2 = static_cast(aArg2); + + return arg1->BaseChar - arg2->BaseChar; +} + +const DeadKeyTable* KeyboardLayout::AddDeadKeyTable( + const DeadKeyEntry* aDeadKeyArray, uint32_t aEntries) { + DeadKeyTableListEntry* next = mDeadKeyTableListHead; + + const size_t bytes = offsetof(DeadKeyTableListEntry, data) + + DeadKeyTable::SizeInBytes(aEntries); + uint8_t* p = new uint8_t[bytes]; + + mDeadKeyTableListHead = reinterpret_cast(p); + mDeadKeyTableListHead->next = next; + + DeadKeyTable* dkt = + reinterpret_cast(mDeadKeyTableListHead->data); + + dkt->Init(aDeadKeyArray, aEntries); + + return dkt; +} + +void KeyboardLayout::ReleaseDeadKeyTables() { + while (mDeadKeyTableListHead) { + uint8_t* p = reinterpret_cast(mDeadKeyTableListHead); + mDeadKeyTableListHead = mDeadKeyTableListHead->next; + + delete[] p; + } +} + +bool KeyboardLayout::EnsureDeadKeyActive(bool aIsActive, uint8_t aDeadKey, + const PBYTE aDeadKeyKbdState) { + int32_t ret; + do { + char16_t dummyChars[5]; + ret = + ::ToUnicodeEx(aDeadKey, 0, (PBYTE)aDeadKeyKbdState, (LPWSTR)dummyChars, + ArrayLength(dummyChars), 0, mKeyboardLayout); + // returned values: + // <0 - Dead key state is active. The keyboard driver will wait for next + // character. + // 1 - Previous pressed key was a valid base character that produced + // exactly one composite character. + // >1 - Previous pressed key does not produce any composite characters. + // Return dead-key character followed by base character(s). + } while ((ret < 0) != aIsActive); + + return (ret < 0); +} + +void KeyboardLayout::ActivateDeadKeyState(const NativeKey& aNativeKey) { + // Dead-key state should be activated at keydown. + if (!aNativeKey.IsKeyDownMessage()) { + return; + } + + mActiveDeadKeys.AppendElement(aNativeKey.mOriginalVirtualKeyCode); + mDeadKeyShiftStates.AppendElement(aNativeKey.GetShiftState()); +} + +void KeyboardLayout::DeactivateDeadKeyState() { + if (mActiveDeadKeys.IsEmpty()) { + return; + } + + BYTE kbdState[256]; + memset(kbdState, 0, sizeof(kbdState)); + + // Assume that the last dead key can finish dead key sequence. + VirtualKey::FillKbdState(kbdState, mDeadKeyShiftStates.LastElement()); + EnsureDeadKeyActive(false, mActiveDeadKeys.LastElement(), kbdState); + mActiveDeadKeys.Clear(); + mDeadKeyShiftStates.Clear(); +} + +bool KeyboardLayout::AddDeadKeyEntry(char16_t aBaseChar, + char16_t aCompositeChar, + DeadKeyEntry* aDeadKeyArray, + uint32_t aEntries) { + for (uint32_t index = 0; index < aEntries; index++) { + if (aDeadKeyArray[index].BaseChar == aBaseChar) { + return false; + } + } + + aDeadKeyArray[aEntries].BaseChar = aBaseChar; + aDeadKeyArray[aEntries].CompositeChar = aCompositeChar; + + return true; +} + +uint32_t KeyboardLayout::GetDeadKeyCombinations( + uint8_t aDeadKey, const PBYTE aDeadKeyKbdState, + uint16_t aShiftStatesWithBaseChars, DeadKeyEntry* aDeadKeyArray, + uint32_t aMaxEntries) { + bool deadKeyActive = false; + uint32_t entries = 0; + BYTE kbdState[256]; + memset(kbdState, 0, sizeof(kbdState)); + + for (uint32_t shiftState = 0; shiftState < 16; shiftState++) { + if (!(aShiftStatesWithBaseChars & (1 << shiftState))) { + continue; + } + + VirtualKey::FillKbdState(kbdState, shiftState); + + for (uint32_t virtualKey = 0; virtualKey < 256; virtualKey++) { + int32_t vki = GetKeyIndex(virtualKey); + // Dead-key can pair only with such key that produces exactly one base + // character. + if (vki >= 0 && + mVirtualKeys[vki].GetNativeUniChars(shiftState).Length() == 1) { + // Ensure dead-key is in active state, when it swallows entered + // character and waits for the next pressed key. + if (!deadKeyActive) { + deadKeyActive = EnsureDeadKeyActive(true, aDeadKey, aDeadKeyKbdState); + } + + // Depending on the character the followed the dead-key, the keyboard + // driver can produce one composite character, or a dead-key character + // followed by a second character. + char16_t compositeChars[5]; + int32_t ret = + ::ToUnicodeEx(virtualKey, 0, kbdState, (LPWSTR)compositeChars, + ArrayLength(compositeChars), 0, mKeyboardLayout); + switch (ret) { + case 0: + // This key combination does not produce any characters. The + // dead-key is still in active state. + break; + case 1: { + char16_t baseChars[5]; + ret = ::ToUnicodeEx(virtualKey, 0, kbdState, (LPWSTR)baseChars, + ArrayLength(baseChars), 0, mKeyboardLayout); + if (entries < aMaxEntries) { + switch (ret) { + case 1: + // Exactly one composite character produced. Now, when + // dead-key is not active, repeat the last character one more + // time to determine the base character. + if (AddDeadKeyEntry(baseChars[0], compositeChars[0], + aDeadKeyArray, entries)) { + entries++; + } + deadKeyActive = false; + break; + case -1: { + // If pressing another dead-key produces different character, + // we should register the dead-key entry with first character + // produced by current key. + + // First inactivate the dead-key state completely. + deadKeyActive = + EnsureDeadKeyActive(false, aDeadKey, aDeadKeyKbdState); + if (NS_WARN_IF(deadKeyActive)) { + MOZ_LOG(gKeyLog, LogLevel::Error, + (" failed to deactivating the dead-key state...")); + break; + } + for (int32_t i = 0; i < 5; ++i) { + ret = ::ToUnicodeEx( + virtualKey, 0, kbdState, (LPWSTR)baseChars, + ArrayLength(baseChars), 0, mKeyboardLayout); + if (ret >= 0) { + break; + } + } + if (ret > 0 && + AddDeadKeyEntry(baseChars[0], compositeChars[0], + aDeadKeyArray, entries)) { + entries++; + } + // Inactivate dead-key state for current virtual keycode. + EnsureDeadKeyActive(false, virtualKey, kbdState); + break; + } + default: + NS_WARNING("File a bug for this dead-key handling!"); + deadKeyActive = false; + break; + } + } + MOZ_LOG( + gKeyLog, LogLevel::Verbose, + (" %s -> %s (%d): DeadKeyEntry(%s, %s) (ret=%d)", + kVirtualKeyName[aDeadKey], kVirtualKeyName[virtualKey], vki, + GetCharacterCodeNames(compositeChars, 1).get(), + ret <= 0 + ? "''" + : GetCharacterCodeNames(baseChars, std::min(ret, 5)).get(), + ret)); + break; + } + default: + // 1. Unexpected dead-key. Dead-key chaining is not supported. + // 2. More than one character generated. This is not a valid + // dead-key and base character combination. + deadKeyActive = false; + MOZ_LOG( + gKeyLog, LogLevel::Verbose, + (" %s -> %s (%d): Unsupport dead key type(%s) (ret=%d)", + kVirtualKeyName[aDeadKey], kVirtualKeyName[virtualKey], vki, + ret <= 0 + ? "''" + : GetCharacterCodeNames(compositeChars, std::min(ret, 5)) + .get(), + ret)); + break; + } + } + } + } + + if (deadKeyActive) { + deadKeyActive = EnsureDeadKeyActive(false, aDeadKey, aDeadKeyKbdState); + } + + NS_QuickSort(aDeadKeyArray, entries, sizeof(DeadKeyEntry), + CompareDeadKeyEntries, nullptr); + return entries; +} + +uint32_t KeyboardLayout::ConvertNativeKeyCodeToDOMKeyCode( + UINT aNativeKeyCode) const { + // Alphabet or Numeric or Numpad or Function keys + if ((aNativeKeyCode >= 0x30 && aNativeKeyCode <= 0x39) || + (aNativeKeyCode >= 0x41 && aNativeKeyCode <= 0x5A) || + (aNativeKeyCode >= 0x60 && aNativeKeyCode <= 0x87)) { + return static_cast(aNativeKeyCode); + } + switch (aNativeKeyCode) { + // Following keycodes are same as our DOM keycodes + case VK_CANCEL: + case VK_BACK: + case VK_TAB: + case VK_CLEAR: + case VK_RETURN: + case VK_SHIFT: + case VK_CONTROL: + case VK_MENU: // Alt + case VK_PAUSE: + case VK_CAPITAL: // CAPS LOCK + case VK_KANA: // same as VK_HANGUL + case VK_JUNJA: + case VK_FINAL: + case VK_HANJA: // same as VK_KANJI + case VK_ESCAPE: + case VK_CONVERT: + case VK_NONCONVERT: + case VK_ACCEPT: + case VK_MODECHANGE: + case VK_SPACE: + case VK_PRIOR: // PAGE UP + case VK_NEXT: // PAGE DOWN + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_SELECT: + case VK_PRINT: + case VK_EXECUTE: + case VK_SNAPSHOT: + case VK_INSERT: + case VK_DELETE: + case VK_APPS: // Context Menu + case VK_SLEEP: + case VK_NUMLOCK: + case VK_SCROLL: // SCROLL LOCK + case VK_ATTN: // Attension key of IBM midrange computers, e.g., AS/400 + case VK_CRSEL: // Cursor Selection + case VK_EXSEL: // Extend Selection + case VK_EREOF: // Erase EOF key of IBM 3270 keyboard layout + case VK_PLAY: + case VK_ZOOM: + case VK_PA1: // PA1 key of IBM 3270 keyboard layout + return uint32_t(aNativeKeyCode); + + case VK_HELP: + return NS_VK_HELP; + + // Windows key should be mapped to a Win keycode + // They should be able to be distinguished by DOM3 KeyboardEvent.location + case VK_LWIN: + case VK_RWIN: + return NS_VK_WIN; + + case VK_VOLUME_MUTE: + return NS_VK_VOLUME_MUTE; + case VK_VOLUME_DOWN: + return NS_VK_VOLUME_DOWN; + case VK_VOLUME_UP: + return NS_VK_VOLUME_UP; + + case VK_LSHIFT: + case VK_RSHIFT: + return NS_VK_SHIFT; + + case VK_LCONTROL: + case VK_RCONTROL: + return NS_VK_CONTROL; + + // Note that even if the key is AltGr, we should return NS_VK_ALT for + // compatibility with both older Gecko and the other browsers. + case VK_LMENU: + case VK_RMENU: + return NS_VK_ALT; + + // Following keycodes are not defined in our DOM keycodes. + case VK_BROWSER_BACK: + case VK_BROWSER_FORWARD: + case VK_BROWSER_REFRESH: + case VK_BROWSER_STOP: + case VK_BROWSER_SEARCH: + case VK_BROWSER_FAVORITES: + case VK_BROWSER_HOME: + case VK_MEDIA_NEXT_TRACK: + case VK_MEDIA_PREV_TRACK: + case VK_MEDIA_STOP: + case VK_MEDIA_PLAY_PAUSE: + case VK_LAUNCH_MAIL: + case VK_LAUNCH_MEDIA_SELECT: + case VK_LAUNCH_APP1: + case VK_LAUNCH_APP2: + return 0; + + // Following OEM specific virtual keycodes should pass through DOM keyCode + // for compatibility with the other browsers on Windows. + + // Following OEM specific virtual keycodes are defined for Fujitsu/OASYS. + case VK_OEM_FJ_JISHO: + case VK_OEM_FJ_MASSHOU: + case VK_OEM_FJ_TOUROKU: + case VK_OEM_FJ_LOYA: + case VK_OEM_FJ_ROYA: + // Not sure what means "ICO". + case VK_ICO_HELP: + case VK_ICO_00: + case VK_ICO_CLEAR: + // Following OEM specific virtual keycodes are defined for Nokia/Ericsson. + case VK_OEM_RESET: + case VK_OEM_JUMP: + case VK_OEM_PA1: + case VK_OEM_PA2: + case VK_OEM_PA3: + case VK_OEM_WSCTRL: + case VK_OEM_CUSEL: + case VK_OEM_ATTN: + case VK_OEM_FINISH: + case VK_OEM_COPY: + case VK_OEM_AUTO: + case VK_OEM_ENLW: + case VK_OEM_BACKTAB: + // VK_OEM_CLEAR is defined as not OEM specific, but let's pass though + // DOM keyCode like other OEM specific virtual keycodes. + case VK_OEM_CLEAR: + return uint32_t(aNativeKeyCode); + + // 0xE1 is an OEM specific virtual keycode. However, the value is already + // used in our DOM keyCode for AltGr on Linux. So, this virtual keycode + // cannot pass through DOM keyCode. + case 0xE1: + return 0; + + // Following keycodes are OEM keys which are keycodes for non-alphabet and + // non-numeric keys, we should compute each keycode of them from unshifted + // character which is inputted by each key. But if the unshifted character + // is not an ASCII character but shifted character is an ASCII character, + // we should refer it. + case VK_OEM_1: + case VK_OEM_PLUS: + case VK_OEM_COMMA: + case VK_OEM_MINUS: + case VK_OEM_PERIOD: + case VK_OEM_2: + case VK_OEM_3: + case VK_OEM_4: + case VK_OEM_5: + case VK_OEM_6: + case VK_OEM_7: + case VK_OEM_8: + case VK_OEM_102: + case VK_ABNT_C1: { + NS_ASSERTION(IsPrintableCharKey(aNativeKeyCode), + "The key must be printable"); + ModifierKeyState modKeyState(0); + UniCharsAndModifiers uniChars = + GetUniCharsAndModifiers(aNativeKeyCode, modKeyState); + if (uniChars.Length() != 1 || uniChars.CharAt(0) < ' ' || + uniChars.CharAt(0) > 0x7F) { + modKeyState.Set(MODIFIER_SHIFT); + uniChars = GetUniCharsAndModifiers(aNativeKeyCode, modKeyState); + if (uniChars.Length() != 1 || uniChars.CharAt(0) < ' ' || + uniChars.CharAt(0) > 0x7F) { + // In this case, we've returned 0 in this case for long time because + // we decided that we should avoid setting same keyCode value to 2 or + // more keys since active keyboard layout may have a key to input the + // punctuation with different key. However, setting keyCode to 0 + // makes some web applications which are aware of neither + // KeyboardEvent.key nor KeyboardEvent.code not work with Firefox + // when user selects non-ASCII capable keyboard layout such as + // Russian and Thai layout. So, let's decide keyCode value with + // major keyboard layout's key which causes the OEM keycode. + // Actually, this maps same keyCode value to 2 keys on Russian + // keyboard layout. "Period" key causes VK_OEM_PERIOD but inputs + // Yu of Cyrillic and "Slash" key causes VK_OEM_2 (same as US + // keyboard layout) but inputs "." (period of ASCII). Therefore, + // we return DOM_VK_PERIOD which is same as VK_OEM_PERIOD for + // "Period" key. On the other hand, we use same keyCode value for + // "Slash" key too because it inputs ".". + CodeNameIndex code; + switch (aNativeKeyCode) { + case VK_OEM_1: + code = CODE_NAME_INDEX_Semicolon; + break; + case VK_OEM_PLUS: + code = CODE_NAME_INDEX_Equal; + break; + case VK_OEM_COMMA: + code = CODE_NAME_INDEX_Comma; + break; + case VK_OEM_MINUS: + code = CODE_NAME_INDEX_Minus; + break; + case VK_OEM_PERIOD: + code = CODE_NAME_INDEX_Period; + break; + case VK_OEM_2: + code = CODE_NAME_INDEX_Slash; + break; + case VK_OEM_3: + code = CODE_NAME_INDEX_Backquote; + break; + case VK_OEM_4: + code = CODE_NAME_INDEX_BracketLeft; + break; + case VK_OEM_5: + code = CODE_NAME_INDEX_Backslash; + break; + case VK_OEM_6: + code = CODE_NAME_INDEX_BracketRight; + break; + case VK_OEM_7: + code = CODE_NAME_INDEX_Quote; + break; + case VK_OEM_8: + // Use keyCode value for "Backquote" key on UK keyboard layout. + code = CODE_NAME_INDEX_Backquote; + break; + case VK_OEM_102: + // Use keyCode value for "IntlBackslash" key. + code = CODE_NAME_INDEX_IntlBackslash; + break; + case VK_ABNT_C1: // "/" of ABNT. + // Use keyCode value for "IntlBackslash" key on ABNT keyboard + // layout. + code = CODE_NAME_INDEX_IntlBackslash; + break; + default: + MOZ_ASSERT_UNREACHABLE("Handle all OEM keycode values"); + return 0; + } + return WidgetKeyboardEvent::GetFallbackKeyCodeOfPunctuationKey(code); + } + } + return WidgetUtils::ComputeKeyCodeFromChar(uniChars.CharAt(0)); + } + + // IE sets 0xC2 to the DOM keyCode for VK_ABNT_C2. However, we're already + // using NS_VK_SEPARATOR for the separator key on Mac and Linux. Therefore, + // We should keep consistency between Gecko on all platforms rather than + // with other browsers since a lot of keyCode values are already different + // between browsers. + case VK_ABNT_C2: + return NS_VK_SEPARATOR; + + // VK_PROCESSKEY means IME already consumed the key event. + case VK_PROCESSKEY: + return NS_VK_PROCESSKEY; + // VK_PACKET is generated by SendInput() API, we don't need to + // care this message as key event. + case VK_PACKET: + return 0; + // If a key is not mapped to a virtual keycode, 0xFF is used. + case 0xFF: + NS_WARNING("The key is failed to be converted to a virtual keycode"); + return 0; + } +#ifdef DEBUG + nsPrintfCString warning( + "Unknown virtual keycode (0x%08X), please check the " + "latest MSDN document, there may be some new " + "keycodes we've never known.", + aNativeKeyCode); + NS_WARNING(warning.get()); +#endif + return 0; +} + +KeyNameIndex KeyboardLayout::ConvertNativeKeyCodeToKeyNameIndex( + uint8_t aVirtualKey) const { + if (IsPrintableCharKey(aVirtualKey) || aVirtualKey == VK_PACKET) { + return KEY_NAME_INDEX_USE_STRING; + } + + // If the keyboard layout has AltGr and AltRight key is pressed, + // return AltGraph. + if (aVirtualKey == VK_RMENU && HasAltGr()) { + return KEY_NAME_INDEX_AltGraph; + } + + switch (aVirtualKey) { +#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX +#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \ + case aNativeKey: \ + return aKeyNameIndex; + +#include "NativeKeyToDOMKeyName.h" + +#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX + + default: + break; + } + + HKL layout = GetLayout(); + WORD langID = LOWORD(static_cast(layout)); + WORD primaryLangID = PRIMARYLANGID(langID); + + if (primaryLangID == LANG_JAPANESE) { + switch (aVirtualKey) { +#undef NS_JAPANESE_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX +#define NS_JAPANESE_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, \ + aKeyNameIndex) \ + case aNativeKey: \ + return aKeyNameIndex; + +#include "NativeKeyToDOMKeyName.h" + +#undef NS_JAPANESE_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX + + default: + break; + } + } else if (primaryLangID == LANG_KOREAN) { + switch (aVirtualKey) { +#undef NS_KOREAN_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX +#define NS_KOREAN_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \ + case aNativeKey: \ + return aKeyNameIndex; + +#include "NativeKeyToDOMKeyName.h" + +#undef NS_KOREAN_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX + + default: + return KEY_NAME_INDEX_Unidentified; + } + } + + switch (aVirtualKey) { +#undef NS_OTHER_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX +#define NS_OTHER_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \ + case aNativeKey: \ + return aKeyNameIndex; + +#include "NativeKeyToDOMKeyName.h" + +#undef NS_OTHER_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX + + default: + return KEY_NAME_INDEX_Unidentified; + } +} + +// static +CodeNameIndex KeyboardLayout::ConvertScanCodeToCodeNameIndex(UINT aScanCode) { + switch (aScanCode) { +#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \ + case aNativeKey: \ + return aCodeNameIndex; + +#include "NativeKeyToDOMCodeName.h" + +#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX + + default: + return CODE_NAME_INDEX_UNKNOWN; + } +} + +nsresult KeyboardLayout::SynthesizeNativeKeyEvent( + nsWindow* aWidget, int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode, + uint32_t aModifierFlags, const nsAString& aCharacters, + const nsAString& aUnmodifiedCharacters) { + UINT keyboardLayoutListCount = ::GetKeyboardLayoutList(0, nullptr); + NS_ASSERTION(keyboardLayoutListCount > 0, + "One keyboard layout must be installed at least"); + HKL keyboardLayoutListBuff[50]; + HKL* keyboardLayoutList = keyboardLayoutListCount < 50 + ? keyboardLayoutListBuff + : new HKL[keyboardLayoutListCount]; + keyboardLayoutListCount = + ::GetKeyboardLayoutList(keyboardLayoutListCount, keyboardLayoutList); + NS_ASSERTION(keyboardLayoutListCount > 0, + "Failed to get all keyboard layouts installed on the system"); + + nsPrintfCString layoutName("%08x", aNativeKeyboardLayout); + HKL loadedLayout = LoadKeyboardLayoutA(layoutName.get(), KLF_NOTELLSHELL); + if (loadedLayout == nullptr) { + if (keyboardLayoutListBuff != keyboardLayoutList) { + delete[] keyboardLayoutList; + } + return NS_ERROR_NOT_AVAILABLE; + } + + // Setup clean key state and load desired layout + BYTE originalKbdState[256]; + ::GetKeyboardState(originalKbdState); + BYTE kbdState[256]; + memset(kbdState, 0, sizeof(kbdState)); + // This changes the state of the keyboard for the current thread only, + // and we'll restore it soon, so this should be OK. + ::SetKeyboardState(kbdState); + + OverrideLayout(loadedLayout); + + bool isAltGrKeyPress = false; + if (aModifierFlags & nsIWidget::ALTGRAPH) { + if (!HasAltGr()) { + return NS_ERROR_INVALID_ARG; + } + // AltGr emulates ControlLeft key press and AltRight key press. + // So, we should remove those flags from aModifierFlags before + // calling WinUtils::SetupKeyModifiersSequence() to create correct + // key sequence. + // FYI: We don't support both ControlLeft and AltRight (AltGr) are + // pressed at the same time unless synthesizing key is + // VK_LCONTROL. + aModifierFlags &= ~(nsIWidget::CTRL_L | nsIWidget::ALT_R); + } + + uint8_t argumentKeySpecific = 0; + switch (aNativeKeyCode & 0xFF) { + case VK_SHIFT: + aModifierFlags &= ~(nsIWidget::SHIFT_L | nsIWidget::SHIFT_R); + argumentKeySpecific = VK_LSHIFT; + break; + case VK_LSHIFT: + aModifierFlags &= ~nsIWidget::SHIFT_L; + argumentKeySpecific = aNativeKeyCode & 0xFF; + aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_SHIFT; + break; + case VK_RSHIFT: + aModifierFlags &= ~nsIWidget::SHIFT_R; + argumentKeySpecific = aNativeKeyCode & 0xFF; + aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_SHIFT; + break; + case VK_CONTROL: + aModifierFlags &= ~(nsIWidget::CTRL_L | nsIWidget::CTRL_R); + argumentKeySpecific = VK_LCONTROL; + break; + case VK_LCONTROL: + aModifierFlags &= ~nsIWidget::CTRL_L; + argumentKeySpecific = aNativeKeyCode & 0xFF; + aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_CONTROL; + break; + case VK_RCONTROL: + aModifierFlags &= ~nsIWidget::CTRL_R; + argumentKeySpecific = aNativeKeyCode & 0xFF; + aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_CONTROL; + break; + case VK_MENU: + aModifierFlags &= ~(nsIWidget::ALT_L | nsIWidget::ALT_R); + argumentKeySpecific = VK_LMENU; + break; + case VK_LMENU: + aModifierFlags &= ~nsIWidget::ALT_L; + argumentKeySpecific = aNativeKeyCode & 0xFF; + aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_MENU; + break; + case VK_RMENU: + aModifierFlags &= ~(nsIWidget::ALT_R | nsIWidget::ALTGRAPH); + argumentKeySpecific = aNativeKeyCode & 0xFF; + aNativeKeyCode = (aNativeKeyCode & 0xFFFF0000) | VK_MENU; + // If AltRight key is AltGr in the keyboard layout, let's use + // SetupKeyModifiersSequence() to emulate the native behavior + // since the same event order between keydown and keyup makes + // the following code complicated. + if (HasAltGr()) { + isAltGrKeyPress = true; + aModifierFlags &= ~nsIWidget::CTRL_L; + aModifierFlags |= nsIWidget::ALTGRAPH; + } + break; + case VK_CAPITAL: + aModifierFlags &= ~nsIWidget::CAPS_LOCK; + argumentKeySpecific = VK_CAPITAL; + break; + case VK_NUMLOCK: + aModifierFlags &= ~nsIWidget::NUM_LOCK; + argumentKeySpecific = VK_NUMLOCK; + break; + } + + AutoTArray keySequence; + WinUtils::SetupKeyModifiersSequence(&keySequence, aModifierFlags, WM_KEYDOWN); + if (!isAltGrKeyPress) { + keySequence.AppendElement(KeyPair(aNativeKeyCode, argumentKeySpecific)); + } + + // Simulate the pressing of each modifier key and then the real key + // FYI: Each NativeKey instance here doesn't need to override keyboard layout + // since this method overrides and restores the keyboard layout. + for (uint32_t i = 0; i < keySequence.Length(); ++i) { + uint8_t key = keySequence[i].mGeneral; + uint8_t keySpecific = keySequence[i].mSpecific; + uint16_t scanCode = keySequence[i].mScanCode; + kbdState[key] = 0x81; // key is down and toggled on if appropriate + if (keySpecific) { + kbdState[keySpecific] = 0x81; + } + ::SetKeyboardState(kbdState); + ModifierKeyState modKeyState; + // If scan code isn't specified explicitly, let's compute it with current + // keyboard layout. + if (!scanCode) { + scanCode = + ComputeScanCodeForVirtualKeyCode(keySpecific ? keySpecific : key); + } + LPARAM lParam = static_cast(scanCode << 16); + // If the scan code is for an extended key, set extended key flag. + if ((scanCode & 0xFF00) == 0xE000) { + lParam |= 0x1000000; + } + // When AltGr key is pressed, both ControlLeft and AltRight cause + // WM_KEYDOWN messages. + bool makeSysKeyMsg = + !(aModifierFlags & nsIWidget::ALTGRAPH) && IsSysKey(key, modKeyState); + MSG keyDownMsg = + WinUtils::InitMSG(makeSysKeyMsg ? WM_SYSKEYDOWN : WM_KEYDOWN, key, + lParam, aWidget->GetWindowHandle()); + if (i == keySequence.Length() - 1) { + bool makeDeadCharMsg = + (IsDeadKey(key, modKeyState) && aCharacters.IsEmpty()); + nsAutoString chars(aCharacters); + if (makeDeadCharMsg) { + UniCharsAndModifiers deadChars = + GetUniCharsAndModifiers(key, modKeyState); + chars = deadChars.ToString(); + NS_ASSERTION(chars.Length() == 1, + "Dead char must be only one character"); + } + if (chars.IsEmpty()) { + NativeKey nativeKey(aWidget, keyDownMsg, modKeyState); + nativeKey.HandleKeyDownMessage(); + } else { + AutoTArray fakeCharMsgs; + for (uint32_t j = 0; j < chars.Length(); j++) { + NativeKey::FakeCharMsg* fakeCharMsg = fakeCharMsgs.AppendElement(); + fakeCharMsg->mCharCode = chars.CharAt(j); + fakeCharMsg->mScanCode = scanCode; + fakeCharMsg->mIsSysKey = makeSysKeyMsg; + fakeCharMsg->mIsDeadKey = makeDeadCharMsg; + } + NativeKey nativeKey(aWidget, keyDownMsg, modKeyState, 0, &fakeCharMsgs); + bool dispatched; + nativeKey.HandleKeyDownMessage(&dispatched); + // If some char messages are not consumed, let's emulate the widget + // receiving the message directly. + for (uint32_t j = 1; j < fakeCharMsgs.Length(); j++) { + if (fakeCharMsgs[j].mConsumed) { + continue; + } + MSG charMsg = fakeCharMsgs[j].GetCharMsg(aWidget->GetWindowHandle()); + NativeKey nativeKey(aWidget, charMsg, modKeyState); + nativeKey.HandleCharMessage(charMsg); + } + } + } else { + NativeKey nativeKey(aWidget, keyDownMsg, modKeyState); + nativeKey.HandleKeyDownMessage(); + } + } + + keySequence.Clear(); + if (!isAltGrKeyPress) { + keySequence.AppendElement(KeyPair(aNativeKeyCode, argumentKeySpecific)); + } + WinUtils::SetupKeyModifiersSequence(&keySequence, aModifierFlags, WM_KEYUP); + for (uint32_t i = 0; i < keySequence.Length(); ++i) { + uint8_t key = keySequence[i].mGeneral; + uint8_t keySpecific = keySequence[i].mSpecific; + uint16_t scanCode = keySequence[i].mScanCode; + kbdState[key] = 0; // key is up and toggled off if appropriate + if (keySpecific) { + kbdState[keySpecific] = 0; + } + ::SetKeyboardState(kbdState); + ModifierKeyState modKeyState; + // If scan code isn't specified explicitly, let's compute it with current + // keyboard layout. + if (!scanCode) { + scanCode = + ComputeScanCodeForVirtualKeyCode(keySpecific ? keySpecific : key); + } + LPARAM lParam = static_cast(scanCode << 16); + // If the scan code is for an extended key, set extended key flag. + if ((scanCode & 0xFF00) == 0xE000) { + lParam |= 0x1000000; + } + // Don't use WM_SYSKEYUP for Alt keyup. + // NOTE: When AltGr was pressed, ControlLeft causes WM_SYSKEYUP normally. + bool makeSysKeyMsg = IsSysKey(key, modKeyState) && key != VK_MENU; + MSG keyUpMsg = WinUtils::InitMSG(makeSysKeyMsg ? WM_SYSKEYUP : WM_KEYUP, + key, lParam, aWidget->GetWindowHandle()); + NativeKey nativeKey(aWidget, keyUpMsg, modKeyState); + nativeKey.HandleKeyUpMessage(); + } + + // Restore old key state and layout + ::SetKeyboardState(originalKbdState); + RestoreLayout(); + + // Don't unload the layout if it's installed actually. + for (uint32_t i = 0; i < keyboardLayoutListCount; i++) { + if (keyboardLayoutList[i] == loadedLayout) { + loadedLayout = 0; + break; + } + } + if (keyboardLayoutListBuff != keyboardLayoutList) { + delete[] keyboardLayoutList; + } + if (loadedLayout) { + ::UnloadKeyboardLayout(loadedLayout); + } + return NS_OK; +} + +/***************************************************************************** + * mozilla::widget::DeadKeyTable + *****************************************************************************/ + +char16_t DeadKeyTable::GetCompositeChar(char16_t aBaseChar) const { + // Dead-key table is sorted by BaseChar in ascending order. + // Usually they are too small to use binary search. + + for (uint32_t index = 0; index < mEntries; index++) { + if (mTable[index].BaseChar == aBaseChar) { + return mTable[index].CompositeChar; + } + if (mTable[index].BaseChar > aBaseChar) { + break; + } + } + + return 0; +} + +/***************************************************************************** + * mozilla::widget::RedirectedKeyDownMessage + *****************************************************************************/ + +MSG RedirectedKeyDownMessageManager::sRedirectedKeyDownMsg; +bool RedirectedKeyDownMessageManager::sDefaultPreventedOfRedirectedMsg = false; + +// static +bool RedirectedKeyDownMessageManager::IsRedirectedMessage(const MSG& aMsg) { + return (aMsg.message == WM_KEYDOWN || aMsg.message == WM_SYSKEYDOWN) && + (sRedirectedKeyDownMsg.message == aMsg.message && + WinUtils::GetScanCode(sRedirectedKeyDownMsg.lParam) == + WinUtils::GetScanCode(aMsg.lParam)); +} + +// static +void RedirectedKeyDownMessageManager::RemoveNextCharMessage(HWND aWnd) { + MSG msg; + if (WinUtils::PeekMessage(&msg, aWnd, WM_KEYFIRST, WM_KEYLAST, + PM_NOREMOVE | PM_NOYIELD) && + (msg.message == WM_CHAR || msg.message == WM_SYSCHAR)) { + WinUtils::PeekMessage(&msg, aWnd, msg.message, msg.message, + PM_REMOVE | PM_NOYIELD); + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/KeyboardLayout.h b/widget/windows/KeyboardLayout.h new file mode 100644 index 0000000000..c7c3bcc56d --- /dev/null +++ b/widget/windows/KeyboardLayout.h @@ -0,0 +1,1120 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef KeyboardLayout_h__ +#define KeyboardLayout_h__ + +#include "mozilla/RefPtr.h" +#include "nscore.h" +#include "nsString.h" +#include "nsWindow.h" +#include "nsWindowDefs.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/widget/WinMessages.h" +#include "mozilla/widget/WinModifierKeyState.h" +#include + +#define NS_NUM_OF_KEYS 70 + +#define VK_OEM_1 0xBA // ';:' for US +#define VK_OEM_PLUS 0xBB // '+' any country +#define VK_OEM_COMMA 0xBC +#define VK_OEM_MINUS 0xBD // '-' any country +#define VK_OEM_PERIOD 0xBE +#define VK_OEM_2 0xBF +#define VK_OEM_3 0xC0 +// '/?' for Brazilian (ABNT) +#define VK_ABNT_C1 0xC1 +// Separator in Numpad for Brazilian (ABNT) or JIS keyboard for Mac. +#define VK_ABNT_C2 0xC2 +#define VK_OEM_4 0xDB +#define VK_OEM_5 0xDC +#define VK_OEM_6 0xDD +#define VK_OEM_7 0xDE +#define VK_OEM_8 0xDF +#define VK_OEM_102 0xE2 +#define VK_OEM_CLEAR 0xFE + +class nsIUserIdleServiceInternal; + +namespace mozilla { +namespace widget { + +enum ScanCode : uint16_t { + eCapsLock = 0x003A, + eNumLock = 0xE045, + eShiftLeft = 0x002A, + eShiftRight = 0x0036, + eControlLeft = 0x001D, + eControlRight = 0xE01D, + eAltLeft = 0x0038, + eAltRight = 0xE038, +}; + +// 0: nsIWidget's native modifier flag +// 1: Virtual keycode which does not distinguish whether left or right location. +// 2: Virtual keycode which distinguishes whether left or right location. +// 3: Scan code. +static const uint32_t sModifierKeyMap[][4] = { + {nsIWidget::CAPS_LOCK, VK_CAPITAL, 0, ScanCode::eCapsLock}, + {nsIWidget::NUM_LOCK, VK_NUMLOCK, 0, ScanCode::eNumLock}, + {nsIWidget::SHIFT_L, VK_SHIFT, VK_LSHIFT, ScanCode::eShiftLeft}, + {nsIWidget::SHIFT_R, VK_SHIFT, VK_RSHIFT, ScanCode::eShiftRight}, + {nsIWidget::CTRL_L, VK_CONTROL, VK_LCONTROL, ScanCode::eControlLeft}, + {nsIWidget::CTRL_R, VK_CONTROL, VK_RCONTROL, ScanCode::eControlRight}, + {nsIWidget::ALT_L, VK_MENU, VK_LMENU, ScanCode::eAltLeft}, + {nsIWidget::ALT_R, VK_MENU, VK_RMENU, ScanCode::eAltRight}}; + +class KeyboardLayout; + +class MOZ_STACK_CLASS UniCharsAndModifiers final { + public: + UniCharsAndModifiers() {} + UniCharsAndModifiers operator+(const UniCharsAndModifiers& aOther) const; + UniCharsAndModifiers& operator+=(const UniCharsAndModifiers& aOther); + + /** + * Append a pair of unicode character and the final modifier. + */ + void Append(char16_t aUniChar, Modifiers aModifiers); + void Clear() { + mChars.Truncate(); + mModifiers.Clear(); + } + bool IsEmpty() const { + MOZ_ASSERT(mChars.Length() == mModifiers.Length()); + return mChars.IsEmpty(); + } + + char16_t CharAt(size_t aIndex) const { + MOZ_ASSERT(aIndex < Length()); + return mChars[aIndex]; + } + Modifiers ModifiersAt(size_t aIndex) const { + MOZ_ASSERT(aIndex < Length()); + return mModifiers[aIndex]; + } + size_t Length() const { + MOZ_ASSERT(mChars.Length() == mModifiers.Length()); + return mChars.Length(); + } + + bool IsProducingCharsWithAltGr() const { + return !IsEmpty() && (ModifiersAt(0) & MODIFIER_ALTGRAPH) != 0; + } + + void FillModifiers(Modifiers aModifiers); + /** + * OverwriteModifiersIfBeginsWith() assigns mModifiers with aOther between + * [0] and [aOther.mLength - 1] only when mChars begins with aOther.mChars. + */ + void OverwriteModifiersIfBeginsWith(const UniCharsAndModifiers& aOther); + + bool UniCharsEqual(const UniCharsAndModifiers& aOther) const; + bool UniCharsCaseInsensitiveEqual(const UniCharsAndModifiers& aOther) const; + bool BeginsWith(const UniCharsAndModifiers& aOther) const; + + const nsString& ToString() const { return mChars; } + + private: + nsAutoString mChars; + // 5 is enough number for normal keyboard layout handling. On Windows, + // a dead key sequence may cause inputting up to 5 characters per key press. + CopyableAutoTArray mModifiers; +}; + +struct DeadKeyEntry { + char16_t BaseChar; + char16_t CompositeChar; +}; + +class DeadKeyTable { + friend class KeyboardLayout; + + uint16_t mEntries; + // KeyboardLayout::AddDeadKeyTable() will allocate as many entries as + // required. It is the only way to create new DeadKeyTable instances. + DeadKeyEntry mTable[1]; + + void Init(const DeadKeyEntry* aDeadKeyArray, uint32_t aEntries) { + mEntries = aEntries; + memcpy(mTable, aDeadKeyArray, aEntries * sizeof(DeadKeyEntry)); + } + + static uint32_t SizeInBytes(uint32_t aEntries) { + return offsetof(DeadKeyTable, mTable) + aEntries * sizeof(DeadKeyEntry); + } + + public: + uint32_t Entries() const { return mEntries; } + + bool IsEqual(const DeadKeyEntry* aDeadKeyArray, uint32_t aEntries) const { + return (mEntries == aEntries && + !memcmp(mTable, aDeadKeyArray, aEntries * sizeof(DeadKeyEntry))); + } + + char16_t GetCompositeChar(char16_t aBaseChar) const; +}; + +class VirtualKey { + public: + enum ShiftStateIndex : uint8_t { + // 0 - Normal + eNormal = 0, + // 1 - Shift + eShift, + // 2 - Control + eControl, + // 3 - Control + Shift + eControlShift, + // 4 - Alt + eAlt, + // 5 - Alt + Shift + eAltShift, + // 6 - Alt + Control (AltGr) + eAltGr, + // 7 - Alt + Control + Shift (AltGr + Shift) + eAltGrShift, + // 8 - CapsLock + eWithCapsLock, + // 9 - CapsLock + Shift + eShiftWithCapsLock, + // 10 - CapsLock + Control + eControlWithCapsLock, + // 11 - CapsLock + Control + Shift + eControlShiftWithCapsLock, + // 12 - CapsLock + Alt + eAltWithCapsLock, + // 13 - CapsLock + Alt + Shift + eAltShiftWithCapsLock, + // 14 - CapsLock + Alt + Control (CapsLock + AltGr) + eAltGrWithCapsLock, + // 15 - CapsLock + Alt + Control + Shift (CapsLock + AltGr + Shift) + eAltGrShiftWithCapsLock, + }; + + enum ShiftStateFlag { + STATE_SHIFT = 0x01, + STATE_CONTROL = 0x02, + STATE_ALT = 0x04, + STATE_CAPSLOCK = 0x08, + // ShiftState needs to have AltGr state separately since this is necessary + // for lossless conversion with Modifiers. + STATE_ALTGRAPH = 0x80, + // Useful to remove or check Ctrl and Alt flags. + STATE_CONTROL_ALT = STATE_CONTROL | STATE_ALT, + }; + + typedef uint8_t ShiftState; + + static ShiftState ModifiersToShiftState(Modifiers aModifiers); + static ShiftState ModifierKeyStateToShiftState( + const ModifierKeyState& aModKeyState) { + return ModifiersToShiftState(aModKeyState.GetModifiers()); + } + static Modifiers ShiftStateToModifiers(ShiftState aShiftState); + static bool IsAltGrIndex(uint8_t aIndex) { + return (aIndex & STATE_CONTROL_ALT) == STATE_CONTROL_ALT; + } + + private: + union KeyShiftState { + struct { + char16_t Chars[4]; + } Normal; + struct { + const DeadKeyTable* Table; + char16_t DeadChar; + } DeadKey; + }; + + KeyShiftState mShiftStates[16]; + uint16_t mIsDeadKey; + + static uint8_t ToShiftStateIndex(ShiftState aShiftState) { + if (!(aShiftState & STATE_ALTGRAPH)) { + MOZ_ASSERT(aShiftState <= eAltGrShiftWithCapsLock); + return static_cast(aShiftState); + } + uint8_t index = aShiftState & ~STATE_ALTGRAPH; + index |= (STATE_ALT | STATE_CONTROL); + MOZ_ASSERT(index <= eAltGrShiftWithCapsLock); + return index; + } + + void SetDeadKey(ShiftState aShiftState, bool aIsDeadKey) { + if (aIsDeadKey) { + mIsDeadKey |= 1 << ToShiftStateIndex(aShiftState); + } else { + mIsDeadKey &= ~(1 << ToShiftStateIndex(aShiftState)); + } + } + + public: + static void FillKbdState(PBYTE aKbdState, const ShiftState aShiftState); + + bool IsDeadKey(ShiftState aShiftState) const { + return (mIsDeadKey & (1 << ToShiftStateIndex(aShiftState))) != 0; + } + + /** + * IsChangedByAltGr() is useful to check if a key with AltGr produces + * different character(s) from the key without AltGr. + * Note that this is designed for checking if a keyboard layout has AltGr + * key. So, this result may not exactly correct for the key since it's + * okay to fails in some edge cases when we check all keys which produce + * character(s) in a layout. + */ + bool IsChangedByAltGr(ShiftState aShiftState) const { + MOZ_ASSERT(aShiftState == ToShiftStateIndex(aShiftState)); + MOZ_ASSERT(IsAltGrIndex(aShiftState)); + MOZ_ASSERT(IsDeadKey(aShiftState) || + mShiftStates[aShiftState].Normal.Chars[0]); + const ShiftState kShiftStateWithoutAltGr = + aShiftState - ShiftStateIndex::eAltGr; + if (IsDeadKey(aShiftState) != IsDeadKey(kShiftStateWithoutAltGr)) { + return false; + } + if (IsDeadKey(aShiftState)) { + return mShiftStates[aShiftState].DeadKey.DeadChar != + mShiftStates[kShiftStateWithoutAltGr].DeadKey.DeadChar; + } + for (size_t i = 0; i < 4; i++) { + if (mShiftStates[aShiftState].Normal.Chars[i] != + mShiftStates[kShiftStateWithoutAltGr].Normal.Chars[i]) { + return true; + } + if (!mShiftStates[aShiftState].Normal.Chars[i] && + !mShiftStates[kShiftStateWithoutAltGr].Normal.Chars[i]) { + return false; + } + } + return false; + } + + void AttachDeadKeyTable(ShiftState aShiftState, + const DeadKeyTable* aDeadKeyTable) { + MOZ_ASSERT(aShiftState == ToShiftStateIndex(aShiftState)); + mShiftStates[aShiftState].DeadKey.Table = aDeadKeyTable; + } + + void SetNormalChars(ShiftState aShiftState, const char16_t* aChars, + uint32_t aNumOfChars); + void SetDeadChar(ShiftState aShiftState, char16_t aDeadChar); + const DeadKeyTable* MatchingDeadKeyTable(const DeadKeyEntry* aDeadKeyArray, + uint32_t aEntries) const; + inline char16_t GetCompositeChar(ShiftState aShiftState, + char16_t aBaseChar) const { + return mShiftStates[ToShiftStateIndex(aShiftState)] + .DeadKey.Table->GetCompositeChar(aBaseChar); + } + + char16_t GetCompositeChar(const ModifierKeyState& aModKeyState, + char16_t aBaseChar) const { + return GetCompositeChar(ModifierKeyStateToShiftState(aModKeyState), + aBaseChar); + } + + /** + * GetNativeUniChars() returns character(s) which is produced by the + * key with given modifiers. This does NOT return proper MODIFIER_ALTGRAPH + * state because this is raw accessor of the database of this key. + */ + UniCharsAndModifiers GetNativeUniChars(ShiftState aShiftState) const; + UniCharsAndModifiers GetNativeUniChars( + const ModifierKeyState& aModKeyState) const { + return GetNativeUniChars(ModifierKeyStateToShiftState(aModKeyState)); + } + + /** + * GetUniChars() returns characters and modifiers which are not consumed + * to input the character. + * For example, if you specify Ctrl key but the key produces no character + * with Ctrl, this returns character(s) which is produced by the key + * without Ctrl. So, the result is useful to decide KeyboardEvent.key + * value. + * Another example is, if you specify Ctrl key and the key produces + * different character(s) from the case without Ctrl key, this returns + * the character(s) *without* MODIFIER_CONTROL. This modifier information + * is useful for eKeyPress since TextEditor does not treat eKeyPress events + * whose modifier includes MODIFIER_ALT and/or MODIFIER_CONTROL. + * + * @param aShiftState Modifiers which you want to retrieve + * KeyboardEvent.key value for the key with. + * If AltGr key is pressed, this should include + * STATE_ALTGRAPH and should NOT include + * STATE_ALT nor STATE_CONTROL. + * If both Alt and Ctrl are pressed to emulate + * AltGr, this should include both STATE_ALT and + * STATE_CONTROL but should NOT include + * MODIFIER_ALTGRAPH. + * Then, this returns proper modifiers when + * this key produces no character with AltGr. + */ + UniCharsAndModifiers GetUniChars(ShiftState aShiftState) const; + UniCharsAndModifiers GetUniChars(const ModifierKeyState& aModKeyState) const { + return GetUniChars(ModifierKeyStateToShiftState(aModKeyState)); + } +}; + +class MOZ_STACK_CLASS NativeKey final { + friend class KeyboardLayout; + + public: + struct FakeCharMsg { + UINT mCharCode; + UINT mScanCode; + bool mIsSysKey; + bool mIsDeadKey; + bool mConsumed; + + FakeCharMsg() + : mCharCode(0), + mScanCode(0), + mIsSysKey(false), + mIsDeadKey(false), + mConsumed(false) {} + + MSG GetCharMsg(HWND aWnd) const { + MSG msg; + msg.hwnd = aWnd; + msg.message = mIsDeadKey && mIsSysKey ? WM_SYSDEADCHAR + : mIsDeadKey ? WM_DEADCHAR + : mIsSysKey ? WM_SYSCHAR + : WM_CHAR; + msg.wParam = static_cast(mCharCode); + msg.lParam = static_cast(mScanCode << 16); + msg.time = 0; + msg.pt.x = msg.pt.y = 0; + return msg; + } + }; + + NativeKey(nsWindow* aWidget, const MSG& aMessage, + const ModifierKeyState& aModKeyState, + HKL aOverrideKeyboardLayout = 0, + nsTArray* aFakeCharMsgs = nullptr); + + ~NativeKey(); + + /** + * Handle WM_KEYDOWN message or WM_SYSKEYDOWN message. The instance must be + * initialized with WM_KEYDOWN or WM_SYSKEYDOWN. + * Returns true if dispatched keydown event or keypress event is consumed. + * Otherwise, false. + */ + bool HandleKeyDownMessage(bool* aEventDispatched = nullptr) const; + + /** + * Handles WM_CHAR message or WM_SYSCHAR message. The instance must be + * initialized with them. + * Returns true if dispatched keypress event is consumed. Otherwise, false. + */ + bool HandleCharMessage(bool* aEventDispatched = nullptr) const; + + /** + * Handles keyup message. Returns true if the event is consumed. + * Otherwise, false. + */ + bool HandleKeyUpMessage(bool* aEventDispatched = nullptr) const; + + /** + * Handles WM_APPCOMMAND message. Returns true if the event is consumed. + * Otherwise, false. + */ + bool HandleAppCommandMessage() const; + + /** + * Callback of TextEventDispatcherListener::WillDispatchKeyboardEvent(). + * This method sets alternative char codes of aKeyboardEvent. + */ + void WillDispatchKeyboardEvent(WidgetKeyboardEvent& aKeyboardEvent, + uint32_t aIndex); + + /** + * Returns true if aChar is a control character which shouldn't be inputted + * into focused text editor. + */ + static bool IsControlChar(char16_t aChar); + + bool IsShift() const { return mModKeyState.IsShift(); } + bool IsControl() const { return mModKeyState.IsControl(); } + bool IsAlt() const { return mModKeyState.IsAlt(); } + bool MaybeEmulatingAltGraph() const; + Modifiers GetModifiers() const { return mModKeyState.GetModifiers(); } + const ModifierKeyState& ModifierKeyStateRef() const { return mModKeyState; } + VirtualKey::ShiftState GetShiftState() const { + return VirtualKey::ModifierKeyStateToShiftState(mModKeyState); + } + + /** + * GenericVirtualKeyCode() returns virtual keycode which cannot distinguish + * position of modifier keys. E.g., VK_CONTROL for both ControlLeft and + * ControlRight. + */ + uint8_t GenericVirtualKeyCode() const { return mOriginalVirtualKeyCode; } + + /** + * SpecificVirtualKeyCode() returns virtual keycode which can distinguish + * position of modifier keys. E.g., returns VK_LCONTROL or VK_RCONTROL + * instead of VK_CONTROL. If the key message is synthesized with not + * enough information, this prefers left position's keycode. + */ + uint8_t SpecificVirtualKeyCode() const { return mVirtualKeyCode; } + + private: + NativeKey* mLastInstance; + // mRemovingMsg is set at removing a char message from + // GetFollowingCharMessage(). + MSG mRemovingMsg; + // mReceivedMsg is set when another instance starts to handle the message + // unexpectedly. + MSG mReceivedMsg; + RefPtr mWidget; + RefPtr mDispatcher; + HKL mKeyboardLayout; + MSG mMsg; + // mFollowingCharMsgs stores WM_CHAR, WM_SYSCHAR, WM_DEADCHAR or + // WM_SYSDEADCHAR message which follows WM_KEYDOWN. + // Note that the stored messaged are already removed from the queue. + // FYI: 5 is enough number for usual keyboard layout handling. On Windows, + // a dead key sequence may cause inputting up to 5 characters per key press. + AutoTArray mFollowingCharMsgs; + // mRemovedOddCharMsgs stores WM_CHAR messages which are caused by ATOK or + // WXG (they are Japanese IME) when the user tries to do "Kakutei-undo" + // (it means "undo the last commit"). + nsTArray mRemovedOddCharMsgs; + // If dispatching eKeyDown or eKeyPress event causes focus change, + // the instance shouldn't handle remaning char messages. For checking it, + // this should store first focused window. + HWND mFocusedWndBeforeDispatch; + + uint32_t mDOMKeyCode; + KeyNameIndex mKeyNameIndex; + CodeNameIndex mCodeNameIndex; + + ModifierKeyState mModKeyState; + + // mVirtualKeyCode distinguishes left key or right key of modifier key. + uint8_t mVirtualKeyCode; + // mOriginalVirtualKeyCode doesn't distinguish left key or right key of + // modifier key. However, if the given keycode is VK_PROCESS, it's resolved + // to a keycode before it's handled by IME. + uint8_t mOriginalVirtualKeyCode; + + // mCommittedChars indicates the inputted characters which is committed by + // the key. If dead key fail to composite a character, mCommittedChars + // indicates both the dead characters and the base characters. + UniCharsAndModifiers mCommittedCharsAndModifiers; + + // Following strings are computed by + // ComputeInputtingStringWithKeyboardLayout() which is typically called + // before dispatching keydown event. + // mInputtingStringAndModifiers's string is the string to be + // inputted into the focused editor and its modifier state is proper + // modifier state for inputting the string into the editor. + UniCharsAndModifiers mInputtingStringAndModifiers; + // mShiftedString is the string to be inputted into the editor with + // current modifier state with active shift state. + UniCharsAndModifiers mShiftedString; + // mUnshiftedString is the string to be inputted into the editor with + // current modifier state without shift state. + UniCharsAndModifiers mUnshiftedString; + // Following integers are computed by + // ComputeInputtingStringWithKeyboardLayout() which is typically called + // before dispatching keydown event. The meaning of these values is same + // as charCode. + uint32_t mShiftedLatinChar; + uint32_t mUnshiftedLatinChar; + + WORD mScanCode; + bool mIsExtended; + // mIsRepeat is true if the key message is caused by the auto-repeat + // feature. + bool mIsRepeat; + bool mIsDeadKey; + // mIsPrintableKey is true if the key may be a printable key without + // any modifier keys. Otherwise, false. + // Please note that the event may not cause any text input even if this + // is true. E.g., it might be dead key state or Ctrl key may be pressed. + bool mIsPrintableKey; + // mIsSkippableInRemoteProcess is false if the key event shouldn't be + // skipped in the remote process even if it's too old event. + bool mIsSkippableInRemoteProcess; + // mCharMessageHasGone is true if the message is a keydown message and + // it's followed by at least one char message but it's gone at removing + // from the queue. This could occur if PeekMessage() or something is + // hooked by odd tool. + bool mCharMessageHasGone; + // mIsOverridingKeyboardLayout is true if the instance temporarily overriding + // keyboard layout with specified by the constructor. + bool mIsOverridingKeyboardLayout; + // mCanIgnoreModifierStateAtKeyPress is true if it's allowed to remove + // Ctrl or Alt modifier state at dispatching eKeyPress. + bool mCanIgnoreModifierStateAtKeyPress; + + nsTArray* mFakeCharMsgs; + + // When a keydown event is dispatched at handling WM_APPCOMMAND, the computed + // virtual keycode is set to this. Even if we consume WM_APPCOMMAND message, + // Windows may send WM_KEYDOWN and WM_KEYUP message for them. + // At that time, we should not dispatch key events for them. + static uint8_t sDispatchedKeyOfAppCommand; + + NativeKey() { + MOZ_CRASH("The default constructor of NativeKey isn't available"); + } + + void InitWithAppCommand(); + void InitWithKeyOrChar(); + + /** + * InitIsSkippableForKeyOrChar() initializes mIsSkippableInRemoteProcess with + * mIsRepeat and previous key message information. So, this must be called + * after mIsRepeat is initialized. + */ + void InitIsSkippableForKeyOrChar(const MSG& aLastKeyMSG); + + /** + * InitCommittedCharsAndModifiersWithFollowingCharMessages() initializes + * mCommittedCharsAndModifiers with mFollowingCharMsgs and mModKeyState. + * If mFollowingCharMsgs includes non-printable char messages, they are + * ignored (skipped). + */ + void InitCommittedCharsAndModifiersWithFollowingCharMessages(); + + UINT GetScanCodeWithExtendedFlag() const; + + // The result is one of eKeyLocation*. + uint32_t GetKeyLocation() const; + + /** + * RemoveFollowingOddCharMessages() removes odd WM_CHAR messages from the + * queue when IsIMEDoingKakuteiUndo() returns true. + */ + void RemoveFollowingOddCharMessages(); + + /** + * "Kakutei-Undo" of ATOK or WXG (both of them are Japanese IME) causes + * strange WM_KEYDOWN/WM_KEYUP/WM_CHAR message pattern. So, when this + * returns true, the caller needs to be careful for processing the messages. + */ + bool IsIMEDoingKakuteiUndo() const; + + /** + * This returns true if user types a number key in numpad with Alt key + * to input a Unicode character from its scalar value. + * Note that inputting Unicode scalar value is available without NumLock. + * Therefore, this returns true even if user presses a function key on + * numpad without NumLock, but that may be intended to perform a shortcut + * key like Alt + Home. + */ + bool MaybeTypingUnicodeScalarValue() const { + return !mIsExtended && IsSysKeyDownOrKeyUpMessage() && IsAlt() && + !IsControl() && !IsShift() && + ((mScanCode >= 0x004F && mScanCode <= 0x0052) || // Numpad0-3 + (mScanCode >= 0x004B && mScanCode <= 0x004D) || // Numpad4-6 + (mScanCode >= 0x0047 && mScanCode <= 0x0049)); // Numpad7-9 + } + + bool IsKeyDownMessage() const { + return mMsg.message == WM_KEYDOWN || mMsg.message == WM_SYSKEYDOWN; + } + bool IsKeyUpMessage() const { + return mMsg.message == WM_KEYUP || mMsg.message == WM_SYSKEYUP; + } + bool IsSysKeyDownOrKeyUpMessage() const { + return mMsg.message == WM_SYSKEYDOWN || mMsg.message == WM_SYSKEYUP; + } + bool IsCharOrSysCharMessage(const MSG& aMSG) const { + return IsCharOrSysCharMessage(aMSG.message); + } + bool IsCharOrSysCharMessage(UINT aMessage) const { + return (aMessage == WM_CHAR || aMessage == WM_SYSCHAR); + } + bool IsCharMessage(const MSG& aMSG) const { + return IsCharMessage(aMSG.message); + } + bool IsCharMessage(UINT aMessage) const { + return (IsCharOrSysCharMessage(aMessage) || IsDeadCharMessage(aMessage)); + } + bool IsDeadCharMessage(const MSG& aMSG) const { + return IsDeadCharMessage(aMSG.message); + } + bool IsDeadCharMessage(UINT aMessage) const { + return (aMessage == WM_DEADCHAR || aMessage == WM_SYSDEADCHAR); + } + bool IsSysCharMessage(const MSG& aMSG) const { + return IsSysCharMessage(aMSG.message); + } + bool IsSysCharMessage(UINT aMessage) const { + return (aMessage == WM_SYSCHAR || aMessage == WM_SYSDEADCHAR); + } + bool MayBeSameCharMessage(const MSG& aCharMsg1, const MSG& aCharMsg2) const; + bool IsSamePhysicalKeyMessage(const MSG& aKeyOrCharMsg1, + const MSG& aKeyOrCharMsg2) const; + bool IsFollowedByPrintableCharMessage() const; + bool IsFollowedByPrintableCharOrSysCharMessage() const; + bool IsFollowedByDeadCharMessage() const; + bool IsPrintableCharMessage(const MSG& aMSG) const { + return aMSG.message == WM_CHAR && + !IsControlChar(static_cast(aMSG.wParam)); + } + bool IsEnterKeyPressCharMessage(const MSG& aMSG) const { + return aMSG.message == WM_CHAR && aMSG.wParam == '\r'; + } + bool IsPrintableCharOrSysCharMessage(const MSG& aMSG) const { + return IsCharOrSysCharMessage(aMSG) && + !IsControlChar(static_cast(aMSG.wParam)); + } + bool IsControlCharMessage(const MSG& aMSG) const { + return IsCharMessage(aMSG.message) && + IsControlChar(static_cast(aMSG.wParam)); + } + + /** + * IsReservedBySystem() returns true if the key combination is reserved by + * the system. Even if it's consumed by web apps, the message should be + * sent to next wndproc. + */ + bool IsReservedBySystem() const; + + /** + * GetFollowingCharMessage() returns following char message of handling + * keydown event. If the message is found, this method returns true. + * Otherwise, returns false. + * + * WARNING: Even if this returns true, aCharMsg may be WM_NULL or its + * hwnd may be different window. + */ + bool GetFollowingCharMessage(MSG& aCharMsg); + + /** + * Wraps MapVirtualKeyEx() with MAPVK_VSC_TO_VK. + */ + uint8_t ComputeVirtualKeyCodeFromScanCode() const; + + /** + * Wraps MapVirtualKeyEx() with MAPVK_VSC_TO_VK_EX. + */ + uint8_t ComputeVirtualKeyCodeFromScanCodeEx() const; + + /** + * Wraps MapVirtualKeyEx() with MAPVK_VK_TO_VSC_EX or MAPVK_VK_TO_VSC. + */ + uint16_t ComputeScanCodeExFromVirtualKeyCode(UINT aVirtualKeyCode) const; + + /** + * Wraps MapVirtualKeyEx() with MAPVK_VSC_TO_VK and MAPVK_VK_TO_CHAR. + */ + char16_t ComputeUnicharFromScanCode() const; + + /** + * Initializes the aKeyEvent with the information stored in the instance. + */ + nsEventStatus InitKeyEvent(WidgetKeyboardEvent& aKeyEvent, + const ModifierKeyState& aModKeyState) const; + nsEventStatus InitKeyEvent(WidgetKeyboardEvent& aKeyEvent) const; + + /** + * Dispatches a command event for aEventCommand. + * Returns true if the event is consumed. Otherwise, false. + */ + bool DispatchCommandEvent(uint32_t aEventCommand) const; + + /** + * DispatchKeyPressEventsWithRetrievedCharMessages() dispatches keypress + * event(s) with retrieved char messages. + */ + bool DispatchKeyPressEventsWithRetrievedCharMessages() const; + + /** + * DispatchKeyPressEventsWithoutCharMessage() dispatches keypress event(s) + * without char messages. So, this should be used only when there are no + * following char messages. + */ + bool DispatchKeyPressEventsWithoutCharMessage() const; + + /** + * Checkes whether the key event down message is handled without following + * WM_CHAR messages. For example, if following WM_CHAR message indicates + * control character input, the WM_CHAR message is unclear whether it's + * caused by a printable key with Ctrl or just a function key such as Enter + * or Backspace. + */ + bool NeedsToHandleWithoutFollowingCharMessages() const; + + /** + * ComputeInputtingStringWithKeyboardLayout() computes string to be inputted + * with the key and the modifier state, without shift state and with shift + * state. + */ + void ComputeInputtingStringWithKeyboardLayout(); + + /** + * IsFocusedWindowChanged() returns true if focused window is changed + * after the instance is created. + */ + bool IsFocusedWindowChanged() const { + return mFocusedWndBeforeDispatch != ::GetFocus(); + } + + /** + * Handles WM_CHAR message or WM_SYSCHAR message. The instance must be + * initialized with WM_KEYDOWN, WM_SYSKEYDOWN or them. + * Returns true if dispatched keypress event is consumed. Otherwise, false. + */ + bool HandleCharMessage(const MSG& aCharMsg, + bool* aEventDispatched = nullptr) const; + + // Calls of PeekMessage() from NativeKey might cause nested message handling + // due to (perhaps) odd API hook. NativeKey should do nothing if given + // message is tried to be retrieved by another instance. + + /** + * sLatestInstacne is a pointer to the newest instance of NativeKey which is + * handling a key or char message(s). + */ + static NativeKey* sLatestInstance; + + static const MSG sEmptyMSG; + + static MSG sLastKeyOrCharMSG; + + static MSG sLastKeyMSG; + + static bool IsEmptyMSG(const MSG& aMSG) { + return !memcmp(&aMSG, &sEmptyMSG, sizeof(MSG)); + } + + bool IsAnotherInstanceRemovingCharMessage() const { + return mLastInstance && !IsEmptyMSG(mLastInstance->mRemovingMsg); + } + + public: + /** + * Returns last key or char MSG. If no MSG has been received yet, the result + * is empty MSG (i.e., .message is WM_NULL). + */ + static const MSG& LastKeyOrCharMSG() { return sLastKeyOrCharMSG; } +}; + +class KeyboardLayout { + public: + static KeyboardLayout* GetInstance(); + static void Shutdown(); + static HKL GetActiveLayout(); + static nsCString GetActiveLayoutName(); + static void NotifyIdleServiceOfUserActivity(); + + static bool IsPrintableCharKey(uint8_t aVirtualKey); + + /** + * HasAltGr() returns true if the keyboard layout's AltRight key is AltGr + * key. + */ + bool HasAltGr() const { return mHasAltGr; } + + /** + * IsDeadKey() returns true if aVirtualKey is a dead key with aModKeyState. + * This method isn't stateful. + */ + bool IsDeadKey(uint8_t aVirtualKey, + const ModifierKeyState& aModKeyState) const; + bool IsDeadKey(const NativeKey& aNativeKey) const { + return IsDeadKey(aNativeKey.GenericVirtualKeyCode(), + aNativeKey.ModifierKeyStateRef()); + } + + /** + * IsInDeadKeySequence() returns true when it's in a dead key sequence. + * It starts when a dead key is down and ends when another key down causes + * inactivating the dead key state. + */ + bool IsInDeadKeySequence() const { return !mActiveDeadKeys.IsEmpty(); } + + /** + * IsSysKey() returns true if aVirtualKey with aModKeyState causes WM_SYSKEY* + * or WM_SYS*CHAR messages. + */ + bool IsSysKey(uint8_t aVirtualKey, + const ModifierKeyState& aModKeyState) const; + bool IsSysKey(const NativeKey& aNativeKey) const { + return IsSysKey(aNativeKey.GenericVirtualKeyCode(), + aNativeKey.ModifierKeyStateRef()); + } + + /** + * GetUniCharsAndModifiers() returns characters which are inputted by + * aVirtualKey with aModKeyState. This method isn't stateful. + * Note that if the combination causes text input, the result's Ctrl and + * Alt key state are never active. + */ + UniCharsAndModifiers GetUniCharsAndModifiers( + uint8_t aVirtualKey, const ModifierKeyState& aModKeyState) const { + VirtualKey::ShiftState shiftState = + VirtualKey::ModifierKeyStateToShiftState(aModKeyState); + return GetUniCharsAndModifiers(aVirtualKey, shiftState); + } + UniCharsAndModifiers GetUniCharsAndModifiers( + const NativeKey& aNativeKey) const { + return GetUniCharsAndModifiers(aNativeKey.GenericVirtualKeyCode(), + aNativeKey.GetShiftState()); + } + + /** + * OnLayoutChange() must be called before the first keydown message is + * received. LoadLayout() changes the keyboard state, that causes breaking + * dead key state. Therefore, we need to load the layout before the first + * keydown message. + */ + void OnLayoutChange(HKL aKeyboardLayout) { + MOZ_ASSERT(!mIsOverridden); + LoadLayout(aKeyboardLayout); + } + + /** + * OverrideLayout() loads the specified keyboard layout. + */ + void OverrideLayout(HKL aLayout) { + mIsOverridden = true; + LoadLayout(aLayout); + } + + /** + * RestoreLayout() loads the current keyboard layout of the thread. + */ + void RestoreLayout() { + mIsOverridden = false; + mIsPendingToRestoreKeyboardLayout = true; + } + + uint32_t ConvertNativeKeyCodeToDOMKeyCode(UINT aNativeKeyCode) const; + + /** + * ConvertNativeKeyCodeToKeyNameIndex() returns KeyNameIndex value for + * non-printable keys (except some special keys like space key). + */ + KeyNameIndex ConvertNativeKeyCodeToKeyNameIndex(uint8_t aVirtualKey) const; + + /** + * ConvertScanCodeToCodeNameIndex() returns CodeNameIndex value for + * the given scan code. aScanCode can be over 0xE000 since this method + * doesn't use Windows API. + */ + static CodeNameIndex ConvertScanCodeToCodeNameIndex(UINT aScanCode); + + HKL GetLayout() const { + return mIsPendingToRestoreKeyboardLayout ? ::GetKeyboardLayout(0) + : mKeyboardLayout; + } + + /** + * This wraps MapVirtualKeyEx() API with MAPVK_VK_TO_VSC. + */ + WORD ComputeScanCodeForVirtualKeyCode(uint8_t aVirtualKeyCode) const; + + /** + * Implementation of nsIWidget::SynthesizeNativeKeyEvent(). + */ + nsresult SynthesizeNativeKeyEvent(nsWindow* aWidget, + int32_t aNativeKeyboardLayout, + int32_t aNativeKeyCode, + uint32_t aModifierFlags, + const nsAString& aCharacters, + const nsAString& aUnmodifiedCharacters); + + private: + KeyboardLayout(); + ~KeyboardLayout(); + + static KeyboardLayout* sInstance; + static nsIUserIdleServiceInternal* sIdleService; + + struct DeadKeyTableListEntry { + DeadKeyTableListEntry* next; + uint8_t data[1]; + }; + + HKL mKeyboardLayout; + + VirtualKey mVirtualKeys[NS_NUM_OF_KEYS]; + DeadKeyTableListEntry* mDeadKeyTableListHead; + // When mActiveDeadKeys is empty, it's not in dead key sequence. + // Otherwise, it contains virtual keycodes which are pressed in current + // dead key sequence. + nsTArray mActiveDeadKeys; + // mDeadKeyShiftStates is always same length as mActiveDeadKeys. + // This stores shift states at pressing each dead key stored in + // mActiveDeadKeys. + nsTArray mDeadKeyShiftStates; + + bool mIsOverridden; + bool mIsPendingToRestoreKeyboardLayout; + bool mHasAltGr; + + static inline int32_t GetKeyIndex(uint8_t aVirtualKey); + static int CompareDeadKeyEntries(const void* aArg1, const void* aArg2, + void* aData); + static bool AddDeadKeyEntry(char16_t aBaseChar, char16_t aCompositeChar, + DeadKeyEntry* aDeadKeyArray, uint32_t aEntries); + bool EnsureDeadKeyActive(bool aIsActive, uint8_t aDeadKey, + const PBYTE aDeadKeyKbdState); + uint32_t GetDeadKeyCombinations(uint8_t aDeadKey, + const PBYTE aDeadKeyKbdState, + uint16_t aShiftStatesWithBaseChars, + DeadKeyEntry* aDeadKeyArray, + uint32_t aMaxEntries); + /** + * Activates or deactivates dead key state. + */ + void ActivateDeadKeyState(const NativeKey& aNativeKey); + void DeactivateDeadKeyState(); + + const DeadKeyTable* AddDeadKeyTable(const DeadKeyEntry* aDeadKeyArray, + uint32_t aEntries); + void ReleaseDeadKeyTables(); + + /** + * Loads the specified keyboard layout. This method always clear the dead key + * state. + */ + void LoadLayout(HKL aLayout); + + /** + * Gets the keyboard layout name of aLayout. Be careful, this may be too + * slow to call at handling user input. + */ + nsCString GetLayoutName(HKL aLayout) const; + + /** + * InitNativeKey() must be called when actually widget receives WM_KEYDOWN or + * WM_KEYUP. This method is stateful. This saves current dead key state at + * WM_KEYDOWN. Additionally, computes current inputted character(s) and set + * them to the aNativeKey. + */ + void InitNativeKey(NativeKey& aNativeKey); + + /** + * MaybeInitNativeKeyAsDeadKey() initializes aNativeKey only when aNativeKey + * is a dead key's event. + * When it's not in a dead key sequence, this activates the dead key state. + * When it's in a dead key sequence, this initializes aNativeKey with a + * composite character or a preceding dead char and a dead char which should + * be caused by aNativeKey. + * Returns true when this initializes aNativeKey. Otherwise, false. + */ + bool MaybeInitNativeKeyAsDeadKey(NativeKey& aNativeKey); + + /** + * MaybeInitNativeKeyWithCompositeChar() may initialize aNativeKey with + * proper composite character when dead key produces a composite character. + * Otherwise, just returns false. + */ + bool MaybeInitNativeKeyWithCompositeChar(NativeKey& aNativeKey); + + /** + * See the comment of GetUniCharsAndModifiers() below. + */ + UniCharsAndModifiers GetUniCharsAndModifiers( + uint8_t aVirtualKey, VirtualKey::ShiftState aShiftState) const; + + /** + * GetDeadUniCharsAndModifiers() returns dead chars which are stored in + * current dead key sequence. So, this is stateful. + */ + UniCharsAndModifiers GetDeadUniCharsAndModifiers() const; + + /** + * GetCompositeChar() returns a composite character with dead character + * caused by mActiveDeadKeys, mDeadKeyShiftStates and a base character + * (aBaseChar). + * If the combination of the dead character and the base character doesn't + * cause a composite character, this returns 0. + */ + char16_t GetCompositeChar(char16_t aBaseChar) const; + + // NativeKey class should access InitNativeKey() directly, but it shouldn't + // be available outside of NativeKey. So, let's make NativeKey a friend + // class of this. + friend class NativeKey; +}; + +class RedirectedKeyDownMessageManager { + public: + /* + * If a window receives WM_KEYDOWN message or WM_SYSKEYDOWM message which is + * a redirected message, NativeKey::DispatchKeyDownAndKeyPressEvent() + * prevents to dispatch eKeyDown event because it has been dispatched + * before the message was redirected. However, in some cases, WM_*KEYDOWN + * message handler may not handle actually. Then, the message handler needs + * to forget the redirected message and remove WM_CHAR message or WM_SYSCHAR + * message for the redirected keydown message. AutoFlusher class is a helper + * class for doing it. This must be created in the stack. + */ + class MOZ_STACK_CLASS AutoFlusher final { + public: + AutoFlusher(nsWindow* aWidget, const MSG& aMsg) + : mCancel(!RedirectedKeyDownMessageManager::IsRedirectedMessage(aMsg)), + mWidget(aWidget), + mMsg(aMsg) {} + + ~AutoFlusher() { + if (mCancel) { + return; + } + // Prevent unnecessary keypress event + if (!mWidget->Destroyed()) { + RedirectedKeyDownMessageManager::RemoveNextCharMessage(mMsg.hwnd); + } + // Foreget the redirected message + RedirectedKeyDownMessageManager::Forget(); + } + + void Cancel() { mCancel = true; } + + private: + bool mCancel; + RefPtr mWidget; + const MSG& mMsg; + }; + + static void WillRedirect(const MSG& aMsg, bool aDefualtPrevented) { + sRedirectedKeyDownMsg = aMsg; + sDefaultPreventedOfRedirectedMsg = aDefualtPrevented; + } + + static void Forget() { sRedirectedKeyDownMsg.message = WM_NULL; } + + static void PreventDefault() { sDefaultPreventedOfRedirectedMsg = true; } + static bool DefaultPrevented() { return sDefaultPreventedOfRedirectedMsg; } + + static bool IsRedirectedMessage(const MSG& aMsg); + + /** + * RemoveNextCharMessage() should be called by WM_KEYDOWN or WM_SYSKEYDOWM + * message handler. If there is no WM_(SYS)CHAR message for it, this + * method does nothing. + * NOTE: WM_(SYS)CHAR message is posted by TranslateMessage() API which is + * called in message loop. So, WM_(SYS)KEYDOWN message should have + * WM_(SYS)CHAR message in the queue if the keydown event causes character + * input. + */ + static void RemoveNextCharMessage(HWND aWnd); + + private: + // sRedirectedKeyDownMsg is WM_KEYDOWN message or WM_SYSKEYDOWN message which + // is reirected with SendInput() API by + // widget::NativeKey::DispatchKeyDownAndKeyPressEvent() + static MSG sRedirectedKeyDownMsg; + static bool sDefaultPreventedOfRedirectedMsg; +}; + +} // namespace widget +} // namespace mozilla + +#endif diff --git a/widget/windows/LSPAnnotator.cpp b/widget/windows/LSPAnnotator.cpp new file mode 100644 index 0000000000..ab3a768d66 --- /dev/null +++ b/widget/windows/LSPAnnotator.cpp @@ -0,0 +1,135 @@ +/* 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/. */ + +/** + * LSPs are evil little bits of code that are allowed to inject into our + * networking stack by Windows. Once they have wormed into our process + * they gnaw at our innards until we crash. Here we force the buggers + * into the light by recording them in our crash information. + * We do the enumeration on a thread because I'm concerned about startup perf + * on machines with several LSPs. + */ + +#include "nsICrashReporter.h" +#include "nsISupportsImpl.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsQueryObject.h" +#include "nsWindowsHelpers.h" +#include +#include +#include + +namespace mozilla { +namespace crashreporter { + +class LSPAnnotationGatherer : public Runnable { + ~LSPAnnotationGatherer() {} + + public: + LSPAnnotationGatherer() : Runnable("crashreporter::LSPAnnotationGatherer") {} + NS_DECL_NSIRUNNABLE + + void Annotate(); + nsCString mString; +}; + +void LSPAnnotationGatherer::Annotate() { + nsCOMPtr cr = + do_GetService("@mozilla.org/toolkit/crash-reporter;1"); + bool enabled; + if (cr && NS_SUCCEEDED(cr->GetCrashReporterEnabled(&enabled)) && enabled) { + cr->AnnotateCrashReport("Winsock_LSP"_ns, mString); + } +} + +NS_IMETHODIMP +LSPAnnotationGatherer::Run() { + DWORD size = 0; + int err; + // Get the size of the buffer we need + if (SOCKET_ERROR != WSCEnumProtocols(nullptr, nullptr, &size, &err) || + err != WSAENOBUFS) { + // Er, what? + MOZ_ASSERT_UNREACHABLE( + "WSCEnumProtocols succeeded when it should have " + "failed"); + return NS_ERROR_FAILURE; + } + + auto byteArray = MakeUnique(size); + WSAPROTOCOL_INFOW* providers = + reinterpret_cast(byteArray.get()); + + int n = WSCEnumProtocols(nullptr, providers, &size, &err); + if (n == SOCKET_ERROR) { + // Lame. We provided the right size buffer; we'll just give up now. + NS_WARNING("Could not get LSP list"); + return NS_ERROR_FAILURE; + } + + nsCString str; + for (int i = 0; i < n; i++) { + AppendUTF16toUTF8(nsDependentString(providers[i].szProtocol), str); + str.AppendLiteral(" : "); + str.AppendInt(providers[i].iVersion); + str.AppendLiteral(" : "); + str.AppendInt(providers[i].iAddressFamily); + str.AppendLiteral(" : "); + str.AppendInt(providers[i].iSocketType); + str.AppendLiteral(" : "); + str.AppendInt(providers[i].iProtocol); + str.AppendLiteral(" : "); + str.AppendPrintf("0x%lx", providers[i].dwServiceFlags1); + str.AppendLiteral(" : "); + str.AppendPrintf("0x%lx", providers[i].dwProviderFlags); + str.AppendLiteral(" : "); + + wchar_t path[MAX_PATH]; + int pathLen = MAX_PATH; + if (!WSCGetProviderPath(&providers[i].ProviderId, path, &pathLen, &err)) { + AppendUTF16toUTF8(nsDependentString(path), str); + } + + str.AppendLiteral(" : "); + // Call WSCGetProviderInfo to obtain the category flags for this provider. + // When present, these flags inform Windows as to which order to chain the + // providers. + DWORD categoryInfo; + size_t categoryInfoSize = sizeof(categoryInfo); + if (!WSCGetProviderInfo(&providers[i].ProviderId, ProviderInfoLspCategories, + (PBYTE)&categoryInfo, &categoryInfoSize, 0, &err)) { + str.AppendPrintf("0x%lx", categoryInfo); + } + + str.AppendLiteral(" : "); + if (providers[i].ProtocolChain.ChainLen <= BASE_PROTOCOL) { + // If we're dealing with a catalog entry that identifies an individual + // base or layer provider, log its provider GUID. + RPC_CSTR provIdStr = nullptr; + if (UuidToStringA(&providers[i].ProviderId, &provIdStr) == RPC_S_OK) { + str.Append(reinterpret_cast(provIdStr)); + RpcStringFreeA(&provIdStr); + } + } + + if (i + 1 != n) { + str.AppendLiteral(" \n "); + } + } + + mString = str; + NS_DispatchToMainThread( + NewRunnableMethod("crashreporter::LSPAnnotationGatherer::Annotate", this, + &LSPAnnotationGatherer::Annotate)); + return NS_OK; +} + +void LSPAnnotate() { + nsCOMPtr runnable(new LSPAnnotationGatherer()); + NS_DispatchBackgroundTask(runnable.forget()); +} + +} // namespace crashreporter +} // namespace mozilla diff --git a/widget/windows/MediaKeysEventSourceFactory.cpp b/widget/windows/MediaKeysEventSourceFactory.cpp new file mode 100644 index 0000000000..525aab19c6 --- /dev/null +++ b/widget/windows/MediaKeysEventSourceFactory.cpp @@ -0,0 +1,20 @@ +/* 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 "MediaKeysEventSourceFactory.h" +#include "WindowsSMTCProvider.h" + +namespace mozilla { +namespace widget { + +mozilla::dom::MediaControlKeySource* CreateMediaControlKeySource() { +#ifndef __MINGW32__ + return new WindowsSMTCProvider(); +#else + return nullptr; // MinGW doesn't support the required Windows 8.1+ APIs +#endif +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/OSKInputPaneManager.cpp b/widget/windows/OSKInputPaneManager.cpp new file mode 100644 index 0000000000..293a84cd28 --- /dev/null +++ b/widget/windows/OSKInputPaneManager.cpp @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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/. */ + +#define NTDDI_VERSION NTDDI_WIN10_RS1 + +#include "OSKInputPaneManager.h" +#include "nsDebug.h" + +#ifndef __MINGW32__ +# include +# include +# include + +using namespace ABI::Windows::UI::ViewManagement; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +#endif + +namespace mozilla { +namespace widget { + +#ifndef __MINGW32__ +static ComPtr GetInputPane(HWND aHwnd) { + ComPtr inputPaneInterop; + HRESULT hr = RoGetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_ViewManagement_InputPane).Get(), + IID_PPV_ARGS(&inputPaneInterop)); + if (NS_WARN_IF(FAILED(hr))) { + return nullptr; + } + + ComPtr inputPane; + hr = inputPaneInterop->GetForWindow(aHwnd, IID_PPV_ARGS(&inputPane)); + if (NS_WARN_IF(FAILED(hr))) { + return nullptr; + } + + ComPtr inputPane2; + inputPane.As(&inputPane2); + return inputPane2; +} + +# ifdef DEBUG +static bool IsInputPaneVisible(ComPtr& aInputPane2) { + ComPtr inputPaneControl; + aInputPane2.As(&inputPaneControl); + if (NS_WARN_IF(!inputPaneControl)) { + return false; + } + boolean visible = false; + inputPaneControl->get_Visible(&visible); + return visible; +} + +static bool IsForegroundApp() { + HWND foregroundWnd = ::GetForegroundWindow(); + if (!foregroundWnd) { + return false; + } + DWORD pid; + ::GetWindowThreadProcessId(foregroundWnd, &pid); + return pid == ::GetCurrentProcessId(); +} +# endif +#endif + +// static +void OSKInputPaneManager::ShowOnScreenKeyboard(HWND aWnd) { +#ifndef __MINGW32__ + ComPtr inputPane2 = GetInputPane(aWnd); + if (!inputPane2) { + return; + } + boolean result; + inputPane2->TryShow(&result); + NS_WARNING_ASSERTION( + result || !IsForegroundApp() || IsInputPaneVisible(inputPane2), + "IInputPane2::TryShow is failure"); +#endif +} + +// static +void OSKInputPaneManager::DismissOnScreenKeyboard(HWND aWnd) { +#ifndef __MINGW32__ + ComPtr inputPane2 = GetInputPane(aWnd); + if (!inputPane2) { + return; + } + boolean result; + inputPane2->TryHide(&result); + NS_WARNING_ASSERTION( + result || !IsForegroundApp() || !IsInputPaneVisible(inputPane2), + "IInputPane2::TryHide is failure"); +#endif +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/OSKInputPaneManager.h b/widget/windows/OSKInputPaneManager.h new file mode 100644 index 0000000000..421a5f1e02 --- /dev/null +++ b/widget/windows/OSKInputPaneManager.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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_widget_OSKInputPaneManager_h +#define mozilla_widget_OSKInputPaneManager_h + +#include + +namespace mozilla { +namespace widget { + +class OSKInputPaneManager final { + public: + static void ShowOnScreenKeyboard(HWND aHwnd); + static void DismissOnScreenKeyboard(HWND aHwnd); +}; + +} // namespace widget +} // namespace mozilla + +#endif // mozilla_widget_OSKInputPaneManager_h diff --git a/widget/windows/OSKTabTipManager.cpp b/widget/windows/OSKTabTipManager.cpp new file mode 100644 index 0000000000..cafed09cbb --- /dev/null +++ b/widget/windows/OSKTabTipManager.cpp @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 4; 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 "OSKTabTipManager.h" + +#include "mozilla/Preferences.h" +#include "nsDebug.h" +#include "WinUtils.h" + +#include +#include +#include + +namespace mozilla { +namespace widget { + +/** + * Get the HWND for the on-screen keyboard, if it's up. Only + * allowed for Windows 8 and higher. + */ +static HWND 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; +} + +// static +void OSKTabTipManager::ShowOnScreenKeyboard() { + const char* kOskPathPrefName = "ui.osk.on_screen_keyboard_path"; + + if (GetOnScreenKeyboardWindow()) { + 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 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(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); +} + +// static +void OSKTabTipManager::DismissOnScreenKeyboard() { + HWND osk = GetOnScreenKeyboardWindow(); + if (osk) { + ::PostMessage(osk, WM_SYSCOMMAND, SC_CLOSE, 0); + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/OSKTabTipManager.h b/widget/windows/OSKTabTipManager.h new file mode 100644 index 0000000000..231403375b --- /dev/null +++ b/widget/windows/OSKTabTipManager.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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 OSKTabTipManager_h +#define OSKTabTipManager_h + +namespace mozilla { +namespace widget { + +class OSKTabTipManager final { + public: + static void ShowOnScreenKeyboard(); + static void DismissOnScreenKeyboard(); +}; + +} // namespace widget +} // namespace mozilla + +#endif // OSKTabTipManager_h diff --git a/widget/windows/OSKVRManager.cpp b/widget/windows/OSKVRManager.cpp new file mode 100644 index 0000000000..158a0f3561 --- /dev/null +++ b/widget/windows/OSKVRManager.cpp @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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 "OSKVRManager.h" + +#include "FxRWindowManager.h" +#include "VRShMem.h" +#include "moz_external_vr.h" + +namespace mozilla { +namespace widget { + +// static +void OSKVRManager::ShowOnScreenKeyboard() { +#ifdef NIGHTLY_BUILD + mozilla::gfx::VRShMem shmem(nullptr, true /*aRequiresMutex*/); + shmem.SendIMEState(FxRWindowManager::GetInstance()->GetWindowID(), + mozilla::gfx::VRFxEventState::FOCUS); +#endif // NIGHTLY_BUILD +} + +// static +void OSKVRManager::DismissOnScreenKeyboard() { +#ifdef NIGHTLY_BUILD + mozilla::gfx::VRShMem shmem(nullptr, true /*aRequiresMutex*/); + shmem.SendIMEState(FxRWindowManager::GetInstance()->GetWindowID(), + mozilla::gfx::VRFxEventState::BLUR); +#endif // NIGHTLY_BUILD +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/OSKVRManager.h b/widget/windows/OSKVRManager.h new file mode 100644 index 0000000000..7c5a1afa07 --- /dev/null +++ b/widget/windows/OSKVRManager.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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 OSKVRManager_h +#define OSKVRManager_h + +namespace mozilla { +namespace widget { + +class OSKVRManager final { + public: + static void ShowOnScreenKeyboard(); + static void DismissOnScreenKeyboard(); +}; + +} // namespace widget +} // namespace mozilla + +#endif // OSKVRManager_h diff --git a/widget/windows/PCompositorWidget.ipdl b/widget/windows/PCompositorWidget.ipdl new file mode 100644 index 0000000000..fe8f7252b6 --- /dev/null +++ b/widget/windows/PCompositorWidget.ipdl @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=99: */ +/* 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 protocol PCompositorBridge; + +include "mozilla/dom/TabMessageUtils.h"; +include "mozilla/widget/WidgetMessageUtils.h"; + +using mozilla::gfx::IntSize from "mozilla/gfx/Point.h"; +using mozilla::WindowsHandle from "mozilla/ipc/IPCTypes.h"; +using nsTransparencyMode from "nsIWidget.h"; +using nsSizeMode from "nsIWidget.h"; + +namespace mozilla { +namespace widget { + +struct RemoteBackbufferHandles { + FileDescriptor fileMapping; + FileDescriptor requestReadyEvent; + FileDescriptor responseReadyEvent; +}; + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +sync protocol PCompositorWidget +{ + manager PCompositorBridge; + +parent: + sync Initialize(RemoteBackbufferHandles aRemoteHandles); + + sync EnterPresentLock(); + sync LeavePresentLock(); + async UpdateTransparency(nsTransparencyMode aMode); + async NotifyVisibilityUpdated(nsSizeMode aSizeMode, bool aIsFullyOccluded); + sync ClearTransparentWindow(); + async __delete__(); + +child: + async ObserveVsync(); + async UnobserveVsync(); + async UpdateCompositorWnd(WindowsHandle aCompositorWnd, WindowsHandle aParentWnd) + returns (bool success); +}; + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/PlatformWidgetTypes.ipdlh b/widget/windows/PlatformWidgetTypes.ipdlh new file mode 100644 index 0000000000..1b7dc66195 --- /dev/null +++ b/widget/windows/PlatformWidgetTypes.ipdlh @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=99: */ +/* 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 "mozilla/dom/TabMessageUtils.h"; +include "mozilla/widget/WidgetMessageUtils.h"; + +include HeadlessWidgetTypes; + +using mozilla::WindowsHandle from "mozilla/ipc/IPCTypes.h"; +using nsSizeMode from "nsIWidgetListener.h"; +using nsTransparencyMode from "nsIWidget.h"; + +namespace mozilla { +namespace widget { + +struct WinCompositorWidgetInitData +{ + WindowsHandle hWnd; + uintptr_t widgetKey; + nsTransparencyMode transparencyMode; + nsSizeMode sizeMode; +}; + +union CompositorWidgetInitData +{ + WinCompositorWidgetInitData; + HeadlessCompositorWidgetInitData; +}; + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/RemoteBackbuffer.cpp b/widget/windows/RemoteBackbuffer.cpp new file mode 100644 index 0000000000..4fc857e96b --- /dev/null +++ b/widget/windows/RemoteBackbuffer.cpp @@ -0,0 +1,711 @@ +/* -*- 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 "RemoteBackbuffer.h" +#include "GeckoProfiler.h" +#include "nsThreadUtils.h" +#include "mozilla/Span.h" +#include +#include + +namespace mozilla { +namespace widget { +namespace remote_backbuffer { + +// This number can be adjusted as a time-memory tradeoff +constexpr uint8_t kMaxDirtyRects = 8; + +struct IpcSafeRect { + explicit IpcSafeRect(const gfx::IntRect& aRect) + : x(aRect.x), y(aRect.y), width(aRect.width), height(aRect.height) {} + int32_t x; + int32_t y; + int32_t width; + int32_t height; +}; + +enum class ResponseResult { + Unknown, + Error, + BorrowSuccess, + BorrowSameBuffer, + PresentSuccess +}; + +enum class SharedDataType { + BorrowRequest, + BorrowRequestAllowSameBuffer, + BorrowResponse, + PresentRequest, + PresentResponse +}; + +struct BorrowResponseData { + ResponseResult result; + int32_t width; + int32_t height; + HANDLE fileMapping; +}; + +struct PresentRequestData { + uint8_t lenDirtyRects; + IpcSafeRect dirtyRects[kMaxDirtyRects]; +}; + +struct PresentResponseData { + ResponseResult result; +}; + +struct SharedData { + SharedDataType dataType; + union { + BorrowResponseData borrowResponse; + PresentRequestData presentRequest; + PresentResponseData presentResponse; + } data; +}; + +static_assert(std::is_trivially_copyable::value && + std::is_standard_layout::value, + "SharedData must be safe to pass over IPC boundaries"); + +class SharedImage { + public: + SharedImage() + : mWidth(0), mHeight(0), mFileMapping(nullptr), mPixelData(nullptr) {} + + ~SharedImage() { + if (mPixelData) { + MOZ_ALWAYS_TRUE(::UnmapViewOfFile(mPixelData)); + } + + if (mFileMapping) { + MOZ_ALWAYS_TRUE(::CloseHandle(mFileMapping)); + } + } + + bool Initialize(int32_t aWidth, int32_t aHeight) { + MOZ_ASSERT(aWidth > 0); + MOZ_ASSERT(aHeight > 0); + + mWidth = aWidth; + mHeight = aHeight; + + DWORD bufferSize = static_cast(mHeight * GetStride()); + + mFileMapping = ::CreateFileMappingW( + INVALID_HANDLE_VALUE, nullptr /*secattr*/, PAGE_READWRITE, + 0 /*sizeHigh*/, bufferSize, nullptr /*name*/); + if (!mFileMapping) { + return false; + } + + void* mappedFilePtr = + ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/, + 0 /*offsetLow*/, 0 /*bytesToMap*/); + if (!mappedFilePtr) { + return false; + } + + mPixelData = reinterpret_cast(mappedFilePtr); + + return true; + } + + bool InitializeRemote(int32_t aWidth, int32_t aHeight, HANDLE aFileMapping) { + MOZ_ASSERT(aWidth > 0); + MOZ_ASSERT(aHeight > 0); + MOZ_ASSERT(aFileMapping); + + mWidth = aWidth; + mHeight = aHeight; + mFileMapping = aFileMapping; + + void* mappedFilePtr = + ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/, + 0 /*offsetLow*/, 0 /*bytesToMap*/); + if (!mappedFilePtr) { + return false; + } + + mPixelData = reinterpret_cast(mappedFilePtr); + + return true; + } + + HBITMAP CreateDIBSection() { + BITMAPINFO bitmapInfo = {}; + bitmapInfo.bmiHeader.biSize = sizeof(bitmapInfo.bmiHeader); + bitmapInfo.bmiHeader.biWidth = mWidth; + bitmapInfo.bmiHeader.biHeight = -mHeight; + bitmapInfo.bmiHeader.biPlanes = 1; + bitmapInfo.bmiHeader.biBitCount = 32; + bitmapInfo.bmiHeader.biCompression = BI_RGB; + void* dummy = nullptr; + return ::CreateDIBSection(nullptr /*paletteDC*/, &bitmapInfo, + DIB_RGB_COLORS, &dummy, mFileMapping, + 0 /*offset*/); + } + + HANDLE CreateRemoteFileMapping(HANDLE aTargetProcess) { + MOZ_ASSERT(aTargetProcess); + + HANDLE fileMapping = nullptr; + if (!::DuplicateHandle(GetCurrentProcess(), mFileMapping, aTargetProcess, + &fileMapping, 0 /*desiredAccess*/, + FALSE /*inheritHandle*/, DUPLICATE_SAME_ACCESS)) { + return nullptr; + } + return fileMapping; + } + + already_AddRefed CreateDrawTarget() { + return gfx::Factory::CreateDrawTargetForData( + gfx::BackendType::CAIRO, mPixelData, IntSize(mWidth, mHeight), + GetStride(), gfx::SurfaceFormat::B8G8R8A8); + } + + void CopyPixelsFrom(const SharedImage& other) { + const unsigned char* src = other.mPixelData; + unsigned char* dst = mPixelData; + + int32_t width = std::min(mWidth, other.mWidth); + int32_t height = std::min(mHeight, other.mHeight); + + for (int32_t row = 0; row < height; ++row) { + memcpy(dst, src, static_cast(width * kBytesPerPixel)); + src += other.GetStride(); + dst += GetStride(); + } + } + + int32_t GetWidth() { return mWidth; } + + int32_t GetHeight() { return mHeight; } + + SharedImage(const SharedImage&) = delete; + SharedImage(SharedImage&&) = delete; + SharedImage& operator=(const SharedImage&) = delete; + SharedImage& operator=(SharedImage&&) = delete; + + private: + static constexpr int32_t kBytesPerPixel = 4; + + int32_t GetStride() const { + // DIB requires 32-bit row alignment + return (((mWidth * kBytesPerPixel) + 3) / 4) * 4; + } + + int32_t mWidth; + int32_t mHeight; + HANDLE mFileMapping; + unsigned char* mPixelData; +}; + +class PresentableSharedImage { + public: + PresentableSharedImage() + : mSharedImage(), + mDeviceContext(nullptr), + mDIBSection(nullptr), + mSavedObject(nullptr) {} + + ~PresentableSharedImage() { + if (mSavedObject) { + MOZ_ALWAYS_TRUE(::SelectObject(mDeviceContext, mSavedObject)); + } + + if (mDIBSection) { + MOZ_ALWAYS_TRUE(::DeleteObject(mDIBSection)); + } + + if (mDeviceContext) { + MOZ_ALWAYS_TRUE(::DeleteDC(mDeviceContext)); + } + } + + bool Initialize(int32_t aWidth, int32_t aHeight) { + if (!mSharedImage.Initialize(aWidth, aHeight)) { + return false; + } + + mDeviceContext = ::CreateCompatibleDC(nullptr); + if (!mDeviceContext) { + return false; + } + + mDIBSection = mSharedImage.CreateDIBSection(); + if (!mDIBSection) { + return false; + } + + mSavedObject = ::SelectObject(mDeviceContext, mDIBSection); + if (!mSavedObject) { + return false; + } + + return true; + } + + bool PresentToWindow(HWND aWindowHandle, nsTransparencyMode aTransparencyMode, + Span aDirtyRects) { + if (aTransparencyMode == eTransparencyTransparent) { + // If our window is a child window or a child-of-a-child, the window + // that needs to be updated is the top level ancestor of the tree + HWND topLevelWindow = WinUtils::GetTopLevelHWND(aWindowHandle, true); + MOZ_ASSERT(::GetWindowLongPtr(topLevelWindow, GWL_EXSTYLE) & + WS_EX_LAYERED); + + BLENDFUNCTION bf = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA}; + POINT srcPos = {0, 0}; + RECT clientRect = {}; + if (!::GetClientRect(aWindowHandle, &clientRect)) { + return false; + } + MOZ_ASSERT(clientRect.left == 0); + MOZ_ASSERT(clientRect.top == 0); + int32_t width = clientRect.right; + int32_t height = clientRect.bottom; + SIZE winSize = {width, height}; + // Window resize could cause the client area to be different than + // mSharedImage's size. If the client area doesn't match, + // PresentToWindow() returns false without calling UpdateLayeredWindow(). + // Another call to UpdateLayeredWindow() will follow shortly, since the + // resize will eventually force the backbuffer to repaint itself again. + // When client area is larger than mSharedImage's size, + // UpdateLayeredWindow() draws the window completely invisible. But it + // does not return false. + if (width != mSharedImage.GetWidth() || + height != mSharedImage.GetHeight()) { + return false; + } + + return !!::UpdateLayeredWindow( + topLevelWindow, nullptr /*paletteDC*/, nullptr /*newPos*/, &winSize, + mDeviceContext, &srcPos, 0 /*colorKey*/, &bf, ULW_ALPHA); + } + + IntRect sharedImageRect{0, 0, mSharedImage.GetWidth(), + mSharedImage.GetHeight()}; + + bool result = true; + + HDC windowDC = ::GetDC(aWindowHandle); + if (!windowDC) { + return false; + } + + for (auto& ipcDirtyRect : aDirtyRects) { + IntRect dirtyRect{ipcDirtyRect.x, ipcDirtyRect.y, ipcDirtyRect.width, + ipcDirtyRect.height}; + IntRect bltRect = dirtyRect.Intersect(sharedImageRect); + + if (!::BitBlt(windowDC, bltRect.x /*dstX*/, bltRect.y /*dstY*/, + bltRect.width, bltRect.height, mDeviceContext, + bltRect.x /*srcX*/, bltRect.y /*srcY*/, SRCCOPY)) { + result = false; + break; + } + } + + MOZ_ALWAYS_TRUE(::ReleaseDC(aWindowHandle, windowDC)); + + return result; + } + + HANDLE CreateRemoteFileMapping(HANDLE aTargetProcess) { + return mSharedImage.CreateRemoteFileMapping(aTargetProcess); + } + + already_AddRefed CreateDrawTarget() { + return mSharedImage.CreateDrawTarget(); + } + + void CopyPixelsFrom(const PresentableSharedImage& other) { + mSharedImage.CopyPixelsFrom(other.mSharedImage); + } + + int32_t GetWidth() { return mSharedImage.GetWidth(); } + + int32_t GetHeight() { return mSharedImage.GetHeight(); } + + PresentableSharedImage(const PresentableSharedImage&) = delete; + PresentableSharedImage(PresentableSharedImage&&) = delete; + PresentableSharedImage& operator=(const PresentableSharedImage&) = delete; + PresentableSharedImage& operator=(PresentableSharedImage&&) = delete; + + private: + SharedImage mSharedImage; + HDC mDeviceContext; + HBITMAP mDIBSection; + HGDIOBJ mSavedObject; +}; + +Provider::Provider() + : mWindowHandle(nullptr), + mTargetProcess(nullptr), + mFileMapping(nullptr), + mRequestReadyEvent(nullptr), + mResponseReadyEvent(nullptr), + mSharedDataPtr(nullptr), + mStopServiceThread(false), + mServiceThread(nullptr), + mBackbuffer() {} + +Provider::~Provider() { + mBackbuffer.reset(); + + if (mServiceThread) { + mStopServiceThread = true; + MOZ_ALWAYS_TRUE(::SetEvent(mRequestReadyEvent)); + MOZ_ALWAYS_TRUE(PR_JoinThread(mServiceThread) == PR_SUCCESS); + } + + if (mSharedDataPtr) { + MOZ_ALWAYS_TRUE(::UnmapViewOfFile(mSharedDataPtr)); + } + + if (mResponseReadyEvent) { + MOZ_ALWAYS_TRUE(::CloseHandle(mResponseReadyEvent)); + } + + if (mRequestReadyEvent) { + MOZ_ALWAYS_TRUE(::CloseHandle(mRequestReadyEvent)); + } + + if (mFileMapping) { + MOZ_ALWAYS_TRUE(::CloseHandle(mFileMapping)); + } + + if (mTargetProcess) { + MOZ_ALWAYS_TRUE(::CloseHandle(mTargetProcess)); + } +} + +bool Provider::Initialize(HWND aWindowHandle, DWORD aTargetProcessId, + nsTransparencyMode aTransparencyMode) { + MOZ_ASSERT(aWindowHandle); + MOZ_ASSERT(aTargetProcessId); + + mWindowHandle = aWindowHandle; + + mTargetProcess = ::OpenProcess(PROCESS_DUP_HANDLE, FALSE /*inheritHandle*/, + aTargetProcessId); + if (!mTargetProcess) { + return false; + } + + mFileMapping = ::CreateFileMappingW( + INVALID_HANDLE_VALUE, nullptr /*secattr*/, PAGE_READWRITE, 0 /*sizeHigh*/, + static_cast(sizeof(SharedData)), nullptr /*name*/); + if (!mFileMapping) { + return false; + } + + mRequestReadyEvent = + ::CreateEventW(nullptr /*secattr*/, FALSE /*manualReset*/, + FALSE /*initialState*/, nullptr /*name*/); + if (!mRequestReadyEvent) { + return false; + } + + mResponseReadyEvent = + ::CreateEventW(nullptr /*secattr*/, FALSE /*manualReset*/, + FALSE /*initialState*/, nullptr /*name*/); + if (!mResponseReadyEvent) { + return false; + } + + void* mappedFilePtr = + ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/, + 0 /*offsetLow*/, 0 /*bytesToMap*/); + if (!mappedFilePtr) { + return false; + } + + mSharedDataPtr = reinterpret_cast(mappedFilePtr); + + mStopServiceThread = false; + + // Use a raw NSPR OS-level thread here instead of nsThread because we are + // performing low-level synchronization across processes using Win32 Events, + // and nsThread is designed around an incompatible "in-process task queue" + // model + mServiceThread = PR_CreateThread( + PR_USER_THREAD, [](void* p) { static_cast(p)->ThreadMain(); }, + this, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, + 0 /*default stack size*/); + if (!mServiceThread) { + return false; + } + + mTransparencyMode = aTransparencyMode; + + return true; +} + +Maybe Provider::CreateRemoteHandles() { + return Some( + RemoteBackbufferHandles(ipc::FileDescriptor(mFileMapping), + ipc::FileDescriptor(mRequestReadyEvent), + ipc::FileDescriptor(mResponseReadyEvent))); +} + +void Provider::UpdateTransparencyMode(nsTransparencyMode aTransparencyMode) { + mTransparencyMode = aTransparencyMode; +} + +void Provider::ThreadMain() { + AUTO_PROFILER_REGISTER_THREAD("RemoteBackbuffer"); + NS_SetCurrentThreadName("RemoteBackbuffer"); + + while (true) { + { + AUTO_PROFILER_THREAD_SLEEP; + MOZ_ALWAYS_TRUE(::WaitForSingleObject(mRequestReadyEvent, INFINITE) == + WAIT_OBJECT_0); + } + + if (mStopServiceThread) { + break; + } + + switch (mSharedDataPtr->dataType) { + case SharedDataType::BorrowRequest: + case SharedDataType::BorrowRequestAllowSameBuffer: { + BorrowResponseData responseData = {}; + + HandleBorrowRequest(&responseData, + mSharedDataPtr->dataType == + SharedDataType::BorrowRequestAllowSameBuffer); + + mSharedDataPtr->dataType = SharedDataType::BorrowResponse; + mSharedDataPtr->data.borrowResponse = responseData; + + MOZ_ALWAYS_TRUE(::SetEvent(mResponseReadyEvent)); + + break; + } + case SharedDataType::PresentRequest: { + PresentRequestData requestData = mSharedDataPtr->data.presentRequest; + PresentResponseData responseData = {}; + + HandlePresentRequest(requestData, &responseData); + + mSharedDataPtr->dataType = SharedDataType::PresentResponse; + mSharedDataPtr->data.presentResponse = responseData; + + MOZ_ALWAYS_TRUE(::SetEvent(mResponseReadyEvent)); + + break; + } + default: + break; + }; + } +} + +void Provider::HandleBorrowRequest(BorrowResponseData* aResponseData, + bool aAllowSameBuffer) { + MOZ_ASSERT(aResponseData); + + aResponseData->result = ResponseResult::Error; + + RECT clientRect = {}; + if (!::GetClientRect(mWindowHandle, &clientRect)) { + return; + } + + MOZ_ASSERT(clientRect.left == 0); + MOZ_ASSERT(clientRect.top == 0); + + int32_t width = clientRect.right ? clientRect.right : 1; + int32_t height = clientRect.bottom ? clientRect.bottom : 1; + + bool needNewBackbuffer = !aAllowSameBuffer || !mBackbuffer || + (mBackbuffer->GetWidth() != width) || + (mBackbuffer->GetHeight() != height); + + if (!needNewBackbuffer) { + aResponseData->result = ResponseResult::BorrowSameBuffer; + return; + } + + auto newBackbuffer = std::make_unique(); + if (!newBackbuffer->Initialize(width, height)) { + return; + } + + // Preserve the contents of the old backbuffer (if it exists) + if (mBackbuffer) { + newBackbuffer->CopyPixelsFrom(*mBackbuffer); + mBackbuffer.reset(); + } + + HANDLE remoteFileMapping = + newBackbuffer->CreateRemoteFileMapping(mTargetProcess); + if (!remoteFileMapping) { + return; + } + + aResponseData->result = ResponseResult::BorrowSuccess; + aResponseData->width = width; + aResponseData->height = height; + aResponseData->fileMapping = remoteFileMapping; + + mBackbuffer = std::move(newBackbuffer); +} + +void Provider::HandlePresentRequest(const PresentRequestData& aRequestData, + PresentResponseData* aResponseData) { + MOZ_ASSERT(aResponseData); + + Span rectSpan(aRequestData.dirtyRects, kMaxDirtyRects); + + aResponseData->result = ResponseResult::Error; + + if (!mBackbuffer) { + return; + } + + if (!mBackbuffer->PresentToWindow( + mWindowHandle, mTransparencyMode, + rectSpan.First(aRequestData.lenDirtyRects))) { + return; + } + + aResponseData->result = ResponseResult::PresentSuccess; +} + +Client::Client() + : mFileMapping(nullptr), + mRequestReadyEvent(nullptr), + mResponseReadyEvent(nullptr), + mSharedDataPtr(nullptr), + mBackbuffer() {} + +Client::~Client() { + mBackbuffer.reset(); + + if (mSharedDataPtr) { + MOZ_ALWAYS_TRUE(::UnmapViewOfFile(mSharedDataPtr)); + } + + if (mResponseReadyEvent) { + MOZ_ALWAYS_TRUE(::CloseHandle(mResponseReadyEvent)); + } + + if (mRequestReadyEvent) { + MOZ_ALWAYS_TRUE(::CloseHandle(mRequestReadyEvent)); + } + + if (mFileMapping) { + MOZ_ALWAYS_TRUE(::CloseHandle(mFileMapping)); + } +} + +bool Client::Initialize(const RemoteBackbufferHandles& aRemoteHandles) { + MOZ_ASSERT(aRemoteHandles.fileMapping().IsValid()); + MOZ_ASSERT(aRemoteHandles.requestReadyEvent().IsValid()); + MOZ_ASSERT(aRemoteHandles.responseReadyEvent().IsValid()); + + // FIXME: Due to PCompositorWidget using virtual Recv methods, + // RemoteBackbufferHandles is passed by const reference, and cannot have its + // signature customized, meaning that we need to clone the handles here. + // + // Once PCompositorWidget is migrated to use direct call semantics, the + // signature can be changed to accept RemoteBackbufferHandles by rvalue + // reference or value, and the DuplicateHandle calls here can be avoided. + mFileMapping = aRemoteHandles.fileMapping().ClonePlatformHandle().release(); + mRequestReadyEvent = + aRemoteHandles.requestReadyEvent().ClonePlatformHandle().release(); + mResponseReadyEvent = + aRemoteHandles.responseReadyEvent().ClonePlatformHandle().release(); + + void* mappedFilePtr = + ::MapViewOfFile(mFileMapping, FILE_MAP_ALL_ACCESS, 0 /*offsetHigh*/, + 0 /*offsetLow*/, 0 /*bytesToMap*/); + if (!mappedFilePtr) { + return false; + } + + mSharedDataPtr = reinterpret_cast(mappedFilePtr); + + return true; +} + +already_AddRefed Client::BorrowDrawTarget() { + mSharedDataPtr->dataType = mBackbuffer + ? SharedDataType::BorrowRequestAllowSameBuffer + : SharedDataType::BorrowRequest; + + MOZ_ALWAYS_TRUE(::SetEvent(mRequestReadyEvent)); + MOZ_ALWAYS_TRUE(::WaitForSingleObject(mResponseReadyEvent, INFINITE) == + WAIT_OBJECT_0); + + if (mSharedDataPtr->dataType != SharedDataType::BorrowResponse) { + return nullptr; + } + + BorrowResponseData responseData = mSharedDataPtr->data.borrowResponse; + + if ((responseData.result != ResponseResult::BorrowSameBuffer) && + (responseData.result != ResponseResult::BorrowSuccess)) { + return nullptr; + } + + if (responseData.result == ResponseResult::BorrowSuccess) { + mBackbuffer.reset(); + + auto newBackbuffer = std::make_unique(); + if (!newBackbuffer->InitializeRemote(responseData.width, + responseData.height, + responseData.fileMapping)) { + return nullptr; + } + + mBackbuffer = std::move(newBackbuffer); + } + + MOZ_ASSERT(mBackbuffer); + + return mBackbuffer->CreateDrawTarget(); +} + +bool Client::PresentDrawTarget(gfx::IntRegion aDirtyRegion) { + mSharedDataPtr->dataType = SharedDataType::PresentRequest; + + // Simplify the region until it has <= kMaxDirtyRects + aDirtyRegion.SimplifyOutward(kMaxDirtyRects); + + Span rectSpan(mSharedDataPtr->data.presentRequest.dirtyRects, kMaxDirtyRects); + + uint8_t rectIndex = 0; + for (auto iter = aDirtyRegion.RectIter(); !iter.Done(); iter.Next()) { + rectSpan[rectIndex] = IpcSafeRect(iter.Get()); + ++rectIndex; + } + + mSharedDataPtr->data.presentRequest.lenDirtyRects = rectIndex; + + MOZ_ALWAYS_TRUE(::SetEvent(mRequestReadyEvent)); + MOZ_ALWAYS_TRUE(::WaitForSingleObject(mResponseReadyEvent, INFINITE) == + WAIT_OBJECT_0); + + if (mSharedDataPtr->dataType != SharedDataType::PresentResponse) { + return false; + } + + if (mSharedDataPtr->data.presentResponse.result != + ResponseResult::PresentSuccess) { + return false; + } + + return true; +} + +} // namespace remote_backbuffer +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/RemoteBackbuffer.h b/widget/windows/RemoteBackbuffer.h new file mode 100644 index 0000000000..090c42fbee --- /dev/null +++ b/widget/windows/RemoteBackbuffer.h @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_windows_RemoteBackbuffer_h +#define widget_windows_RemoteBackbuffer_h + +#include "nsIWidget.h" +#include "mozilla/widget/PCompositorWidgetParent.h" +#include "mozilla/Maybe.h" +#include "prthread.h" +#include + +namespace mozilla { +namespace widget { +namespace remote_backbuffer { + +struct IpcRect; +struct SharedData; +struct BorrowResponseData; +struct PresentRequestData; +struct PresentResponseData; +class SharedImage; +class PresentableSharedImage; + +class Provider { + public: + Provider(); + ~Provider(); + + bool Initialize(HWND aWindowHandle, DWORD aTargetProcessId, + nsTransparencyMode aTransparencyMode); + + Maybe CreateRemoteHandles(); + + void UpdateTransparencyMode(nsTransparencyMode aTransparencyMode); + + Provider(const Provider&) = delete; + Provider(Provider&&) = delete; + Provider& operator=(const Provider&) = delete; + Provider& operator=(Provider&&) = delete; + + private: + void ThreadMain(); + + void HandleBorrowRequest(BorrowResponseData* aResponseData, + bool aAllowSameBuffer); + void HandlePresentRequest(const PresentRequestData& aRequestData, + PresentResponseData* aResponseData); + + HWND mWindowHandle; + HANDLE mTargetProcess; + HANDLE mFileMapping; + HANDLE mRequestReadyEvent; + HANDLE mResponseReadyEvent; + SharedData* mSharedDataPtr; + bool mStopServiceThread; + PRThread* mServiceThread; + std::unique_ptr mBackbuffer; + mozilla::Atomic + mTransparencyMode; +}; + +class Client { + public: + Client(); + ~Client(); + + bool Initialize(const RemoteBackbufferHandles& aRemoteHandles); + + already_AddRefed BorrowDrawTarget(); + bool PresentDrawTarget(gfx::IntRegion aDirtyRegion); + + Client(const Client&) = delete; + Client(Client&&) = delete; + Client& operator=(const Client&) = delete; + Client& operator=(Client&&) = delete; + + private: + HANDLE mFileMapping; + HANDLE mRequestReadyEvent; + HANDLE mResponseReadyEvent; + SharedData* mSharedDataPtr; + std::unique_ptr mBackbuffer; +}; + +} // namespace remote_backbuffer +} // namespace widget +} // namespace mozilla + +#endif // widget_windows_RemoteBackbuffer_h diff --git a/widget/windows/ScreenHelperWin.cpp b/widget/windows/ScreenHelperWin.cpp new file mode 100644 index 0000000000..b3a48bae23 --- /dev/null +++ b/widget/windows/ScreenHelperWin.cpp @@ -0,0 +1,157 @@ +/* -*- 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 "ScreenHelperWin.h" + +#include "mozilla/Logging.h" +#include "nsTArray.h" +#include "WinUtils.h" + +static mozilla::LazyLogModule sScreenLog("WidgetScreen"); + +namespace mozilla { +namespace widget { + +static void GetDisplayInfo(const char16ptr_t aName, + hal::ScreenOrientation& aOrientation, + uint16_t& aAngle, bool& aIsPseudoDisplay, + uint32_t& aRefreshRate) { + DISPLAY_DEVICEW displayDevice = {.cb = sizeof(DISPLAY_DEVICEW)}; + + // XXX Is the pseudodisplay status really useful? + aIsPseudoDisplay = + EnumDisplayDevicesW(aName, 0, &displayDevice, 0) && + (displayDevice.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER); + + DEVMODEW mode = {.dmSize = sizeof(DEVMODEW)}; + if (!EnumDisplaySettingsW(aName, ENUM_CURRENT_SETTINGS, &mode)) { + return; + } + MOZ_ASSERT(mode.dmFields & DM_DISPLAYORIENTATION); + + aRefreshRate = mode.dmDisplayFrequency; + + // conver to default/natural size + if (mode.dmDisplayOrientation == DMDO_90 || + mode.dmDisplayOrientation == DMDO_270) { + DWORD temp = mode.dmPelsHeight; + mode.dmPelsHeight = mode.dmPelsWidth; + mode.dmPelsWidth = temp; + } + + bool defaultIsLandscape = mode.dmPelsWidth >= mode.dmPelsHeight; + switch (mode.dmDisplayOrientation) { + case DMDO_DEFAULT: + aOrientation = defaultIsLandscape + ? hal::ScreenOrientation::LandscapePrimary + : hal::ScreenOrientation::PortraitPrimary; + aAngle = 0; + break; + case DMDO_90: + aOrientation = defaultIsLandscape + ? hal::ScreenOrientation::PortraitPrimary + : hal::ScreenOrientation::LandscapeSecondary; + aAngle = 270; + break; + case DMDO_180: + aOrientation = defaultIsLandscape + ? hal::ScreenOrientation::LandscapeSecondary + : hal::ScreenOrientation::PortraitSecondary; + aAngle = 180; + break; + case DMDO_270: + aOrientation = defaultIsLandscape + ? hal::ScreenOrientation::PortraitSecondary + : hal::ScreenOrientation::LandscapePrimary; + aAngle = 90; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected angle"); + break; + } +} + +BOOL CALLBACK CollectMonitors(HMONITOR aMon, HDC, LPRECT, LPARAM ioParam) { + auto screens = reinterpret_cast>*>(ioParam); + BOOL success = FALSE; + MONITORINFOEX info; + info.cbSize = sizeof(MONITORINFOEX); + success = ::GetMonitorInfoW(aMon, &info); + if (!success) { + MOZ_LOG(sScreenLog, LogLevel::Error, ("GetMonitorInfoW failed")); + return TRUE; // continue the enumeration + } + + double scale = WinUtils::LogToPhysFactor(aMon); + DesktopToLayoutDeviceScale contentsScaleFactor; + if (WinUtils::IsPerMonitorDPIAware()) { + contentsScaleFactor.scale = 1.0; + } else { + contentsScaleFactor.scale = scale; + } + CSSToLayoutDeviceScale defaultCssScaleFactor(scale); + LayoutDeviceIntRect rect(info.rcMonitor.left, info.rcMonitor.top, + info.rcMonitor.right - info.rcMonitor.left, + info.rcMonitor.bottom - info.rcMonitor.top); + LayoutDeviceIntRect availRect(info.rcWork.left, info.rcWork.top, + info.rcWork.right - info.rcWork.left, + info.rcWork.bottom - info.rcWork.top); + + HDC hDC = CreateDC(nullptr, info.szDevice, nullptr, nullptr); + if (!hDC) { + MOZ_LOG(sScreenLog, LogLevel::Error, ("CollectMonitors CreateDC failed")); + return TRUE; + } + uint32_t pixelDepth = ::GetDeviceCaps(hDC, BITSPIXEL); + DeleteDC(hDC); + if (pixelDepth == 32) { + // If a device uses 32 bits per pixel, it's still only using 8 bits + // per color component, which is what our callers want to know. + // (Some devices report 32 and some devices report 24.) + pixelDepth = 24; + } + + float dpi = WinUtils::MonitorDPI(aMon); + + auto orientation = hal::ScreenOrientation::None; + uint16_t angle = 0; + bool isPseudoDisplay = false; + uint32_t refreshRate = 0; + GetDisplayInfo(info.szDevice, orientation, angle, isPseudoDisplay, + refreshRate); + + MOZ_LOG(sScreenLog, LogLevel::Debug, + ("New screen [%s (%s) %d %u %f %f %f %d %d %d]", + ToString(rect).c_str(), ToString(availRect).c_str(), pixelDepth, + refreshRate, contentsScaleFactor.scale, defaultCssScaleFactor.scale, + dpi, isPseudoDisplay, orientation, angle)); + auto screen = MakeRefPtr( + rect, availRect, pixelDepth, pixelDepth, refreshRate, contentsScaleFactor, + defaultCssScaleFactor, dpi, Screen::IsPseudoDisplay(isPseudoDisplay), + orientation, angle); + if (info.dwFlags & MONITORINFOF_PRIMARY) { + // The primary monitor must be the first element of the screen list. + screens->InsertElementAt(0, std::move(screen)); + } else { + screens->AppendElement(std::move(screen)); + } + return TRUE; +} + +void ScreenHelperWin::RefreshScreens() { + MOZ_LOG(sScreenLog, LogLevel::Debug, ("Refreshing screens")); + + AutoTArray, 4> screens; + BOOL result = ::EnumDisplayMonitors( + nullptr, nullptr, (MONITORENUMPROC)CollectMonitors, (LPARAM)&screens); + if (!result) { + NS_WARNING("Unable to EnumDisplayMonitors"); + } + ScreenManager::Refresh(std::move(screens)); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/ScreenHelperWin.h b/widget/windows/ScreenHelperWin.h new file mode 100644 index 0000000000..275014232a --- /dev/null +++ b/widget/windows/ScreenHelperWin.h @@ -0,0 +1,26 @@ +/* -*- 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_widget_windows_ScreenHelperWin_h +#define mozilla_widget_windows_ScreenHelperWin_h + +#include "mozilla/widget/ScreenManager.h" + +namespace mozilla { +namespace widget { + +class ScreenHelperWin final : public ScreenManager::Helper { + public: + ScreenHelperWin(){}; + ~ScreenHelperWin() override {} + + static void RefreshScreens(); +}; + +} // namespace widget +} // namespace mozilla + +#endif // mozilla_widget_windows_ScreenHelperWin_h diff --git a/widget/windows/ShellHeaderOnlyUtils.h b/widget/windows/ShellHeaderOnlyUtils.h new file mode 100644 index 0000000000..11849c99cc --- /dev/null +++ b/widget/windows/ShellHeaderOnlyUtils.h @@ -0,0 +1,178 @@ +/* -*- 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_ShellHeaderOnlyUtils_h +#define mozilla_ShellHeaderOnlyUtils_h + +#include "mozilla/WinHeaderOnlyUtils.h" + +#include + +#include +#include +#include +#include +#include +#include +// NB: include this after shldisp.h so its macros do not conflict with COM +// interfaces defined by shldisp.h +#include +#include + +#include +#include + +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +/** + * Ask the current user's Desktop to ShellExecute on our behalf, thus causing + * the resulting launched process to inherit its security priviliges from + * Explorer instead of our process. + * + * This is useful in two scenarios, in particular: + * * We are running as an elevated user and we want to start something as the + * "normal" user; + * * We are starting a process that is incompatible with our process's + * process mitigation policies. By delegating to Explorer, the child process + * will not be affected by our process mitigations. + * + * Since this communication happens over DCOM, Explorer's COM DACL governs + * whether or not we can execute against it, thus avoiding privilege escalation. + */ +inline LauncherVoidResult ShellExecuteByExplorer(const _bstr_t& aPath, + const _variant_t& aArgs, + const _variant_t& aVerb, + const _variant_t& aWorkingDir, + const _variant_t& aShowCmd) { + // NB: Explorer may be a local server, not an inproc server + RefPtr shellWindows; + HRESULT hr = ::CoCreateInstance( + CLSID_ShellWindows, nullptr, CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER, + IID_IShellWindows, getter_AddRefs(shellWindows)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + // 1. Find the shell view for the desktop. + _variant_t loc(int(CSIDL_DESKTOP)); + _variant_t empty; + long hwnd; + RefPtr dispDesktop; + hr = shellWindows->FindWindowSW(&loc, &empty, SWC_DESKTOP, &hwnd, + SWFO_NEEDDISPATCH, + getter_AddRefs(dispDesktop)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + if (hr == S_FALSE) { + // The call succeeded but the window was not found. + return LAUNCHER_ERROR_FROM_WIN32(ERROR_NOT_FOUND); + } + + RefPtr servProv; + hr = dispDesktop->QueryInterface(IID_IServiceProvider, + getter_AddRefs(servProv)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + RefPtr browser; + hr = servProv->QueryService(SID_STopLevelBrowser, IID_IShellBrowser, + getter_AddRefs(browser)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + RefPtr activeShellView; + hr = browser->QueryActiveShellView(getter_AddRefs(activeShellView)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + // 2. Get the automation object for the desktop. + RefPtr dispView; + hr = activeShellView->GetItemObject(SVGIO_BACKGROUND, IID_IDispatch, + getter_AddRefs(dispView)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + RefPtr folderView; + hr = dispView->QueryInterface(IID_IShellFolderViewDual, + getter_AddRefs(folderView)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + // 3. Get the interface to IShellDispatch2 + RefPtr dispShell; + hr = folderView->get_Application(getter_AddRefs(dispShell)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + RefPtr shellDisp; + hr = + dispShell->QueryInterface(IID_IShellDispatch2, getter_AddRefs(shellDisp)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + // Passing the foreground privilege so that the shell can launch an + // application in the foreground. This fails with E_ACCESSDENIED if the + // current window is shown in the background. We keep a soft assert for + // the other failures to investigate how it happened. + hr = ::CoAllowSetForegroundWindow(shellDisp, nullptr); + MOZ_ASSERT(SUCCEEDED(hr) || hr == E_ACCESSDENIED); + + // shellapi.h macros interfere with the correct naming of the method being + // called on IShellDispatch2. Temporarily remove that definition. +#if defined(ShellExecute) +# define MOZ_REDEFINE_SHELLEXECUTE +# undef ShellExecute +#endif // defined(ShellExecute) + + // 4. Now call IShellDispatch2::ShellExecute to ask Explorer to execute. + hr = shellDisp->ShellExecute(aPath, aArgs, aWorkingDir, aVerb, aShowCmd); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + // Restore the macro that was removed prior to IShellDispatch2::ShellExecute +#if defined(MOZ_REDEFINE_SHELLEXECUTE) +# if defined(UNICODE) +# define ShellExecute ShellExecuteW +# else +# define ShellExecute ShellExecuteA +# endif +# undef MOZ_REDEFINE_SHELLEXECUTE +#endif // defined(MOZ_REDEFINE_SHELLEXECUTE) + + return Ok(); +} + +using UniqueAbsolutePidl = + UniquePtr, CoTaskMemFreeDeleter>; + +inline LauncherResult ShellParseDisplayName( + const wchar_t* aPath) { + PIDLIST_ABSOLUTE rawAbsPidl = nullptr; + SFGAOF sfgao; + HRESULT hr = ::SHParseDisplayName(aPath, nullptr, &rawAbsPidl, 0, &sfgao); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + return UniqueAbsolutePidl(rawAbsPidl); +} + +} // namespace mozilla + +#endif // mozilla_ShellHeaderOnlyUtils_h diff --git a/widget/windows/SystemStatusBar.cpp b/widget/windows/SystemStatusBar.cpp new file mode 100644 index 0000000000..2adc862c1d --- /dev/null +++ b/widget/windows/SystemStatusBar.cpp @@ -0,0 +1,337 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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 +#include "SystemStatusBar.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/LinkedList.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/widget/IconLoader.h" +#include "mozilla/dom/XULButtonElement.h" +#include "nsComputedDOMStyle.h" +#include "nsIContentPolicy.h" +#include "nsISupports.h" +#include "nsMenuPopupFrame.h" +#include "nsXULPopupManager.h" +#include "nsIDocShell.h" +#include "nsDocShell.h" +#include "nsWindowGfx.h" + +namespace mozilla::widget { + +using mozilla::LinkedListElement; +using mozilla::dom::Element; + +class StatusBarEntry final : public LinkedListElement>, + public IconLoader::Listener, + public nsISupports { + public: + explicit StatusBarEntry(Element* aMenu); + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(StatusBarEntry) + nsresult Init(); + void Destroy(); + + MOZ_CAN_RUN_SCRIPT LRESULT OnMessage(HWND hWnd, UINT msg, WPARAM wp, + LPARAM lp); + const Element* GetMenu() { return mMenu; }; + + nsresult OnComplete(imgIContainer* aImage) override; + + private: + ~StatusBarEntry(); + RefPtr mIconLoader; + // Effectively const but is cycle collected + MOZ_KNOWN_LIVE RefPtr mMenu; + NOTIFYICONDATAW mIconData; + boolean mInitted; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(StatusBarEntry) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(StatusBarEntry) + tmp->Destroy(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(StatusBarEntry) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIconLoader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMenu) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StatusBarEntry) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(StatusBarEntry) +NS_IMPL_CYCLE_COLLECTING_RELEASE(StatusBarEntry) + +StatusBarEntry::StatusBarEntry(Element* aMenu) : mMenu(aMenu), mInitted(false) { + mIconData = {/* cbSize */ sizeof(NOTIFYICONDATA), + /* hWnd */ 0, + /* uID */ 2, + /* uFlags */ NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP, + /* uCallbackMessage */ WM_USER, + /* hIcon */ 0, + /* szTip */ L"", // This is updated in Init() + /* dwState */ 0, + /* dwStateMask */ 0, + /* szInfo */ L"", + /* uVersion */ {NOTIFYICON_VERSION_4}, + /* szInfoTitle */ L"", + /* dwInfoFlags */ 0}; + MOZ_ASSERT(mMenu); +} + +StatusBarEntry::~StatusBarEntry() { + if (!mInitted) { + return; + } + Destroy(); + ::Shell_NotifyIconW(NIM_DELETE, &mIconData); + VERIFY(::DestroyWindow(mIconData.hWnd)); +} + +void StatusBarEntry::Destroy() { + if (mIconLoader) { + mIconLoader->Destroy(); + mIconLoader = nullptr; + } +} + +nsresult StatusBarEntry::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + // First, look at the content node's "image" attribute. + nsAutoString imageURIString; + bool hasImageAttr = mMenu->GetAttr(nsGkAtoms::image, imageURIString); + + nsresult rv; + nsCOMPtr iconURI; + if (!hasImageAttr) { + // If the content node has no "image" attribute, get the + // "list-style-image" property from CSS. + RefPtr document = mMenu->GetComposedDoc(); + if (!document) { + return NS_ERROR_FAILURE; + } + + RefPtr sc = + nsComputedDOMStyle::GetComputedStyle(mMenu); + if (!sc) { + return NS_ERROR_FAILURE; + } + + iconURI = sc->StyleList()->GetListStyleImageURI(); + } else { + uint64_t dummy = 0; + nsContentPolicyType policyType; + nsCOMPtr triggeringPrincipal = mMenu->NodePrincipal(); + nsContentUtils::GetContentPolicyTypeForUIImageLoading( + mMenu, getter_AddRefs(triggeringPrincipal), policyType, &dummy); + if (policyType != nsIContentPolicy::TYPE_INTERNAL_IMAGE) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // If this menu item shouldn't have an icon, the string will be empty, + // and NS_NewURI will fail. + rv = NS_NewURI(getter_AddRefs(iconURI), imageURIString); + if (NS_FAILED(rv)) return rv; + } + + mIconLoader = new IconLoader(this); + + if (iconURI) { + rv = mIconLoader->LoadIcon(iconURI, mMenu); + } + + HWND iconWindow; + NS_ENSURE_TRUE(iconWindow = ::CreateWindowExW( + /* extended style */ 0, + /* className */ L"IconWindowClass", + /* title */ 0, + /* style */ WS_CAPTION, + /* x, y, cx, cy */ 0, 0, 0, 0, + /* parent */ 0, + /* menu */ 0, + /* instance */ 0, + /* create struct */ 0), + NS_ERROR_FAILURE); + ::SetWindowLongPtr(iconWindow, GWLP_USERDATA, (LONG_PTR)this); + + mIconData.hWnd = iconWindow; + mIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), IDI_APPLICATION); + + nsAutoString labelAttr; + mMenu->GetAttr(kNameSpaceID_None, nsGkAtoms::label, labelAttr); + const nsString& label = PromiseFlatString(labelAttr); + + size_t destLength = sizeof mIconData.szTip / (sizeof mIconData.szTip[0]); + wchar_t* tooltip = &(mIconData.szTip[0]); + ::StringCchCopyNW(tooltip, destLength, label.get(), label.Length()); + + ::Shell_NotifyIconW(NIM_ADD, &mIconData); + ::Shell_NotifyIconW(NIM_SETVERSION, &mIconData); + + mInitted = true; + return NS_OK; +} + +nsresult StatusBarEntry::OnComplete(imgIContainer* aImage) { + NS_ENSURE_ARG_POINTER(aImage); + + RefPtr kungFuDeathGrip = this; + + nsresult rv = nsWindowGfx::CreateIcon( + aImage, false, LayoutDeviceIntPoint(), + nsWindowGfx::GetIconMetrics(nsWindowGfx::kRegularIcon), &mIconData.hIcon); + NS_ENSURE_SUCCESS(rv, rv); + + ::Shell_NotifyIconW(NIM_MODIFY, &mIconData); + + if (mIconData.hIcon) { + ::DestroyIcon(mIconData.hIcon); + mIconData.hIcon = nullptr; + } + + // To simplify things, we won't react to CSS changes to update the icon + // with this implementation. We can get rid of the IconLoader at this point. + mIconLoader->Destroy(); + mIconLoader = nullptr; + return NS_OK; +} + +LRESULT StatusBarEntry::OnMessage(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { + if (msg == WM_USER && + (LOWORD(lp) == NIN_SELECT || LOWORD(lp) == NIN_KEYSELECT || + LOWORD(lp) == WM_CONTEXTMENU)) { + auto* menu = dom::XULButtonElement::FromNode(mMenu); + if (!menu) { + return TRUE; + } + + nsMenuPopupFrame* popupFrame = menu->GetMenuPopup(FlushType::None); + if (NS_WARN_IF(!popupFrame)) { + return TRUE; + } + + nsIWidget* widget = popupFrame->GetNearestWidget(); + MOZ_DIAGNOSTIC_ASSERT(widget); + if (!widget) { + return TRUE; + } + + HWND win = static_cast(widget->GetNativeData(NS_NATIVE_WINDOW)); + MOZ_DIAGNOSTIC_ASSERT(win); + if (!win) { + return TRUE; + } + + if (LOWORD(lp) == NIN_KEYSELECT && ::GetForegroundWindow() == win) { + // When enter is pressed on the icon, the shell sends two NIN_KEYSELECT + // notifications. This might cause us to open two windows. To work around + // this, if we're already the foreground window (which happens below), + // ignore this notification. + return TRUE; + } + + if (LOWORD(lp) != WM_CONTEXTMENU && + mMenu->HasAttr(kNameSpaceID_None, nsGkAtoms::contextmenu)) { + ::SetForegroundWindow(win); + nsEventStatus status = nsEventStatus_eIgnore; + WidgetMouseEvent event(true, eXULSystemStatusBarClick, nullptr, + WidgetMouseEvent::eReal); + RefPtr presContext = popupFrame->PresContext(); + EventDispatcher::Dispatch(mMenu, presContext, &event, nullptr, &status); + return DefWindowProc(hWnd, msg, wp, lp); + } + + nsPresContext* pc = popupFrame->PresContext(); + const CSSIntPoint point = gfx::RoundedToInt( + LayoutDeviceIntPoint(GET_X_LPARAM(wp), GET_Y_LPARAM(wp)) / + pc->CSSToDevPixelScale()); + + // The menu that is being opened is a Gecko , and the popup code + // that manages it expects that the window that the belongs to + // will be in the foreground when it opens. If we don't do this, then if the + // icon is clicked when the window is _not_ in the foreground, then the + // opened menu will not be keyboard focusable, nor will it close on its own + // if the user clicks away from the menu (at least, not until the user + // focuses any window in the parent process). + ::SetForegroundWindow(win); + nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); + pm->ShowPopupAtScreen(popupFrame->GetContent(), point.x, point.y, false, + nullptr); + } + + return DefWindowProc(hWnd, msg, wp, lp); +} + +NS_IMPL_ISUPPORTS(SystemStatusBar, nsISystemStatusBar) + +MOZ_CAN_RUN_SCRIPT static LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, + WPARAM wp, LPARAM lp) { + if (RefPtr entry = + (StatusBarEntry*)GetWindowLongPtr(hWnd, GWLP_USERDATA)) { + return entry->OnMessage(hWnd, msg, wp, lp); + } + return TRUE; +} + +static StaticRefPtr sSingleton; + +SystemStatusBar& SystemStatusBar::GetSingleton() { + if (!sSingleton) { + sSingleton = new SystemStatusBar(); + ClearOnShutdown(&sSingleton); + } + return *sSingleton; +} + +already_AddRefed SystemStatusBar::GetAddRefedSingleton() { + RefPtr sm = &GetSingleton(); + return sm.forget(); +} + +nsresult SystemStatusBar::Init() { + WNDCLASS classStruct = {/* style */ 0, + /* lpfnWndProc */ &WindowProc, + /* cbClsExtra */ 0, + /* cbWndExtra */ 0, + /* hInstance */ 0, + /* hIcon */ 0, + /* hCursor */ 0, + /* hbrBackground */ 0, + /* lpszMenuName */ 0, + /* lpszClassName */ L"IconWindowClass"}; + NS_ENSURE_TRUE(::RegisterClass(&classStruct), NS_ERROR_FAILURE); + return NS_OK; +} + +NS_IMETHODIMP +SystemStatusBar::AddItem(Element* aElement) { + RefPtr entry = new StatusBarEntry(aElement); + nsresult rv = entry->Init(); + NS_ENSURE_SUCCESS(rv, rv); + + mStatusBarEntries.insertBack(entry); + return NS_OK; +} + +NS_IMETHODIMP +SystemStatusBar::RemoveItem(Element* aElement) { + for (StatusBarEntry* entry : mStatusBarEntries) { + if (entry->GetMenu() == aElement) { + entry->removeFrom(mStatusBarEntries); + return NS_OK; + } + } + return NS_ERROR_NOT_AVAILABLE; +} + +} // namespace mozilla::widget diff --git a/widget/windows/SystemStatusBar.h b/widget/windows/SystemStatusBar.h new file mode 100644 index 0000000000..6c90fc0097 --- /dev/null +++ b/widget/windows/SystemStatusBar.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_windows_SystemStatusBar_h +#define widget_windows_SystemStatusBar_h + +#include "nsISystemStatusBar.h" + +namespace mozilla::widget { +class StatusBarEntry; + +class SystemStatusBar final : public nsISystemStatusBar { + public: + explicit SystemStatusBar() = default; + NS_DECL_ISUPPORTS + NS_DECL_NSISYSTEMSTATUSBAR + + static SystemStatusBar& GetSingleton(); + static already_AddRefed GetAddRefedSingleton(); + + nsresult Init(); + + private: + ~SystemStatusBar() = default; + mozilla::LinkedList> mStatusBarEntries; +}; + +} // namespace mozilla::widget + +#endif // widget_windows_SystemStatusBar_h diff --git a/widget/windows/TSFTextStore.cpp b/widget/windows/TSFTextStore.cpp new file mode 100644 index 0000000000..b34f473c92 --- /dev/null +++ b/widget/windows/TSFTextStore.cpp @@ -0,0 +1,7462 @@ +/* -*- 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/. */ + +#define INPUTSCOPE_INIT_GUID +#define TEXTATTRS_INIT_GUID +#include "TSFTextStore.h" + +#include +#include // for _bstr_t +#include // for SysAllocString +#include +#include "nscore.h" + +#include "IMMHandler.h" +#include "KeyboardLayout.h" +#include "WinIMEHandler.h" +#include "WinUtils.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_intl.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/ToString.h" +#include "mozilla/WindowsVersion.h" +#include "nsWindow.h" +#include "nsPrintfCString.h" + +// Workaround for mingw32 +#ifndef TS_SD_INPUTPANEMANUALDISPLAYENABLE +# define TS_SD_INPUTPANEMANUALDISPLAYENABLE 0x40 +#endif + +// For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync` +// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too +// big file. +// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior. +mozilla::LazyLogModule gIMELog("IMEHandler"); + +// TODO: GUID_PROP_URL has not been declared in the SDK yet. We should drop the +// `s` prefix after it's released by a new SDK and define it with #if. +static const GUID sGUID_PROP_URL = { + 0xd5138268, + 0xa1bf, + 0x4308, + {0xbc, 0xbf, 0x2e, 0x73, 0x93, 0x98, 0xe2, 0x34}}; + +namespace mozilla { +namespace widget { + +/** + * TSF related code should log its behavior even on release build especially + * in the interface methods. + * + * In interface methods, use LogLevel::Info. + * In internal methods, use LogLevel::Debug for logging normal behavior. + * For logging error, use LogLevel::Error. + * + * When an instance method is called, start with following text: + * "0x%p TSFFoo::Bar(", the 0x%p should be the "this" of the nsFoo. + * after that, start with: + * "0x%p TSFFoo::Bar(" + * In an internal method, start with following text: + * "0x%p TSFFoo::Bar(" + * When a static method is called, start with following text: + * "TSFFoo::Bar(" + */ + +enum class TextInputProcessorID { + // Internal use only. This won't be returned by TSFStaticSink::ActiveTIP(). + eNotComputed, + + // Not a TIP. E.g., simple keyboard layout or IMM-IME. + eNone, + + // Used for other TIPs, i.e., any TIPs which we don't support specifically. + eUnknown, + + // TIP for Japanese. + eMicrosoftIMEForJapanese, + eMicrosoftOfficeIME2010ForJapanese, + eGoogleJapaneseInput, + eATOK2011, + eATOK2012, + eATOK2013, + eATOK2014, + eATOK2015, + eATOK2016, + eATOKUnknown, + eJapanist10, + + // TIP for Traditional Chinese. + eMicrosoftBopomofo, + eMicrosoftChangJie, + eMicrosoftPhonetic, + eMicrosoftQuick, + eMicrosoftNewChangJie, + eMicrosoftNewPhonetic, + eMicrosoftNewQuick, + eFreeChangJie, + + // TIP for Simplified Chinese. + eMicrosoftPinyin, + eMicrosoftPinyinNewExperienceInputStyle, + eMicrosoftWubi, + + // TIP for Korean. + eMicrosoftIMEForKorean, + eMicrosoftOldHangul, + + // Keyman Desktop, which can install various language keyboards. + eKeymanDesktop, +}; + +static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; } + +static void HandleSeparator(nsCString& aDesc) { + if (!aDesc.IsEmpty()) { + aDesc.AppendLiteral(" | "); + } +} + +static const nsCString GetFindFlagName(DWORD aFindFlag) { + nsCString description; + if (!aFindFlag) { + description.AppendLiteral("no flags (0)"); + return description; + } + if (aFindFlag & TS_ATTR_FIND_BACKWARDS) { + description.AppendLiteral("TS_ATTR_FIND_BACKWARDS"); + } + if (aFindFlag & TS_ATTR_FIND_WANT_OFFSET) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_WANT_OFFSET"); + } + if (aFindFlag & TS_ATTR_FIND_UPDATESTART) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_UPDATESTART"); + } + if (aFindFlag & TS_ATTR_FIND_WANT_VALUE) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_WANT_VALUE"); + } + if (aFindFlag & TS_ATTR_FIND_WANT_END) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_WANT_END"); + } + if (aFindFlag & TS_ATTR_FIND_HIDDEN) { + HandleSeparator(description); + description.AppendLiteral("TS_ATTR_FIND_HIDDEN"); + } + if (description.IsEmpty()) { + description.AppendLiteral("Unknown ("); + description.AppendInt(static_cast(aFindFlag)); + description.Append(')'); + } + return description; +} + +class GetACPFromPointFlagName : public nsAutoCString { + public: + explicit GetACPFromPointFlagName(DWORD aFlags) { + if (!aFlags) { + AppendLiteral("no flags (0)"); + return; + } + if (aFlags & GXFPF_ROUND_NEAREST) { + AppendLiteral("GXFPF_ROUND_NEAREST"); + aFlags &= ~GXFPF_ROUND_NEAREST; + } + if (aFlags & GXFPF_NEAREST) { + HandleSeparator(*this); + AppendLiteral("GXFPF_NEAREST"); + aFlags &= ~GXFPF_NEAREST; + } + if (aFlags) { + HandleSeparator(*this); + AppendLiteral("Unknown("); + AppendInt(static_cast(aFlags)); + Append(')'); + } + } + virtual ~GetACPFromPointFlagName() {} +}; + +static const char* GetFocusChangeName( + InputContextAction::FocusChange aFocusChange) { + switch (aFocusChange) { + case InputContextAction::FOCUS_NOT_CHANGED: + return "FOCUS_NOT_CHANGED"; + case InputContextAction::GOT_FOCUS: + return "GOT_FOCUS"; + case InputContextAction::LOST_FOCUS: + return "LOST_FOCUS"; + case InputContextAction::MENU_GOT_PSEUDO_FOCUS: + return "MENU_GOT_PSEUDO_FOCUS"; + case InputContextAction::MENU_LOST_PSEUDO_FOCUS: + return "MENU_LOST_PSEUDO_FOCUS"; + case InputContextAction::WIDGET_CREATED: + return "WIDGET_CREATED"; + default: + return "Unknown"; + } +} + +static nsCString GetCLSIDNameStr(REFCLSID aCLSID) { + LPOLESTR str = nullptr; + HRESULT hr = ::StringFromCLSID(aCLSID, &str); + if (FAILED(hr) || !str || !str[0]) { + return ""_ns; + } + + nsCString result; + result = NS_ConvertUTF16toUTF8(str); + ::CoTaskMemFree(str); + return result; +} + +static nsCString GetGUIDNameStr(REFGUID aGUID) { + OLECHAR str[40]; + int len = ::StringFromGUID2(aGUID, str, ArrayLength(str)); + if (!len || !str[0]) { + return ""_ns; + } + + return NS_ConvertUTF16toUTF8(str); +} + +static nsCString GetGUIDNameStrWithTable(REFGUID aGUID) { +#define RETURN_GUID_NAME(aNamedGUID) \ + if (IsEqualGUID(aGUID, aNamedGUID)) { \ + return nsLiteralCString(#aNamedGUID); \ + } + + RETURN_GUID_NAME(GUID_PROP_INPUTSCOPE) + RETURN_GUID_NAME(sGUID_PROP_URL) + RETURN_GUID_NAME(TSATTRID_OTHERS) + RETURN_GUID_NAME(TSATTRID_Font) + RETURN_GUID_NAME(TSATTRID_Font_FaceName) + RETURN_GUID_NAME(TSATTRID_Font_SizePts) + RETURN_GUID_NAME(TSATTRID_Font_Style) + RETURN_GUID_NAME(TSATTRID_Font_Style_Bold) + RETURN_GUID_NAME(TSATTRID_Font_Style_Italic) + RETURN_GUID_NAME(TSATTRID_Font_Style_SmallCaps) + RETURN_GUID_NAME(TSATTRID_Font_Style_Capitalize) + RETURN_GUID_NAME(TSATTRID_Font_Style_Uppercase) + RETURN_GUID_NAME(TSATTRID_Font_Style_Lowercase) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_LasVegasLights) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_BlinkingBackground) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_SparkleText) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_MarchingBlackAnts) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_MarchingRedAnts) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_Shimmer) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_WipeDown) + RETURN_GUID_NAME(TSATTRID_Font_Style_Animation_WipeRight) + RETURN_GUID_NAME(TSATTRID_Font_Style_Emboss) + RETURN_GUID_NAME(TSATTRID_Font_Style_Engrave) + RETURN_GUID_NAME(TSATTRID_Font_Style_Hidden) + RETURN_GUID_NAME(TSATTRID_Font_Style_Kerning) + RETURN_GUID_NAME(TSATTRID_Font_Style_Outlined) + RETURN_GUID_NAME(TSATTRID_Font_Style_Position) + RETURN_GUID_NAME(TSATTRID_Font_Style_Protected) + RETURN_GUID_NAME(TSATTRID_Font_Style_Shadow) + RETURN_GUID_NAME(TSATTRID_Font_Style_Spacing) + RETURN_GUID_NAME(TSATTRID_Font_Style_Weight) + RETURN_GUID_NAME(TSATTRID_Font_Style_Height) + RETURN_GUID_NAME(TSATTRID_Font_Style_Underline) + RETURN_GUID_NAME(TSATTRID_Font_Style_Underline_Single) + RETURN_GUID_NAME(TSATTRID_Font_Style_Underline_Double) + RETURN_GUID_NAME(TSATTRID_Font_Style_Strikethrough) + RETURN_GUID_NAME(TSATTRID_Font_Style_Strikethrough_Single) + RETURN_GUID_NAME(TSATTRID_Font_Style_Strikethrough_Double) + RETURN_GUID_NAME(TSATTRID_Font_Style_Overline) + RETURN_GUID_NAME(TSATTRID_Font_Style_Overline_Single) + RETURN_GUID_NAME(TSATTRID_Font_Style_Overline_Double) + RETURN_GUID_NAME(TSATTRID_Font_Style_Blink) + RETURN_GUID_NAME(TSATTRID_Font_Style_Subscript) + RETURN_GUID_NAME(TSATTRID_Font_Style_Superscript) + RETURN_GUID_NAME(TSATTRID_Font_Style_Color) + RETURN_GUID_NAME(TSATTRID_Font_Style_BackgroundColor) + RETURN_GUID_NAME(TSATTRID_Text) + RETURN_GUID_NAME(TSATTRID_Text_VerticalWriting) + RETURN_GUID_NAME(TSATTRID_Text_RightToLeft) + RETURN_GUID_NAME(TSATTRID_Text_Orientation) + RETURN_GUID_NAME(TSATTRID_Text_Language) + RETURN_GUID_NAME(TSATTRID_Text_ReadOnly) + RETURN_GUID_NAME(TSATTRID_Text_EmbeddedObject) + RETURN_GUID_NAME(TSATTRID_Text_Alignment) + RETURN_GUID_NAME(TSATTRID_Text_Alignment_Left) + RETURN_GUID_NAME(TSATTRID_Text_Alignment_Right) + RETURN_GUID_NAME(TSATTRID_Text_Alignment_Center) + RETURN_GUID_NAME(TSATTRID_Text_Alignment_Justify) + RETURN_GUID_NAME(TSATTRID_Text_Link) + RETURN_GUID_NAME(TSATTRID_Text_Hyphenation) + RETURN_GUID_NAME(TSATTRID_Text_Para) + RETURN_GUID_NAME(TSATTRID_Text_Para_FirstLineIndent) + RETURN_GUID_NAME(TSATTRID_Text_Para_LeftIndent) + RETURN_GUID_NAME(TSATTRID_Text_Para_RightIndent) + RETURN_GUID_NAME(TSATTRID_Text_Para_SpaceAfter) + RETURN_GUID_NAME(TSATTRID_Text_Para_SpaceBefore) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Single) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_OnePtFive) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Double) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_AtLeast) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Exactly) + RETURN_GUID_NAME(TSATTRID_Text_Para_LineSpacing_Multiple) + RETURN_GUID_NAME(TSATTRID_List) + RETURN_GUID_NAME(TSATTRID_List_LevelIndel) + RETURN_GUID_NAME(TSATTRID_List_Type) + RETURN_GUID_NAME(TSATTRID_List_Type_Bullet) + RETURN_GUID_NAME(TSATTRID_List_Type_Arabic) + RETURN_GUID_NAME(TSATTRID_List_Type_LowerLetter) + RETURN_GUID_NAME(TSATTRID_List_Type_UpperLetter) + RETURN_GUID_NAME(TSATTRID_List_Type_LowerRoman) + RETURN_GUID_NAME(TSATTRID_List_Type_UpperRoman) + RETURN_GUID_NAME(TSATTRID_App) + RETURN_GUID_NAME(TSATTRID_App_IncorrectSpelling) + RETURN_GUID_NAME(TSATTRID_App_IncorrectGrammar) + +#undef RETURN_GUID_NAME + + return GetGUIDNameStr(aGUID); +} + +static nsCString GetRIIDNameStr(REFIID aRIID) { + LPOLESTR str = nullptr; + HRESULT hr = ::StringFromIID(aRIID, &str); + if (FAILED(hr) || !str || !str[0]) { + return ""_ns; + } + + nsAutoString key(L"Interface\\"); + key += str; + + nsCString result; + wchar_t buf[256]; + if (WinUtils::GetRegistryKey(HKEY_CLASSES_ROOT, key.get(), nullptr, buf, + sizeof(buf))) { + result = NS_ConvertUTF16toUTF8(buf); + } else { + result = NS_ConvertUTF16toUTF8(str); + } + + ::CoTaskMemFree(str); + return result; +} + +static const char* GetCommonReturnValueName(HRESULT aResult) { + switch (aResult) { + case S_OK: + return "S_OK"; + case E_ABORT: + return "E_ABORT"; + case E_ACCESSDENIED: + return "E_ACCESSDENIED"; + case E_FAIL: + return "E_FAIL"; + case E_HANDLE: + return "E_HANDLE"; + case E_INVALIDARG: + return "E_INVALIDARG"; + case E_NOINTERFACE: + return "E_NOINTERFACE"; + case E_NOTIMPL: + return "E_NOTIMPL"; + case E_OUTOFMEMORY: + return "E_OUTOFMEMORY"; + case E_POINTER: + return "E_POINTER"; + case E_UNEXPECTED: + return "E_UNEXPECTED"; + default: + return SUCCEEDED(aResult) ? "Succeeded" : "Failed"; + } +} + +static const char* GetTextStoreReturnValueName(HRESULT aResult) { + switch (aResult) { + case TS_E_FORMAT: + return "TS_E_FORMAT"; + case TS_E_INVALIDPOINT: + return "TS_E_INVALIDPOINT"; + case TS_E_INVALIDPOS: + return "TS_E_INVALIDPOS"; + case TS_E_NOINTERFACE: + return "TS_E_NOINTERFACE"; + case TS_E_NOLAYOUT: + return "TS_E_NOLAYOUT"; + case TS_E_NOLOCK: + return "TS_E_NOLOCK"; + case TS_E_NOOBJECT: + return "TS_E_NOOBJECT"; + case TS_E_NOSELECTION: + return "TS_E_NOSELECTION"; + case TS_E_NOSERVICE: + return "TS_E_NOSERVICE"; + case TS_E_READONLY: + return "TS_E_READONLY"; + case TS_E_SYNCHRONOUS: + return "TS_E_SYNCHRONOUS"; + case TS_S_ASYNC: + return "TS_S_ASYNC"; + default: + return GetCommonReturnValueName(aResult); + } +} + +static const nsCString GetSinkMaskNameStr(DWORD aSinkMask) { + nsCString description; + if (aSinkMask & TS_AS_TEXT_CHANGE) { + description.AppendLiteral("TS_AS_TEXT_CHANGE"); + } + if (aSinkMask & TS_AS_SEL_CHANGE) { + HandleSeparator(description); + description.AppendLiteral("TS_AS_SEL_CHANGE"); + } + if (aSinkMask & TS_AS_LAYOUT_CHANGE) { + HandleSeparator(description); + description.AppendLiteral("TS_AS_LAYOUT_CHANGE"); + } + if (aSinkMask & TS_AS_ATTR_CHANGE) { + HandleSeparator(description); + description.AppendLiteral("TS_AS_ATTR_CHANGE"); + } + if (aSinkMask & TS_AS_STATUS_CHANGE) { + HandleSeparator(description); + description.AppendLiteral("TS_AS_STATUS_CHANGE"); + } + if (description.IsEmpty()) { + description.AppendLiteral("not-specified"); + } + return description; +} + +static const nsCString GetLockFlagNameStr(DWORD aLockFlags) { + nsCString description; + if ((aLockFlags & TS_LF_READWRITE) == TS_LF_READWRITE) { + description.AppendLiteral("TS_LF_READWRITE"); + } else if (aLockFlags & TS_LF_READ) { + description.AppendLiteral("TS_LF_READ"); + } + if (aLockFlags & TS_LF_SYNC) { + if (!description.IsEmpty()) { + description.AppendLiteral(" | "); + } + description.AppendLiteral("TS_LF_SYNC"); + } + if (description.IsEmpty()) { + description.AppendLiteral("not-specified"); + } + return description; +} + +static const char* GetTextRunTypeName(TsRunType aRunType) { + switch (aRunType) { + case TS_RT_PLAIN: + return "TS_RT_PLAIN"; + case TS_RT_HIDDEN: + return "TS_RT_HIDDEN"; + case TS_RT_OPAQUE: + return "TS_RT_OPAQUE"; + default: + return "Unknown"; + } +} + +static nsCString GetColorName(const TF_DA_COLOR& aColor) { + switch (aColor.type) { + case TF_CT_NONE: + return "TF_CT_NONE"_ns; + case TF_CT_SYSCOLOR: + return nsPrintfCString("TF_CT_SYSCOLOR, nIndex:0x%08X", + static_cast(aColor.nIndex)); + case TF_CT_COLORREF: + return nsPrintfCString("TF_CT_COLORREF, cr:0x%08X", + static_cast(aColor.cr)); + break; + default: + return nsPrintfCString("Unknown(%08X)", + static_cast(aColor.type)); + } +} + +static nsCString GetLineStyleName(TF_DA_LINESTYLE aLineStyle) { + switch (aLineStyle) { + case TF_LS_NONE: + return "TF_LS_NONE"_ns; + case TF_LS_SOLID: + return "TF_LS_SOLID"_ns; + case TF_LS_DOT: + return "TF_LS_DOT"_ns; + case TF_LS_DASH: + return "TF_LS_DASH"_ns; + case TF_LS_SQUIGGLE: + return "TF_LS_SQUIGGLE"_ns; + default: { + return nsPrintfCString("Unknown(%08X)", static_cast(aLineStyle)); + } + } +} + +static nsCString GetClauseAttrName(TF_DA_ATTR_INFO aAttr) { + switch (aAttr) { + case TF_ATTR_INPUT: + return "TF_ATTR_INPUT"_ns; + case TF_ATTR_TARGET_CONVERTED: + return "TF_ATTR_TARGET_CONVERTED"_ns; + case TF_ATTR_CONVERTED: + return "TF_ATTR_CONVERTED"_ns; + case TF_ATTR_TARGET_NOTCONVERTED: + return "TF_ATTR_TARGET_NOTCONVERTED"_ns; + case TF_ATTR_INPUT_ERROR: + return "TF_ATTR_INPUT_ERROR"_ns; + case TF_ATTR_FIXEDCONVERTED: + return "TF_ATTR_FIXEDCONVERTED"_ns; + case TF_ATTR_OTHER: + return "TF_ATTR_OTHER"_ns; + default: { + return nsPrintfCString("Unknown(%08X)", static_cast(aAttr)); + } + } +} + +static nsCString GetDisplayAttrStr(const TF_DISPLAYATTRIBUTE& aDispAttr) { + nsCString str; + str = "crText:{ "; + str += GetColorName(aDispAttr.crText); + str += " }, crBk:{ "; + str += GetColorName(aDispAttr.crBk); + str += " }, lsStyle: "; + str += GetLineStyleName(aDispAttr.lsStyle); + str += ", fBoldLine: "; + str += GetBoolName(aDispAttr.fBoldLine); + str += ", crLine:{ "; + str += GetColorName(aDispAttr.crLine); + str += " }, bAttr: "; + str += GetClauseAttrName(aDispAttr.bAttr); + return str; +} + +static const char* GetMouseButtonName(int16_t aButton) { + switch (aButton) { + case MouseButton::ePrimary: + return "LeftButton"; + case MouseButton::eMiddle: + return "MiddleButton"; + case MouseButton::eSecondary: + return "RightButton"; + default: + return "UnknownButton"; + } +} + +#define ADD_SEPARATOR_IF_NECESSARY(aStr) \ + if (!aStr.IsEmpty()) { \ + aStr.AppendLiteral(", "); \ + } + +static nsCString GetMouseButtonsName(int16_t aButtons) { + if (!aButtons) { + return "no buttons"_ns; + } + nsCString names; + if (aButtons & MouseButtonsFlag::ePrimaryFlag) { + names = "LeftButton"; + } + if (aButtons & MouseButtonsFlag::eSecondaryFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "RightButton"; + } + if (aButtons & MouseButtonsFlag::eMiddleFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "MiddleButton"; + } + if (aButtons & MouseButtonsFlag::e4thFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "4thButton"; + } + if (aButtons & MouseButtonsFlag::e5thFlag) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += "5thButton"; + } + return names; +} + +static nsCString GetModifiersName(Modifiers aModifiers) { + if (aModifiers == MODIFIER_NONE) { + return "no modifiers"_ns; + } + nsCString names; + if (aModifiers & MODIFIER_ALT) { + names = NS_DOM_KEYNAME_ALT; + } + if (aModifiers & MODIFIER_ALTGRAPH) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_ALTGRAPH; + } + if (aModifiers & MODIFIER_CAPSLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_CAPSLOCK; + } + if (aModifiers & MODIFIER_CONTROL) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_CONTROL; + } + if (aModifiers & MODIFIER_FN) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_FN; + } + if (aModifiers & MODIFIER_FNLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_FNLOCK; + } + if (aModifiers & MODIFIER_META) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_META; + } + if (aModifiers & MODIFIER_NUMLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_NUMLOCK; + } + if (aModifiers & MODIFIER_SCROLLLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_SCROLLLOCK; + } + if (aModifiers & MODIFIER_SHIFT) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_SHIFT; + } + if (aModifiers & MODIFIER_SYMBOL) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_SYMBOL; + } + if (aModifiers & MODIFIER_SYMBOLLOCK) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_SYMBOLLOCK; + } + if (aModifiers & MODIFIER_OS) { + ADD_SEPARATOR_IF_NECESSARY(names); + names += NS_DOM_KEYNAME_OS; + } + return names; +} + +class GetWritingModeName : public nsAutoCString { + public: + explicit GetWritingModeName(const WritingMode& aWritingMode) { + if (!aWritingMode.IsVertical()) { + AssignLiteral("Horizontal"); + return; + } + if (aWritingMode.IsVerticalLR()) { + AssignLiteral("Vertical (LR)"); + return; + } + AssignLiteral("Vertical (RL)"); + } + virtual ~GetWritingModeName() {} +}; + +class GetEscapedUTF8String final : public NS_ConvertUTF16toUTF8 { + public: + explicit GetEscapedUTF8String(const nsAString& aString) + : NS_ConvertUTF16toUTF8(aString) { + Escape(); + } + explicit GetEscapedUTF8String(const char16ptr_t aString) + : NS_ConvertUTF16toUTF8(aString) { + Escape(); + } + GetEscapedUTF8String(const char16ptr_t aString, uint32_t aLength) + : NS_ConvertUTF16toUTF8(aString, aLength) { + Escape(); + } + + private: + void Escape() { + ReplaceSubstring("\r", "\\r"); + ReplaceSubstring("\n", "\\n"); + ReplaceSubstring("\t", "\\t"); + } +}; + +class GetInputScopeString : public nsAutoCString { + public: + explicit GetInputScopeString(const nsTArray& aList) { + for (InputScope inputScope : aList) { + if (!IsEmpty()) { + AppendLiteral(", "); + } + switch (inputScope) { + case IS_DEFAULT: + AppendLiteral("IS_DEFAULT"); + break; + case IS_URL: + AppendLiteral("IS_URL"); + break; + case IS_FILE_FULLFILEPATH: + AppendLiteral("IS_FILE_FULLFILEPATH"); + break; + case IS_FILE_FILENAME: + AppendLiteral("IS_FILE_FILENAME"); + break; + case IS_EMAIL_USERNAME: + AppendLiteral("IS_EMAIL_USERNAME"); + break; + case IS_EMAIL_SMTPEMAILADDRESS: + AppendLiteral("IS_EMAIL_SMTPEMAILADDRESS"); + break; + case IS_LOGINNAME: + AppendLiteral("IS_LOGINNAME"); + break; + case IS_PERSONALNAME_FULLNAME: + AppendLiteral("IS_PERSONALNAME_FULLNAME"); + break; + case IS_PERSONALNAME_PREFIX: + AppendLiteral("IS_PERSONALNAME_PREFIX"); + break; + case IS_PERSONALNAME_GIVENNAME: + AppendLiteral("IS_PERSONALNAME_GIVENNAME"); + break; + case IS_PERSONALNAME_MIDDLENAME: + AppendLiteral("IS_PERSONALNAME_MIDDLENAME"); + break; + case IS_PERSONALNAME_SURNAME: + AppendLiteral("IS_PERSONALNAME_SURNAME"); + break; + case IS_PERSONALNAME_SUFFIX: + AppendLiteral("IS_PERSONALNAME_SUFFIX"); + break; + case IS_ADDRESS_FULLPOSTALADDRESS: + AppendLiteral("IS_ADDRESS_FULLPOSTALADDRESS"); + break; + case IS_ADDRESS_POSTALCODE: + AppendLiteral("IS_ADDRESS_POSTALCODE"); + break; + case IS_ADDRESS_STREET: + AppendLiteral("IS_ADDRESS_STREET"); + break; + case IS_ADDRESS_STATEORPROVINCE: + AppendLiteral("IS_ADDRESS_STATEORPROVINCE"); + break; + case IS_ADDRESS_CITY: + AppendLiteral("IS_ADDRESS_CITY"); + break; + case IS_ADDRESS_COUNTRYNAME: + AppendLiteral("IS_ADDRESS_COUNTRYNAME"); + break; + case IS_ADDRESS_COUNTRYSHORTNAME: + AppendLiteral("IS_ADDRESS_COUNTRYSHORTNAME"); + break; + case IS_CURRENCY_AMOUNTANDSYMBOL: + AppendLiteral("IS_CURRENCY_AMOUNTANDSYMBOL"); + break; + case IS_CURRENCY_AMOUNT: + AppendLiteral("IS_CURRENCY_AMOUNT"); + break; + case IS_DATE_FULLDATE: + AppendLiteral("IS_DATE_FULLDATE"); + break; + case IS_DATE_MONTH: + AppendLiteral("IS_DATE_MONTH"); + break; + case IS_DATE_DAY: + AppendLiteral("IS_DATE_DAY"); + break; + case IS_DATE_YEAR: + AppendLiteral("IS_DATE_YEAR"); + break; + case IS_DATE_MONTHNAME: + AppendLiteral("IS_DATE_MONTHNAME"); + break; + case IS_DATE_DAYNAME: + AppendLiteral("IS_DATE_DAYNAME"); + break; + case IS_DIGITS: + AppendLiteral("IS_DIGITS"); + break; + case IS_NUMBER: + AppendLiteral("IS_NUMBER"); + break; + case IS_ONECHAR: + AppendLiteral("IS_ONECHAR"); + break; + case IS_PASSWORD: + AppendLiteral("IS_PASSWORD"); + break; + case IS_TELEPHONE_FULLTELEPHONENUMBER: + AppendLiteral("IS_TELEPHONE_FULLTELEPHONENUMBER"); + break; + case IS_TELEPHONE_COUNTRYCODE: + AppendLiteral("IS_TELEPHONE_COUNTRYCODE"); + break; + case IS_TELEPHONE_AREACODE: + AppendLiteral("IS_TELEPHONE_AREACODE"); + break; + case IS_TELEPHONE_LOCALNUMBER: + AppendLiteral("IS_TELEPHONE_LOCALNUMBER"); + break; + case IS_TIME_FULLTIME: + AppendLiteral("IS_TIME_FULLTIME"); + break; + case IS_TIME_HOUR: + AppendLiteral("IS_TIME_HOUR"); + break; + case IS_TIME_MINORSEC: + AppendLiteral("IS_TIME_MINORSEC"); + break; + case IS_NUMBER_FULLWIDTH: + AppendLiteral("IS_NUMBER_FULLWIDTH"); + break; + case IS_ALPHANUMERIC_HALFWIDTH: + AppendLiteral("IS_ALPHANUMERIC_HALFWIDTH"); + break; + case IS_ALPHANUMERIC_FULLWIDTH: + AppendLiteral("IS_ALPHANUMERIC_FULLWIDTH"); + break; + case IS_CURRENCY_CHINESE: + AppendLiteral("IS_CURRENCY_CHINESE"); + break; + case IS_BOPOMOFO: + AppendLiteral("IS_BOPOMOFO"); + break; + case IS_HIRAGANA: + AppendLiteral("IS_HIRAGANA"); + break; + case IS_KATAKANA_HALFWIDTH: + AppendLiteral("IS_KATAKANA_HALFWIDTH"); + break; + case IS_KATAKANA_FULLWIDTH: + AppendLiteral("IS_KATAKANA_FULLWIDTH"); + break; + case IS_HANJA: + AppendLiteral("IS_HANJA"); + break; + case IS_PHRASELIST: + AppendLiteral("IS_PHRASELIST"); + break; + case IS_REGULAREXPRESSION: + AppendLiteral("IS_REGULAREXPRESSION"); + break; + case IS_SRGS: + AppendLiteral("IS_SRGS"); + break; + case IS_XML: + AppendLiteral("IS_XML"); + break; + case IS_PRIVATE: + AppendLiteral("IS_PRIVATE"); + break; + default: + AppendPrintf("Unknown Value(%d)", inputScope); + break; + } + } + } +}; + +/******************************************************************/ +/* InputScopeImpl */ +/******************************************************************/ + +class InputScopeImpl final : public ITfInputScope { + ~InputScopeImpl() {} + + public: + explicit InputScopeImpl(const nsTArray& aList) + : mInputScopes(aList.Clone()) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p InputScopeImpl(%s)", this, GetInputScopeString(aList).get())); + } + + NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(InputScopeImpl) + + STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + if ((IID_IUnknown == riid) || (IID_ITfInputScope == riid)) { + *ppv = static_cast(this); + } + if (*ppv) { + AddRef(); + return S_OK; + } + return E_NOINTERFACE; + } + + STDMETHODIMP GetInputScopes(InputScope** pprgInputScopes, UINT* pcCount) { + uint32_t count = (mInputScopes.IsEmpty() ? 1 : mInputScopes.Length()); + + InputScope* pScope = + (InputScope*)CoTaskMemAlloc(sizeof(InputScope) * count); + NS_ENSURE_TRUE(pScope, E_OUTOFMEMORY); + + if (mInputScopes.IsEmpty()) { + *pScope = IS_DEFAULT; + *pcCount = 1; + *pprgInputScopes = pScope; + return S_OK; + } + + *pcCount = 0; + + for (uint32_t idx = 0; idx < count; idx++) { + *(pScope + idx) = mInputScopes[idx]; + (*pcCount)++; + } + + *pprgInputScopes = pScope; + return S_OK; + } + + STDMETHODIMP GetPhrase(BSTR** ppbstrPhrases, UINT* pcCount) { + return E_NOTIMPL; + } + STDMETHODIMP GetRegularExpression(BSTR* pbstrRegExp) { return E_NOTIMPL; } + STDMETHODIMP GetSRGS(BSTR* pbstrSRGS) { return E_NOTIMPL; } + STDMETHODIMP GetXML(BSTR* pbstrXML) { return E_NOTIMPL; } + + private: + nsTArray mInputScopes; +}; + +/******************************************************************/ +/* TSFStaticSink */ +/******************************************************************/ + +class TSFStaticSink final : public ITfInputProcessorProfileActivationSink { + public: + static TSFStaticSink* GetInstance() { + if (!sInstance) { + RefPtr threadMgr = TSFTextStore::GetThreadMgr(); + if (NS_WARN_IF(!threadMgr)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("TSFStaticSink::GetInstance() FAILED to initialize TSFStaticSink " + "instance due to no ThreadMgr instance")); + return nullptr; + } + RefPtr inputProcessorProfiles = + TSFTextStore::GetInputProcessorProfiles(); + if (NS_WARN_IF(!inputProcessorProfiles)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("TSFStaticSink::GetInstance() FAILED to initialize TSFStaticSink " + "instance due to no InputProcessorProfiles instance")); + return nullptr; + } + RefPtr staticSink = new TSFStaticSink(); + if (NS_WARN_IF(!staticSink->Init(threadMgr, inputProcessorProfiles))) { + staticSink->Destroy(); + MOZ_LOG( + gIMELog, LogLevel::Error, + ("TSFStaticSink::GetInstance() FAILED to initialize TSFStaticSink " + "instance")); + return nullptr; + } + sInstance = staticSink.forget(); + } + return sInstance; + } + + static void Shutdown() { + if (sInstance) { + sInstance->Destroy(); + sInstance = nullptr; + } + } + + bool Init(ITfThreadMgr* aThreadMgr, + ITfInputProcessorProfiles* aInputProcessorProfiles); + STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + if (IID_IUnknown == riid || + IID_ITfInputProcessorProfileActivationSink == riid) { + *ppv = static_cast(this); + } + if (*ppv) { + AddRef(); + return S_OK; + } + return E_NOINTERFACE; + } + + NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(TSFStaticSink) + + const nsString& GetActiveTIPKeyboardDescription() const { + return mActiveTIPKeyboardDescription; + } + + static bool IsIMM_IMEActive() { + // Use IMM API until TSFStaticSink starts to work. + if (!sInstance || !sInstance->EnsureInitActiveTIPKeyboard()) { + return IsIMM_IME(::GetKeyboardLayout(0)); + } + return sInstance->mIsIMM_IME; + } + + static bool IsIMM_IME(HKL aHKL) { + return (::ImmGetIMEFileNameW(aHKL, nullptr, 0) > 0); + } + + static bool IsTraditionalChinese() { + EnsureInstance(); + return sInstance && sInstance->IsTraditionalChineseInternal(); + } + static bool IsSimplifiedChinese() { + EnsureInstance(); + return sInstance && sInstance->IsSimplifiedChineseInternal(); + } + static bool IsJapanese() { + EnsureInstance(); + return sInstance && sInstance->IsJapaneseInternal(); + } + static bool IsKorean() { + EnsureInstance(); + return sInstance && sInstance->IsKoreanInternal(); + } + + /** + * ActiveTIP() returns an ID for currently active TIP. + * Please note that this method is expensive due to needs a lot of GUID + * comparations if active language ID is one of CJKT. If you need to + * check TIPs for a specific language, you should check current language + * first. + */ + static TextInputProcessorID ActiveTIP() { + EnsureInstance(); + if (!sInstance || !sInstance->EnsureInitActiveTIPKeyboard()) { + return TextInputProcessorID::eUnknown; + } + sInstance->ComputeActiveTextInputProcessor(); + if (NS_WARN_IF(sInstance->mActiveTIP == + TextInputProcessorID::eNotComputed)) { + return TextInputProcessorID::eUnknown; + } + return sInstance->mActiveTIP; + } + + static bool IsMSChangJieOrMSQuickActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if the language + // for current TIP is Traditional Chinese. + if (!IsTraditionalChinese()) { + return false; + } + switch (ActiveTIP()) { + case TextInputProcessorID::eMicrosoftChangJie: + case TextInputProcessorID::eMicrosoftQuick: + return true; + default: + return false; + } + } + + static bool IsMSPinyinOrMSWubiActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if the language + // for current TIP is Simplified Chinese. + if (!IsSimplifiedChinese()) { + return false; + } + switch (ActiveTIP()) { + case TextInputProcessorID::eMicrosoftPinyin: + case TextInputProcessorID::eMicrosoftWubi: + return true; + default: + return false; + } + } + + static bool IsMSJapaneseIMEActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if the language + // for current TIP is Japanese. + if (!IsJapanese()) { + return false; + } + return ActiveTIP() == TextInputProcessorID::eMicrosoftIMEForJapanese; + } + + static bool IsGoogleJapaneseInputActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if the language + // for current TIP is Japanese. + if (!IsJapanese()) { + return false; + } + return ActiveTIP() == TextInputProcessorID::eGoogleJapaneseInput; + } + + static bool IsATOKActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if active TIP is + // ATOK first since it's cheaper. + return IsJapanese() && sInstance->IsATOKActiveInternal(); + } + + // Note that ATOK 2011 - 2016 refers native caret position for deciding its + // popup window position. + static bool IsATOKReferringNativeCaretActive() { + // ActiveTIP() is expensive if it hasn't computed active TIP yet. + // For avoiding unnecessary computation, we should check if active TIP is + // ATOK first since it's cheaper. + if (!IsJapanese() || !sInstance->IsATOKActiveInternal()) { + return false; + } + switch (ActiveTIP()) { + case TextInputProcessorID::eATOK2011: + case TextInputProcessorID::eATOK2012: + case TextInputProcessorID::eATOK2013: + case TextInputProcessorID::eATOK2014: + case TextInputProcessorID::eATOK2015: + return true; + default: + return false; + } + } + + private: + static void EnsureInstance() { + if (!sInstance) { + RefPtr staticSink = GetInstance(); + Unused << staticSink; + } + } + + bool IsTraditionalChineseInternal() const { return mLangID == 0x0404; } + bool IsSimplifiedChineseInternal() const { return mLangID == 0x0804; } + bool IsJapaneseInternal() const { return mLangID == 0x0411; } + bool IsKoreanInternal() const { return mLangID == 0x0412; } + + bool IsATOKActiveInternal() { + EnsureInitActiveTIPKeyboard(); + // FYI: Name of packaged ATOK includes the release year like "ATOK 2015". + // Name of ATOK Passport (subscription) equals "ATOK". + return StringBeginsWith(mActiveTIPKeyboardDescription, u"ATOK "_ns) || + mActiveTIPKeyboardDescription.EqualsLiteral("ATOK"); + } + + void ComputeActiveTextInputProcessor() { + if (mActiveTIP != TextInputProcessorID::eNotComputed) { + return; + } + + if (mActiveTIPGUID == GUID_NULL) { + mActiveTIP = TextInputProcessorID::eNone; + return; + } + + // Comparing GUID is slow. So, we should use language information to + // reduce the comparing cost for TIP which is not we do not support + // specifically since they are always compared with all supported TIPs. + switch (mLangID) { + case 0x0404: + mActiveTIP = ComputeActiveTIPAsTraditionalChinese(); + break; + case 0x0411: + mActiveTIP = ComputeActiveTIPAsJapanese(); + break; + case 0x0412: + mActiveTIP = ComputeActiveTIPAsKorean(); + break; + case 0x0804: + mActiveTIP = ComputeActiveTIPAsSimplifiedChinese(); + break; + default: + mActiveTIP = TextInputProcessorID::eUnknown; + break; + } + // Special case for Keyman Desktop, it is available for any languages. + // Therefore, we need to check it only if we don't know the active TIP. + if (mActiveTIP != TextInputProcessorID::eUnknown) { + return; + } + + // Note that keyboard layouts for Keyman assign its GUID on install + // randomly, but CLSID is constant in any environments. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1670834#c7 + // https://github.com/keymanapp/keyman/blob/318c73a9e1d571d942837ff9964590626e5bd5aa/windows/src/engine/kmtip/globals.cpp#L37 + // {FE0420F1-38D1-4B4C-96BF-E7E20A74CFB7} + static constexpr CLSID kKeymanDesktop_CLSID = { + 0xFE0420F1, + 0x38D1, + 0x4B4C, + {0x96, 0xBF, 0xE7, 0xE2, 0x0A, 0x74, 0xCF, 0xB7}}; + if (mActiveTIPCLSID == kKeymanDesktop_CLSID) { + mActiveTIP = TextInputProcessorID::eKeymanDesktop; + } + } + + TextInputProcessorID ComputeActiveTIPAsJapanese() { + // {A76C93D9-5523-4E90-AAFA-4DB112F9AC76} (Win7, Win8.1, Win10) + static constexpr GUID kMicrosoftIMEForJapaneseGUID = { + 0xA76C93D9, + 0x5523, + 0x4E90, + {0xAA, 0xFA, 0x4D, 0xB1, 0x12, 0xF9, 0xAC, 0x76}}; + if (mActiveTIPGUID == kMicrosoftIMEForJapaneseGUID) { + return TextInputProcessorID::eMicrosoftIMEForJapanese; + } + // {54EDCC94-1524-4BB1-9FB7-7BABE4F4CA64} + static constexpr GUID kMicrosoftOfficeIME2010ForJapaneseGUID = { + 0x54EDCC94, + 0x1524, + 0x4BB1, + {0x9F, 0xB7, 0x7B, 0xAB, 0xE4, 0xF4, 0xCA, 0x64}}; + if (mActiveTIPGUID == kMicrosoftOfficeIME2010ForJapaneseGUID) { + return TextInputProcessorID::eMicrosoftOfficeIME2010ForJapanese; + } + // {773EB24E-CA1D-4B1B-B420-FA985BB0B80D} + static constexpr GUID kGoogleJapaneseInputGUID = { + 0x773EB24E, + 0xCA1D, + 0x4B1B, + {0xB4, 0x20, 0xFA, 0x98, 0x5B, 0xB0, 0xB8, 0x0D}}; + if (mActiveTIPGUID == kGoogleJapaneseInputGUID) { + return TextInputProcessorID::eGoogleJapaneseInput; + } + // {F9C24A5C-8A53-499D-9572-93B2FF582115} + static const GUID kATOK2011GUID = { + 0xF9C24A5C, + 0x8A53, + 0x499D, + {0x95, 0x72, 0x93, 0xB2, 0xFF, 0x58, 0x21, 0x15}}; + if (mActiveTIPGUID == kATOK2011GUID) { + return TextInputProcessorID::eATOK2011; + } + // {1DE01562-F445-401B-B6C3-E5B18DB79461} + static constexpr GUID kATOK2012GUID = { + 0x1DE01562, + 0xF445, + 0x401B, + {0xB6, 0xC3, 0xE5, 0xB1, 0x8D, 0xB7, 0x94, 0x61}}; + if (mActiveTIPGUID == kATOK2012GUID) { + return TextInputProcessorID::eATOK2012; + } + // {3C4DB511-189A-4168-B6EA-BFD0B4C85615} + static constexpr GUID kATOK2013GUID = { + 0x3C4DB511, + 0x189A, + 0x4168, + {0xB6, 0xEA, 0xBF, 0xD0, 0xB4, 0xC8, 0x56, 0x15}}; + if (mActiveTIPGUID == kATOK2013GUID) { + return TextInputProcessorID::eATOK2013; + } + // {4EF33B79-6AA9-4271-B4BF-9321C279381B} + static constexpr GUID kATOK2014GUID = { + 0x4EF33B79, + 0x6AA9, + 0x4271, + {0xB4, 0xBF, 0x93, 0x21, 0xC2, 0x79, 0x38, 0x1B}}; + if (mActiveTIPGUID == kATOK2014GUID) { + return TextInputProcessorID::eATOK2014; + } + // {EAB4DC00-CE2E-483D-A86A-E6B99DA9599A} + static constexpr GUID kATOK2015GUID = { + 0xEAB4DC00, + 0xCE2E, + 0x483D, + {0xA8, 0x6A, 0xE6, 0xB9, 0x9D, 0xA9, 0x59, 0x9A}}; + if (mActiveTIPGUID == kATOK2015GUID) { + return TextInputProcessorID::eATOK2015; + } + // {0B557B4C-5740-4110-A60A-1493FA10BF2B} + static constexpr GUID kATOK2016GUID = { + 0x0B557B4C, + 0x5740, + 0x4110, + {0xA6, 0x0A, 0x14, 0x93, 0xFA, 0x10, 0xBF, 0x2B}}; + if (mActiveTIPGUID == kATOK2016GUID) { + return TextInputProcessorID::eATOK2016; + } + + // * ATOK 2017 + // - {6DBFD8F5-701D-11E6-920F-782BCBA6348F} + // * ATOK Passport (confirmed with version 31.1.2) + // - {A38F2FD9-7199-45E1-841C-BE0313D8052F} + + if (IsATOKActiveInternal()) { + return TextInputProcessorID::eATOKUnknown; + } + + // {E6D66705-1EDA-4373-8D01-1D0CB2D054C7} + static constexpr GUID kJapanist10GUID = { + 0xE6D66705, + 0x1EDA, + 0x4373, + {0x8D, 0x01, 0x1D, 0x0C, 0xB2, 0xD0, 0x54, 0xC7}}; + if (mActiveTIPGUID == kJapanist10GUID) { + return TextInputProcessorID::eJapanist10; + } + + return TextInputProcessorID::eUnknown; + } + + TextInputProcessorID ComputeActiveTIPAsTraditionalChinese() { + // {B2F9C502-1742-11D4-9790-0080C882687E} (Win8.1, Win10) + static constexpr GUID kMicrosoftBopomofoGUID = { + 0xB2F9C502, + 0x1742, + 0x11D4, + {0x97, 0x90, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftBopomofoGUID) { + return TextInputProcessorID::eMicrosoftBopomofo; + } + // {4BDF9F03-C7D3-11D4-B2AB-0080C882687E} (Win7, Win8.1, Win10) + static const GUID kMicrosoftChangJieGUID = { + 0x4BDF9F03, + 0xC7D3, + 0x11D4, + {0xB2, 0xAB, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftChangJieGUID) { + return TextInputProcessorID::eMicrosoftChangJie; + } + // {761309DE-317A-11D4-9B5D-0080C882687E} (Win7) + static constexpr GUID kMicrosoftPhoneticGUID = { + 0x761309DE, + 0x317A, + 0x11D4, + {0x9B, 0x5D, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftPhoneticGUID) { + return TextInputProcessorID::eMicrosoftPhonetic; + } + // {6024B45F-5C54-11D4-B921-0080C882687E} (Win7, Win8.1, Win10) + static constexpr GUID kMicrosoftQuickGUID = { + 0x6024B45F, + 0x5C54, + 0x11D4, + {0xB9, 0x21, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftQuickGUID) { + return TextInputProcessorID::eMicrosoftQuick; + } + // {F3BA907A-6C7E-11D4-97FA-0080C882687E} (Win7) + static constexpr GUID kMicrosoftNewChangJieGUID = { + 0xF3BA907A, + 0x6C7E, + 0x11D4, + {0x97, 0xFA, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftNewChangJieGUID) { + return TextInputProcessorID::eMicrosoftNewChangJie; + } + // {B2F9C502-1742-11D4-9790-0080C882687E} (Win7) + static constexpr GUID kMicrosoftNewPhoneticGUID = { + 0xB2F9C502, + 0x1742, + 0x11D4, + {0x97, 0x90, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftNewPhoneticGUID) { + return TextInputProcessorID::eMicrosoftNewPhonetic; + } + // {0B883BA0-C1C7-11D4-87F9-0080C882687E} (Win7) + static constexpr GUID kMicrosoftNewQuickGUID = { + 0x0B883BA0, + 0xC1C7, + 0x11D4, + {0x87, 0xF9, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftNewQuickGUID) { + return TextInputProcessorID::eMicrosoftNewQuick; + } + + // NOTE: There are some other Traditional Chinese TIPs installed in Windows: + // * Chinese Traditional Array (version 6.0) + // - {D38EFF65-AA46-4FD5-91A7-67845FB02F5B} (Win7, Win8.1) + // * Chinese Traditional DaYi (version 6.0) + // - {037B2C25-480C-4D7F-B027-D6CA6B69788A} (Win7, Win8.1) + + // {B58630B5-0ED3-4335-BBC9-E77BBCB43CAD} + static const GUID kFreeChangJieGUID = { + 0xB58630B5, + 0x0ED3, + 0x4335, + {0xBB, 0xC9, 0xE7, 0x7B, 0xBC, 0xB4, 0x3C, 0xAD}}; + if (mActiveTIPGUID == kFreeChangJieGUID) { + return TextInputProcessorID::eFreeChangJie; + } + + return TextInputProcessorID::eUnknown; + } + + TextInputProcessorID ComputeActiveTIPAsSimplifiedChinese() { + // FYI: This matches with neither "Microsoft Pinyin ABC Input Style" nor + // "Microsoft Pinyin New Experience Input Style" on Win7. + // {FA550B04-5AD7-411F-A5AC-CA038EC515D7} (Win8.1, Win10) + static constexpr GUID kMicrosoftPinyinGUID = { + 0xFA550B04, + 0x5AD7, + 0x411F, + {0xA5, 0xAC, 0xCA, 0x03, 0x8E, 0xC5, 0x15, 0xD7}}; + if (mActiveTIPGUID == kMicrosoftPinyinGUID) { + return TextInputProcessorID::eMicrosoftPinyin; + } + + // {F3BA9077-6C7E-11D4-97FA-0080C882687E} (Win7) + static constexpr GUID kMicrosoftPinyinNewExperienceInputStyleGUID = { + 0xF3BA9077, + 0x6C7E, + 0x11D4, + {0x97, 0xFA, 0x00, 0x80, 0xC8, 0x82, 0x68, 0x7E}}; + if (mActiveTIPGUID == kMicrosoftPinyinNewExperienceInputStyleGUID) { + return TextInputProcessorID::eMicrosoftPinyinNewExperienceInputStyle; + } + // {82590C13-F4DD-44F4-BA1D-8667246FDF8E} (Win8.1, Win10) + static constexpr GUID kMicrosoftWubiGUID = { + 0x82590C13, + 0xF4DD, + 0x44F4, + {0xBA, 0x1D, 0x86, 0x67, 0x24, 0x6F, 0xDF, 0x8E}}; + if (mActiveTIPGUID == kMicrosoftWubiGUID) { + return TextInputProcessorID::eMicrosoftWubi; + } + // NOTE: There are some other Simplified Chinese TIPs installed in Windows: + // * Chinese Simplified QuanPin (version 6.0) + // - {54FC610E-6ABD-4685-9DDD-A130BDF1B170} (Win8.1) + // * Chinese Simplified ZhengMa (version 6.0) + // - {733B4D81-3BC3-4132-B91A-E9CDD5E2BFC9} (Win8.1) + // * Chinese Simplified ShuangPin (version 6.0) + // - {EF63706D-31C4-490E-9DBB-BD150ADC454B} (Win8.1) + // * Microsoft Pinyin ABC Input Style + // - {FCA121D2-8C6D-41FB-B2DE-A2AD110D4820} (Win7) + return TextInputProcessorID::eUnknown; + } + + TextInputProcessorID ComputeActiveTIPAsKorean() { + // {B5FE1F02-D5F2-4445-9C03-C568F23C99A1} (Win7, Win8.1, Win10) + static constexpr GUID kMicrosoftIMEForKoreanGUID = { + 0xB5FE1F02, + 0xD5F2, + 0x4445, + {0x9C, 0x03, 0xC5, 0x68, 0xF2, 0x3C, 0x99, 0xA1}}; + if (mActiveTIPGUID == kMicrosoftIMEForKoreanGUID) { + return TextInputProcessorID::eMicrosoftIMEForKorean; + } + // {B60AF051-257A-46BC-B9D3-84DAD819BAFB} (Win8.1, Win10) + static constexpr GUID kMicrosoftOldHangulGUID = { + 0xB60AF051, + 0x257A, + 0x46BC, + {0xB9, 0xD3, 0x84, 0xDA, 0xD8, 0x19, 0xBA, 0xFB}}; + if (mActiveTIPGUID == kMicrosoftOldHangulGUID) { + return TextInputProcessorID::eMicrosoftOldHangul; + } + + // NOTE: There is the other Korean TIP installed in Windows: + // * Microsoft IME 2010 + // - {48878C45-93F9-4aaf-A6A1-272CD863C4F5} (Win7) + + return TextInputProcessorID::eUnknown; + } + + public: // ITfInputProcessorProfileActivationSink + STDMETHODIMP OnActivated(DWORD, LANGID, REFCLSID, REFGUID, REFGUID, HKL, + DWORD); + + private: + TSFStaticSink(); + virtual ~TSFStaticSink() {} + + bool EnsureInitActiveTIPKeyboard(); + + void Destroy(); + + void GetTIPDescription(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile, nsAString& aDescription); + bool IsTIPCategoryKeyboard(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile); + + TextInputProcessorID mActiveTIP; + + // Cookie of installing ITfInputProcessorProfileActivationSink + DWORD mIPProfileCookie; + + LANGID mLangID; + + // True if current IME is implemented with IMM. + bool mIsIMM_IME; + // True if OnActivated() is already called + bool mOnActivatedCalled; + + RefPtr mThreadMgr; + RefPtr mInputProcessorProfiles; + + // Active TIP keyboard's description. If active language profile isn't TIP, + // i.e., IMM-IME or just a keyboard layout, this is empty. + nsString mActiveTIPKeyboardDescription; + + // Active TIP's GUID and CLSID + GUID mActiveTIPGUID; + CLSID mActiveTIPCLSID; + + static StaticRefPtr sInstance; +}; + +StaticRefPtr TSFStaticSink::sInstance; + +TSFStaticSink::TSFStaticSink() + : mActiveTIP(TextInputProcessorID::eNotComputed), + mIPProfileCookie(TF_INVALID_COOKIE), + mLangID(0), + mIsIMM_IME(false), + mOnActivatedCalled(false), + mActiveTIPGUID(GUID_NULL) {} + +bool TSFStaticSink::Init(ITfThreadMgr* aThreadMgr, + ITfInputProcessorProfiles* aInputProcessorProfiles) { + MOZ_ASSERT(!mThreadMgr && !mInputProcessorProfiles, + "TSFStaticSink::Init() must be called only once"); + + mThreadMgr = aThreadMgr; + mInputProcessorProfiles = aInputProcessorProfiles; + + RefPtr source; + HRESULT hr = + mThreadMgr->QueryInterface(IID_ITfSource, getter_AddRefs(source)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::Init() FAILED to get ITfSource " + "instance (0x%08lX)", + this, hr)); + return false; + } + + // NOTE: On Vista or later, Windows let us know activate IME changed only + // with ITfInputProcessorProfileActivationSink. + hr = source->AdviseSink( + IID_ITfInputProcessorProfileActivationSink, + static_cast(this), + &mIPProfileCookie); + if (FAILED(hr) || mIPProfileCookie == TF_INVALID_COOKIE) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::Init() FAILED to install " + "ITfInputProcessorProfileActivationSink (0x%08lX)", + this, hr)); + return false; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::Init(), " + "mIPProfileCookie=0x%08lX", + this, mIPProfileCookie)); + return true; +} + +void TSFStaticSink::Destroy() { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::Shutdown() " + "mIPProfileCookie=0x%08lX", + this, mIPProfileCookie)); + + if (mIPProfileCookie != TF_INVALID_COOKIE) { + RefPtr source; + HRESULT hr = + mThreadMgr->QueryInterface(IID_ITfSource, getter_AddRefs(source)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::Shutdown() FAILED to get " + "ITfSource instance (0x%08lX)", + this, hr)); + } else { + hr = source->UnadviseSink(mIPProfileCookie); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Shutdown() FAILED to uninstall " + "ITfInputProcessorProfileActivationSink (0x%08lX)", + this, hr)); + } + } + } + + mThreadMgr = nullptr; + mInputProcessorProfiles = nullptr; +} + +STDMETHODIMP +TSFStaticSink::OnActivated(DWORD dwProfileType, LANGID langid, REFCLSID rclsid, + REFGUID catid, REFGUID guidProfile, HKL hkl, + DWORD dwFlags) { + if ((dwFlags & TF_IPSINK_FLAG_ACTIVE) && + (dwProfileType == TF_PROFILETYPE_KEYBOARDLAYOUT || + catid == GUID_TFCAT_TIP_KEYBOARD)) { + mOnActivatedCalled = true; + mActiveTIP = TextInputProcessorID::eNotComputed; + mActiveTIPGUID = guidProfile; + mActiveTIPCLSID = rclsid; + mLangID = langid & 0xFFFF; + mIsIMM_IME = IsIMM_IME(hkl); + GetTIPDescription(rclsid, langid, guidProfile, + mActiveTIPKeyboardDescription); + if (mActiveTIPGUID != GUID_NULL) { + // key should be "LocaleID|Description". Although GUID of the + // profile is unique key since description may be localized for system + // language, unfortunately, it's too long to record as key with its + // description. Therefore, we should record only the description with + // LocaleID because Microsoft IME may not include language information. + // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp + nsAutoString key; + key.AppendPrintf("0x%04X|", mLangID); + nsAutoString description(mActiveTIPKeyboardDescription); + static const uint32_t kMaxDescriptionLength = 72 - key.Length(); + if (description.Length() > kMaxDescriptionLength) { + if (NS_IS_LOW_SURROGATE(description[kMaxDescriptionLength - 1]) && + NS_IS_HIGH_SURROGATE(description[kMaxDescriptionLength - 2])) { + description.Truncate(kMaxDescriptionLength - 2); + } else { + description.Truncate(kMaxDescriptionLength - 1); + } + // U+2026 is "..." + description.Append(char16_t(0x2026)); + } + key.Append(description); + Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_WINDOWS, key, + true); + } + // Notify IMEHandler of changing active keyboard layout. + IMEHandler::OnKeyboardLayoutChanged(); + } + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::OnActivated(dwProfileType=%s (0x%08lX), " + "langid=0x%08X, rclsid=%s, catid=%s, guidProfile=%s, hkl=0x%p, " + "dwFlags=0x%08lX (TF_IPSINK_FLAG_ACTIVE: %s)), mIsIMM_IME=%s, " + "mActiveTIPDescription=\"%s\"", + this, + dwProfileType == TF_PROFILETYPE_INPUTPROCESSOR + ? "TF_PROFILETYPE_INPUTPROCESSOR" + : dwProfileType == TF_PROFILETYPE_KEYBOARDLAYOUT + ? "TF_PROFILETYPE_KEYBOARDLAYOUT" + : "Unknown", + dwProfileType, langid, GetCLSIDNameStr(rclsid).get(), + GetGUIDNameStr(catid).get(), GetGUIDNameStr(guidProfile).get(), hkl, + dwFlags, GetBoolName(dwFlags & TF_IPSINK_FLAG_ACTIVE), + GetBoolName(mIsIMM_IME), + NS_ConvertUTF16toUTF8(mActiveTIPKeyboardDescription).get())); + return S_OK; +} + +bool TSFStaticSink::EnsureInitActiveTIPKeyboard() { + if (mOnActivatedCalled) { + return true; + } + + RefPtr profileMgr; + HRESULT hr = mInputProcessorProfiles->QueryInterface( + IID_ITfInputProcessorProfileMgr, getter_AddRefs(profileMgr)); + if (FAILED(hr) || !profileMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED " + "to get input processor profile manager, hr=0x%08lX", + this, hr)); + return false; + } + + TF_INPUTPROCESSORPROFILE profile; + hr = profileMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &profile); + if (hr == S_FALSE) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED " + "to get active keyboard layout profile due to no active profile, " + "hr=0x%08lX", + this, hr)); + // XXX Should we call OnActivated() with arguments like non-TIP in this + // case? + return false; + } + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), FAILED " + "to get active TIP keyboard, hr=0x%08lX", + this, hr)); + return false; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFStaticSink::EnsureInitActiveLanguageProfile(), " + "calling OnActivated() manually...", + this)); + OnActivated(profile.dwProfileType, profile.langid, profile.clsid, + profile.catid, profile.guidProfile, ::GetKeyboardLayout(0), + TF_IPSINK_FLAG_ACTIVE); + return true; +} + +void TSFStaticSink::GetTIPDescription(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile, + nsAString& aDescription) { + aDescription.Truncate(); + + if (aTextService == CLSID_NULL || aProfile == GUID_NULL) { + return; + } + + BSTR description = nullptr; + HRESULT hr = mInputProcessorProfiles->GetLanguageProfileDescription( + aTextService, aLangID, aProfile, &description); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::InitActiveTIPDescription() FAILED " + "due to GetLanguageProfileDescription() failure, hr=0x%08lX", + this, hr)); + return; + } + + if (description && description[0]) { + aDescription.Assign(description); + } + ::SysFreeString(description); +} + +bool TSFStaticSink::IsTIPCategoryKeyboard(REFCLSID aTextService, LANGID aLangID, + REFGUID aProfile) { + if (aTextService == CLSID_NULL || aProfile == GUID_NULL) { + return false; + } + + RefPtr enumLangProfiles; + HRESULT hr = mInputProcessorProfiles->EnumLanguageProfiles( + aLangID, getter_AddRefs(enumLangProfiles)); + if (FAILED(hr) || !enumLangProfiles) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFStaticSink::IsTIPCategoryKeyboard(), FAILED " + "to get language profiles enumerator, hr=0x%08lX", + this, hr)); + return false; + } + + TF_LANGUAGEPROFILE profile; + ULONG fetch = 0; + while (SUCCEEDED(enumLangProfiles->Next(1, &profile, &fetch)) && fetch) { + // XXX We're not sure a profile is registered with two or more categories. + if (profile.clsid == aTextService && profile.guidProfile == aProfile && + profile.catid == GUID_TFCAT_TIP_KEYBOARD) { + return true; + } + } + return false; +} + +/******************************************************************/ +/* TSFTextStore */ +/******************************************************************/ + +StaticRefPtr TSFTextStore::sThreadMgr; +StaticRefPtr TSFTextStore::sMessagePump; +StaticRefPtr TSFTextStore::sKeystrokeMgr; +StaticRefPtr TSFTextStore::sDisplayAttrMgr; +StaticRefPtr TSFTextStore::sCategoryMgr; +StaticRefPtr TSFTextStore::sCompartmentForOpenClose; +StaticRefPtr TSFTextStore::sDisabledDocumentMgr; +StaticRefPtr TSFTextStore::sDisabledContext; +StaticRefPtr TSFTextStore::sInputProcessorProfiles; +StaticRefPtr TSFTextStore::sEnabledTextStore; +const MSG* TSFTextStore::sHandlingKeyMsg = nullptr; +DWORD TSFTextStore::sClientId = 0; +bool TSFTextStore::sIsKeyboardEventDispatched = false; + +#define TEXTSTORE_DEFAULT_VIEW (1) + +TSFTextStore::TSFTextStore() + : mEditCookie(0), + mSinkMask(0), + mLock(0), + mLockQueued(0), + mHandlingKeyMessage(0) { + // We hope that 5 or more actions don't occur at once. + mPendingActions.SetCapacity(5); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::TSFTextStore() SUCCEEDED", this)); +} + +TSFTextStore::~TSFTextStore() { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore instance is destroyed", this)); +} + +bool TSFTextStore::Init(nsWindow* aWidget, const InputContext& aContext) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::Init(aWidget=0x%p)", this, aWidget)); + + if (NS_WARN_IF(!aWidget) || NS_WARN_IF(aWidget->Destroyed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED due to being initialized with " + "destroyed widget", + this)); + return false; + } + + if (mDocumentMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED due to already initialized", + this)); + return false; + } + + mWidget = aWidget; + if (NS_WARN_IF(!mWidget)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED " + "due to aWidget is nullptr ", + this)); + return false; + } + mDispatcher = mWidget->GetTextEventDispatcher(); + if (NS_WARN_IF(!mDispatcher)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED " + "due to aWidget->GetTextEventDispatcher() failure", + this)); + return false; + } + + mInPrivateBrowsing = aContext.mInPrivateBrowsing; + SetInputScope(aContext.mHTMLInputType, aContext.mHTMLInputMode); + + if (aContext.mURI) { + // We don't need the document URL if it fails, let's ignore the error. + nsAutoCString spec; + if (NS_SUCCEEDED(aContext.mURI->GetSpec(spec))) { + CopyUTF8toUTF16(spec, mDocumentURL); + } + } + + // Create document manager + RefPtr threadMgr = sThreadMgr; + RefPtr documentMgr; + HRESULT hr = threadMgr->CreateDocumentMgr(getter_AddRefs(documentMgr)); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create ITfDocumentMgr " + "(0x%08lX)", + this, hr)); + return false; + } + if (NS_WARN_IF(mDestroyed)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create ITfDocumentMgr due to " + "TextStore being destroyed during calling " + "ITfThreadMgr::CreateDocumentMgr()", + this)); + return false; + } + // Create context and add it to document manager + RefPtr context; + hr = documentMgr->CreateContext(sClientId, 0, + static_cast(this), + getter_AddRefs(context), &mEditCookie); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create the context " + "(0x%08lX)", + this, hr)); + return false; + } + if (NS_WARN_IF(mDestroyed)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create ITfContext due to " + "TextStore being destroyed during calling " + "ITfDocumentMgr::CreateContext()", + this)); + return false; + } + + hr = documentMgr->Push(context); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to push the context (0x%08lX)", + this, hr)); + return false; + } + if (NS_WARN_IF(mDestroyed)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::Init() FAILED to create ITfContext due to " + "TextStore being destroyed during calling ITfDocumentMgr::Push()", + this)); + documentMgr->Pop(TF_POPF_ALL); + return false; + } + + mDocumentMgr = documentMgr; + mContext = context; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::Init() succeeded: " + "mDocumentMgr=0x%p, mContext=0x%p, mEditCookie=0x%08lX", + this, mDocumentMgr.get(), mContext.get(), mEditCookie)); + + return true; +} + +void TSFTextStore::Destroy() { + if (mBeingDestroyed) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::Destroy(), mLock=%s, " + "mComposition=%s, mHandlingKeyMessage=%u", + this, GetLockFlagNameStr(mLock).get(), + ToString(mComposition).c_str(), mHandlingKeyMessage)); + + mDestroyed = true; + + // Destroy native caret first because it's not directly related to TSF and + // there may be another textstore which gets focus. So, we should avoid + // to destroy caret after the new one recreates caret. + IMEHandler::MaybeDestroyNativeCaret(); + + if (mLock) { + mPendingDestroy = true; + return; + } + + AutoRestore savedBeingDestroyed(mBeingDestroyed); + mBeingDestroyed = true; + + // If there is composition, TSF keeps the composition even after the text + // store destroyed. So, we should clear the composition here. + if (mComposition.isSome()) { + CommitCompositionInternal(false); + } + + if (mSink) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::Destroy(), calling " + "ITextStoreACPSink::OnLayoutChange(TS_LC_DESTROY)...", + this)); + RefPtr sink = mSink; + sink->OnLayoutChange(TS_LC_DESTROY, TEXTSTORE_DEFAULT_VIEW); + } + + // If this is called during handling a keydown or keyup message, we should + // put off to release TSF objects until it completely finishes since + // MS-IME for Japanese refers some objects without grabbing them. + if (!mHandlingKeyMessage) { + ReleaseTSFObjects(); + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::Destroy() succeeded", this)); +} + +void TSFTextStore::ReleaseTSFObjects() { + MOZ_ASSERT(!mHandlingKeyMessage); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::ReleaseTSFObjects()", this)); + + mDocumentURL.Truncate(); + mContext = nullptr; + if (mDocumentMgr) { + RefPtr documentMgr = mDocumentMgr.forget(); + documentMgr->Pop(TF_POPF_ALL); + } + mSink = nullptr; + mWidget = nullptr; + mDispatcher = nullptr; + + if (!mMouseTrackers.IsEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::ReleaseTSFObjects(), " + "removing a mouse tracker...", + this)); + mMouseTrackers.Clear(); + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::ReleaseTSFObjects() completed", this)); +} + +STDMETHODIMP +TSFTextStore::QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + if ((IID_IUnknown == riid) || (IID_ITextStoreACP == riid)) { + *ppv = static_cast(this); + } else if (IID_ITfContextOwnerCompositionSink == riid) { + *ppv = static_cast(this); + } else if (IID_ITfMouseTrackerACP == riid) { + *ppv = static_cast(this); + } + if (*ppv) { + AddRef(); + return S_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::QueryInterface() FAILED, riid=%s", this, + GetRIIDNameStr(riid).get())); + return E_NOINTERFACE; +} + +STDMETHODIMP +TSFTextStore::AdviseSink(REFIID riid, IUnknown* punk, DWORD dwMask) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::AdviseSink(riid=%s, punk=0x%p, dwMask=%s), " + "mSink=0x%p, mSinkMask=%s", + this, GetRIIDNameStr(riid).get(), punk, GetSinkMaskNameStr(dwMask).get(), + mSink.get(), GetSinkMaskNameStr(mSinkMask).get())); + + if (!punk) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseSink() FAILED due to the null punk", + this)); + return E_UNEXPECTED; + } + + if (IID_ITextStoreACPSink != riid) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseSink() FAILED due to " + "unsupported interface", + this)); + return E_INVALIDARG; // means unsupported interface. + } + + if (!mSink) { + // Install sink + punk->QueryInterface(IID_ITextStoreACPSink, getter_AddRefs(mSink)); + if (!mSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseSink() FAILED due to " + "punk not having the interface", + this)); + return E_UNEXPECTED; + } + } else { + // If sink is already installed we check to see if they are the same + // Get IUnknown from both sides for comparison + RefPtr comparison1, comparison2; + punk->QueryInterface(IID_IUnknown, getter_AddRefs(comparison1)); + mSink->QueryInterface(IID_IUnknown, getter_AddRefs(comparison2)); + if (comparison1 != comparison2) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseSink() FAILED due to " + "the sink being different from the stored sink", + this)); + return CONNECT_E_ADVISELIMIT; + } + } + // Update mask either for a new sink or an existing sink + mSinkMask = dwMask; + return S_OK; +} + +STDMETHODIMP +TSFTextStore::UnadviseSink(IUnknown* punk) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::UnadviseSink(punk=0x%p), mSink=0x%p", this, punk, + mSink.get())); + + if (!punk) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseSink() FAILED due to the null punk", + this)); + return E_INVALIDARG; + } + if (!mSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseSink() FAILED due to " + "any sink not stored", + this)); + return CONNECT_E_NOCONNECTION; + } + // Get IUnknown from both sides for comparison + RefPtr comparison1, comparison2; + punk->QueryInterface(IID_IUnknown, getter_AddRefs(comparison1)); + mSink->QueryInterface(IID_IUnknown, getter_AddRefs(comparison2)); + // Unadvise only if sinks are the same + if (comparison1 != comparison2) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseSink() FAILED due to " + "the sink being different from the stored sink", + this)); + return CONNECT_E_NOCONNECTION; + } + mSink = nullptr; + mSinkMask = 0; + return S_OK; +} + +STDMETHODIMP +TSFTextStore::RequestLock(DWORD dwLockFlags, HRESULT* phrSession) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestLock(dwLockFlags=%s, phrSession=0x%p), " + "mLock=%s, mDestroyed=%s", + this, GetLockFlagNameStr(dwLockFlags).get(), phrSession, + GetLockFlagNameStr(mLock).get(), GetBoolName(mDestroyed))); + + if (!mSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RequestLock() FAILED due to " + "any sink not stored", + this)); + return E_FAIL; + } + if (mDestroyed && + (mContentForTSF.isNothing() || mSelectionForTSF.isNothing())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RequestLock() FAILED due to " + "being destroyed and no information of the contents", + this)); + return E_FAIL; + } + if (!phrSession) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RequestLock() FAILED due to " + "null phrSession", + this)); + return E_INVALIDARG; + } + + if (!mLock) { + // put on lock + mLock = dwLockFlags & (~TS_LF_SYNC); + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p Locking (%s) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", + this, GetLockFlagNameStr(mLock).get())); + // Don't release this instance during this lock because this is called by + // TSF but they don't grab us during this call. + RefPtr kungFuDeathGrip(this); + RefPtr sink = mSink; + *phrSession = sink->OnLockGranted(mLock); + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p Unlocked (%s) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", + this, GetLockFlagNameStr(mLock).get())); + DidLockGranted(); + while (mLockQueued) { + mLock = mLockQueued; + mLockQueued = 0; + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p Locking for the request in the queue (%s) >>>>>>>>>>>>>>" + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + ">>>>>", + this, GetLockFlagNameStr(mLock).get())); + sink->OnLockGranted(mLock); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p Unlocked (%s) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + "<<<<<", + this, GetLockFlagNameStr(mLock).get())); + DidLockGranted(); + } + + // The document is now completely unlocked. + mLock = 0; + + MaybeFlushPendingNotifications(); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestLock() succeeded: *phrSession=%s", + this, GetTextStoreReturnValueName(*phrSession))); + return S_OK; + } + + // only time when reentrant lock is allowed is when caller holds a + // read-only lock and is requesting an async write lock + if (IsReadLocked() && !IsReadWriteLocked() && IsReadWriteLock(dwLockFlags) && + !(dwLockFlags & TS_LF_SYNC)) { + *phrSession = TS_S_ASYNC; + mLockQueued = dwLockFlags & (~TS_LF_SYNC); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestLock() stores the request in the " + "queue, *phrSession=TS_S_ASYNC", + this)); + return S_OK; + } + + // no more locks allowed + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestLock() didn't allow to lock, " + "*phrSession=TS_E_SYNCHRONOUS", + this)); + *phrSession = TS_E_SYNCHRONOUS; + return E_FAIL; +} + +void TSFTextStore::DidLockGranted() { + if (IsReadWriteLocked()) { + // FreeCJ (TIP for Traditional Chinese) calls SetSelection() to set caret + // to the start of composition string and insert a full width space for + // a placeholder with a call of SetText(). After that, it calls + // OnUpdateComposition() without new range. Therefore, let's record the + // composition update information here. + CompleteLastActionIfStillIncomplete(); + + FlushPendingActions(); + } + + // If the widget has gone, we don't need to notify anything. + if (mDestroyed || !mWidget || mWidget->Destroyed()) { + mPendingSelectionChangeData.reset(); + mHasReturnedNoLayoutError = false; + } +} + +void TSFTextStore::DispatchEvent(WidgetGUIEvent& aEvent) { + if (NS_WARN_IF(!mWidget) || NS_WARN_IF(mWidget->Destroyed())) { + return; + } + // If the event isn't a query content event, the event may be handled + // asynchronously. So, we should put off to answer from GetTextExt() etc. + if (!aEvent.AsQueryContentEvent()) { + mDeferNotifyingTSFUntilNextUpdate = true; + } + mWidget->DispatchWindowEvent(aEvent); +} + +void TSFTextStore::FlushPendingActions() { + if (!mWidget || mWidget->Destroyed()) { + // Note that don't clear mContentForTSF because TIP may try to commit + // composition with a document lock. In such case, TSFTextStore needs to + // behave as expected by TIP. + mPendingActions.Clear(); + mPendingSelectionChangeData.reset(); + mHasReturnedNoLayoutError = false; + return; + } + + // Some TIP may request lock but does nothing during the lock. In such case, + // this should do nothing. For example, when MS-IME for Japanese is active + // and we're inactivating, this case occurs and causes different behavior + // from the other TIPs. + if (mPendingActions.IsEmpty()) { + return; + } + + RefPtr widget(mWidget); + nsresult rv = mDispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED due to BeginNativeInputTransaction() failure", + this)); + return; + } + for (uint32_t i = 0; i < mPendingActions.Length(); i++) { + PendingAction& action = mPendingActions[i]; + switch (action.mType) { + case PendingAction::Type::eKeyboardEvent: + if (mDestroyed) { + MOZ_LOG( + gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::FlushPendingActions() " + "IGNORED pending KeyboardEvent(%s) due to already destroyed", + this, + action.mKeyMsg.message == WM_KEYDOWN ? "eKeyDown" : "eKeyUp")); + } + MOZ_DIAGNOSTIC_ASSERT(action.mKeyMsg.message == WM_KEYDOWN || + action.mKeyMsg.message == WM_KEYUP); + DispatchKeyboardEventAsProcessedByIME(action.mKeyMsg); + if (!widget || widget->Destroyed()) { + break; + } + break; + case PendingAction::Type::eCompositionStart: { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing Type::eCompositionStart={ mSelectionStart=%ld, " + "mSelectionLength=%ld }, mDestroyed=%s", + this, action.mSelectionStart, action.mSelectionLength, + GetBoolName(mDestroyed))); + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::FlushPendingActions() " + "IGNORED pending compositionstart due to already destroyed", + this)); + break; + } + + if (action.mAdjustSelection) { + // Select composition range so the new composition replaces the range + WidgetSelectionEvent selectionSet(true, eSetSelection, widget); + widget->InitEvent(selectionSet); + selectionSet.mOffset = static_cast(action.mSelectionStart); + selectionSet.mLength = static_cast(action.mSelectionLength); + selectionSet.mReversed = false; + selectionSet.mExpandToClusterBoundary = + TSFStaticSink::ActiveTIP() != + TextInputProcessorID::eKeymanDesktop && + StaticPrefs:: + intl_tsf_hack_extend_setting_selection_range_to_cluster_boundaries(); + DispatchEvent(selectionSet); + if (!selectionSet.mSucceeded) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED due to eSetSelection failure", + this)); + break; + } + } + + // eCompositionStart always causes + // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED. Therefore, we should + // wait to clear mContentForTSF until it's notified. + mDeferClearingContentForTSF = true; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "dispatching compositionstart event...", + this)); + WidgetEventTime eventTime = widget->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = mDispatcher->StartComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to dispatch compositionstart event, " + "IsHandlingCompositionInContent()=%s", + this, GetBoolName(IsHandlingCompositionInContent()))); + // XXX Is this right? If there is a composition in content, + // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED? + mDeferClearingContentForTSF = !IsHandlingCompositionInContent(); + } + if (!widget || widget->Destroyed()) { + break; + } + break; + } + case PendingAction::Type::eCompositionUpdate: { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing Type::eCompositionUpdate={ mData=\"%s\", " + "mRanges=0x%p, mRanges->Length()=%zu }", + this, GetEscapedUTF8String(action.mData).get(), + action.mRanges.get(), + action.mRanges ? action.mRanges->Length() : 0)); + + // eCompositionChange causes a DOM text event, the IME will be notified + // of NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED. In this case, we + // should not clear mContentForTSF until we notify the IME of the + // composition update. + mDeferClearingContentForTSF = true; + + rv = mDispatcher->SetPendingComposition(action.mData, action.mRanges); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to setting pending composition... " + "IsHandlingCompositionInContent()=%s", + this, GetBoolName(IsHandlingCompositionInContent()))); + // XXX Is this right? If there is a composition in content, + // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED? + mDeferClearingContentForTSF = !IsHandlingCompositionInContent(); + } else { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "dispatching compositionchange event...", + this)); + WidgetEventTime eventTime = widget->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = mDispatcher->FlushPendingComposition(status, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to dispatch compositionchange event, " + "IsHandlingCompositionInContent()=%s", + this, GetBoolName(IsHandlingCompositionInContent()))); + // XXX Is this right? If there is a composition in content, + // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED? + mDeferClearingContentForTSF = !IsHandlingCompositionInContent(); + } + // Be aware, the mWidget might already have been destroyed. + } + break; + } + case PendingAction::Type::eCompositionEnd: { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing Type::eCompositionEnd={ mData=\"%s\" }", + this, GetEscapedUTF8String(action.mData).get())); + + // Dispatching eCompositionCommit causes a DOM text event, then, + // the IME will be notified of NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED + // when focused content actually handles the event. For example, + // when focused content is in a remote process, it's sent when + // all dispatched composition events have been handled in the remote + // process. So, until then, we don't have newer content information. + // Therefore, we need to put off to clear mContentForTSF. + mDeferClearingContentForTSF = true; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions(), " + "dispatching compositioncommit event...", + this)); + WidgetEventTime eventTime = widget->CurrentMessageWidgetEventTime(); + nsEventStatus status; + rv = mDispatcher->CommitComposition(status, &action.mData, &eventTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED to dispatch compositioncommit event, " + "IsHandlingCompositionInContent()=%s", + this, GetBoolName(IsHandlingCompositionInContent()))); + // XXX Is this right? If there is a composition in content, + // shouldn't we wait NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED? + mDeferClearingContentForTSF = !IsHandlingCompositionInContent(); + } + break; + } + case PendingAction::Type::eSetSelection: { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::FlushPendingActions() " + "flushing Type::eSetSelection={ mSelectionStart=%ld, " + "mSelectionLength=%ld, mSelectionReversed=%s }, " + "mDestroyed=%s", + this, action.mSelectionStart, action.mSelectionLength, + GetBoolName(action.mSelectionReversed), GetBoolName(mDestroyed))); + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::FlushPendingActions() " + "IGNORED pending selectionset due to already destroyed", + this)); + break; + } + + WidgetSelectionEvent selectionSet(true, eSetSelection, widget); + selectionSet.mOffset = static_cast(action.mSelectionStart); + selectionSet.mLength = static_cast(action.mSelectionLength); + selectionSet.mReversed = action.mSelectionReversed; + selectionSet.mExpandToClusterBoundary = + TSFStaticSink::ActiveTIP() != + TextInputProcessorID::eKeymanDesktop && + StaticPrefs:: + intl_tsf_hack_extend_setting_selection_range_to_cluster_boundaries(); + DispatchEvent(selectionSet); + if (!selectionSet.mSucceeded) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::FlushPendingActions() " + "FAILED due to eSetSelection failure", + this)); + break; + } + break; + } + default: + MOZ_CRASH("unexpected action type"); + } + + if (widget && !widget->Destroyed()) { + continue; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::FlushPendingActions(), " + "qutting since the mWidget has gone", + this)); + break; + } + mPendingActions.Clear(); +} + +void TSFTextStore::MaybeFlushPendingNotifications() { + if (mDeferNotifyingTSF) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "putting off flushing pending notifications due to initializing " + "something...", + this)); + return; + } + + if (IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "putting off flushing pending notifications due to being the " + "document locked...", + this)); + return; + } + + if (mDeferCommittingComposition) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::CommitCompositionInternal(false)...", + this)); + mDeferCommittingComposition = mDeferCancellingComposition = false; + CommitCompositionInternal(false); + } else if (mDeferCancellingComposition) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::CommitCompositionInternal(true)...", + this)); + mDeferCommittingComposition = mDeferCancellingComposition = false; + CommitCompositionInternal(true); + } + + if (mDeferNotifyingTSFUntilNextUpdate) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "putting off flushing pending notifications due to being " + "dispatching events...", + this)); + return; + } + + if (mPendingDestroy) { + Destroy(); + return; + } + + if (mDestroyed) { + // If it's already been destroyed completely, this shouldn't notify TSF of + // anything anymore. + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "does nothing because this has already destroyed completely...", + this)); + return; + } + + if (!mDeferClearingContentForTSF && mContentForTSF.isSome()) { + mContentForTSF.reset(); + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "mContentForTSF is set to `Nothing`", + this)); + } + + // When there is no cached content, we can sync actual contents and TSF/TIP + // expecting contents. + RefPtr kungFuDeathGrip = this; + Unused << kungFuDeathGrip; + if (mContentForTSF.isNothing()) { + if (mPendingTextChangeData.IsValid()) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::NotifyTSFOfTextChange()...", + this)); + NotifyTSFOfTextChange(); + } + if (mPendingSelectionChangeData.isSome()) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::NotifyTSFOfSelectionChange()...", + this)); + NotifyTSFOfSelectionChange(); + } + } + + if (mHasReturnedNoLayoutError) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::MaybeFlushPendingNotifications(), " + "calling TSFTextStore::NotifyTSFOfLayoutChange()...", + this)); + NotifyTSFOfLayoutChange(); + } +} + +void TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME() { + // If we've already been destroyed, we cannot do anything. + if (mDestroyed) { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), " + "does nothing because it's already been destroyed", + this)); + return; + } + + // If we're not handling key message or we've already dispatched a keyboard + // event for the handling key message, we should do nothing anymore. + if (!sHandlingKeyMsg || sIsKeyboardEventDispatched) { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), " + "does nothing because not necessary to dispatch keyboard event", + this)); + return; + } + + sIsKeyboardEventDispatched = true; + // If the document is locked, just adding the task to dispatching an event + // to the queue. + if (IsReadLocked()) { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), " + "adding to dispatch a keyboard event into the queue...", + this)); + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::Type::eKeyboardEvent; + memcpy(&action->mKeyMsg, sHandlingKeyMsg, sizeof(MSG)); + return; + } + + // Otherwise, dispatch a keyboard event. + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MaybeDispatchKeyboardEventAsProcessedByIME(), " + "trying to dispatch a keyboard event...", + this)); + DispatchKeyboardEventAsProcessedByIME(*sHandlingKeyMsg); +} + +void TSFTextStore::DispatchKeyboardEventAsProcessedByIME(const MSG& aMsg) { + MOZ_ASSERT(mWidget); + MOZ_ASSERT(!mWidget->Destroyed()); + MOZ_ASSERT(!mDestroyed); + + ModifierKeyState modKeyState; + MSG msg(aMsg); + msg.wParam = VK_PROCESSKEY; + NativeKey nativeKey(mWidget, msg, modKeyState); + switch (aMsg.message) { + case WM_KEYDOWN: + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::DispatchKeyboardEventAsProcessedByIME(), " + "dispatching an eKeyDown event...", + this)); + nativeKey.HandleKeyDownMessage(); + break; + case WM_KEYUP: + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::DispatchKeyboardEventAsProcessedByIME(), " + "dispatching an eKeyUp event...", + this)); + nativeKey.HandleKeyUpMessage(); + break; + default: + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::DispatchKeyboardEventAsProcessedByIME(), " + "ERROR, it doesn't handle the message", + this)); + break; + } +} + +STDMETHODIMP +TSFTextStore::GetStatus(TS_STATUS* pdcs) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetStatus(pdcs=0x%p)", this, pdcs)); + + if (!pdcs) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetStatus() FAILED due to null pdcs", this)); + return E_INVALIDARG; + } + // We manage on-screen keyboard by own. + pdcs->dwDynamicFlags = TS_SD_INPUTPANEMANUALDISPLAYENABLE; + // we use a "flat" text model for TSF support so no hidden text + pdcs->dwStaticFlags = TS_SS_NOHIDDENTEXT; + return S_OK; +} + +STDMETHODIMP +TSFTextStore::QueryInsert(LONG acpTestStart, LONG acpTestEnd, ULONG cch, + LONG* pacpResultStart, LONG* pacpResultEnd) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::QueryInsert(acpTestStart=%ld, " + "acpTestEnd=%ld, cch=%lu, pacpResultStart=0x%p, pacpResultEnd=0x%p)", + this, acpTestStart, acpTestEnd, cch, pacpResultStart, pacpResultEnd)); + + if (!pacpResultStart || !pacpResultEnd) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::QueryInsert() FAILED due to " + "the null argument", + this)); + return E_INVALIDARG; + } + + if (acpTestStart < 0 || acpTestStart > acpTestEnd) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::QueryInsert() FAILED due to " + "wrong argument", + this)); + return E_INVALIDARG; + } + + // XXX need to adjust to cluster boundary + // Assume we are given good offsets for now + if (IsWin8OrLater() && mComposition.isNothing() && + ((StaticPrefs:: + intl_tsf_hack_ms_traditional_chinese_query_insert_result() && + TSFStaticSink::IsMSChangJieOrMSQuickActive()) || + (StaticPrefs:: + intl_tsf_hack_ms_simplified_chinese_query_insert_result() && + TSFStaticSink::IsMSPinyinOrMSWubiActive()))) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::QueryInsert() WARNING using different " + "result for the TIP", + this)); + // Chinese TIPs of Microsoft assume that QueryInsert() returns selected + // range which should be removed. + *pacpResultStart = acpTestStart; + *pacpResultEnd = acpTestEnd; + } else { + *pacpResultStart = acpTestStart; + *pacpResultEnd = acpTestStart + cch; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::QueryInsert() succeeded: " + "*pacpResultStart=%ld, *pacpResultEnd=%ld)", + this, *pacpResultStart, *pacpResultEnd)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetSelection(ULONG ulIndex, ULONG ulCount, + TS_SELECTION_ACP* pSelection, ULONG* pcFetched) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetSelection(ulIndex=%lu, ulCount=%lu, " + "pSelection=0x%p, pcFetched=0x%p)", + this, ulIndex, ulCount, pSelection, pcFetched)); + + if (!IsReadLocked()) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to not locked", this)); + return TS_E_NOLOCK; + } + if (!ulCount || !pSelection || !pcFetched) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + *pcFetched = 0; + + if (ulIndex != static_cast(TS_DEFAULT_SELECTION) && ulIndex != 0) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to " + "unsupported selection", + this)); + return TS_E_NOSELECTION; + } + + Maybe& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + if (DoNotReturnErrorFromGetSelection()) { + *pSelection = Selection::EmptyACP(); + *pcFetched = 1; + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetSelection() returns fake selection range " + "for avoiding a crash in TSF, *pSelection=%s", + this, mozilla::ToString(*pSelection).c_str())); + return S_OK; + } + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetSelection() FAILED due to " + "SelectionForTSF() failure", + this)); + return E_FAIL; + } + if (!selectionForTSF->HasRange()) { + *pSelection = Selection::EmptyACP(); + *pcFetched = 0; + return TS_E_NOSELECTION; + } + *pSelection = selectionForTSF->ACPRef(); + *pcFetched = 1; + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetSelection() succeeded, *pSelection=%s", + this, mozilla::ToString(*pSelection).c_str())); + return S_OK; +} + +// static +bool TSFTextStore::DoNotReturnErrorFromGetSelection() { + // There is a crash bug of TSF if we return error from GetSelection(). + // That was introduced in Anniversary Update (build 14393, see bug 1312302) + // TODO: We should avoid to run this hack on fixed builds. When we get + // exact build number, we should get back here. + static bool sTSFMayCrashIfGetSelectionReturnsError = + IsWin10AnniversaryUpdateOrLater(); + return sTSFMayCrashIfGetSelectionReturnsError; +} + +Maybe& TSFTextStore::ContentForTSF() { + // This should be called when the document is locked or the content hasn't + // been abandoned yet. + if (NS_WARN_IF(!IsReadLocked() && mContentForTSF.isNothing())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::ContentForTSF(), FAILED, due to " + "called wrong timing, IsReadLocked()=%s, mContentForTSF=Nothing", + this, GetBoolName(IsReadLocked()))); + return mContentForTSF; + } + + Maybe& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::ContentForTSF(), FAILED, due to " + "SelectionForTSF() failure", + this)); + mContentForTSF.reset(); + return mContentForTSF; + } + + if (mContentForTSF.isNothing()) { + AutoNotifyingTSFBatch deferNotifyingTSF(*this); + + nsString text; // Don't use auto string for avoiding to copy long string. + if (NS_WARN_IF(!GetCurrentText(text))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::ContentForTSF(), FAILED, due to " + "GetCurrentText() failure", + this)); + return mContentForTSF; + } + + MOZ_DIAGNOSTIC_ASSERT(mContentForTSF.isNothing(), + "How was it initialized recursively?"); + mContentForTSF.reset(); // For avoiding crash in release channel + mContentForTSF.emplace(*this, text); + // Basically, the cached content which is expected by TSF/TIP should be + // cleared after active composition is committed or the document lock is + // unlocked. However, in e10s mode, content will be modified + // asynchronously. In such case, mDeferClearingContentForTSF may be + // true until whole dispatched events are handled by the focused editor. + mDeferClearingContentForTSF = false; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::ContentForTSF(): mContentForTSF=%s", this, + mozilla::ToString(mContentForTSF).c_str())); + + return mContentForTSF; +} + +bool TSFTextStore::CanAccessActualContentDirectly() const { + if (mContentForTSF.isNothing() || mSelectionForTSF.isNothing()) { + return true; + } + + // If the cached content has been changed by something except composition, + // the content cache may be different from actual content. + if (mPendingTextChangeData.IsValid() && + !mPendingTextChangeData.mCausedOnlyByComposition) { + return false; + } + + // If the cached selection isn't changed, cached content and actual content + // should be same. + if (mPendingSelectionChangeData.isNothing()) { + return true; + } + + return mSelectionForTSF->EqualsExceptDirection(*mPendingSelectionChangeData); +} + +bool TSFTextStore::GetCurrentText(nsAString& aTextContent) { + if (mContentForTSF.isSome()) { + aTextContent = mContentForTSF->TextRef(); + return true; + } + + MOZ_ASSERT(!mDestroyed); + MOZ_ASSERT(mWidget && !mWidget->Destroyed()); + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetCurrentText(): " + "retrieving text from the content...", + this)); + + WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, + mWidget); + queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX); + mWidget->InitEvent(queryTextContentEvent); + DispatchEvent(queryTextContentEvent); + if (NS_WARN_IF(queryTextContentEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetCurrentText(), FAILED, due to " + "eQueryTextContent failure", + this)); + aTextContent.Truncate(); + return false; + } + + aTextContent = queryTextContentEvent.mReply->DataRef(); + return true; +} + +Maybe& TSFTextStore::SelectionForTSF() { + if (mSelectionForTSF.isNothing()) { + MOZ_ASSERT(!mDestroyed); + // If the window has never been available, we should crash since working + // with broken values may make TIP confused. + if (!mWidget || mWidget->Destroyed()) { + MOZ_ASSERT_UNREACHABLE("There should be non-destroyed widget"); + } + + AutoNotifyingTSFBatch deferNotifyingTSF(*this); + + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + mWidget); + mWidget->InitEvent(querySelectedTextEvent); + DispatchEvent(querySelectedTextEvent); + if (NS_WARN_IF(querySelectedTextEvent.Failed())) { + return mSelectionForTSF; + } + MOZ_DIAGNOSTIC_ASSERT(mSelectionForTSF.isNothing(), + "How was it initialized recursively?"); + mSelectionForTSF = Some(Selection(querySelectedTextEvent)); + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::SelectionForTSF() succeeded, " + "mSelectionForTSF=%s", + this, ToString(mSelectionForTSF).c_str())); + + return mSelectionForTSF; +} + +static HRESULT GetRangeExtent(ITfRange* aRange, LONG* aStart, LONG* aLength) { + RefPtr rangeACP; + aRange->QueryInterface(IID_ITfRangeACP, getter_AddRefs(rangeACP)); + NS_ENSURE_TRUE(rangeACP, E_FAIL); + return rangeACP->GetExtent(aStart, aLength); +} + +static TextRangeType GetGeckoSelectionValue(TF_DISPLAYATTRIBUTE& aDisplayAttr) { + switch (aDisplayAttr.bAttr) { + case TF_ATTR_TARGET_CONVERTED: + return TextRangeType::eSelectedClause; + case TF_ATTR_CONVERTED: + return TextRangeType::eConvertedClause; + case TF_ATTR_TARGET_NOTCONVERTED: + return TextRangeType::eSelectedRawClause; + default: + return TextRangeType::eRawClause; + } +} + +HRESULT +TSFTextStore::GetDisplayAttribute(ITfProperty* aAttrProperty, ITfRange* aRange, + TF_DISPLAYATTRIBUTE* aResult) { + NS_ENSURE_TRUE(aAttrProperty, E_FAIL); + NS_ENSURE_TRUE(aRange, E_FAIL); + NS_ENSURE_TRUE(aResult, E_FAIL); + + HRESULT hr; + + if (MOZ_LOG_TEST(gIMELog, LogLevel::Debug)) { + LONG start = 0, length = 0; + hr = GetRangeExtent(aRange, &start, &length); + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetDisplayAttribute(): " + "GetDisplayAttribute range=%ld-%ld (hr=%s)", + this, start - mComposition->StartOffset(), + start - mComposition->StartOffset() + length, + GetCommonReturnValueName(hr))); + } + + VARIANT propValue; + ::VariantInit(&propValue); + hr = aAttrProperty->GetValue(TfEditCookie(mEditCookie), aRange, &propValue); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfProperty::GetValue() failed", + this)); + return hr; + } + if (VT_I4 != propValue.vt) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfProperty::GetValue() returns non-VT_I4 value", + this)); + ::VariantClear(&propValue); + return E_FAIL; + } + + RefPtr categoryMgr = GetCategoryMgr(); + if (NS_WARN_IF(!categoryMgr)) { + return E_FAIL; + } + GUID guid; + hr = categoryMgr->GetGUID(DWORD(propValue.lVal), &guid); + ::VariantClear(&propValue); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfCategoryMgr::GetGUID() failed", + this)); + return hr; + } + + RefPtr displayAttrMgr = GetDisplayAttributeMgr(); + if (NS_WARN_IF(!displayAttrMgr)) { + return E_FAIL; + } + RefPtr info; + hr = displayAttrMgr->GetDisplayAttributeInfo(guid, getter_AddRefs(info), + nullptr); + if (FAILED(hr) || !info) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfDisplayAttributeMgr::GetDisplayAttributeInfo() failed", + this)); + return hr; + } + + hr = info->GetAttributeInfo(aResult); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetDisplayAttribute() FAILED due to " + "ITfDisplayAttributeInfo::GetAttributeInfo() failed", + this)); + return hr; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetDisplayAttribute() succeeded: " + "Result={ %s }", + this, GetDisplayAttrStr(*aResult).get())); + return S_OK; +} + +HRESULT +TSFTextStore::RestartCompositionIfNecessary(ITfRange* aRangeNew) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartCompositionIfNecessary(" + "aRangeNew=0x%p), mComposition=%s", + this, aRangeNew, ToString(mComposition).c_str())); + + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() FAILED " + "due to no composition view", + this)); + return E_FAIL; + } + + HRESULT hr; + RefPtr pComposition(mComposition->GetView()); + RefPtr composingRange(aRangeNew); + if (!composingRange) { + hr = pComposition->GetRange(getter_AddRefs(composingRange)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() " + "FAILED due to pComposition->GetRange() failure", + this)); + return hr; + } + } + + // Get starting offset of the composition + LONG compStart = 0, compLength = 0; + hr = GetRangeExtent(composingRange, &compStart, &compLength); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() FAILED " + "due to GetRangeExtent() failure", + this)); + return hr; + } + + if (mComposition->StartOffset() == compStart && + mComposition->Length() == compLength) { + return S_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartCompositionIfNecessary(), " + "restaring composition because of compostion range is changed " + "(range=%ld-%ld, mComposition=%s)", + this, compStart, compStart + compLength, + ToString(mComposition).c_str())); + + // If the queried composition length is different from the length + // of our composition string, OnUpdateComposition is being called + // because a part of the original composition was committed. + hr = RestartComposition(*mComposition, pComposition, composingRange); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() " + "FAILED due to RestartComposition() failure", + this)); + return hr; + } + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartCompositionIfNecessary() succeeded", this)); + return S_OK; +} + +HRESULT TSFTextStore::RestartComposition(Composition& aCurrentComposition, + ITfCompositionView* aCompositionView, + ITfRange* aNewRange) { + Maybe& selectionForTSF = SelectionForTSF(); + + LONG newStart, newLength; + HRESULT hr = GetRangeExtent(aNewRange, &newStart, &newLength); + LONG newEnd = newStart + newLength; + + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartComposition() FAILED " + "due to SelectionForTSF() failure", + this)); + return E_FAIL; + } + + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartComposition() FAILED " + "due to GetRangeExtent() failure", + this)); + return hr; + } + + // If the new range has no overlap with the crrent range, we just commit + // the composition and restart new composition with the new range but + // current selection range should be preserved. + if (newStart >= aCurrentComposition.EndOffset() || + newEnd <= aCurrentComposition.StartOffset()) { + RecordCompositionEndAction(); + RecordCompositionStartAction(aCompositionView, newStart, newLength, true); + return S_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartComposition(aCompositionView=0x%p, " + "aNewRange=0x%p { newStart=%ld, newLength=%ld }), " + "aCurrentComposition=%s, " + "selectionForTSF=%s", + this, aCompositionView, aNewRange, newStart, newLength, + ToString(aCurrentComposition).c_str(), + ToString(selectionForTSF).c_str())); + + // If the new range has an overlap with the current one, we should not commit + // the whole current range to avoid creating an odd undo transaction. + // I.e., the overlapped range which is being composed should not appear in + // undo transaction. + + // Backup current composition data and selection data. + Composition oldComposition = aCurrentComposition; + Selection oldSelection = *selectionForTSF; + + // Commit only the part of composition. + LONG keepComposingStartOffset = + std::max(oldComposition.StartOffset(), newStart); + LONG keepComposingEndOffset = std::min(oldComposition.EndOffset(), newEnd); + MOZ_ASSERT( + keepComposingStartOffset <= keepComposingEndOffset, + "Why keepComposingEndOffset is smaller than keepComposingStartOffset?"); + LONG keepComposingLength = keepComposingEndOffset - keepComposingStartOffset; + // Remove the overlapped part from the commit string. + nsAutoString commitString(oldComposition.DataRef()); + commitString.Cut(keepComposingStartOffset - oldComposition.StartOffset(), + keepComposingLength); + // Update the composition string. + Maybe& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RestartComposition() FAILED " + "due to ContentForTSF() failure", + this)); + return E_FAIL; + } + contentForTSF->ReplaceTextWith(oldComposition.StartOffset(), + oldComposition.Length(), commitString); + MOZ_ASSERT(mComposition.isSome()); + // Record a compositionupdate action for commit the part of composing string. + PendingAction* action = LastOrNewPendingCompositionUpdate(); + if (mComposition.isSome()) { + action->mData = mComposition->DataRef(); + } + action->mRanges->Clear(); + // Note that we shouldn't append ranges when composition string + // is empty because it may cause TextComposition confused. + if (!action->mData.IsEmpty()) { + TextRange caretRange; + caretRange.mStartOffset = caretRange.mEndOffset = static_cast( + oldComposition.StartOffset() + commitString.Length()); + caretRange.mRangeType = TextRangeType::eCaret; + action->mRanges->AppendElement(caretRange); + } + action->mIncomplete = false; + + // Record compositionend action. + RecordCompositionEndAction(); + + // Record compositionstart action only with the new start since this method + // hasn't restored composing string yet. + RecordCompositionStartAction(aCompositionView, newStart, 0, false); + + // Restore the latest text content and selection. + contentForTSF->ReplaceSelectedTextWith(nsDependentSubstring( + oldComposition.DataRef(), + keepComposingStartOffset - oldComposition.StartOffset(), + keepComposingLength)); + selectionForTSF = Some(oldSelection); + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RestartComposition() succeeded, " + "mComposition=%s, selectionForTSF=%s", + this, ToString(mComposition).c_str(), + ToString(selectionForTSF).c_str())); + + return S_OK; +} + +static bool GetColor(const TF_DA_COLOR& aTSFColor, nscolor& aResult) { + switch (aTSFColor.type) { + case TF_CT_SYSCOLOR: { + DWORD sysColor = ::GetSysColor(aTSFColor.nIndex); + aResult = + NS_RGB(GetRValue(sysColor), GetGValue(sysColor), GetBValue(sysColor)); + return true; + } + case TF_CT_COLORREF: + aResult = NS_RGB(GetRValue(aTSFColor.cr), GetGValue(aTSFColor.cr), + GetBValue(aTSFColor.cr)); + return true; + case TF_CT_NONE: + default: + return false; + } +} + +static bool GetLineStyle(TF_DA_LINESTYLE aTSFLineStyle, + TextRangeStyle::LineStyle& aTextRangeLineStyle) { + switch (aTSFLineStyle) { + case TF_LS_NONE: + aTextRangeLineStyle = TextRangeStyle::LineStyle::None; + return true; + case TF_LS_SOLID: + aTextRangeLineStyle = TextRangeStyle::LineStyle::Solid; + return true; + case TF_LS_DOT: + aTextRangeLineStyle = TextRangeStyle::LineStyle::Dotted; + return true; + case TF_LS_DASH: + aTextRangeLineStyle = TextRangeStyle::LineStyle::Dashed; + return true; + case TF_LS_SQUIGGLE: + aTextRangeLineStyle = TextRangeStyle::LineStyle::Wavy; + return true; + default: + return false; + } +} + +HRESULT +TSFTextStore::RecordCompositionUpdateAction() { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionUpdateAction(), mComposition=%s", + this, ToString(mComposition).c_str())); + + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to no composition view", + this)); + return E_FAIL; + } + + // Getting display attributes is *really* complicated! + // We first get the context and the property objects to query for + // attributes, but since a big range can have a variety of values for + // the attribute, we have to find out all the ranges that have distinct + // attribute values. Then we query for what the value represents through + // the display attribute manager and translate that to TextRange to be + // sent in eCompositionChange + + RefPtr attrPropetry; + HRESULT hr = + mContext->GetProperty(GUID_PROP_ATTRIBUTE, getter_AddRefs(attrPropetry)); + if (FAILED(hr) || !attrPropetry) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to mContext->GetProperty() failure", + this)); + return FAILED(hr) ? hr : E_FAIL; + } + + RefPtr composingRange; + hr = mComposition->GetView()->GetRange(getter_AddRefs(composingRange)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "FAILED due to mComposition->GetView()->GetRange() failure", + this)); + return hr; + } + + RefPtr enumRanges; + hr = attrPropetry->EnumRanges(TfEditCookie(mEditCookie), + getter_AddRefs(enumRanges), composingRange); + if (FAILED(hr) || !enumRanges) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to attrPropetry->EnumRanges() failure", + this)); + return FAILED(hr) ? hr : E_FAIL; + } + + // First, put the log of content and selection here. + Maybe& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() FAILED " + "due to SelectionForTSF() failure", + this)); + return E_FAIL; + } + + PendingAction* action = LastOrNewPendingCompositionUpdate(); + action->mData = mComposition->DataRef(); + // The ranges might already have been initialized, however, if this is + // called again, that means we need to overwrite the ranges with current + // information. + action->mRanges->Clear(); + + // Note that we shouldn't append ranges when composition string + // is empty because it may cause TextComposition confused. + if (!action->mData.IsEmpty()) { + TextRange newRange; + // No matter if we have display attribute info or not, + // we always pass in at least one range to eCompositionChange + newRange.mStartOffset = 0; + newRange.mEndOffset = action->mData.Length(); + newRange.mRangeType = TextRangeType::eRawClause; + action->mRanges->AppendElement(newRange); + + RefPtr range; + while (enumRanges->Next(1, getter_AddRefs(range), nullptr) == S_OK) { + if (NS_WARN_IF(!range)) { + break; + } + + LONG rangeStart = 0, rangeLength = 0; + if (FAILED(GetRangeExtent(range, &rangeStart, &rangeLength))) { + continue; + } + // The range may include out of composition string. We should ignore + // outside of the composition string. + LONG start = std::min(std::max(rangeStart, mComposition->StartOffset()), + mComposition->EndOffset()); + LONG end = std::max( + std::min(rangeStart + rangeLength, mComposition->EndOffset()), + mComposition->StartOffset()); + LONG length = end - start; + if (length < 0) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "ignores invalid range (%ld-%ld)", + this, rangeStart - mComposition->StartOffset(), + rangeStart - mComposition->StartOffset() + rangeLength)); + continue; + } + if (!length) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "ignores a range due to outside of the composition or empty " + "(%ld-%ld)", + this, rangeStart - mComposition->StartOffset(), + rangeStart - mComposition->StartOffset() + rangeLength)); + continue; + } + + TextRange newRange; + newRange.mStartOffset = + static_cast(start - mComposition->StartOffset()); + // The end of the last range in the array is + // always kept at the end of composition + newRange.mEndOffset = mComposition->Length(); + + TF_DISPLAYATTRIBUTE attr; + hr = GetDisplayAttribute(attrPropetry, range, &attr); + if (FAILED(hr)) { + newRange.mRangeType = TextRangeType::eRawClause; + } else { + newRange.mRangeType = GetGeckoSelectionValue(attr); + if (GetColor(attr.crText, newRange.mRangeStyle.mForegroundColor)) { + newRange.mRangeStyle.mDefinedStyles |= + TextRangeStyle::DEFINED_FOREGROUND_COLOR; + } + if (GetColor(attr.crBk, newRange.mRangeStyle.mBackgroundColor)) { + newRange.mRangeStyle.mDefinedStyles |= + TextRangeStyle::DEFINED_BACKGROUND_COLOR; + } + if (GetColor(attr.crLine, newRange.mRangeStyle.mUnderlineColor)) { + newRange.mRangeStyle.mDefinedStyles |= + TextRangeStyle::DEFINED_UNDERLINE_COLOR; + } + if (GetLineStyle(attr.lsStyle, newRange.mRangeStyle.mLineStyle)) { + newRange.mRangeStyle.mDefinedStyles |= + TextRangeStyle::DEFINED_LINESTYLE; + newRange.mRangeStyle.mIsBoldLine = attr.fBoldLine != 0; + } + } + + TextRange& lastRange = action->mRanges->LastElement(); + if (lastRange.mStartOffset == newRange.mStartOffset) { + // Replace range if last range is the same as this one + // So that ranges don't overlap and confuse the editor + lastRange = newRange; + } else { + lastRange.mEndOffset = newRange.mStartOffset; + action->mRanges->AppendElement(newRange); + } + } + + // We need to hack for Korean Input System which is Korean standard TIP. + // It sets no change style to IME selection (the selection is always only + // one). So, the composition string looks like normal (or committed) + // string. At this time, current selection range is same as the + // composition string range. Other applications set a wide caret which + // covers the composition string, however, Gecko doesn't support the wide + // caret drawing now (Gecko doesn't support XOR drawing), unfortunately. + // For now, we should change the range style to undefined. + if (!selectionForTSF->Collapsed() && action->mRanges->Length() == 1) { + TextRange& range = action->mRanges->ElementAt(0); + LONG start = selectionForTSF->MinOffset(); + LONG end = selectionForTSF->MaxOffset(); + if (static_cast(range.mStartOffset) == + start - mComposition->StartOffset() && + static_cast(range.mEndOffset) == + end - mComposition->StartOffset() && + range.mRangeStyle.IsNoChangeStyle()) { + range.mRangeStyle.Clear(); + // The looks of selected type is better than others. + range.mRangeType = TextRangeType::eSelectedRawClause; + } + } + + // The caret position has to be collapsed. + uint32_t caretPosition = static_cast( + selectionForTSF->HasRange() + ? selectionForTSF->MaxOffset() - mComposition->StartOffset() + : mComposition->StartOffset()); + + // If caret is in the target clause and it doesn't have specific style, + // the target clause will be painted as normal selection range. Since + // caret shouldn't be in selection range on Windows, we shouldn't append + // caret range in such case. + const TextRange* targetClause = action->mRanges->GetTargetClause(); + if (!targetClause || targetClause->mRangeStyle.IsDefined() || + caretPosition < targetClause->mStartOffset || + caretPosition > targetClause->mEndOffset) { + TextRange caretRange; + caretRange.mStartOffset = caretRange.mEndOffset = caretPosition; + caretRange.mRangeType = TextRangeType::eCaret; + action->mRanges->AppendElement(caretRange); + } + } + + action->mIncomplete = false; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionUpdateAction() " + "succeeded", + this)); + + return S_OK; +} + +HRESULT +TSFTextStore::SetSelectionInternal(const TS_SELECTION_ACP* pSelection, + bool aDispatchCompositionChangeEvent) { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::SetSelectionInternal(pSelection=%s, " + "aDispatchCompositionChangeEvent=%s), mComposition=%s", + this, pSelection ? mozilla::ToString(*pSelection).c_str() : "nullptr", + GetBoolName(aDispatchCompositionChangeEvent), + ToString(mComposition).c_str())); + + MOZ_ASSERT(IsReadWriteLocked()); + + Maybe& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "SelectionForTSF() failure", + this)); + return E_FAIL; + } + + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return E_FAIL; + } + + // If actually the range is not changing, we should do nothing. + // Perhaps, we can ignore the difference change because it must not be + // important for following edit. + if (selectionForTSF->EqualsExceptDirection(*pSelection)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::SetSelectionInternal() Succeeded but " + "did nothing because the selection range isn't changing", + this)); + selectionForTSF->SetSelection(*pSelection); + return S_OK; + } + + if (mComposition.isSome()) { + if (aDispatchCompositionChangeEvent) { + HRESULT hr = RestartCompositionIfNecessary(); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "RestartCompositionIfNecessary() failure", + this)); + return hr; + } + } + if (pSelection->acpStart < mComposition->StartOffset() || + pSelection->acpEnd > mComposition->EndOffset()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "the selection being out of the composition string", + this)); + return TS_E_INVALIDPOS; + } + // Emulate selection during compositions + selectionForTSF->SetSelection(*pSelection); + if (aDispatchCompositionChangeEvent) { + HRESULT hr = RecordCompositionUpdateAction(); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "RecordCompositionUpdateAction() failure", + this)); + return hr; + } + } + return S_OK; + } + + TS_SELECTION_ACP selectionInContent(*pSelection); + + // If mContentForTSF caches old contents which is now different from + // actual contents, we need some complicated hack here... + // Note that this hack assumes that this is used for reconversion. + if (mContentForTSF.isSome() && mPendingTextChangeData.IsValid() && + !mPendingTextChangeData.mCausedOnlyByComposition) { + uint32_t startOffset = static_cast(selectionInContent.acpStart); + uint32_t endOffset = static_cast(selectionInContent.acpEnd); + if (mPendingTextChangeData.mStartOffset >= endOffset) { + // Setting selection before any changed ranges is fine. + } else if (mPendingTextChangeData.mRemovedEndOffset <= startOffset) { + // Setting selection after removed range is fine with following + // adjustment. + selectionInContent.acpStart += mPendingTextChangeData.Difference(); + selectionInContent.acpEnd += mPendingTextChangeData.Difference(); + } else if (startOffset == endOffset) { + // Moving caret position may be fine in most cases even if the insertion + // point has already gone but in this case, composition will be inserted + // to unexpected position, though. + // It seems that moving caret into middle of the new text is odd. + // Perhaps, end of it is expected by users in most cases. + selectionInContent.acpStart = mPendingTextChangeData.mAddedEndOffset; + selectionInContent.acpEnd = selectionInContent.acpStart; + } else { + // Otherwise, i.e., setting range has already gone, we cannot set + // selection properly. + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelectionInternal() FAILED due to " + "there is unknown content change", + this)); + return E_FAIL; + } + } + + CompleteLastActionIfStillIncomplete(); + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::Type::eSetSelection; + action->mSelectionStart = selectionInContent.acpStart; + action->mSelectionLength = + selectionInContent.acpEnd - selectionInContent.acpStart; + action->mSelectionReversed = (selectionInContent.style.ase == TS_AE_START); + + // Use TSF specified selection for updating mSelectionForTSF. + selectionForTSF->SetSelection(*pSelection); + + return S_OK; +} + +STDMETHODIMP +TSFTextStore::SetSelection(ULONG ulCount, const TS_SELECTION_ACP* pSelection) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::SetSelection(ulCount=%lu, pSelection=%s }), " + "mComposition=%s", + this, ulCount, + pSelection ? mozilla::ToString(pSelection).c_str() : "nullptr", + ToString(mComposition).c_str())); + + if (!IsReadWriteLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "not locked (read-write)", + this)); + return TS_E_NOLOCK; + } + if (ulCount != 1) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "trying setting multiple selection", + this)); + return E_INVALIDARG; + } + if (!pSelection) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + HRESULT hr = SetSelectionInternal(pSelection, true); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetSelection() FAILED due to " + "SetSelectionInternal() failure", + this)); + } else { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::SetSelection() succeeded", this)); + } + return hr; +} + +STDMETHODIMP +TSFTextStore::GetText(LONG acpStart, LONG acpEnd, WCHAR* pchPlain, + ULONG cchPlainReq, ULONG* pcchPlainOut, + TS_RUNINFO* prgRunInfo, ULONG ulRunInfoReq, + ULONG* pulRunInfoOut, LONG* pacpNext) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetText(acpStart=%ld, acpEnd=%ld, pchPlain=0x%p, " + "cchPlainReq=%lu, pcchPlainOut=0x%p, prgRunInfo=0x%p, ulRunInfoReq=%lu, " + "pulRunInfoOut=0x%p, pacpNext=0x%p), mComposition=%s", + this, acpStart, acpEnd, pchPlain, cchPlainReq, pcchPlainOut, prgRunInfo, + ulRunInfoReq, pulRunInfoOut, pacpNext, ToString(mComposition).c_str())); + + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (!pcchPlainOut || (!pchPlain && !prgRunInfo) || + !cchPlainReq != !pchPlain || !ulRunInfoReq != !prgRunInfo) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "invalid argument", + this)); + return E_INVALIDARG; + } + + if (acpStart < 0 || acpEnd < -1 || (acpEnd != -1 && acpStart > acpEnd)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "invalid position", + this)); + return TS_E_INVALIDPOS; + } + + // Making sure to null-terminate string just to be on the safe side + *pcchPlainOut = 0; + if (pchPlain && cchPlainReq) *pchPlain = 0; + if (pulRunInfoOut) *pulRunInfoOut = 0; + if (pacpNext) *pacpNext = acpStart; + if (prgRunInfo && ulRunInfoReq) { + prgRunInfo->uCount = 0; + prgRunInfo->type = TS_RT_PLAIN; + } + + Maybe& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "ContentForTSF() failure", + this)); + return E_FAIL; + } + if (contentForTSF->TextRef().Length() < static_cast(acpStart)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "acpStart is larger offset than the actual text length", + this)); + return TS_E_INVALIDPOS; + } + if (acpEnd != -1 && + contentForTSF->TextRef().Length() < static_cast(acpEnd)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetText() FAILED due to " + "acpEnd is larger offset than the actual text length", + this)); + return TS_E_INVALIDPOS; + } + uint32_t length = (acpEnd == -1) ? contentForTSF->TextRef().Length() - + static_cast(acpStart) + : static_cast(acpEnd - acpStart); + if (cchPlainReq && cchPlainReq - 1 < length) { + length = cchPlainReq - 1; + } + if (length) { + if (pchPlain && cchPlainReq) { + const char16_t* startChar = + contentForTSF->TextRef().BeginReading() + acpStart; + memcpy(pchPlain, startChar, length * sizeof(*pchPlain)); + pchPlain[length] = 0; + *pcchPlainOut = length; + } + if (prgRunInfo && ulRunInfoReq) { + prgRunInfo->uCount = length; + prgRunInfo->type = TS_RT_PLAIN; + if (pulRunInfoOut) *pulRunInfoOut = 1; + } + if (pacpNext) *pacpNext = acpStart + length; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetText() succeeded: pcchPlainOut=0x%p, " + "*prgRunInfo={ uCount=%lu, type=%s }, *pulRunInfoOut=%lu, " + "*pacpNext=%ld)", + this, pcchPlainOut, prgRunInfo ? prgRunInfo->uCount : 0, + prgRunInfo ? GetTextRunTypeName(prgRunInfo->type) : "N/A", + pulRunInfoOut ? *pulRunInfoOut : 0, pacpNext ? *pacpNext : 0)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::SetText(DWORD dwFlags, LONG acpStart, LONG acpEnd, + const WCHAR* pchText, ULONG cch, TS_TEXTCHANGE* pChange) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::SetText(dwFlags=%s, acpStart=%ld, acpEnd=%ld, " + "pchText=0x%p \"%s\", cch=%lu, pChange=0x%p), mComposition=%s", + this, dwFlags == TS_ST_CORRECTION ? "TS_ST_CORRECTION" : "not-specified", + acpStart, acpEnd, pchText, + pchText && cch ? GetEscapedUTF8String(pchText, cch).get() : "", cch, + pChange, ToString(mComposition).c_str())); + + // Per SDK documentation, and since we don't have better + // ways to do this, this method acts as a helper to + // call SetSelection followed by InsertTextAtSelection + if (!IsReadWriteLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetText() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + TS_SELECTION_ACP selection; + selection.acpStart = acpStart; + selection.acpEnd = acpEnd; + selection.style.ase = TS_AE_END; + selection.style.fInterimChar = 0; + // Set selection to desired range + HRESULT hr = SetSelectionInternal(&selection); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetText() FAILED due to " + "SetSelectionInternal() failure", + this)); + return hr; + } + // Replace just selected text + if (!InsertTextAtSelectionInternal(nsDependentSubstring(pchText, cch), + pChange)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::SetText() FAILED due to " + "InsertTextAtSelectionInternal() failure", + this)); + return E_FAIL; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::SetText() succeeded: pChange={ " + "acpStart=%ld, acpOldEnd=%ld, acpNewEnd=%ld }", + this, pChange ? pChange->acpStart : 0, + pChange ? pChange->acpOldEnd : 0, pChange ? pChange->acpNewEnd : 0)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetFormattedText(LONG acpStart, LONG acpEnd, + IDataObject** ppDataObject) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetFormattedText() called " + "but not supported (E_NOTIMPL)", + this)); + + // no support for formatted text + return E_NOTIMPL; +} + +STDMETHODIMP +TSFTextStore::GetEmbedded(LONG acpPos, REFGUID rguidService, REFIID riid, + IUnknown** ppunk) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetEmbedded() called " + "but not supported (E_NOTIMPL)", + this)); + + // embedded objects are not supported + return E_NOTIMPL; +} + +STDMETHODIMP +TSFTextStore::QueryInsertEmbedded(const GUID* pguidService, + const FORMATETC* pFormatEtc, + BOOL* pfInsertable) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::QueryInsertEmbedded() called " + "but not supported, *pfInsertable=FALSE (S_OK)", + this)); + + // embedded objects are not supported + *pfInsertable = FALSE; + return S_OK; +} + +STDMETHODIMP +TSFTextStore::InsertEmbedded(DWORD dwFlags, LONG acpStart, LONG acpEnd, + IDataObject* pDataObject, TS_TEXTCHANGE* pChange) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::InsertEmbedded() called " + "but not supported (E_NOTIMPL)", + this)); + + // embedded objects are not supported + return E_NOTIMPL; +} + +// static +bool TSFTextStore::ShouldSetInputScopeOfURLBarToDefault() { + // FYI: Google Japanese Input may be an IMM-IME. If it's installed on + // Win7, it's always IMM-IME. Otherwise, basically, it's a TIP. + // However, if it's installed on Win7 and has not been updated yet + // after the OS is upgraded to Win8 or later, it's still an IMM-IME. + // Therefore, we also need to check with IMMHandler here. + if (!StaticPrefs::intl_ime_hack_set_input_scope_of_url_bar_to_default()) { + return false; + } + + if (IMMHandler::IsGoogleJapaneseInputActive()) { + return true; + } + + switch (TSFStaticSink::ActiveTIP()) { + case TextInputProcessorID::eMicrosoftIMEForJapanese: + case TextInputProcessorID::eGoogleJapaneseInput: + case TextInputProcessorID::eMicrosoftBopomofo: + case TextInputProcessorID::eMicrosoftChangJie: + case TextInputProcessorID::eMicrosoftPhonetic: + case TextInputProcessorID::eMicrosoftQuick: + case TextInputProcessorID::eMicrosoftNewChangJie: + case TextInputProcessorID::eMicrosoftNewPhonetic: + case TextInputProcessorID::eMicrosoftNewQuick: + case TextInputProcessorID::eMicrosoftPinyin: + case TextInputProcessorID::eMicrosoftPinyinNewExperienceInputStyle: + case TextInputProcessorID::eMicrosoftOldHangul: + case TextInputProcessorID::eMicrosoftWubi: + return true; + case TextInputProcessorID::eMicrosoftIMEForKorean: + return IsWin8OrLater(); + default: + return false; + } +} + +void TSFTextStore::SetInputScope(const nsString& aHTMLInputType, + const nsString& aHTMLInputMode) { + mInputScopes.Clear(); + + // IME may refer only first input scope, but we will append inputmode's + // input scopes too like Chrome since IME may refer it. + IMEHandler::AppendInputScopeFromType(aHTMLInputType, mInputScopes); + IMEHandler::AppendInputScopeFromInputMode(aHTMLInputMode, mInputScopes); + + if (mInPrivateBrowsing) { + mInputScopes.AppendElement(IS_PRIVATE); + } +} + +int32_t TSFTextStore::GetRequestedAttrIndex(const TS_ATTRID& aAttrID) { + if (IsEqualGUID(aAttrID, GUID_PROP_INPUTSCOPE)) { + return eInputScope; + } + if (IsEqualGUID(aAttrID, sGUID_PROP_URL)) { + return eDocumentURL; + } + if (IsEqualGUID(aAttrID, TSATTRID_Text_VerticalWriting)) { + return eTextVerticalWriting; + } + if (IsEqualGUID(aAttrID, TSATTRID_Text_Orientation)) { + return eTextOrientation; + } + return eNotSupported; +} + +TS_ATTRID +TSFTextStore::GetAttrID(int32_t aIndex) { + switch (aIndex) { + case eInputScope: + return GUID_PROP_INPUTSCOPE; + case eDocumentURL: + return sGUID_PROP_URL; + case eTextVerticalWriting: + return TSATTRID_Text_VerticalWriting; + case eTextOrientation: + return TSATTRID_Text_Orientation; + default: + MOZ_CRASH("Invalid index? Or not implemented yet?"); + return GUID_NULL; + } +} + +HRESULT +TSFTextStore::HandleRequestAttrs(DWORD aFlags, ULONG aFilterCount, + const TS_ATTRID* aFilterAttrs) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::HandleRequestAttrs(aFlags=%s, " + "aFilterCount=%lu)", + this, GetFindFlagName(aFlags).get(), aFilterCount)); + + // This is a little weird! RequestSupportedAttrs gives us advanced notice + // of a support query via RetrieveRequestedAttrs for a specific attribute. + // RetrieveRequestedAttrs needs to return valid data for all attributes we + // support, but the text service will only want the input scope object + // returned in RetrieveRequestedAttrs if the dwFlags passed in here contains + // TS_ATTR_FIND_WANT_VALUE. + for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) { + mRequestedAttrs[i] = false; + } + mRequestedAttrValues = !!(aFlags & TS_ATTR_FIND_WANT_VALUE); + + for (uint32_t i = 0; i < aFilterCount; i++) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::HandleRequestAttrs(), " + "requested attr=%s", + this, GetGUIDNameStrWithTable(aFilterAttrs[i]).get())); + int32_t index = GetRequestedAttrIndex(aFilterAttrs[i]); + if (index != eNotSupported) { + mRequestedAttrs[index] = true; + } + } + return S_OK; +} + +STDMETHODIMP +TSFTextStore::RequestSupportedAttrs(DWORD dwFlags, ULONG cFilterAttrs, + const TS_ATTRID* paFilterAttrs) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestSupportedAttrs(dwFlags=%s, " + "cFilterAttrs=%lu)", + this, GetFindFlagName(dwFlags).get(), cFilterAttrs)); + + return HandleRequestAttrs(dwFlags, cFilterAttrs, paFilterAttrs); +} + +STDMETHODIMP +TSFTextStore::RequestAttrsAtPosition(LONG acpPos, ULONG cFilterAttrs, + const TS_ATTRID* paFilterAttrs, + DWORD dwFlags) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestAttrsAtPosition(acpPos=%ld, " + "cFilterAttrs=%lu, dwFlags=%s)", + this, acpPos, cFilterAttrs, GetFindFlagName(dwFlags).get())); + + return HandleRequestAttrs(dwFlags | TS_ATTR_FIND_WANT_VALUE, cFilterAttrs, + paFilterAttrs); +} + +STDMETHODIMP +TSFTextStore::RequestAttrsTransitioningAtPosition(LONG acpPos, + ULONG cFilterAttrs, + const TS_ATTRID* paFilterAttr, + DWORD dwFlags) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RequestAttrsTransitioningAtPosition(" + "acpPos=%ld, cFilterAttrs=%lu, dwFlags=%s) called but not supported " + "(S_OK)", + this, acpPos, cFilterAttrs, GetFindFlagName(dwFlags).get())); + + // no per character attributes defined + return S_OK; +} + +STDMETHODIMP +TSFTextStore::FindNextAttrTransition(LONG acpStart, LONG acpHalt, + ULONG cFilterAttrs, + const TS_ATTRID* paFilterAttrs, + DWORD dwFlags, LONG* pacpNext, + BOOL* pfFound, LONG* plFoundOffset) { + if (!pacpNext || !pfFound || !plFoundOffset) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" 0x%p TSFTextStore::FindNextAttrTransition() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::FindNextAttrTransition() called " + "but not supported (S_OK)", + this)); + + // no per character attributes defined + *pacpNext = *plFoundOffset = acpHalt; + *pfFound = FALSE; + return S_OK; +} + +// To test the document URL result, define this to out put it to the stdout +// #define DEBUG_PRINT_DOCUMENT_URL + +STDMETHODIMP +TSFTextStore::RetrieveRequestedAttrs(ULONG ulCount, TS_ATTRVAL* paAttrVals, + ULONG* pcFetched) { + if (!pcFetched || !paAttrVals) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + ULONG expectedCount = 0; + for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) { + if (mRequestedAttrs[i]) { + expectedCount++; + } + } + if (ulCount < expectedCount) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() FAILED due to " + "not enough count ulCount=%lu, expectedCount=%lu", + this, ulCount, expectedCount)); + return E_INVALIDARG; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() called " + "ulCount=%lu, mRequestedAttrValues=%s", + this, ulCount, GetBoolName(mRequestedAttrValues))); + + auto GetExposingURL = [&]() -> BSTR { + const bool allowed = + StaticPrefs::intl_tsf_expose_url_allowed() && + (!mInPrivateBrowsing || + StaticPrefs::intl_tsf_expose_url_in_private_browsing_allowed()); + if (!allowed || mDocumentURL.IsEmpty()) { + BSTR emptyString = ::SysAllocString(L""); + MOZ_ASSERT( + emptyString, + "We need to return valid BSTR pointer to notify TSF of supporting it " + "with a pointer to empty string"); + return emptyString; + } + return ::SysAllocString(mDocumentURL.get()); + }; + +#ifdef DEBUG_PRINT_DOCUMENT_URL + { + BSTR exposingURL = GetExposingURL(); + printf("TSFTextStore::RetrieveRequestedAttrs: DocumentURL=\"%s\"\n", + NS_ConvertUTF16toUTF8(static_cast(_bstr_t(exposingURL))) + .get()); + ::SysFreeString(exposingURL); + } +#endif // #ifdef DEBUG_PRINT_DOCUMENT_URL + + int32_t count = 0; + for (int32_t i = 0; i < NUM_OF_SUPPORTED_ATTRS; i++) { + if (!mRequestedAttrs[i]) { + continue; + } + mRequestedAttrs[i] = false; + + TS_ATTRID attrID = GetAttrID(i); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() for %s", this, + GetGUIDNameStrWithTable(attrID).get())); + + paAttrVals[count].idAttr = attrID; + paAttrVals[count].dwOverlapId = 0; + + if (!mRequestedAttrValues) { + paAttrVals[count].varValue.vt = VT_EMPTY; + } else { + switch (i) { + case eInputScope: { + paAttrVals[count].varValue.vt = VT_UNKNOWN; + RefPtr inputScope = new InputScopeImpl(mInputScopes); + paAttrVals[count].varValue.punkVal = inputScope.forget().take(); + break; + } + case eDocumentURL: { + paAttrVals[count].varValue.vt = VT_BSTR; + paAttrVals[count].varValue.bstrVal = GetExposingURL(); + break; + } + case eTextVerticalWriting: { + Maybe& selectionForTSF = SelectionForTSF(); + paAttrVals[count].varValue.vt = VT_BOOL; + paAttrVals[count].varValue.boolVal = + selectionForTSF.isSome() && + selectionForTSF->WritingModeRef().IsVertical() + ? VARIANT_TRUE + : VARIANT_FALSE; + break; + } + case eTextOrientation: { + Maybe& selectionForTSF = SelectionForTSF(); + paAttrVals[count].varValue.vt = VT_I4; + paAttrVals[count].varValue.lVal = + selectionForTSF.isSome() && + selectionForTSF->WritingModeRef().IsVertical() + ? 2700 + : 0; + break; + } + default: + MOZ_CRASH("Invalid index? Or not implemented yet?"); + break; + } + } + count++; + } + + mRequestedAttrValues = false; + + if (count) { + *pcFetched = count; + return S_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RetrieveRequestedAttrs() called " + "for unknown TS_ATTRVAL, *pcFetched=0 (S_OK)", + this)); + + paAttrVals->dwOverlapId = 0; + paAttrVals->varValue.vt = VT_EMPTY; + *pcFetched = 0; + return S_OK; +} + +#undef DEBUG_PRINT_DOCUMENT_URL + +STDMETHODIMP +TSFTextStore::GetEndACP(LONG* pacp) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetEndACP(pacp=0x%p)", this, pacp)); + + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetEndACP() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (!pacp) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetEndACP() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + Maybe& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetEndACP() FAILED due to " + "ContentForTSF() failure", + this)); + return E_FAIL; + } + *pacp = static_cast(contentForTSF->TextRef().Length()); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetActiveView(TsViewCookie* pvcView) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetActiveView(pvcView=0x%p)", this, pvcView)); + + if (!pvcView) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetActiveView() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + *pvcView = TEXTSTORE_DEFAULT_VIEW; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetActiveView() succeeded: *pvcView=%ld", this, + *pvcView)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetACPFromPoint(TsViewCookie vcView, const POINT* pt, + DWORD dwFlags, LONG* pacp) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetACPFromPoint(pvcView=%ld, pt=%p (x=%ld, " + "y=%ld), dwFlags=%s, pacp=%p, mDeferNotifyingTSFUntilNextUpdate=%s, " + "mWaitingQueryLayout=%s", + this, vcView, pt, pt ? pt->x : 0, pt ? pt->y : 0, + GetACPFromPointFlagName(dwFlags).get(), pacp, + GetBoolName(mDeferNotifyingTSFUntilNextUpdate), + GetBoolName(mWaitingQueryLayout))); + + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "called with invalid view", + this)); + return E_INVALIDARG; + } + + if (!pt) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "null pt", + this)); + return E_INVALIDARG; + } + + if (!pacp) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "null pacp", + this)); + return E_INVALIDARG; + } + + mWaitingQueryLayout = false; + + if (mDestroyed || + (mContentForTSF.isSome() && mContentForTSF->IsLayoutChanged())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() returned " + "TS_E_NOLAYOUT", + this)); + mHasReturnedNoLayoutError = true; + return TS_E_NOLAYOUT; + } + + LayoutDeviceIntPoint ourPt(pt->x, pt->y); + // Convert to widget relative coordinates from screen's. + ourPt -= mWidget->WidgetToScreenOffset(); + + // NOTE: Don't check if the point is in the widget since the point can be + // outside of the widget if focused editor is in a XUL . + + WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint, + mWidget); + mWidget->InitEvent(queryCharAtPointEvent, &ourPt); + + // FYI: WidgetQueryContentEvent may cause flushing pending layout and it + // may cause focus change or something. + RefPtr kungFuDeathGrip(this); + DispatchEvent(queryCharAtPointEvent); + if (!mWidget || mWidget->Destroyed()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "mWidget was destroyed during eQueryCharacterAtPoint", + this)); + return E_FAIL; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetACPFromPoint(), queryCharAtPointEvent={ " + "mReply=%s }", + this, ToString(queryCharAtPointEvent.mReply).c_str())); + + if (NS_WARN_IF(queryCharAtPointEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "eQueryCharacterAtPoint failure", + this)); + return E_FAIL; + } + + // If dwFlags isn't set and the point isn't in any character's bounding box, + // we should return TS_E_INVALIDPOINT. + if (!(dwFlags & GXFPF_NEAREST) && queryCharAtPointEvent.DidNotFindChar()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to the " + "point contained by no bounding box", + this)); + return TS_E_INVALIDPOINT; + } + + // Although, we're not sure if mTentativeCaretOffset becomes NOT_FOUND, + // let's assume that there is no content in such case. + NS_WARNING_ASSERTION(queryCharAtPointEvent.DidNotFindTentativeCaretOffset(), + "Tentative caret offset was not found"); + + uint32_t offset; + + // If dwFlags includes GXFPF_ROUND_NEAREST, we should return tentative + // caret offset (MSDN calls it "range position"). + if (dwFlags & GXFPF_ROUND_NEAREST) { + offset = queryCharAtPointEvent.mReply->mTentativeCaretOffset.valueOr(0); + } else if (queryCharAtPointEvent.FoundChar()) { + // Otherwise, we should return character offset whose bounding box contains + // the point. + offset = queryCharAtPointEvent.mReply->StartOffset(); + } else { + // If the point isn't in any character's bounding box but we need to return + // the nearest character from the point, we should *guess* the character + // offset since there is no inexpensive API to check it strictly. + // XXX If we retrieve 2 bounding boxes, one is before the offset and + // the other is after the offset, we could resolve the offset. + // However, dispatching 2 eQueryTextRect may be expensive. + + // So, use tentative offset for now. + offset = queryCharAtPointEvent.mReply->mTentativeCaretOffset.valueOr(0); + + // However, if it's after the last character, we need to decrement the + // offset. + Maybe& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to " + "ContentForTSF() failure", + this)); + return E_FAIL; + } + if (contentForTSF->TextRef().Length() <= offset) { + // If the tentative caret is after the last character, let's return + // the last character's offset. + offset = contentForTSF->TextRef().Length() - 1; + } + } + + if (NS_WARN_IF(offset > LONG_MAX)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetACPFromPoint() FAILED due to out of " + "range of the result", + this)); + return TS_E_INVALIDPOINT; + } + + *pacp = static_cast(offset); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetACPFromPoint() succeeded: *pacp=%ld", this, + *pacp)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::GetTextExt(TsViewCookie vcView, LONG acpStart, LONG acpEnd, + RECT* prc, BOOL* pfClipped) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetTextExt(vcView=%ld, " + "acpStart=%ld, acpEnd=%ld, prc=0x%p, pfClipped=0x%p), " + "IsHandlingCompositionInParent()=%s, " + "IsHandlingCompositionInContent()=%s, mContentForTSF=%s, " + "mSelectionForTSF=%s, mComposition=%s, " + "mDeferNotifyingTSFUntilNextUpdate=%s, mWaitingQueryLayout=%s, " + "IMEHandler::IsA11yHandlingNativeCaret()=%s", + this, vcView, acpStart, acpEnd, prc, pfClipped, + GetBoolName(IsHandlingCompositionInParent()), + GetBoolName(IsHandlingCompositionInContent()), + mozilla::ToString(mContentForTSF).c_str(), + ToString(mSelectionForTSF).c_str(), ToString(mComposition).c_str(), + GetBoolName(mDeferNotifyingTSFUntilNextUpdate), + GetBoolName(mWaitingQueryLayout), + GetBoolName(IMEHandler::IsA11yHandlingNativeCaret()))); + + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "called with invalid view", + this)); + return E_INVALIDARG; + } + + if (!prc || !pfClipped) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + // According to MSDN, ITextStoreACP::GetTextExt() should return + // TS_E_INVALIDARG when acpStart and acpEnd are same (i.e., collapsed range). + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms538435(v=vs.85).aspx + // > TS_E_INVALIDARG: The specified start and end character positions are + // > equal. + // However, some TIPs (including Microsoft's Chinese TIPs!) call this with + // collapsed range and if we return TS_E_INVALIDARG, they stops showing their + // owning window or shows it but odd position. So, we should just return + // error only when acpStart and/or acpEnd are really odd. + + if (acpStart < 0 || acpEnd < acpStart) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "invalid position", + this)); + return TS_E_INVALIDPOS; + } + + mWaitingQueryLayout = false; + + if (IsHandlingCompositionInContent() && mContentForTSF.isSome() && + mContentForTSF->HasOrHadComposition() && + mContentForTSF->IsLayoutChanged() && + mContentForTSF->MinModifiedOffset().value() > + static_cast(LONG_MAX)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt(), FAILED due to the text " + "is too big for TSF (cannot treat modified offset as LONG), " + "mContentForTSF=%s", + this, ToString(mContentForTSF).c_str())); + return E_FAIL; + } + + // At Windows 10 build 17643 (an insider preview for RS5), Microsoft fixed + // the bug of TS_E_NOLAYOUT (even when we returned TS_E_NOLAYOUT, TSF + // returned E_FAIL to TIP). However, until we drop to support older Windows + // and all TIPs are aware of TS_E_NOLAYOUT result, we need to keep returning + // S_OK and available rectangle only for them. + if (!MaybeHackNoErrorLayoutBugs(acpStart, acpEnd) && + mContentForTSF.isSome() && mContentForTSF->IsLayoutChangedAt(acpEnd)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() returned TS_E_NOLAYOUT " + "(acpEnd=%ld)", + this, acpEnd)); + mHasReturnedNoLayoutError = true; + return TS_E_NOLAYOUT; + } + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() returned TS_E_NOLAYOUT " + "(acpEnd=%ld) because this has already been destroyed", + this, acpEnd)); + mHasReturnedNoLayoutError = true; + return TS_E_NOLAYOUT; + } + + // use eQueryTextRect to get rect in system, screen coordinates + WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, mWidget); + mWidget->InitEvent(queryTextRectEvent); + + WidgetQueryContentEvent::Options options; + int64_t startOffset = acpStart; + if (mComposition.isSome()) { + // If there is a composition, TSF must want character rects related to + // the composition. Therefore, we should use insertion point relative + // query because the composition might be at different position from + // the position where TSFTextStore believes it at. + options.mRelativeToInsertionPoint = true; + startOffset -= mComposition->StartOffset(); + } else if (IsHandlingCompositionInParent() && mContentForTSF.isSome() && + mContentForTSF->HasOrHadComposition()) { + // If there was a composition and its commit event hasn't been dispatched + // yet, ContentCacheInParent is still open for relative offset query from + // the latest composition. + options.mRelativeToInsertionPoint = true; + startOffset -= mContentForTSF->LatestCompositionRange()->StartOffset(); + } else if (!CanAccessActualContentDirectly() && + mSelectionForTSF->HasRange()) { + // If TSF/TIP cannot access actual content directly, there may be pending + // text and/or selection changes which have not been notified TSF yet. + // Therefore, we should use relative to insertion point query since + // TSF/TIP computes the offset from the cached selection. + options.mRelativeToInsertionPoint = true; + startOffset -= mSelectionForTSF->StartOffset(); + } + // ContentEventHandler and ContentCache return actual caret rect when + // the queried range is collapsed and selection is collapsed at the + // queried range. Then, its height (in horizontal layout, width in vertical + // layout) may be different from actual font height of the line. In such + // case, users see "dancing" of candidate or suggest window of TIP. + // For preventing it, we should query text rect with at least 1 length. + uint32_t length = std::max(static_cast(acpEnd - acpStart), 1); + queryTextRectEvent.InitForQueryTextRect(startOffset, length, options); + + DispatchEvent(queryTextRectEvent); + if (NS_WARN_IF(queryTextRectEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "eQueryTextRect failure", + this)); + return TS_E_INVALIDPOS; // but unexpected failure, maybe. + } + + // IMEs don't like empty rects, fix here + if (queryTextRectEvent.mReply->mRect.Width() <= 0) { + queryTextRectEvent.mReply->mRect.SetWidth(1); + } + if (queryTextRectEvent.mReply->mRect.Height() <= 0) { + queryTextRectEvent.mReply->mRect.SetHeight(1); + } + + // convert to unclipped screen rect + nsWindow* refWindow = + static_cast(!!queryTextRectEvent.mReply->mFocusedWidget + ? queryTextRectEvent.mReply->mFocusedWidget + : static_cast(mWidget.get())); + // Result rect is in top level widget coordinates + refWindow = refWindow->GetTopLevelWindow(false); + if (!refWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "no top level window", + this)); + return E_FAIL; + } + + queryTextRectEvent.mReply->mRect.MoveBy(refWindow->WidgetToScreenOffset()); + + // get bounding screen rect to test for clipping + if (!GetScreenExtInternal(*prc)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetTextExt() FAILED due to " + "GetScreenExtInternal() failure", + this)); + return E_FAIL; + } + + // clip text rect to bounding rect + RECT textRect; + ::SetRect(&textRect, queryTextRectEvent.mReply->mRect.X(), + queryTextRectEvent.mReply->mRect.Y(), + queryTextRectEvent.mReply->mRect.XMost(), + queryTextRectEvent.mReply->mRect.YMost()); + if (!::IntersectRect(prc, prc, &textRect)) + // Text is not visible + ::SetRectEmpty(prc); + + // not equal if text rect was clipped + *pfClipped = !::EqualRect(prc, &textRect); + + // ATOK 2011 - 2016 refers native caret position and size on windows whose + // class name is one of Mozilla's windows for deciding candidate window + // position. Additionally, ATOK 2015 and earlier behaves really odd when + // we don't create native caret. Therefore, we need to create native caret + // only when ATOK 2011 - 2015 is active (i.e., not necessary for ATOK 2016). + // However, if a11y module is handling native caret, we shouldn't touch it. + // Note that ATOK must require the latest information of the caret. So, + // even if we'll create native caret later, we need to creat it here with + // current information. + if (!IMEHandler::IsA11yHandlingNativeCaret() && + StaticPrefs::intl_tsf_hack_atok_create_native_caret() && + TSFStaticSink::IsATOKReferringNativeCaretActive() && + mComposition.isSome() && + mComposition->IsOffsetInRangeOrEndOffset(acpStart) && + mComposition->IsOffsetInRangeOrEndOffset(acpEnd)) { + CreateNativeCaret(); + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetTextExt() succeeded: " + "*prc={ left=%ld, top=%ld, right=%ld, bottom=%ld }, *pfClipped=%s", + this, prc->left, prc->top, prc->right, prc->bottom, + GetBoolName(*pfClipped))); + + return S_OK; +} + +bool TSFTextStore::MaybeHackNoErrorLayoutBugs(LONG& aACPStart, LONG& aACPEnd) { + // When ITextStoreACP::GetTextExt() returns TS_E_NOLAYOUT, TSF returns E_FAIL + // to its caller (typically, active TIP). Then, most TIPs abort current job + // or treat such application as non-GUI apps. E.g., some of them give up + // showing candidate window, some others show candidate window at top-left of + // the screen. For avoiding this issue, when there is composition (until + // composition is actually committed in remote content), we should not + // return TS_E_NOLAYOUT error for TIPs whose some features are broken by + // this issue. + // Note that ideally, this issue should be avoided by each TIP since this + // won't be fixed at least on non-latest Windows. Actually, Google Japanese + // Input (based on Mozc) does it. When GetTextExt() returns E_FAIL, TIPs + // should try to check result of GetRangeFromPoint() because TSF returns + // TS_E_NOLAYOUT correctly in this case. See: + // https://github.com/google/mozc/blob/6b878e31fb6ac4347dc9dfd8ccc1080fe718479f/src/win32/tip/tip_range_util.cc#L237-L257 + + if (!IsHandlingCompositionInContent() || mContentForTSF.isNothing() || + !mContentForTSF->HasOrHadComposition() || + !mContentForTSF->IsLayoutChangedAt(aACPEnd)) { + return false; + } + + MOZ_ASSERT(mComposition.isNothing() || + mComposition->StartOffset() == + mContentForTSF->LatestCompositionRange()->StartOffset()); + MOZ_ASSERT(mComposition.isNothing() || + mComposition->EndOffset() == + mContentForTSF->LatestCompositionRange()->EndOffset()); + + // If TSF does not have the bug, we need to hack only with a few TIPs. + static const bool sAlllowToStopHackingIfFine = + IsWindows10BuildOrLater(17643) && + StaticPrefs:: + intl_tsf_hack_allow_to_stop_hacking_on_build_17643_or_later(); + + // We need to compute active TIP now. This may take a couple of milliseconds, + // however, it'll be cached, so, must be faster than check active TIP every + // GetTextExt() calls. + const Maybe& selectionForTSF = SelectionForTSF(); + switch (TSFStaticSink::ActiveTIP()) { + // MS IME for Japanese doesn't support asynchronous handling at deciding + // its suggest list window position. The feature was implemented + // starting from Windows 8. And also we may meet same trouble in e10s + // mode on Win7. So, we should never return TS_E_NOLAYOUT to MS IME for + // Japanese. + case TextInputProcessorID::eMicrosoftIMEForJapanese: + // Basically, MS-IME tries to retrieve whole composition string rect + // at deciding suggest window immediately after unlocking the document. + // However, in e10s mode, the content hasn't updated yet in most cases. + // Therefore, if the first character at the retrieving range rect is + // available, we should use it as the result. + // Note that according to bug 1609675, MS-IME for Japanese itself does + // not handle TS_E_NOLAYOUT correctly at least on Build 18363.657 (1909). + if (StaticPrefs:: + intl_tsf_hack_ms_japanese_ime_do_not_return_no_layout_error_at_first_char() && + aACPStart < aACPEnd) { + aACPEnd = aACPStart; + break; + } + if (sAlllowToStopHackingIfFine) { + return false; + } + // Although, the condition is not clear, MS-IME sometimes retrieves the + // caret rect immediately after modifying the composition string but + // before unlocking the document. In such case, we should return the + // nearest character rect. + // (Let's return true if there is no selection which must be not expected + // by MS-IME nor TSF.) + if (StaticPrefs:: + intl_tsf_hack_ms_japanese_ime_do_not_return_no_layout_error_at_caret() && + aACPStart == aACPEnd && selectionForTSF.isSome() && + (!selectionForTSF->HasRange() || + (selectionForTSF->Collapsed() && + selectionForTSF->EndOffset() == aACPEnd))) { + int32_t minOffsetOfLayoutChanged = + static_cast(mContentForTSF->MinModifiedOffset().value()); + aACPEnd = aACPStart = std::max(minOffsetOfLayoutChanged - 1, 0); + } else { + return false; + } + break; + // The bug of Microsoft Office IME 2010 for Japanese is similar to + // MS-IME for Win 8.1 and Win 10. Newer version of MS Office IME is not + // released yet. So, we can hack it without prefs because there must be + // no developers who want to disable this hack for tests. + // XXX We have not tested with Microsoft Office IME 2010 since it's + // installable only with Win7 and Win8 (i.e., cannot install Win8.1 + // and Win10), and requires upgrade to Win10. + case TextInputProcessorID::eMicrosoftOfficeIME2010ForJapanese: + // Basically, MS-IME tries to retrieve whole composition string rect + // at deciding suggest window immediately after unlocking the document. + // However, in e10s mode, the content hasn't updated yet in most cases. + // Therefore, if the first character at the retrieving range rect is + // available, we should use it as the result. + if (aACPStart < aACPEnd) { + aACPEnd = aACPStart; + } + // Although, the condition is not clear, MS-IME sometimes retrieves the + // caret rect immediately after modifying the composition string but + // before unlocking the document. In such case, we should return the + // nearest character rect. + // (Let's return true if there is no selection which must be not expected + // by MS-IME nor TSF.) + else if (aACPStart == aACPEnd && selectionForTSF.isSome() && + (!selectionForTSF->HasRange() || + (selectionForTSF->Collapsed() && + selectionForTSF->EndOffset() == aACPEnd))) { + int32_t minOffsetOfLayoutChanged = + static_cast(mContentForTSF->MinModifiedOffset().value()); + aACPEnd = aACPStart = std::max(minOffsetOfLayoutChanged - 1, 0); + } else { + return false; + } + break; + // ATOK fails to handle TS_E_NOLAYOUT only when it decides the position of + // suggest window. In such case, ATOK tries to query rect of whole or a + // part of composition string. + // FYI: ATOK changes their implementation around candidate window and + // suggest widget at ATOK 2016. Therefore, there are some differences + // ATOK 2015 (or older) and ATOK 2016 (or newer). + // FYI: ATOK 2017 stops referring our window class name. I.e., ATOK 2016 + // and older may behave differently only on Gecko but this must be + // finished from ATOK 2017. + // FYI: For testing with legacy ATOK, we should hack it even if current ATOK + // refers native caret rect on windows whose window class is one of + // Mozilla window classes and we stop creating native caret for ATOK + // because creating native caret causes ATOK refers caret position + // when GetTextExt() returns TS_E_NOLAYOUT. + case TextInputProcessorID::eATOK2011: + case TextInputProcessorID::eATOK2012: + case TextInputProcessorID::eATOK2013: + case TextInputProcessorID::eATOK2014: + case TextInputProcessorID::eATOK2015: + // ATOK 2016 and later may temporarily show candidate window at odd + // position when you convert a word quickly (e.g., keep pressing + // space bar). So, on ATOK 2016 or later, we need to keep hacking the + // result of GetTextExt(). + if (sAlllowToStopHackingIfFine) { + return false; + } + // If we'll create native caret where we paint our caret. Then, ATOK + // will refer native caret. So, we don't need to hack anything in + // this case. + if (StaticPrefs::intl_tsf_hack_atok_create_native_caret()) { + MOZ_ASSERT(TSFStaticSink::IsATOKReferringNativeCaretActive()); + return false; + } + [[fallthrough]]; + case TextInputProcessorID::eATOK2016: + case TextInputProcessorID::eATOKUnknown: + if (!StaticPrefs:: + intl_tsf_hack_atok_do_not_return_no_layout_error_of_composition_string()) { + return false; + } + // If the range is in the composition string, we should return rectangle + // in it as far as possible. + if (!mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset( + aACPStart) || + !mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset( + aACPEnd)) { + return false; + } + break; + // Japanist 10 fails to handle TS_E_NOLAYOUT when it decides the position + // of candidate window. In such case, Japanist shows candidate window at + // top-left of the screen. So, we should return the nearest caret rect + // where we know. This is Japanist's bug. So, even after build 17643, + // we need this hack. + case TextInputProcessorID::eJapanist10: + if (!StaticPrefs:: + intl_tsf_hack_japanist10_do_not_return_no_layout_error_of_composition_string()) { + return false; + } + if (!mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset( + aACPStart) || + !mContentForTSF->LatestCompositionRange()->IsOffsetInRangeOrEndOffset( + aACPEnd)) { + return false; + } + break; + // Free ChangJie 2010 doesn't handle ITfContextView::GetTextExt() properly. + // This must be caused by the bug of TSF since Free ChangJie works fine on + // build 17643 and later. + case TextInputProcessorID::eFreeChangJie: + if (sAlllowToStopHackingIfFine) { + return false; + } + if (!StaticPrefs:: + intl_tsf_hack_free_chang_jie_do_not_return_no_layout_error()) { + return false; + } + aACPEnd = mContentForTSF->LatestCompositionRange()->StartOffset(); + aACPStart = std::min(aACPStart, aACPEnd); + break; + // Some Traditional Chinese TIPs of Microsoft don't show candidate window + // in e10s mode on Win8 or later. + case TextInputProcessorID::eMicrosoftQuick: + if (sAlllowToStopHackingIfFine) { + return false; // MS Quick works fine with Win10 build 17643. + } + [[fallthrough]]; + case TextInputProcessorID::eMicrosoftChangJie: + if (!IsWin8OrLater() || + !StaticPrefs:: + intl_tsf_hack_ms_traditional_chinese_do_not_return_no_layout_error()) { + return false; + } + aACPEnd = mContentForTSF->LatestCompositionRange()->StartOffset(); + aACPStart = std::min(aACPStart, aACPEnd); + break; + // Some Simplified Chinese TIPs of Microsoft don't show candidate window + // in e10s mode on Win8 or later. + // FYI: Only Simplified Chinese TIPs of Microsoft still require this hack + // because they sometimes do not show candidate window when we return + // TS_E_NOLAYOUT for first query. Note that even when they show + // candidate window properly, we return TS_E_NOLAYOUT and following + // log looks same as when they don't show candidate window. Perhaps, + // there is stateful cause or race in them. + case TextInputProcessorID::eMicrosoftPinyin: + case TextInputProcessorID::eMicrosoftWubi: + if (!IsWin8OrLater() || + !StaticPrefs:: + intl_tsf_hack_ms_simplified_chinese_do_not_return_no_layout_error()) { + return false; + } + aACPEnd = mContentForTSF->LatestCompositionRange()->StartOffset(); + aACPStart = std::min(aACPStart, aACPEnd); + break; + default: + return false; + } + + // If we hack the queried range for active TIP, that means we should not + // return TS_E_NOLAYOUT even if hacked offset is still modified. So, as + // far as possible, we should adjust the offset. + MOZ_ASSERT(mContentForTSF->IsLayoutChanged()); + bool collapsed = aACPStart == aACPEnd; + // Note that even if all characters in the editor or the composition + // string was modified, 0 or start offset of the composition string is + // useful because it may return caret rect or old character's rect which + // the user still see. That must be useful information for TIP. + int32_t firstModifiedOffset = + static_cast(mContentForTSF->MinModifiedOffset().value()); + LONG lastUnmodifiedOffset = std::max(firstModifiedOffset - 1, 0); + if (mContentForTSF->IsLayoutChangedAt(aACPStart)) { + if (aACPStart >= mContentForTSF->LatestCompositionRange()->StartOffset()) { + // If mContentForTSF has last composition string and current + // composition string, we can assume that ContentCacheInParent has + // cached rects of composition string at least length of current + // composition string. Otherwise, we can assume that rect for + // first character of composition string is stored since it was + // selection start or caret position. + LONG maxCachedOffset = + mContentForTSF->LatestCompositionRange()->EndOffset(); + if (mContentForTSF->LastComposition().isSome()) { + maxCachedOffset = std::min( + maxCachedOffset, mContentForTSF->LastComposition()->EndOffset()); + } + aACPStart = std::min(aACPStart, maxCachedOffset); + } + // Otherwise, we don't know which character rects are cached. So, we + // need to use first unmodified character's rect in this case. Even + // if there is no character, the query event will return caret rect + // instead. + else { + aACPStart = lastUnmodifiedOffset; + } + MOZ_ASSERT(aACPStart <= aACPEnd); + } + + // If TIP requests caret rect with collapsed range, we should keep + // collapsing the range. + if (collapsed) { + aACPEnd = aACPStart; + } + // Let's set aACPEnd to larger offset of last unmodified offset or + // aACPStart which may be the first character offset of the composition + // string. However, some TIPs may want to know the right edge of the + // range. Therefore, if aACPEnd is in composition string and active TIP + // doesn't retrieve caret rect (i.e., the range isn't collapsed), we + // should keep using the original aACPEnd. Otherwise, we should set + // aACPEnd to larger value of aACPStart and lastUnmodifiedOffset. + else if (mContentForTSF->IsLayoutChangedAt(aACPEnd) && + !mContentForTSF->LatestCompositionRange() + ->IsOffsetInRangeOrEndOffset(aACPEnd)) { + aACPEnd = std::max(aACPStart, lastUnmodifiedOffset); + } + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::HackNoErrorLayoutBugs() hacked the queried range " + "for not returning TS_E_NOLAYOUT, new values are: " + "aACPStart=%ld, aACPEnd=%ld", + this, aACPStart, aACPEnd)); + + return true; +} + +STDMETHODIMP +TSFTextStore::GetScreenExt(TsViewCookie vcView, RECT* prc) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetScreenExt(vcView=%ld, prc=0x%p)", this, + vcView, prc)); + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "called with invalid view", + this)); + return E_INVALIDARG; + } + + if (!prc) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() returns empty rect " + "due to already destroyed", + this)); + prc->left = prc->top = prc->right = prc->bottom = 0; + return S_OK; + } + + if (!GetScreenExtInternal(*prc)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "GetScreenExtInternal() failure", + this)); + return E_FAIL; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetScreenExt() succeeded: " + "*prc={ left=%ld, top=%ld, right=%ld, bottom=%ld }", + this, prc->left, prc->top, prc->right, prc->bottom)); + return S_OK; +} + +bool TSFTextStore::GetScreenExtInternal(RECT& aScreenExt) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetScreenExtInternal()", this)); + + MOZ_ASSERT(!mDestroyed); + + // use NS_QUERY_EDITOR_RECT to get rect in system, screen coordinates + WidgetQueryContentEvent queryEditorRectEvent(true, eQueryEditorRect, mWidget); + mWidget->InitEvent(queryEditorRectEvent); + DispatchEvent(queryEditorRectEvent); + if (queryEditorRectEvent.Failed()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExtInternal() FAILED due to " + "eQueryEditorRect failure", + this)); + return false; + } + + nsWindow* refWindow = + static_cast(!!queryEditorRectEvent.mReply->mFocusedWidget + ? queryEditorRectEvent.mReply->mFocusedWidget + : static_cast(mWidget.get())); + // Result rect is in top level widget coordinates + refWindow = refWindow->GetTopLevelWindow(false); + if (!refWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExtInternal() FAILED due to " + "no top level window", + this)); + return false; + } + + LayoutDeviceIntRect boundRect = refWindow->GetClientBounds(); + boundRect.MoveTo(0, 0); + + // Clip frame rect to window rect + boundRect.IntersectRect(queryEditorRectEvent.mReply->mRect, boundRect); + if (!boundRect.IsEmpty()) { + boundRect.MoveBy(refWindow->WidgetToScreenOffset()); + ::SetRect(&aScreenExt, boundRect.X(), boundRect.Y(), boundRect.XMost(), + boundRect.YMost()); + } else { + ::SetRectEmpty(&aScreenExt); + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::GetScreenExtInternal() succeeded: " + "aScreenExt={ left=%ld, top=%ld, right=%ld, bottom=%ld }", + this, aScreenExt.left, aScreenExt.top, aScreenExt.right, + aScreenExt.bottom)); + return true; +} + +STDMETHODIMP +TSFTextStore::GetWnd(TsViewCookie vcView, HWND* phwnd) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetWnd(vcView=%ld, phwnd=0x%p), " + "mWidget=0x%p", + this, vcView, phwnd, mWidget.get())); + + if (vcView != TEXTSTORE_DEFAULT_VIEW) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetWnd() FAILED due to " + "called with invalid view", + this)); + return E_INVALIDARG; + } + + if (!phwnd) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::GetScreenExt() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + *phwnd = mWidget ? mWidget->GetWindowHandle() : nullptr; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::GetWnd() succeeded: *phwnd=0x%p", this, + static_cast(*phwnd))); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::InsertTextAtSelection(DWORD dwFlags, const WCHAR* pchText, + ULONG cch, LONG* pacpStart, LONG* pacpEnd, + TS_TEXTCHANGE* pChange) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::InsertTextAtSelection(dwFlags=%s, " + "pchText=0x%p \"%s\", cch=%lu, pacpStart=0x%p, pacpEnd=0x%p, " + "pChange=0x%p), mComposition=%s", + this, + dwFlags == 0 ? "0" + : dwFlags == TF_IAS_NOQUERY ? "TF_IAS_NOQUERY" + : dwFlags == TF_IAS_QUERYONLY ? "TF_IAS_QUERYONLY" + : "Unknown", + pchText, pchText && cch ? GetEscapedUTF8String(pchText, cch).get() : "", + cch, pacpStart, pacpEnd, pChange, ToString(mComposition).c_str())); + + if (cch && !pchText) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null pchText", + this)); + return E_INVALIDARG; + } + + if (TS_IAS_QUERYONLY == dwFlags) { + if (!IsReadLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "not locked (read)", + this)); + return TS_E_NOLOCK; + } + + if (!pacpStart || !pacpEnd) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + // Get selection first + Maybe& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "SelectionForTSF() failure", + this)); + return E_FAIL; + } + + // Simulate text insertion + if (selectionForTSF->HasRange()) { + *pacpStart = selectionForTSF->StartOffset(); + *pacpEnd = selectionForTSF->EndOffset(); + if (pChange) { + *pChange = TS_TEXTCHANGE{.acpStart = selectionForTSF->StartOffset(), + .acpOldEnd = selectionForTSF->EndOffset(), + .acpNewEnd = selectionForTSF->StartOffset() + + static_cast(cch)}; + } + } else { + // There is no error code to return "no selection" state from this method. + // This means that TSF/TIP should check `GetSelection` result first and + // stop using this. However, this could be called by TIP/TSF if they do + // not do so. Therefore, we should use start of editor instead, but + // notify the caller of nothing will be inserted with pChange->acpNewEnd. + *pacpStart = *pacpEnd = 0; + if (pChange) { + *pChange = TS_TEXTCHANGE{.acpStart = 0, .acpOldEnd = 0, .acpNewEnd = 0}; + } + } + } else { + if (!IsReadWriteLocked()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "not locked (read-write)", + this)); + return TS_E_NOLOCK; + } + + if (!pChange) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null pChange", + this)); + return E_INVALIDARG; + } + + if (TS_IAS_NOQUERY != dwFlags && (!pacpStart || !pacpEnd)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "null argument", + this)); + return E_INVALIDARG; + } + + if (!InsertTextAtSelectionInternal(nsDependentSubstring(pchText, cch), + pChange)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelection() FAILED due to " + "InsertTextAtSelectionInternal() failure", + this)); + return E_FAIL; + } + + if (TS_IAS_NOQUERY != dwFlags) { + *pacpStart = pChange->acpStart; + *pacpEnd = pChange->acpNewEnd; + } + } + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::InsertTextAtSelection() succeeded: " + "*pacpStart=%ld, *pacpEnd=%ld, " + "*pChange={ acpStart=%ld, acpOldEnd=%ld, acpNewEnd=%ld })", + this, pacpStart ? *pacpStart : 0, pacpEnd ? *pacpEnd : 0, + pChange ? pChange->acpStart : 0, pChange ? pChange->acpOldEnd : 0, + pChange ? pChange->acpNewEnd : 0)); + return S_OK; +} + +bool TSFTextStore::InsertTextAtSelectionInternal(const nsAString& aInsertStr, + TS_TEXTCHANGE* aTextChange) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal(" + "aInsertStr=\"%s\", aTextChange=0x%p), mComposition=%s", + this, GetEscapedUTF8String(aInsertStr).get(), aTextChange, + ToString(mComposition).c_str())); + + Maybe& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() failed " + "due to ContentForTSF() failure()", + this)); + return false; + } + + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return false; + } + + TS_SELECTION_ACP oldSelection = contentForTSF->Selection()->ACPRef(); + if (mComposition.isNothing()) { + // Use a temporary composition to contain the text + PendingAction* compositionStart = mPendingActions.AppendElements(2); + PendingAction* compositionEnd = compositionStart + 1; + + compositionStart->mType = PendingAction::Type::eCompositionStart; + compositionStart->mSelectionStart = oldSelection.acpStart; + compositionStart->mSelectionLength = + oldSelection.acpEnd - oldSelection.acpStart; + compositionStart->mAdjustSelection = false; + + compositionEnd->mType = PendingAction::Type::eCompositionEnd; + compositionEnd->mData = aInsertStr; + compositionEnd->mSelectionStart = compositionStart->mSelectionStart; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() " + "appending pending compositionstart and compositionend... " + "PendingCompositionStart={ mSelectionStart=%ld, " + "mSelectionLength=%ld }, PendingCompositionEnd={ mData=\"%s\" " + "(Length()=%zu), mSelectionStart=%ld }", + this, compositionStart->mSelectionStart, + compositionStart->mSelectionLength, + GetEscapedUTF8String(compositionEnd->mData).get(), + compositionEnd->mData.Length(), compositionEnd->mSelectionStart)); + } + + contentForTSF->ReplaceSelectedTextWith(aInsertStr); + + if (aTextChange) { + aTextChange->acpStart = oldSelection.acpStart; + aTextChange->acpOldEnd = oldSelection.acpEnd; + aTextChange->acpNewEnd = contentForTSF->Selection()->EndOffset(); + } + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::InsertTextAtSelectionInternal() " + "succeeded: mWidget=0x%p, mWidget->Destroyed()=%s, aTextChange={ " + "acpStart=%ld, acpOldEnd=%ld, acpNewEnd=%ld }", + this, mWidget.get(), GetBoolName(mWidget ? mWidget->Destroyed() : true), + aTextChange ? aTextChange->acpStart : 0, + aTextChange ? aTextChange->acpOldEnd : 0, + aTextChange ? aTextChange->acpNewEnd : 0)); + return true; +} + +STDMETHODIMP +TSFTextStore::InsertEmbeddedAtSelection(DWORD dwFlags, IDataObject* pDataObject, + LONG* pacpStart, LONG* pacpEnd, + TS_TEXTCHANGE* pChange) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::InsertEmbeddedAtSelection() called " + "but not supported (E_NOTIMPL)", + this)); + + // embedded objects are not supported + return E_NOTIMPL; +} + +HRESULT TSFTextStore::RecordCompositionStartAction( + ITfCompositionView* aCompositionView, ITfRange* aRange, + bool aPreserveSelection) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionStartAction(" + "aCompositionView=0x%p, aRange=0x%p, aPreserveSelection=%s), " + "mComposition=%s", + this, aCompositionView, aRange, GetBoolName(aPreserveSelection), + ToString(mComposition).c_str())); + + LONG start = 0, length = 0; + HRESULT hr = GetRangeExtent(aRange, &start, &length); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED " + "due to GetRangeExtent() failure", + this)); + return hr; + } + + return RecordCompositionStartAction(aCompositionView, start, length, + aPreserveSelection); +} + +HRESULT TSFTextStore::RecordCompositionStartAction( + ITfCompositionView* aCompositionView, LONG aStart, LONG aLength, + bool aPreserveSelection) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionStartAction(" + "aCompositionView=0x%p, aStart=%ld, aLength=%ld, " + "aPreserveSelection=%s), " + "mComposition=%s", + this, aCompositionView, aStart, aLength, + GetBoolName(aPreserveSelection), ToString(mComposition).c_str())); + + Maybe& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED " + "due to ContentForTSF() failure", + this)); + return E_FAIL; + } + + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return false; + } + + CompleteLastActionIfStillIncomplete(); + + // TIP may have inserted text at selection before calling + // OnStartComposition(). In this case, we've already created a pending + // compositionend. If new composition replaces all commit string of the + // pending compositionend, we should cancel the pending compositionend and + // keep the previous composition normally. + // On Windows 7, MS-IME for Korean, MS-IME 2010 for Korean and MS Old Hangul + // may start composition with calling InsertTextAtSelection() and + // OnStartComposition() with this order (bug 1208043). + // On Windows 10, MS Pinyin, MS Wubi, MS ChangJie and MS Quick commits + // last character and replace it with empty string with new composition + // when user removes last character of composition string with Backspace + // key (bug 1462257). + if (!aPreserveSelection && + IsLastPendingActionCompositionEndAt(aStart, aLength)) { + const PendingAction& pendingCompositionEnd = mPendingActions.LastElement(); + contentForTSF->RestoreCommittedComposition(aCompositionView, + pendingCompositionEnd); + mPendingActions.RemoveLastElement(); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionStartAction() " + "succeeded: restoring the committed string as composing string, " + "mComposition=%s, mSelectionForTSF=%s", + this, ToString(mComposition).c_str(), + ToString(mSelectionForTSF).c_str())); + return S_OK; + } + + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::Type::eCompositionStart; + action->mSelectionStart = aStart; + action->mSelectionLength = aLength; + + Maybe& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionStartAction() FAILED " + "due to SelectionForTSF() failure", + this)); + action->mAdjustSelection = true; + } else if (!selectionForTSF->HasRange()) { + // If there is no selection, let's collapse seletion to the insertion point. + action->mAdjustSelection = true; + } else if (selectionForTSF->MinOffset() != aStart || + selectionForTSF->MaxOffset() != aStart + aLength) { + // If new composition range is different from current selection range, + // we need to set selection before dispatching compositionstart event. + action->mAdjustSelection = true; + } else { + // We shouldn't dispatch selection set event before dispatching + // compositionstart event because it may cause put caret different + // position in HTML editor since generated flat text content and offset in + // it are lossy data of HTML contents. + action->mAdjustSelection = false; + } + + contentForTSF->StartComposition(aCompositionView, *action, + aPreserveSelection); + MOZ_ASSERT(mComposition.isSome()); + action->mData = mComposition->DataRef(); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionStartAction() succeeded: " + "mComposition=%s, mSelectionForTSF=%s }", + this, ToString(mComposition).c_str(), + ToString(mSelectionForTSF).c_str())); + return S_OK; +} + +HRESULT +TSFTextStore::RecordCompositionEndAction() { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::RecordCompositionEndAction(), " + "mComposition=%s", + this, ToString(mComposition).c_str())); + + MOZ_ASSERT(mComposition.isSome()); + + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due to " + "no composition", + this)); + return false; + } + + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return false; + } + + // If we're handling incomplete composition update or already handled + // composition update, we can forget them since composition end will send + // the latest composition string and it overwrites the composition string + // even if we dispatch eCompositionChange event before that. So, let's + // forget all composition updates now. + RemoveLastCompositionUpdateActions(); + PendingAction* action = mPendingActions.AppendElement(); + action->mType = PendingAction::Type::eCompositionEnd; + action->mData = mComposition->DataRef(); + action->mSelectionStart = mComposition->StartOffset(); + + Maybe& contentForTSF = ContentForTSF(); + if (contentForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::RecordCompositionEndAction() FAILED due " + "to ContentForTSF() failure", + this)); + return E_FAIL; + } + contentForTSF->EndComposition(*action); + + // If this composition was restart but the composition doesn't modify + // anything, we should remove the pending composition for preventing to + // dispatch redundant composition events. + for (size_t i = mPendingActions.Length(), j = 1; i > 0; --i, ++j) { + PendingAction& pendingAction = mPendingActions[i - 1]; + if (pendingAction.mType == PendingAction::Type::eCompositionStart) { + if (pendingAction.mData != action->mData) { + break; + } + // When only setting selection is necessary, we should append it. + if (pendingAction.mAdjustSelection) { + LONG selectionStart = pendingAction.mSelectionStart; + LONG selectionLength = pendingAction.mSelectionLength; + + PendingAction* setSelection = mPendingActions.AppendElement(); + setSelection->mType = PendingAction::Type::eSetSelection; + setSelection->mSelectionStart = selectionStart; + setSelection->mSelectionLength = selectionLength; + setSelection->mSelectionReversed = false; + } + // Remove the redundant pending composition. + mPendingActions.RemoveElementsAt(i - 1, j); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionEndAction(), " + "succeeded, but the composition was canceled due to redundant", + this)); + return S_OK; + } + } + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::RecordCompositionEndAction(), succeeded", this)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::OnStartComposition(ITfCompositionView* pComposition, BOOL* pfOk) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnStartComposition(pComposition=0x%p, " + "pfOk=0x%p), mComposition=%s", + this, pComposition, pfOk, ToString(mComposition).c_str())); + + AutoPendingActionAndContentFlusher flusher(this); + + *pfOk = FALSE; + + // Only one composition at a time + if (mComposition.isSome()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnStartComposition() FAILED due to " + "there is another composition already (but returns S_OK)", + this)); + return S_OK; + } + + RefPtr range; + HRESULT hr = pComposition->GetRange(getter_AddRefs(range)); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnStartComposition() FAILED due to " + "pComposition->GetRange() failure", + this)); + return hr; + } + hr = RecordCompositionStartAction(pComposition, range, false); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnStartComposition() FAILED due to " + "RecordCompositionStartAction() failure", + this)); + return hr; + } + + *pfOk = TRUE; + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnStartComposition() succeeded", this)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::OnUpdateComposition(ITfCompositionView* pComposition, + ITfRange* pRangeNew) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnUpdateComposition(pComposition=0x%p, " + "pRangeNew=0x%p), mComposition=%s", + this, pComposition, pRangeNew, ToString(mComposition).c_str())); + + AutoPendingActionAndContentFlusher flusher(this); + + if (!mDocumentMgr || !mContext) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "not ready for the composition", + this)); + return E_UNEXPECTED; + } + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "no active composition", + this)); + return E_UNEXPECTED; + } + if (mComposition->GetView() != pComposition) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "different composition view specified", + this)); + return E_UNEXPECTED; + } + + // pRangeNew is null when the update is not complete + if (!pRangeNew) { + MaybeDispatchKeyboardEventAsProcessedByIME(); + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "destroyed during dispatching a keyboard event", + this)); + return E_FAIL; + } + PendingAction* action = LastOrNewPendingCompositionUpdate(); + action->mIncomplete = true; + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnUpdateComposition() succeeded but " + "not complete", + this)); + return S_OK; + } + + HRESULT hr = RestartCompositionIfNecessary(pRangeNew); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "RestartCompositionIfNecessary() failure", + this)); + return hr; + } + + hr = RecordCompositionUpdateAction(); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "RecordCompositionUpdateAction() failure", + this)); + return hr; + } + + if (MOZ_LOG_TEST(gIMELog, LogLevel::Info)) { + Maybe& selectionForTSF = SelectionForTSF(); + if (selectionForTSF.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnUpdateComposition() FAILED due to " + "SelectionForTSF() failure", + this)); + return S_OK; // Don't return error only when we're logging. + } + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnUpdateComposition() succeeded: " + "mComposition=%s, SelectionForTSF()=%s", + this, ToString(mComposition).c_str(), + ToString(selectionForTSF).c_str())); + } + return S_OK; +} + +STDMETHODIMP +TSFTextStore::OnEndComposition(ITfCompositionView* pComposition) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnEndComposition(pComposition=0x%p), " + "mComposition=%s", + this, pComposition, ToString(mComposition).c_str())); + + AutoPendingActionAndContentFlusher flusher(this); + + if (mComposition.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnEndComposition() FAILED due to " + "no active composition", + this)); + return E_UNEXPECTED; + } + + if (mComposition->GetView() != pComposition) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnEndComposition() FAILED due to " + "different composition view specified", + this)); + return E_UNEXPECTED; + } + + HRESULT hr = RecordCompositionEndAction(); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::OnEndComposition() FAILED due to " + "RecordCompositionEndAction() failure", + this)); + return hr; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnEndComposition(), succeeded", this)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::AdviseMouseSink(ITfRangeACP* range, ITfMouseSink* pSink, + DWORD* pdwCookie) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::AdviseMouseSink(range=0x%p, pSink=0x%p, " + "pdwCookie=0x%p)", + this, range, pSink, pdwCookie)); + + if (!pdwCookie) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the " + "pdwCookie is null", + this)); + return E_INVALIDARG; + } + // Initialize the result with invalid cookie for safety. + *pdwCookie = MouseTracker::kInvalidCookie; + + if (!range) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the " + "range is null", + this)); + return E_INVALIDARG; + } + if (!pSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to the " + "pSink is null", + this)); + return E_INVALIDARG; + } + + // Looking for an unusing tracker. + MouseTracker* tracker = nullptr; + for (size_t i = 0; i < mMouseTrackers.Length(); i++) { + if (mMouseTrackers[i].IsUsing()) { + continue; + } + tracker = &mMouseTrackers[i]; + } + // If there is no unusing tracker, create new one. + // XXX Should we make limitation of the number of installs? + if (!tracker) { + tracker = mMouseTrackers.AppendElement(); + HRESULT hr = tracker->Init(this); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to " + "failure of MouseTracker::Init()", + this)); + return hr; + } + } + HRESULT hr = tracker->AdviseSink(this, range, pSink); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::AdviseMouseSink() FAILED due to failure " + "of MouseTracker::Init()", + this)); + return hr; + } + *pdwCookie = tracker->Cookie(); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::AdviseMouseSink(), succeeded, " + "*pdwCookie=%ld", + this, *pdwCookie)); + return S_OK; +} + +STDMETHODIMP +TSFTextStore::UnadviseMouseSink(DWORD dwCookie) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::UnadviseMouseSink(dwCookie=%ld)", this, dwCookie)); + if (dwCookie == MouseTracker::kInvalidCookie) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to " + "the cookie is invalid value", + this)); + return E_INVALIDARG; + } + // The cookie value must be an index of mMouseTrackers. + // We can use this shortcut for now. + if (static_cast(dwCookie) >= mMouseTrackers.Length()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to " + "the cookie is too large value", + this)); + return E_INVALIDARG; + } + MouseTracker& tracker = mMouseTrackers[dwCookie]; + if (!tracker.IsUsing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::UnadviseMouseSink() FAILED due to " + "the found tracker uninstalled already", + this)); + return E_INVALIDARG; + } + tracker.UnadviseSink(); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::UnadviseMouseSink(), succeeded", this)); + return S_OK; +} + +// static +nsresult TSFTextStore::OnFocusChange(bool aGotFocus, nsWindow* aFocusedWidget, + const InputContext& aContext) { + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::OnFocusChange(aGotFocus=%s, " + "aFocusedWidget=0x%p, aContext=%s), " + "sThreadMgr=0x%p, sEnabledTextStore=0x%p", + GetBoolName(aGotFocus), aFocusedWidget, + mozilla::ToString(aContext).c_str(), sThreadMgr.get(), + sEnabledTextStore.get())); + + if (NS_WARN_IF(!IsInTSFMode())) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr prevFocusedDocumentMgr; + bool hasFocus = ThinksHavingFocus(); + RefPtr oldTextStore = sEnabledTextStore.forget(); + + // If currently oldTextStore still has focus, notifies TSF of losing focus. + if (hasFocus) { + RefPtr threadMgr = sThreadMgr; + DebugOnly hr = threadMgr->AssociateFocus( + oldTextStore->mWidget->GetWindowHandle(), nullptr, + getter_AddRefs(prevFocusedDocumentMgr)); + NS_ASSERTION(SUCCEEDED(hr), "Disassociating focus failed"); + NS_ASSERTION(prevFocusedDocumentMgr == oldTextStore->mDocumentMgr, + "different documentMgr has been associated with the window"); + } + + // Even if there was a focused TextStore, we won't use it with new focused + // editor. So, release it now. + if (oldTextStore) { + oldTextStore->Destroy(); + } + + if (NS_WARN_IF(!sThreadMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::OnFocusChange() FAILED, due to " + "sThreadMgr being destroyed during calling " + "ITfThreadMgr::AssociateFocus()")); + return NS_ERROR_FAILURE; + } + if (NS_WARN_IF(sEnabledTextStore)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + (" TSFTextStore::OnFocusChange() FAILED, due to " + "nested event handling has created another focused TextStore during " + "calling ITfThreadMgr::AssociateFocus()")); + return NS_ERROR_FAILURE; + } + + // If this is a notification of blur, move focus to the dummy document + // manager. + if (!aGotFocus || !aContext.mIMEState.IsEditable()) { + RefPtr threadMgr = sThreadMgr; + RefPtr disabledDocumentMgr = sDisabledDocumentMgr; + HRESULT hr = threadMgr->SetFocus(disabledDocumentMgr); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::OnFocusChange() FAILED due to " + "ITfThreadMgr::SetFocus() failure")); + return NS_ERROR_FAILURE; + } + return NS_OK; + } + + // If an editor is getting focus, create new TextStore and set focus. + if (NS_WARN_IF(!CreateAndSetFocus(aFocusedWidget, aContext))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::OnFocusChange() FAILED due to " + "ITfThreadMgr::CreateAndSetFocus() failure")); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +// static +void TSFTextStore::EnsureToDestroyAndReleaseEnabledTextStoreIf( + RefPtr& aTextStore) { + aTextStore->Destroy(); + if (sEnabledTextStore == aTextStore) { + sEnabledTextStore = nullptr; + } + aTextStore = nullptr; +} + +// static +bool TSFTextStore::CreateAndSetFocus(nsWindow* aFocusedWidget, + const InputContext& aContext) { + // TSF might do something which causes that we need to access static methods + // of TSFTextStore. At that time, sEnabledTextStore may be necessary. + // So, we should set sEnabledTextStore directly. + RefPtr textStore = new TSFTextStore(); + sEnabledTextStore = textStore; + if (NS_WARN_IF(!textStore->Init(aFocusedWidget, aContext))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "TSFTextStore::Init() failure")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + RefPtr newDocMgr = textStore->mDocumentMgr; + if (NS_WARN_IF(!newDocMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "invalid TSFTextStore::mDocumentMgr")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (aContext.mIMEState.mEnabled == IMEEnabled::Password) { + MarkContextAsKeyboardDisabled(textStore->mContext); + RefPtr topContext; + newDocMgr->GetTop(getter_AddRefs(topContext)); + if (topContext && topContext != textStore->mContext) { + MarkContextAsKeyboardDisabled(topContext); + } + } + + HRESULT hr; + RefPtr threadMgr = sThreadMgr; + hr = threadMgr->SetFocus(newDocMgr); + + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "ITfTheadMgr::SetFocus() failure")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(!sThreadMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "sThreadMgr being destroyed during calling " + "ITfTheadMgr::SetFocus()")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(sEnabledTextStore != textStore)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "creating TextStore has lost focus during calling " + "ITfThreadMgr::SetFocus()")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + + // Use AssociateFocus() for ensuring that any native focus event + // never steal focus from our documentMgr. + RefPtr prevFocusedDocumentMgr; + hr = threadMgr->AssociateFocus(aFocusedWidget->GetWindowHandle(), newDocMgr, + getter_AddRefs(prevFocusedDocumentMgr)); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "ITfTheadMgr::AssociateFocus() failure")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(!sThreadMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "sThreadMgr being destroyed during calling " + "ITfTheadMgr::AssociateFocus()")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + if (NS_WARN_IF(sEnabledTextStore != textStore)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "creating TextStore has lost focus during calling " + "ITfTheadMgr::AssociateFocus()")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + + if (textStore->mSink) { + MOZ_LOG(gIMELog, LogLevel::Info, + (" TSFTextStore::CreateAndSetFocus(), calling " + "ITextStoreACPSink::OnLayoutChange(TS_LC_CREATE) for 0x%p...", + textStore.get())); + RefPtr sink = textStore->mSink; + sink->OnLayoutChange(TS_LC_CREATE, TEXTSTORE_DEFAULT_VIEW); + if (NS_WARN_IF(sEnabledTextStore != textStore)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CreateAndSetFocus() FAILED due to " + "creating TextStore has lost focus during calling " + "ITextStoreACPSink::OnLayoutChange(TS_LC_CREATE)")); + EnsureToDestroyAndReleaseEnabledTextStoreIf(textStore); + return false; + } + } + return true; +} + +// static +IMENotificationRequests TSFTextStore::GetIMENotificationRequests() { + if (!sEnabledTextStore || NS_WARN_IF(!sEnabledTextStore->mDocumentMgr)) { + // If there is no active text store, we don't need any notifications + // since there is no sink which needs notifications. + return IMENotificationRequests(); + } + + // Otherwise, requests all notifications since even if some of them may not + // be required by the sink of active TIP, active TIP may be changed and + // other TIPs may need all notifications. + // Note that Windows temporarily steal focus from active window if the main + // process which created the window becomes busy. In this case, we shouldn't + // commit composition since user may want to continue to compose the + // composition after becoming not busy. Therefore, we need notifications + // even during deactive. + // Be aware, we don't need to check actual focused text store. For example, + // MS-IME for Japanese handles focus messages by themselves and sets focused + // text store to nullptr when the process is being inactivated. However, + // we still need to reuse sEnabledTextStore if the process is activated and + // focused element isn't changed. Therefore, if sEnabledTextStore isn't + // nullptr, we need to keep notifying the sink even when it is not focused + // text store for the thread manager. + return IMENotificationRequests( + IMENotificationRequests::NOTIFY_TEXT_CHANGE | + IMENotificationRequests::NOTIFY_POSITION_CHANGE | + IMENotificationRequests::NOTIFY_MOUSE_BUTTON_EVENT_ON_CHAR | + IMENotificationRequests::NOTIFY_DURING_DEACTIVE); +} + +nsresult TSFTextStore::OnTextChangeInternal( + const IMENotification& aIMENotification) { + const TextChangeDataBase& textChangeData = aIMENotification.mTextChangeData; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnTextChangeInternal(aIMENotification={ " + "mMessage=0x%08X, mTextChangeData=%s }), " + "mDestroyed=%s, mSink=0x%p, mSinkMask=%s, " + "mComposition=%s", + this, aIMENotification.mMessage, + mozilla::ToString(textChangeData).c_str(), GetBoolName(mDestroyed), + mSink.get(), GetSinkMaskNameStr(mSinkMask).get(), + ToString(mComposition).c_str())); + + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // changes. + return NS_OK; + } + + mDeferNotifyingTSFUntilNextUpdate = false; + + // Different from selection change, we don't modify anything with text + // change data. Therefore, if neither TSF not TIP wants text change + // notifications, we don't need to store the changes. + if (!mSink || !(mSinkMask & TS_AS_TEXT_CHANGE)) { + return NS_OK; + } + + // Merge any text change data even if it's caused by composition. + mPendingTextChangeData.MergeWith(textChangeData); + + MaybeFlushPendingNotifications(); + + return NS_OK; +} + +void TSFTextStore::NotifyTSFOfTextChange() { + MOZ_ASSERT(!mDestroyed); + MOZ_ASSERT(!IsReadLocked()); + MOZ_ASSERT(mComposition.isNothing()); + MOZ_ASSERT(mPendingTextChangeData.IsValid()); + + // If the text changes are caused only by composition, we don't need to + // notify TSF of the text changes. + if (mPendingTextChangeData.mCausedOnlyByComposition) { + mPendingTextChangeData.Clear(); + return; + } + + // First, forget cached selection. + mSelectionForTSF.reset(); + + // For making it safer, we should check if there is a valid sink to receive + // text change notification. + if (NS_WARN_IF(!mSink) || NS_WARN_IF(!(mSinkMask & TS_AS_TEXT_CHANGE))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::NotifyTSFOfTextChange() FAILED due to " + "mSink is not ready to call ITextStoreACPSink::OnTextChange()...", + this)); + mPendingTextChangeData.Clear(); + return; + } + + if (NS_WARN_IF(!mPendingTextChangeData.IsInInt32Range())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::NotifyTSFOfTextChange() FAILED due to " + "offset is too big for calling " + "ITextStoreACPSink::OnTextChange()...", + this)); + mPendingTextChangeData.Clear(); + return; + } + + TS_TEXTCHANGE textChange; + textChange.acpStart = static_cast(mPendingTextChangeData.mStartOffset); + textChange.acpOldEnd = + static_cast(mPendingTextChangeData.mRemovedEndOffset); + textChange.acpNewEnd = + static_cast(mPendingTextChangeData.mAddedEndOffset); + mPendingTextChangeData.Clear(); + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfTextChange(), calling " + "ITextStoreACPSink::OnTextChange(0, { acpStart=%ld, acpOldEnd=%ld, " + "acpNewEnd=%ld })...", + this, textChange.acpStart, textChange.acpOldEnd, textChange.acpNewEnd)); + RefPtr sink = mSink; + sink->OnTextChange(0, &textChange); +} + +nsresult TSFTextStore::OnSelectionChangeInternal( + const IMENotification& aIMENotification) { + const SelectionChangeDataBase& selectionChangeData = + aIMENotification.mSelectionChangeData; + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnSelectionChangeInternal(" + "aIMENotification={ mSelectionChangeData=%s }), mDestroyed=%s, " + "mSink=0x%p, mSinkMask=%s, mIsRecordingActionsWithoutLock=%s, " + "mComposition=%s", + this, mozilla::ToString(selectionChangeData).c_str(), + GetBoolName(mDestroyed), mSink.get(), + GetSinkMaskNameStr(mSinkMask).get(), + GetBoolName(mIsRecordingActionsWithoutLock), + ToString(mComposition).c_str())); + + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // changes. + return NS_OK; + } + + mDeferNotifyingTSFUntilNextUpdate = false; + + // Assign the new selection change data to the pending selection change data + // because only the latest selection data is necessary. + // Note that this is necessary to update mSelectionForTSF. Therefore, even if + // neither TSF nor TIP wants selection change notifications, we need to + // store the selection information. + mPendingSelectionChangeData = Some(selectionChangeData); + + // Flush remaining pending notifications here if it's possible. + MaybeFlushPendingNotifications(); + + // If we're available, we should create native caret instead of IMEHandler + // because we may have some cache to do it. + // Note that if we have composition, we'll notified composition-updated + // later so that we don't need to create native caret in such case. + if (!IsHandlingCompositionInContent() && + IMEHandler::NeedsToCreateNativeCaret()) { + CreateNativeCaret(); + } + + return NS_OK; +} + +void TSFTextStore::NotifyTSFOfSelectionChange() { + MOZ_ASSERT(!mDestroyed); + MOZ_ASSERT(!IsReadLocked()); + MOZ_ASSERT(mComposition.isNothing()); + MOZ_ASSERT(mPendingSelectionChangeData.isSome()); + + // If selection range isn't actually changed, we don't need to notify TSF + // of this selection change. + if (mSelectionForTSF.isNothing()) { + mSelectionForTSF.emplace(*mPendingSelectionChangeData); + } else if (!mSelectionForTSF->SetSelection(*mPendingSelectionChangeData)) { + mPendingSelectionChangeData.reset(); + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::NotifyTSFOfSelectionChange(), " + "selection isn't actually changed.", + this)); + return; + } + + mPendingSelectionChangeData.reset(); + + if (!mSink || !(mSinkMask & TS_AS_SEL_CHANGE)) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfSelectionChange(), calling " + "ITextStoreACPSink::OnSelectionChange()...", + this)); + RefPtr sink = mSink; + sink->OnSelectionChange(); +} + +nsresult TSFTextStore::OnLayoutChangeInternal() { + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // changes. + return NS_OK; + } + + NS_ENSURE_TRUE(mContext, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(mSink, NS_ERROR_FAILURE); + + mDeferNotifyingTSFUntilNextUpdate = false; + + nsresult rv = NS_OK; + + // We need to notify TSF of layout change even if the document is locked. + // So, don't use MaybeFlushPendingNotifications() for flushing pending + // layout change. + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::OnLayoutChangeInternal(), calling " + "NotifyTSFOfLayoutChange()...", + this)); + if (NS_WARN_IF(!NotifyTSFOfLayoutChange())) { + rv = NS_ERROR_FAILURE; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnLayoutChangeInternal(), calling " + "MaybeFlushPendingNotifications()...", + this)); + MaybeFlushPendingNotifications(); + + return rv; +} + +bool TSFTextStore::NotifyTSFOfLayoutChange() { + MOZ_ASSERT(!mDestroyed); + + // If we're waiting a query of layout information from TIP, it means that + // we've returned TS_E_NOLAYOUT error. + bool returnedNoLayoutError = mHasReturnedNoLayoutError || mWaitingQueryLayout; + + // If we returned TS_E_NOLAYOUT, TIP should query the computed layout again. + mWaitingQueryLayout = returnedNoLayoutError; + + // For avoiding to call this method again at unlocking the document during + // calls of OnLayoutChange(), reset mHasReturnedNoLayoutError. + mHasReturnedNoLayoutError = false; + + // Now, layout has been computed. We should notify mContentForTSF for + // making GetTextExt() and GetACPFromPoint() not return TS_E_NOLAYOUT. + if (mContentForTSF.isSome()) { + mContentForTSF->OnLayoutChanged(); + } + + if (IMEHandler::NeedsToCreateNativeCaret()) { + // If we're available, we should create native caret instead of IMEHandler + // because we may have some cache to do it. + CreateNativeCaret(); + } else { + // Now, the caret position is different from ours. Destroy the native caret + // if we've create it only for GetTextExt(). + IMEHandler::MaybeDestroyNativeCaret(); + } + + // This method should return true if either way succeeds. + bool ret = true; + + if (mSink) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "calling ITextStoreACPSink::OnLayoutChange()...", + this)); + RefPtr sink = mSink; + HRESULT hr = sink->OnLayoutChange(TS_LC_CHANGE, TEXTSTORE_DEFAULT_VIEW); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "called ITextStoreACPSink::OnLayoutChange()", + this)); + ret = SUCCEEDED(hr); + } + + // The layout change caused by composition string change should cause + // calling ITfContextOwnerServices::OnLayoutChange() too. + if (returnedNoLayoutError && mContext) { + RefPtr service; + mContext->QueryInterface(IID_ITfContextOwnerServices, + getter_AddRefs(service)); + if (service) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "calling ITfContextOwnerServices::OnLayoutChange()...", + this)); + HRESULT hr = service->OnLayoutChange(); + ret = ret && SUCCEEDED(hr); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "called ITfContextOwnerServices::OnLayoutChange()", + this)); + } + } + + if (!mWidget || mWidget->Destroyed()) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "the widget is destroyed during calling OnLayoutChange()", + this)); + return ret; + } + + if (mDestroyed) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "the TSFTextStore instance is destroyed during calling " + "OnLayoutChange()", + this)); + return ret; + } + + // If we returned TS_E_NOLAYOUT again, we need another call of + // OnLayoutChange() later. So, let's wait a query from TIP. + if (mHasReturnedNoLayoutError) { + mWaitingQueryLayout = true; + } + + if (!mWaitingQueryLayout) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "succeeded notifying TIP of our layout change", + this)); + return ret; + } + + // If we believe that TIP needs to retry to retrieve our layout information + // later, we should call it with ::PostMessage() hack. + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChange(), " + "posing MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE for calling " + "OnLayoutChange() again...", + this)); + ::PostMessage(mWidget->GetWindowHandle(), MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE, + reinterpret_cast(this), 0); + + return true; +} + +void TSFTextStore::NotifyTSFOfLayoutChangeAgain() { + // Don't notify TSF of layout change after destroyed. + if (mDestroyed) { + mWaitingQueryLayout = false; + return; + } + + // Before preforming this method, TIP has accessed our layout information by + // itself. In such case, we don't need to call OnLayoutChange() anymore. + if (!mWaitingQueryLayout) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), " + "calling NotifyTSFOfLayoutChange()...", + this)); + NotifyTSFOfLayoutChange(); + + // If TIP didn't retrieved our layout information during a call of + // NotifyTSFOfLayoutChange(), it means that the TIP already gave up to + // retry to retrieve layout information or doesn't necessary it anymore. + // But don't forget that the call may have caused returning TS_E_NOLAYOUT + // error again. In such case we still need to call OnLayoutChange() later. + if (!mHasReturnedNoLayoutError && mWaitingQueryLayout) { + mWaitingQueryLayout = false; + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), " + "called NotifyTSFOfLayoutChange() but TIP didn't retry to " + "retrieve the layout information", + this)); + } else { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::NotifyTSFOfLayoutChangeAgain(), " + "called NotifyTSFOfLayoutChange()", + this)); + } +} + +nsresult TSFTextStore::OnUpdateCompositionInternal() { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnUpdateCompositionInternal(), " + "mDestroyed=%s, mDeferNotifyingTSFUntilNextUpdate=%s", + this, GetBoolName(mDestroyed), + GetBoolName(mDeferNotifyingTSFUntilNextUpdate))); + + // There are nothing to do after destroyed. + if (mDestroyed) { + return NS_OK; + } + + // Update cached data now because all pending events have been handled now. + if (mContentForTSF.isSome()) { + mContentForTSF->OnCompositionEventsHandled(); + } + + // If composition is completely finished both in TSF/TIP and the focused + // editor which may be in a remote process, we can clear the cache and don't + // have it until starting next composition. + if (mComposition.isNothing() && !IsHandlingCompositionInContent()) { + mDeferClearingContentForTSF = false; + } + mDeferNotifyingTSFUntilNextUpdate = false; + MaybeFlushPendingNotifications(); + + // If we're available, we should create native caret instead of IMEHandler + // because we may have some cache to do it. + if (IMEHandler::NeedsToCreateNativeCaret()) { + CreateNativeCaret(); + } + + return NS_OK; +} + +nsresult TSFTextStore::OnMouseButtonEventInternal( + const IMENotification& aIMENotification) { + if (mDestroyed) { + // If this instance is already destroyed, we shouldn't notify TSF of any + // events. + return NS_OK; + } + + if (mMouseTrackers.IsEmpty()) { + return NS_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::OnMouseButtonEventInternal(" + "aIMENotification={ mEventMessage=%s, mOffset=%u, mCursorPos=%s, " + "mCharRect=%s, mButton=%s, mButtons=%s, mModifiers=%s })", + this, ToChar(aIMENotification.mMouseButtonEventData.mEventMessage), + aIMENotification.mMouseButtonEventData.mOffset, + ToString(aIMENotification.mMouseButtonEventData.mCursorPos).c_str(), + ToString(aIMENotification.mMouseButtonEventData.mCharRect).c_str(), + GetMouseButtonName(aIMENotification.mMouseButtonEventData.mButton), + GetMouseButtonsName(aIMENotification.mMouseButtonEventData.mButtons) + .get(), + GetModifiersName(aIMENotification.mMouseButtonEventData.mModifiers) + .get())); + + uint32_t offset = aIMENotification.mMouseButtonEventData.mOffset; + if (offset > static_cast(LONG_MAX)) { + return NS_OK; + } + LayoutDeviceIntRect charRect = + aIMENotification.mMouseButtonEventData.mCharRect; + LayoutDeviceIntPoint cursorPos = + aIMENotification.mMouseButtonEventData.mCursorPos; + ULONG quadrant = 1; + if (charRect.Width() > 0) { + int32_t cursorXInChar = cursorPos.x - charRect.X(); + quadrant = cursorXInChar * 4 / charRect.Width(); + quadrant = (quadrant + 2) % 4; + } + ULONG edge = quadrant < 2 ? offset + 1 : offset; + DWORD buttonStatus = 0; + bool isMouseUp = + aIMENotification.mMouseButtonEventData.mEventMessage == eMouseUp; + if (!isMouseUp) { + switch (aIMENotification.mMouseButtonEventData.mButton) { + case MouseButton::ePrimary: + buttonStatus = MK_LBUTTON; + break; + case MouseButton::eMiddle: + buttonStatus = MK_MBUTTON; + break; + case MouseButton::eSecondary: + buttonStatus = MK_RBUTTON; + break; + } + } + if (aIMENotification.mMouseButtonEventData.mModifiers & MODIFIER_CONTROL) { + buttonStatus |= MK_CONTROL; + } + if (aIMENotification.mMouseButtonEventData.mModifiers & MODIFIER_SHIFT) { + buttonStatus |= MK_SHIFT; + } + for (size_t i = 0; i < mMouseTrackers.Length(); i++) { + MouseTracker& tracker = mMouseTrackers[i]; + if (!tracker.IsUsing() || tracker.Range().isNothing() || + !tracker.Range()->IsOffsetInRange(offset)) { + continue; + } + if (tracker.OnMouseButtonEvent(edge - tracker.Range()->StartOffset(), + quadrant, buttonStatus)) { + return NS_SUCCESS_EVENT_CONSUMED; + } + } + return NS_OK; +} + +void TSFTextStore::CreateNativeCaret() { + MOZ_ASSERT(!IMEHandler::IsA11yHandlingNativeCaret()); + + IMEHandler::MaybeDestroyNativeCaret(); + + // Don't create native caret after destroyed. + if (mDestroyed) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CreateNativeCaret(), mComposition=%s", this, + ToString(mComposition).c_str())); + + Maybe& selectionForTSF = SelectionForTSF(); + if (MOZ_UNLIKELY(selectionForTSF.isNothing())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "SelectionForTSF() failure", + this)); + return; + } + if (!selectionForTSF->HasRange() && mComposition.isNothing()) { + // If there is no selection range nor composition, then, we don't have a + // good position to show windows of TIP... + // XXX It seems that storing last caret rect and using it in this case might + // be better? + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p TSFTextStore::CreateNativeCaret() couludn't create native " + "caret due to no selection range", + this)); + return; + } + + WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, mWidget); + mWidget->InitEvent(queryCaretRectEvent); + + WidgetQueryContentEvent::Options options; + // XXX If this is called without composition and the selection isn't + // collapsed, is it OK? + int64_t caretOffset = selectionForTSF->HasRange() + ? selectionForTSF->MaxOffset() + : mComposition->StartOffset(); + if (mComposition.isSome()) { + // If there is a composition, use the relative query for deciding caret + // position because composition might be different place from that + // TSFTextStore assumes. + options.mRelativeToInsertionPoint = true; + caretOffset -= mComposition->StartOffset(); + } else if (!CanAccessActualContentDirectly()) { + // If TSF/TIP cannot access actual content directly, there may be pending + // text and/or selection changes which have not been notified TSF yet. + // Therefore, we should use the relative query from start of selection where + // TSFTextStore assumes since TSF/TIP computes the offset from our cached + // selection. + options.mRelativeToInsertionPoint = true; + caretOffset -= selectionForTSF->StartOffset(); + } + queryCaretRectEvent.InitForQueryCaretRect(caretOffset, options); + + DispatchEvent(queryCaretRectEvent); + if (NS_WARN_IF(queryCaretRectEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "eQueryCaretRect failure (offset=%lld)", + this, caretOffset)); + return; + } + + if (!IMEHandler::CreateNativeCaret(static_cast(mWidget.get()), + queryCaretRectEvent.mReply->mRect)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::CreateNativeCaret() FAILED due to " + "IMEHandler::CreateNativeCaret() failure", + this)); + return; + } +} + +void TSFTextStore::CommitCompositionInternal(bool aDiscard) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(aDiscard=%s), " + "mSink=0x%p, mContext=0x%p, mComposition=%s", + this, GetBoolName(aDiscard), mSink.get(), mContext.get(), + ToString(mComposition).c_str())); + + // If the document is locked, TSF will fail to commit composition since + // TSF needs another document lock. So, let's put off the request. + // Note that TextComposition will commit composition in the focused editor + // with the latest composition string for web apps and waits asynchronous + // committing messages. Therefore, we can and need to perform this + // asynchronously. + if (IsReadLocked()) { + if (mDeferCommittingComposition || mDeferCancellingComposition) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(), " + "does nothing because already called and waiting unlock...", + this)); + return; + } + if (aDiscard) { + mDeferCancellingComposition = true; + } else { + mDeferCommittingComposition = true; + } + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(), " + "putting off to request to %s composition after unlocking the " + "document", + this, aDiscard ? "cancel" : "commit")); + return; + } + + if (mComposition.isSome() && aDiscard) { + LONG endOffset = mComposition->EndOffset(); + mComposition->SetData(EmptyString()); + // Note that don't notify TSF of text change after this is destroyed. + if (mSink && !mDestroyed) { + TS_TEXTCHANGE textChange; + textChange.acpStart = mComposition->StartOffset(); + textChange.acpOldEnd = endOffset; + textChange.acpNewEnd = mComposition->StartOffset(); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p TSFTextStore::CommitCompositionInternal(), calling" + "mSink->OnTextChange(0, { acpStart=%ld, acpOldEnd=%ld, " + "acpNewEnd=%ld })...", + this, textChange.acpStart, textChange.acpOldEnd, + textChange.acpNewEnd)); + RefPtr sink = mSink; + sink->OnTextChange(0, &textChange); + } + } + // Terminate two contexts, the base context (mContext) and the top + // if the top context is not the same as the base context + RefPtr context = mContext; + do { + if (context) { + RefPtr services; + context->QueryInterface(IID_ITfContextOwnerCompositionServices, + getter_AddRefs(services)); + if (services) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::CommitCompositionInternal(), " + "requesting TerminateComposition() for the context 0x%p...", + this, context.get())); + services->TerminateComposition(nullptr); + } + } + if (context != mContext) break; + if (mDocumentMgr) mDocumentMgr->GetTop(getter_AddRefs(context)); + } while (context != mContext); +} + +static bool GetCompartment(IUnknown* pUnk, const GUID& aID, + ITfCompartment** aCompartment) { + if (!pUnk) return false; + + RefPtr compMgr; + pUnk->QueryInterface(IID_ITfCompartmentMgr, getter_AddRefs(compMgr)); + if (!compMgr) return false; + + return SUCCEEDED(compMgr->GetCompartment(aID, aCompartment)) && + (*aCompartment) != nullptr; +} + +// static +void TSFTextStore::SetIMEOpenState(bool aState) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("TSFTextStore::SetIMEOpenState(aState=%s)", GetBoolName(aState))); + + if (!sThreadMgr) { + return; + } + + RefPtr comp = GetCompartmentForOpenClose(); + if (NS_WARN_IF(!comp)) { + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::SetIMEOpenState() FAILED due to" + "no compartment available")); + return; + } + + VARIANT variant; + variant.vt = VT_I4; + variant.lVal = aState; + HRESULT hr = comp->SetValue(sClientId, &variant); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::SetIMEOpenState() FAILED due to " + "ITfCompartment::SetValue() failure, hr=0x%08lX", + hr)); + return; + } + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::SetIMEOpenState(), setting " + "0x%04lX to GUID_COMPARTMENT_KEYBOARD_OPENCLOSE...", + variant.lVal)); +} + +// static +bool TSFTextStore::GetIMEOpenState() { + if (!sThreadMgr) { + return false; + } + + RefPtr comp = GetCompartmentForOpenClose(); + if (NS_WARN_IF(!comp)) { + return false; + } + + VARIANT variant; + ::VariantInit(&variant); + HRESULT hr = comp->GetValue(&variant); + if (NS_WARN_IF(FAILED(hr))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetIMEOpenState() FAILED due to " + "ITfCompartment::GetValue() failure, hr=0x%08lX", + hr)); + return false; + } + // Until IME is open in this process, the result may be empty. + if (variant.vt == VT_EMPTY) { + return false; + } + if (NS_WARN_IF(variant.vt != VT_I4)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetIMEOpenState() FAILED due to " + "invalid result of ITfCompartment::GetValue()")); + ::VariantClear(&variant); + return false; + } + + return variant.lVal != 0; +} + +// static +void TSFTextStore::SetInputContext(nsWindow* aWidget, + const InputContext& aContext, + const InputContextAction& aAction) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("TSFTextStore::SetInputContext(aWidget=%p, " + "aContext=%s, aAction.mFocusChange=%s), " + "sEnabledTextStore(0x%p)={ mWidget=0x%p }, ThinksHavingFocus()=%s", + aWidget, mozilla::ToString(aContext).c_str(), + GetFocusChangeName(aAction.mFocusChange), sEnabledTextStore.get(), + sEnabledTextStore ? sEnabledTextStore->mWidget.get() : nullptr, + GetBoolName(ThinksHavingFocus()))); + + switch (aAction.mFocusChange) { + case InputContextAction::WIDGET_CREATED: + // If this is called when the widget is created, there is nothing to do. + return; + case InputContextAction::FOCUS_NOT_CHANGED: + case InputContextAction::MENU_LOST_PSEUDO_FOCUS: + if (NS_WARN_IF(!IsInTSFMode())) { + return; + } + // In these cases, `NOTIFY_IME_OF_FOCUS` won't be sent. Therefore, + // we need to reset text store for new state right now. + break; + default: + NS_WARNING_ASSERTION(IsInTSFMode(), + "Why is this called when TSF is disabled?"); + if (sEnabledTextStore) { + RefPtr textStore(sEnabledTextStore); + textStore->mInPrivateBrowsing = aContext.mInPrivateBrowsing; + textStore->SetInputScope(aContext.mHTMLInputType, + aContext.mHTMLInputMode); + if (aContext.mURI) { + nsAutoCString spec; + if (NS_SUCCEEDED(aContext.mURI->GetSpec(spec))) { + CopyUTF8toUTF16(spec, textStore->mDocumentURL); + } else { + textStore->mDocumentURL.Truncate(); + } + } else { + textStore->mDocumentURL.Truncate(); + } + } + return; + } + + // If focus isn't actually changed but the enabled state is changed, + // emulate the focus move. + if (!ThinksHavingFocus() && aContext.mIMEState.IsEditable()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::SetInputContent() emulates focus for IME " + "state change")); + OnFocusChange(true, aWidget, aContext); + } else if (ThinksHavingFocus() && !aContext.mIMEState.IsEditable()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + (" TSFTextStore::SetInputContent() emulates blur for IME " + "state change")); + OnFocusChange(false, aWidget, aContext); + } +} + +// static +void TSFTextStore::MarkContextAsKeyboardDisabled(ITfContext* aContext) { + VARIANT variant_int4_value1; + variant_int4_value1.vt = VT_I4; + variant_int4_value1.lVal = 1; + + RefPtr comp; + if (!GetCompartment(aContext, GUID_COMPARTMENT_KEYBOARD_DISABLED, + getter_AddRefs(comp))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::MarkContextAsKeyboardDisabled() failed" + "aContext=0x%p...", + aContext)); + return; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("TSFTextStore::MarkContextAsKeyboardDisabled(), setting " + "to disable context 0x%p...", + aContext)); + comp->SetValue(sClientId, &variant_int4_value1); +} + +// static +void TSFTextStore::MarkContextAsEmpty(ITfContext* aContext) { + VARIANT variant_int4_value1; + variant_int4_value1.vt = VT_I4; + variant_int4_value1.lVal = 1; + + RefPtr comp; + if (!GetCompartment(aContext, GUID_COMPARTMENT_EMPTYCONTEXT, + getter_AddRefs(comp))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::MarkContextAsEmpty() failed" + "aContext=0x%p...", + aContext)); + return; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("TSFTextStore::MarkContextAsEmpty(), setting " + "to mark empty context 0x%p...", + aContext)); + comp->SetValue(sClientId, &variant_int4_value1); +} + +// static +void TSFTextStore::Initialize() { + MOZ_LOG(gIMELog, LogLevel::Info, ("TSFTextStore::Initialize() is called...")); + + if (sThreadMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED due to already initialized")); + return; + } + + const bool enableTsf = StaticPrefs::intl_tsf_enabled_AtStartup(); + MOZ_LOG(gIMELog, LogLevel::Info, + (" TSFTextStore::Initialize(), TSF is %s", + enableTsf ? "enabled" : "disabled")); + if (!enableTsf) { + return; + } + + RefPtr threadMgr; + HRESULT hr = + ::CoCreateInstance(CLSID_TF_ThreadMgr, nullptr, CLSCTX_INPROC_SERVER, + IID_ITfThreadMgr, getter_AddRefs(threadMgr)); + if (FAILED(hr) || !threadMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to " + "create the thread manager, hr=0x%08lX", + hr)); + return; + } + + hr = threadMgr->Activate(&sClientId); + if (FAILED(hr)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to activate, hr=0x%08lX", hr)); + return; + } + + RefPtr disabledDocumentMgr; + hr = threadMgr->CreateDocumentMgr(getter_AddRefs(disabledDocumentMgr)); + if (FAILED(hr) || !disabledDocumentMgr) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to create " + "a document manager for disabled mode, hr=0x%08lX", + hr)); + return; + } + + RefPtr disabledContext; + DWORD editCookie = 0; + hr = disabledDocumentMgr->CreateContext( + sClientId, 0, nullptr, getter_AddRefs(disabledContext), &editCookie); + if (FAILED(hr) || !disabledContext) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::Initialize() FAILED to create " + "a context for disabled mode, hr=0x%08lX", + hr)); + return; + } + + MarkContextAsKeyboardDisabled(disabledContext); + MarkContextAsEmpty(disabledContext); + + sThreadMgr = threadMgr; + sDisabledDocumentMgr = disabledDocumentMgr; + sDisabledContext = disabledContext; + + MOZ_LOG(gIMELog, LogLevel::Info, + (" TSFTextStore::Initialize(), sThreadMgr=0x%p, " + "sClientId=0x%08lX, sDisabledDocumentMgr=0x%p, sDisabledContext=%p", + sThreadMgr.get(), sClientId, sDisabledDocumentMgr.get(), + sDisabledContext.get())); +} + +// static +already_AddRefed TSFTextStore::GetThreadMgr() { + RefPtr threadMgr = sThreadMgr; + return threadMgr.forget(); +} + +// static +already_AddRefed TSFTextStore::GetMessagePump() { + static bool sInitialized = false; + if (!sThreadMgr) { + return nullptr; + } + if (sMessagePump) { + RefPtr messagePump = sMessagePump; + return messagePump.forget(); + } + // If it tried to retrieve ITfMessagePump from sThreadMgr but it failed, + // we shouldn't retry it at every message due to performance reason. + // Although this shouldn't occur actually. + if (sInitialized) { + return nullptr; + } + sInitialized = true; + + RefPtr messagePump; + HRESULT hr = sThreadMgr->QueryInterface(IID_ITfMessagePump, + getter_AddRefs(messagePump)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!messagePump)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetMessagePump() FAILED to " + "QI message pump from the thread manager, hr=0x%08lX", + hr)); + return nullptr; + } + sMessagePump = messagePump; + return messagePump.forget(); +} + +// static +already_AddRefed +TSFTextStore::GetDisplayAttributeMgr() { + RefPtr displayAttributeMgr; + if (sDisplayAttrMgr) { + displayAttributeMgr = sDisplayAttrMgr; + return displayAttributeMgr.forget(); + } + + HRESULT hr = ::CoCreateInstance( + CLSID_TF_DisplayAttributeMgr, nullptr, CLSCTX_INPROC_SERVER, + IID_ITfDisplayAttributeMgr, getter_AddRefs(displayAttributeMgr)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!displayAttributeMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetDisplayAttributeMgr() FAILED to create " + "a display attribute manager instance, hr=0x%08lX", + hr)); + return nullptr; + } + sDisplayAttrMgr = displayAttributeMgr; + return displayAttributeMgr.forget(); +} + +// static +already_AddRefed TSFTextStore::GetCategoryMgr() { + RefPtr categoryMgr; + if (sCategoryMgr) { + categoryMgr = sCategoryMgr; + return categoryMgr.forget(); + } + HRESULT hr = + ::CoCreateInstance(CLSID_TF_CategoryMgr, nullptr, CLSCTX_INPROC_SERVER, + IID_ITfCategoryMgr, getter_AddRefs(categoryMgr)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!categoryMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetCategoryMgr() FAILED to create " + "a category manager instance, hr=0x%08lX", + hr)); + return nullptr; + } + sCategoryMgr = categoryMgr; + return categoryMgr.forget(); +} + +// static +already_AddRefed TSFTextStore::GetCompartmentForOpenClose() { + if (sCompartmentForOpenClose) { + RefPtr compartment = sCompartmentForOpenClose; + return compartment.forget(); + } + + if (!sThreadMgr) { + return nullptr; + } + + RefPtr compartmentMgr; + HRESULT hr = sThreadMgr->QueryInterface(IID_ITfCompartmentMgr, + getter_AddRefs(compartmentMgr)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!compartmentMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetCompartmentForOpenClose() FAILED due to" + "sThreadMgr not having ITfCompartmentMgr, hr=0x%08lX", + hr)); + return nullptr; + } + + RefPtr compartment; + hr = compartmentMgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_OPENCLOSE, + getter_AddRefs(compartment)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!compartment)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetCompartmentForOpenClose() FAILED due to" + "ITfCompartmentMgr::GetCompartment() failuere, hr=0x%08lX", + hr)); + return nullptr; + } + + sCompartmentForOpenClose = compartment; + return compartment.forget(); +} + +// static +already_AddRefed +TSFTextStore::GetInputProcessorProfiles() { + RefPtr inputProcessorProfiles; + if (sInputProcessorProfiles) { + inputProcessorProfiles = sInputProcessorProfiles; + return inputProcessorProfiles.forget(); + } + // XXX MSDN documents that ITfInputProcessorProfiles is available only on + // desktop apps. However, there is no known way to obtain + // ITfInputProcessorProfileMgr instance without ITfInputProcessorProfiles + // instance. + HRESULT hr = ::CoCreateInstance( + CLSID_TF_InputProcessorProfiles, nullptr, CLSCTX_INPROC_SERVER, + IID_ITfInputProcessorProfiles, getter_AddRefs(inputProcessorProfiles)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!inputProcessorProfiles)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::GetInputProcessorProfiles() FAILED to create input " + "processor profiles, hr=0x%08lX", + hr)); + return nullptr; + } + sInputProcessorProfiles = inputProcessorProfiles; + return inputProcessorProfiles.forget(); +} + +// static +void TSFTextStore::Terminate() { + MOZ_LOG(gIMELog, LogLevel::Info, ("TSFTextStore::Terminate()")); + + TSFStaticSink::Shutdown(); + + sDisplayAttrMgr = nullptr; + sCategoryMgr = nullptr; + sEnabledTextStore = nullptr; + sDisabledDocumentMgr = nullptr; + sDisabledContext = nullptr; + sCompartmentForOpenClose = nullptr; + sInputProcessorProfiles = nullptr; + sClientId = 0; + if (sThreadMgr) { + sThreadMgr->Deactivate(); + sThreadMgr = nullptr; + sMessagePump = nullptr; + sKeystrokeMgr = nullptr; + } +} + +// static +bool TSFTextStore::ProcessRawKeyMessage(const MSG& aMsg) { + if (!sThreadMgr) { + return false; // not in TSF mode + } + static bool sInitialized = false; + if (!sKeystrokeMgr) { + // If it tried to retrieve ITfKeystrokeMgr from sThreadMgr but it failed, + // we shouldn't retry it at every keydown nor keyup due to performance + // reason. Although this shouldn't occur actually. + if (sInitialized) { + return false; + } + sInitialized = true; + RefPtr keystrokeMgr; + HRESULT hr = sThreadMgr->QueryInterface(IID_ITfKeystrokeMgr, + getter_AddRefs(keystrokeMgr)); + if (NS_WARN_IF(FAILED(hr)) || NS_WARN_IF(!keystrokeMgr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::ProcessRawKeyMessage() FAILED to " + "QI keystroke manager from the thread manager, hr=0x%08lX", + hr)); + return false; + } + sKeystrokeMgr = keystrokeMgr.forget(); + } + + if (aMsg.message == WM_KEYDOWN) { + RefPtr textStore(sEnabledTextStore); + if (textStore) { + textStore->OnStartToHandleKeyMessage(); + if (NS_WARN_IF(textStore != sEnabledTextStore)) { + // Let's handle the key message with new focused TSFTextStore. + textStore = sEnabledTextStore; + } + } + AutoRestore savePreviousKeyMsg(sHandlingKeyMsg); + AutoRestore saveKeyEventDispatched(sIsKeyboardEventDispatched); + sHandlingKeyMsg = &aMsg; + sIsKeyboardEventDispatched = false; + BOOL eaten; + RefPtr keystrokeMgr = sKeystrokeMgr; + HRESULT hr = keystrokeMgr->TestKeyDown(aMsg.wParam, aMsg.lParam, &eaten); + if (FAILED(hr) || !sKeystrokeMgr || !eaten) { + return false; + } + hr = keystrokeMgr->KeyDown(aMsg.wParam, aMsg.lParam, &eaten); + if (textStore) { + textStore->OnEndHandlingKeyMessage(!!eaten); + } + return SUCCEEDED(hr) && + (eaten || !sKeystrokeMgr || sIsKeyboardEventDispatched); + } + if (aMsg.message == WM_KEYUP) { + RefPtr textStore(sEnabledTextStore); + if (textStore) { + textStore->OnStartToHandleKeyMessage(); + if (NS_WARN_IF(textStore != sEnabledTextStore)) { + // Let's handle the key message with new focused TSFTextStore. + textStore = sEnabledTextStore; + } + } + AutoRestore savePreviousKeyMsg(sHandlingKeyMsg); + AutoRestore saveKeyEventDispatched(sIsKeyboardEventDispatched); + sHandlingKeyMsg = &aMsg; + sIsKeyboardEventDispatched = false; + BOOL eaten; + RefPtr keystrokeMgr = sKeystrokeMgr; + HRESULT hr = keystrokeMgr->TestKeyUp(aMsg.wParam, aMsg.lParam, &eaten); + if (FAILED(hr) || !sKeystrokeMgr || !eaten) { + return false; + } + hr = keystrokeMgr->KeyUp(aMsg.wParam, aMsg.lParam, &eaten); + if (textStore) { + textStore->OnEndHandlingKeyMessage(!!eaten); + } + return SUCCEEDED(hr) && + (eaten || !sKeystrokeMgr || sIsKeyboardEventDispatched); + } + return false; +} + +// static +void TSFTextStore::ProcessMessage(nsWindow* aWindow, UINT aMessage, + WPARAM& aWParam, LPARAM& aLParam, + MSGResult& aResult) { + switch (aMessage) { + case WM_IME_SETCONTEXT: + // If a windowless plugin had focus and IME was handled on it, composition + // window was set the position. After that, even in TSF mode, WinXP keeps + // to use composition window at the position if the active IME is not + // aware TSF. For avoiding this issue, we need to hide the composition + // window here. + if (aWParam) { + aLParam &= ~ISC_SHOWUICOMPOSITIONWINDOW; + } + break; + case WM_ENTERIDLE: + // When an modal dialog such as a file picker is open, composition + // should be committed because IME might be used on it. + if (!IsComposingOn(aWindow)) { + break; + } + CommitComposition(false); + break; + case MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE: { + TSFTextStore* maybeTextStore = reinterpret_cast(aWParam); + if (maybeTextStore == sEnabledTextStore) { + RefPtr textStore(maybeTextStore); + textStore->NotifyTSFOfLayoutChangeAgain(); + } + break; + } + } +} + +// static +bool TSFTextStore::IsIMM_IMEActive() { + return TSFStaticSink::IsIMM_IMEActive(); +} + +// static +bool TSFTextStore::IsMSJapaneseIMEActive() { + return TSFStaticSink::IsMSJapaneseIMEActive(); +} + +// static +bool TSFTextStore::IsGoogleJapaneseInputActive() { + return TSFStaticSink::IsGoogleJapaneseInputActive(); +} + +// static +bool TSFTextStore::IsATOKActive() { return TSFStaticSink::IsATOKActive(); } + +/****************************************************************************** + * TSFTextStore::Content + *****************************************************************************/ + +const nsDependentSubstring TSFTextStore::Content::GetSelectedText() const { + if (NS_WARN_IF(mSelection.isNothing())) { + return nsDependentSubstring(); + } + return GetSubstring(static_cast(mSelection->StartOffset()), + static_cast(mSelection->Length())); +} + +const nsDependentSubstring TSFTextStore::Content::GetSubstring( + uint32_t aStart, uint32_t aLength) const { + return nsDependentSubstring(mText, aStart, aLength); +} + +void TSFTextStore::Content::ReplaceSelectedTextWith(const nsAString& aString) { + if (NS_WARN_IF(mSelection.isNothing())) { + return; + } + ReplaceTextWith(mSelection->StartOffset(), mSelection->Length(), aString); +} + +inline uint32_t FirstDifferentCharOffset(const nsAString& aStr1, + const nsAString& aStr2) { + MOZ_ASSERT(aStr1 != aStr2); + uint32_t i = 0; + uint32_t minLength = std::min(aStr1.Length(), aStr2.Length()); + for (; i < minLength && aStr1[i] == aStr2[i]; i++) { + /* nothing to do */ + } + return i; +} + +void TSFTextStore::Content::ReplaceTextWith(LONG aStart, LONG aLength, + const nsAString& aReplaceString) { + MOZ_ASSERT(aStart >= 0); + MOZ_ASSERT(aLength >= 0); + const nsDependentSubstring replacedString = GetSubstring( + static_cast(aStart), static_cast(aLength)); + if (aReplaceString != replacedString) { + uint32_t firstDifferentOffset = mMinModifiedOffset.valueOr(UINT32_MAX); + if (mComposition.isSome()) { + // Emulate text insertion during compositions, because during a + // composition, editor expects the whole composition string to + // be sent in eCompositionChange, not just the inserted part. + // The actual eCompositionChange will be sent in SetSelection + // or OnUpdateComposition. + MOZ_ASSERT(aStart >= mComposition->StartOffset()); + MOZ_ASSERT(aStart + aLength <= mComposition->EndOffset()); + mComposition->ReplaceData( + static_cast(aStart - mComposition->StartOffset()), + static_cast(aLength), aReplaceString); + // TIP may set composition string twice or more times during a document + // lock. Therefore, we should compute the first difference offset with + // mLastComposition. + if (mLastComposition.isNothing()) { + firstDifferentOffset = mComposition->StartOffset(); + } else if (mComposition->DataRef() != mLastComposition->DataRef()) { + firstDifferentOffset = + mComposition->StartOffset() + + FirstDifferentCharOffset(mComposition->DataRef(), + mLastComposition->DataRef()); + // The previous change to the composition string is canceled. + if (mMinModifiedOffset.isSome() && + mMinModifiedOffset.value() >= + static_cast(mComposition->StartOffset()) && + mMinModifiedOffset.value() < firstDifferentOffset) { + mMinModifiedOffset = Some(firstDifferentOffset); + } + } else if (mMinModifiedOffset.isSome() && + mMinModifiedOffset.value() < static_cast(LONG_MAX) && + mComposition->IsOffsetInRange( + static_cast(mMinModifiedOffset.value()))) { + // The previous change to the composition string is canceled. + firstDifferentOffset = mComposition->EndOffset(); + mMinModifiedOffset = Some(firstDifferentOffset); + } + mLatestCompositionRange = Some(mComposition->CreateStartAndEndOffsets()); + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::Content::ReplaceTextWith(aStart=%ld, " + "aLength=%ld, aReplaceString=\"%s\"), mComposition=%s, " + "mLastComposition=%s, mMinModifiedOffset=%s, " + "firstDifferentOffset=%u", + this, aStart, aLength, GetEscapedUTF8String(aReplaceString).get(), + ToString(mComposition).c_str(), ToString(mLastComposition).c_str(), + ToString(mMinModifiedOffset).c_str(), firstDifferentOffset)); + } else { + firstDifferentOffset = + static_cast(aStart) + + FirstDifferentCharOffset(aReplaceString, replacedString); + } + mMinModifiedOffset = + mMinModifiedOffset.isNothing() + ? Some(firstDifferentOffset) + : Some(std::min(mMinModifiedOffset.value(), firstDifferentOffset)); + mText.Replace(static_cast(aStart), static_cast(aLength), + aReplaceString); + } + // Selection should be collapsed at the end of the inserted string. + mSelection = Some(TSFTextStore::Selection(static_cast(aStart) + + aReplaceString.Length())); +} + +void TSFTextStore::Content::StartComposition( + ITfCompositionView* aCompositionView, const PendingAction& aCompStart, + bool aPreserveSelection) { + MOZ_ASSERT(aCompositionView); + MOZ_ASSERT(mComposition.isNothing()); + MOZ_ASSERT(aCompStart.mType == PendingAction::Type::eCompositionStart); + + mComposition.reset(); // Avoid new crash in the beta and nightly channels. + mComposition.emplace( + aCompositionView, aCompStart.mSelectionStart, + GetSubstring(static_cast(aCompStart.mSelectionStart), + static_cast(aCompStart.mSelectionLength))); + mLatestCompositionRange = Some(mComposition->CreateStartAndEndOffsets()); + if (!aPreserveSelection) { + // XXX Do we need to set a new writing-mode here when setting a new + // selection? Currently, we just preserve the existing value. + WritingMode writingMode = + mSelection.isNothing() ? WritingMode() : mSelection->WritingModeRef(); + mSelection = Some(TSFTextStore::Selection(mComposition->StartOffset(), + mComposition->Length(), false, + writingMode)); + } +} + +void TSFTextStore::Content::RestoreCommittedComposition( + ITfCompositionView* aCompositionView, + const PendingAction& aCanceledCompositionEnd) { + MOZ_ASSERT(aCompositionView); + MOZ_ASSERT(mComposition.isNothing()); + MOZ_ASSERT(aCanceledCompositionEnd.mType == + PendingAction::Type::eCompositionEnd); + MOZ_ASSERT( + GetSubstring( + static_cast(aCanceledCompositionEnd.mSelectionStart), + static_cast(aCanceledCompositionEnd.mData.Length())) == + aCanceledCompositionEnd.mData); + + // Restore the committed string as composing string. + mComposition.reset(); // Avoid new crash in the beta and nightly channels. + mComposition.emplace(aCompositionView, + aCanceledCompositionEnd.mSelectionStart, + aCanceledCompositionEnd.mData); + mLatestCompositionRange = Some(mComposition->CreateStartAndEndOffsets()); +} + +void TSFTextStore::Content::EndComposition(const PendingAction& aCompEnd) { + MOZ_ASSERT(mComposition.isSome()); + MOZ_ASSERT(aCompEnd.mType == PendingAction::Type::eCompositionEnd); + + if (mComposition.isNothing()) { + return; // Avoid new crash in the beta and nightly channels. + } + + mSelection = Some(TSFTextStore::Selection(mComposition->StartOffset() + + aCompEnd.mData.Length())); + mComposition.reset(); +} + +/****************************************************************************** + * TSFTextStore::MouseTracker + *****************************************************************************/ + +TSFTextStore::MouseTracker::MouseTracker() : mCookie(kInvalidCookie) {} + +HRESULT +TSFTextStore::MouseTracker::Init(TSFTextStore* aTextStore) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::Init(aTextStore=0x%p), " + "aTextStore->mMouseTrackers.Length()=%zu", + this, aTextStore, aTextStore->mMouseTrackers.Length())); + + if (&aTextStore->mMouseTrackers.LastElement() != this) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::Init() FAILED due to " + "this is not the last element of mMouseTrackers", + this)); + return E_FAIL; + } + if (aTextStore->mMouseTrackers.Length() > kInvalidCookie) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::Init() FAILED due to " + "no new cookie available", + this)); + return E_FAIL; + } + MOZ_ASSERT(!aTextStore->mMouseTrackers.IsEmpty(), + "This instance must be in TSFTextStore::mMouseTrackers"); + mCookie = static_cast(aTextStore->mMouseTrackers.Length() - 1); + return S_OK; +} + +HRESULT +TSFTextStore::MouseTracker::AdviseSink(TSFTextStore* aTextStore, + ITfRangeACP* aTextRange, + ITfMouseSink* aMouseSink) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::AdviseSink(aTextStore=0x%p, " + "aTextRange=0x%p, aMouseSink=0x%p), mCookie=%ld, mSink=0x%p", + this, aTextStore, aTextRange, aMouseSink, mCookie, mSink.get())); + MOZ_ASSERT(mCookie != kInvalidCookie, "This hasn't been initalized?"); + + if (mSink) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to already being used", + this)); + return E_FAIL; + } + + MOZ_ASSERT(mRange.isNothing()); + + LONG start = 0, length = 0; + HRESULT hr = aTextRange->GetExtent(&start, &length); + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to failure of ITfRangeACP::GetExtent()", + this)); + return hr; + } + + if (start < 0 || length <= 0 || start + length > LONG_MAX) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to odd result of ITfRangeACP::GetExtent(), " + "start=%ld, length=%ld", + this, start, length)); + return E_INVALIDARG; + } + + nsAutoString textContent; + if (NS_WARN_IF(!aTextStore->GetCurrentText(textContent))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to failure of TSFTextStore::GetCurrentText()", + this)); + return E_FAIL; + } + + if (textContent.Length() <= static_cast(start) || + textContent.Length() < static_cast(start + length)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink() FAILED " + "due to out of range, start=%ld, length=%ld, " + "textContent.Length()=%zu", + this, start, length, textContent.Length())); + return E_INVALIDARG; + } + + mRange.emplace(start, start + length); + + mSink = aMouseSink; + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::AdviseMouseSink(), " + "succeeded, mRange=%s, textContent.Length()=%zu", + this, ToString(mRange).c_str(), textContent.Length())); + return S_OK; +} + +void TSFTextStore::MouseTracker::UnadviseSink() { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::UnadviseSink(), " + "mCookie=%ld, mSink=0x%p, mRange=%s", + this, mCookie, mSink.get(), ToString(mRange).c_str())); + mSink = nullptr; + mRange.reset(); +} + +bool TSFTextStore::MouseTracker::OnMouseButtonEvent(ULONG aEdge, + ULONG aQuadrant, + DWORD aButtonStatus) { + MOZ_ASSERT(IsUsing(), "The caller must check before calling OnMouseEvent()"); + + BOOL eaten = FALSE; + RefPtr sink = mSink; + HRESULT hr = sink->OnMouseEvent(aEdge, aQuadrant, aButtonStatus, &eaten); + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p TSFTextStore::MouseTracker::OnMouseEvent(aEdge=%ld, " + "aQuadrant=%ld, aButtonStatus=0x%08lX), hr=0x%08lX, eaten=%s", + this, aEdge, aQuadrant, aButtonStatus, hr, GetBoolName(!!eaten))); + + return SUCCEEDED(hr) && eaten; +} + +#ifdef DEBUG +// static +bool TSFTextStore::CurrentKeyboardLayoutHasIME() { + RefPtr inputProcessorProfiles = + TSFTextStore::GetInputProcessorProfiles(); + if (!inputProcessorProfiles) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED due to " + "there is no input processor profiles instance")); + return false; + } + RefPtr profileMgr; + HRESULT hr = inputProcessorProfiles->QueryInterface( + IID_ITfInputProcessorProfileMgr, getter_AddRefs(profileMgr)); + if (FAILED(hr) || !profileMgr) { + // On Windows Vista or later, ImmIsIME() API always returns true. + // If we failed to obtain the profile manager, we cannot know if current + // keyboard layout has IME. + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED to query " + "ITfInputProcessorProfileMgr")); + return false; + } + + TF_INPUTPROCESSORPROFILE profile; + hr = profileMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &profile); + if (hr == S_FALSE) { + return false; // not found or not active + } + if (FAILED(hr)) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::CurrentKeyboardLayoutHasIME() FAILED to retreive " + "active profile")); + return false; + } + return (profile.dwProfileType == TF_PROFILETYPE_INPUTPROCESSOR); +} +#endif // #ifdef DEBUG + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/TSFTextStore.h b/widget/windows/TSFTextStore.h new file mode 100644 index 0000000000..a8a72fa170 --- /dev/null +++ b/widget/windows/TSFTextStore.h @@ -0,0 +1,1157 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TSFTextStore_h_ +#define TSFTextStore_h_ + +#include "nsCOMPtr.h" +#include "nsIWidget.h" +#include "nsString.h" +#include "nsWindow.h" + +#include "WinUtils.h" +#include "WritingModes.h" + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TextRange.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/widget/IMEData.h" + +#include +#include + +// GUID_PROP_INPUTSCOPE is declared in inputscope.h using INIT_GUID. +// With initguid.h, we get its instance instead of extern declaration. +#ifdef INPUTSCOPE_INIT_GUID +# include +#endif +#ifdef TEXTATTRS_INIT_GUID +# include +#endif +#include + +// TSF InputScope, for earlier SDK 8 +#define IS_SEARCH static_cast(50) + +struct ITfThreadMgr; +struct ITfDocumentMgr; +struct ITfDisplayAttributeMgr; +struct ITfCategoryMgr; +class nsWindow; + +inline std::ostream& operator<<(std::ostream& aStream, + const TS_SELECTIONSTYLE& aSelectionStyle) { + const char* ase = "Unknown"; + switch (aSelectionStyle.ase) { + case TS_AE_START: + ase = "TS_AE_START"; + break; + case TS_AE_END: + ase = "TS_AE_END"; + break; + case TS_AE_NONE: + ase = "TS_AE_NONE"; + break; + } + aStream << "{ ase=" << ase << ", fInterimChar=" + << (aSelectionStyle.fInterimChar ? "TRUE" : "FALSE") << " }"; + return aStream; +} + +inline std::ostream& operator<<(std::ostream& aStream, + const TS_SELECTION_ACP& aACP) { + aStream << "{ acpStart=" << aACP.acpStart << ", acpEnd=" << aACP.acpEnd + << ", style=" << mozilla::ToString(aACP.style).c_str() << " }"; + return aStream; +} + +namespace mozilla { +namespace widget { + +class TSFStaticSink; +struct MSGResult; + +/* + * Text Services Framework text store + */ + +class TSFTextStore final : public ITextStoreACP, + public ITfContextOwnerCompositionSink, + public ITfMouseTrackerACP { + friend class TSFStaticSink; + + private: + typedef IMENotification::SelectionChangeDataBase SelectionChangeDataBase; + typedef IMENotification::SelectionChangeData SelectionChangeData; + typedef IMENotification::TextChangeDataBase TextChangeDataBase; + typedef IMENotification::TextChangeData TextChangeData; + + public: /*IUnknown*/ + STDMETHODIMP QueryInterface(REFIID, void**); + + NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(TSFTextStore) + + public: /*ITextStoreACP*/ + STDMETHODIMP AdviseSink(REFIID, IUnknown*, DWORD); + STDMETHODIMP UnadviseSink(IUnknown*); + STDMETHODIMP RequestLock(DWORD, HRESULT*); + STDMETHODIMP GetStatus(TS_STATUS*); + STDMETHODIMP QueryInsert(LONG, LONG, ULONG, LONG*, LONG*); + STDMETHODIMP GetSelection(ULONG, ULONG, TS_SELECTION_ACP*, ULONG*); + STDMETHODIMP SetSelection(ULONG, const TS_SELECTION_ACP*); + STDMETHODIMP GetText(LONG, LONG, WCHAR*, ULONG, ULONG*, TS_RUNINFO*, ULONG, + ULONG*, LONG*); + STDMETHODIMP SetText(DWORD, LONG, LONG, const WCHAR*, ULONG, TS_TEXTCHANGE*); + STDMETHODIMP GetFormattedText(LONG, LONG, IDataObject**); + STDMETHODIMP GetEmbedded(LONG, REFGUID, REFIID, IUnknown**); + STDMETHODIMP QueryInsertEmbedded(const GUID*, const FORMATETC*, BOOL*); + STDMETHODIMP InsertEmbedded(DWORD, LONG, LONG, IDataObject*, TS_TEXTCHANGE*); + STDMETHODIMP RequestSupportedAttrs(DWORD, ULONG, const TS_ATTRID*); + STDMETHODIMP RequestAttrsAtPosition(LONG, ULONG, const TS_ATTRID*, DWORD); + STDMETHODIMP RequestAttrsTransitioningAtPosition(LONG, ULONG, + const TS_ATTRID*, DWORD); + STDMETHODIMP FindNextAttrTransition(LONG, LONG, ULONG, const TS_ATTRID*, + DWORD, LONG*, BOOL*, LONG*); + STDMETHODIMP RetrieveRequestedAttrs(ULONG, TS_ATTRVAL*, ULONG*); + STDMETHODIMP GetEndACP(LONG*); + STDMETHODIMP GetActiveView(TsViewCookie*); + STDMETHODIMP GetACPFromPoint(TsViewCookie, const POINT*, DWORD, LONG*); + STDMETHODIMP GetTextExt(TsViewCookie, LONG, LONG, RECT*, BOOL*); + STDMETHODIMP GetScreenExt(TsViewCookie, RECT*); + STDMETHODIMP GetWnd(TsViewCookie, HWND*); + STDMETHODIMP InsertTextAtSelection(DWORD, const WCHAR*, ULONG, LONG*, LONG*, + TS_TEXTCHANGE*); + STDMETHODIMP InsertEmbeddedAtSelection(DWORD, IDataObject*, LONG*, LONG*, + TS_TEXTCHANGE*); + + public: /*ITfContextOwnerCompositionSink*/ + STDMETHODIMP OnStartComposition(ITfCompositionView*, BOOL*); + STDMETHODIMP OnUpdateComposition(ITfCompositionView*, ITfRange*); + STDMETHODIMP OnEndComposition(ITfCompositionView*); + + public: /*ITfMouseTrackerACP*/ + STDMETHODIMP AdviseMouseSink(ITfRangeACP*, ITfMouseSink*, DWORD*); + STDMETHODIMP UnadviseMouseSink(DWORD); + + public: + static void Initialize(void); + static void Terminate(void); + + static bool ProcessRawKeyMessage(const MSG& aMsg); + static void ProcessMessage(nsWindow* aWindow, UINT aMessage, WPARAM& aWParam, + LPARAM& aLParam, MSGResult& aResult); + + static void SetIMEOpenState(bool); + static bool GetIMEOpenState(void); + + static void CommitComposition(bool aDiscard) { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return; + } + RefPtr textStore(sEnabledTextStore); + textStore->CommitCompositionInternal(aDiscard); + } + + static void SetInputContext(nsWindow* aWidget, const InputContext& aContext, + const InputContextAction& aAction); + + static nsresult OnFocusChange(bool aGotFocus, nsWindow* aFocusedWidget, + const InputContext& aContext); + static nsresult OnTextChange(const IMENotification& aIMENotification) { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr textStore(sEnabledTextStore); + return textStore->OnTextChangeInternal(aIMENotification); + } + + static nsresult OnSelectionChange(const IMENotification& aIMENotification) { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr textStore(sEnabledTextStore); + return textStore->OnSelectionChangeInternal(aIMENotification); + } + + static nsresult OnLayoutChange() { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr textStore(sEnabledTextStore); + return textStore->OnLayoutChangeInternal(); + } + + static nsresult OnUpdateComposition() { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr textStore(sEnabledTextStore); + return textStore->OnUpdateCompositionInternal(); + } + + static nsresult OnMouseButtonEvent(const IMENotification& aIMENotification) { + NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called"); + if (!sEnabledTextStore) { + return NS_OK; + } + RefPtr textStore(sEnabledTextStore); + return textStore->OnMouseButtonEventInternal(aIMENotification); + } + + static IMENotificationRequests GetIMENotificationRequests(); + + // Returns the address of the pointer so that the TSF automatic test can + // replace the system object with a custom implementation for testing. + // XXX TSF doesn't work now. Should we remove it? + static void* GetNativeData(uint32_t aDataType) { + switch (aDataType) { + case NS_NATIVE_TSF_THREAD_MGR: + Initialize(); // Apply any previous changes + return static_cast(&sThreadMgr); + case NS_NATIVE_TSF_CATEGORY_MGR: + return static_cast(&sCategoryMgr); + case NS_NATIVE_TSF_DISPLAY_ATTR_MGR: + return static_cast(&sDisplayAttrMgr); + default: + return nullptr; + } + } + + static void* GetThreadManager() { return static_cast(sThreadMgr); } + + static bool ThinksHavingFocus() { + return (sEnabledTextStore && sEnabledTextStore->mContext); + } + + static bool IsInTSFMode() { return sThreadMgr != nullptr; } + + static bool IsComposing() { + return (sEnabledTextStore && sEnabledTextStore->mComposition.isSome()); + } + + static bool IsComposingOn(nsWindow* aWidget) { + return (IsComposing() && sEnabledTextStore->mWidget == aWidget); + } + + static nsWindow* GetEnabledWindowBase() { + return sEnabledTextStore ? sEnabledTextStore->mWidget.get() : nullptr; + } + + /** + * Returns true if active keyboard layout is a legacy IMM-IME. + */ + static bool IsIMM_IMEActive(); + + /** + * Returns true if active TIP is MS-IME for Japanese. + */ + static bool IsMSJapaneseIMEActive(); + + /** + * Returns true if active TIP is Google Japanese Input. + * Note that if Google Japanese Input is installed as an IMM-IME, + * this return false even if Google Japanese Input is active. + * So, you may need to check IMMHandler::IsGoogleJapaneseInputActive() too. + */ + static bool IsGoogleJapaneseInputActive(); + + /** + * Returns true if active TIP is ATOK. + */ + static bool IsATOKActive(); + + /** + * Returns true if active TIP or IME is a black listed one and we should + * set input scope of URL bar to IS_DEFAULT rather than IS_URL. + */ + static bool ShouldSetInputScopeOfURLBarToDefault(); + + /** + * Returns true if TSF may crash if GetSelection() returns E_FAIL. + */ + static bool DoNotReturnErrorFromGetSelection(); + +#ifdef DEBUG + // Returns true when keyboard layout has IME (TIP). + static bool CurrentKeyboardLayoutHasIME(); +#endif // #ifdef DEBUG + + protected: + TSFTextStore(); + ~TSFTextStore(); + + static bool CreateAndSetFocus(nsWindow* aFocusedWidget, + const InputContext& aContext); + static void EnsureToDestroyAndReleaseEnabledTextStoreIf( + RefPtr& aTextStore); + static void MarkContextAsKeyboardDisabled(ITfContext* aContext); + static void MarkContextAsEmpty(ITfContext* aContext); + + bool Init(nsWindow* aWidget, const InputContext& aContext); + void Destroy(); + void ReleaseTSFObjects(); + + bool IsReadLock(DWORD aLock) const { + return (TS_LF_READ == (aLock & TS_LF_READ)); + } + bool IsReadWriteLock(DWORD aLock) const { + return (TS_LF_READWRITE == (aLock & TS_LF_READWRITE)); + } + bool IsReadLocked() const { return IsReadLock(mLock); } + bool IsReadWriteLocked() const { return IsReadWriteLock(mLock); } + + // This is called immediately after a call of OnLockGranted() of mSink. + // Note that mLock isn't cleared yet when this is called. + void DidLockGranted(); + + bool GetScreenExtInternal(RECT& aScreenExt); + // If aDispatchCompositionChangeEvent is true, this method will dispatch + // compositionchange event if this is called during IME composing. + // aDispatchCompositionChangeEvent should be true only when this is called + // from SetSelection. Because otherwise, the compositionchange event should + // not be sent from here. + HRESULT SetSelectionInternal(const TS_SELECTION_ACP*, + bool aDispatchCompositionChangeEvent = false); + bool InsertTextAtSelectionInternal(const nsAString& aInsertStr, + TS_TEXTCHANGE* aTextChange); + void CommitCompositionInternal(bool); + HRESULT GetDisplayAttribute(ITfProperty* aProperty, ITfRange* aRange, + TF_DISPLAYATTRIBUTE* aResult); + HRESULT RestartCompositionIfNecessary(ITfRange* pRangeNew = nullptr); + class Composition; + HRESULT RestartComposition(Composition& aCurrentComposition, + ITfCompositionView* aCompositionView, + ITfRange* aNewRange); + + // Following methods record composing action(s) to mPendingActions. + // They will be flushed FlushPendingActions(). + HRESULT RecordCompositionStartAction(ITfCompositionView* aCompositionView, + ITfRange* aRange, + bool aPreserveSelection); + HRESULT RecordCompositionStartAction(ITfCompositionView* aCompositionView, + LONG aStart, LONG aLength, + bool aPreserveSelection); + HRESULT RecordCompositionUpdateAction(); + HRESULT RecordCompositionEndAction(); + + // DispatchEvent() dispatches the event and if it may not be handled + // synchronously, this makes the instance not notify TSF of pending + // notifications until next notification from content. + void DispatchEvent(WidgetGUIEvent& aEvent); + void OnLayoutInformationAvaliable(); + + // FlushPendingActions() performs pending actions recorded in mPendingActions + // and clear it. + void FlushPendingActions(); + // MaybeFlushPendingNotifications() performs pending notifications to TSF. + void MaybeFlushPendingNotifications(); + + nsresult OnTextChangeInternal(const IMENotification& aIMENotification); + nsresult OnSelectionChangeInternal(const IMENotification& aIMENotification); + nsresult OnMouseButtonEventInternal(const IMENotification& aIMENotification); + nsresult OnLayoutChangeInternal(); + nsresult OnUpdateCompositionInternal(); + + // mPendingSelectionChangeData stores selection change data until notifying + // TSF of selection change. If two or more selection changes occur, this + // stores the latest selection change data because only it is necessary. + Maybe mPendingSelectionChangeData; + + // mPendingTextChangeData stores one or more text change data until notifying + // TSF of text change. If two or more text changes occur, this merges + // every text change data. + TextChangeData mPendingTextChangeData; + + void NotifyTSFOfTextChange(); + void NotifyTSFOfSelectionChange(); + bool NotifyTSFOfLayoutChange(); + void NotifyTSFOfLayoutChangeAgain(); + + HRESULT HandleRequestAttrs(DWORD aFlags, ULONG aFilterCount, + const TS_ATTRID* aFilterAttrs); + void SetInputScope(const nsString& aHTMLInputType, + const nsString& aHTMLInputMode); + + // Creates native caret over our caret. This method only works on desktop + // application. Otherwise, this does nothing. + void CreateNativeCaret(); + // Destroys native caret if there is. + void MaybeDestroyNativeCaret(); + + /** + * MaybeHackNoErrorLayoutBugs() is a helper method of GetTextExt(). In + * strictly speaking, TSF is aware of asynchronous layout computation like us. + * However, Windows 10 version 1803 and older (including Windows 8.1 and + * older) Windows has a bug which is that the caller of GetTextExt() of TSF + * does not return TS_E_NOLAYOUT to TIP as is. Additionally, even after + * fixing this bug, some TIPs are not work well when we return TS_E_NOLAYOUT. + * For avoiding this issue, this method checks current Windows version and + * active TIP, and if in case we cannot return TS_E_NOLAYOUT, this modifies + * aACPStart and aACPEnd to making sure that they are in range of unmodified + * characters. + * + * @param aACPStart Initial value should be acpStart of GetTextExt(). + * If this method returns true, this may be modified + * to be in range of unmodified characters. + * @param aACPEnd Initial value should be acpEnd of GetTextExt(). + * If this method returns true, this may be modified + * to be in range of unmodified characters. + * And also this may become same as aACPStart. + * @return true if the caller shouldn't return TS_E_NOLAYOUT. + * In this case, this method modifies aACPStart and/or + * aASCPEnd to compute rectangle of unmodified characters. + * false if the caller can return TS_E_NOLAYOUT or + * we cannot have proper unmodified characters. + */ + bool MaybeHackNoErrorLayoutBugs(LONG& aACPStart, LONG& aACPEnd); + + // Holds the pointer to our current win32 widget + RefPtr mWidget; + // mDispatcher is a helper class to dispatch composition events. + RefPtr mDispatcher; + // Document manager for the currently focused editor + RefPtr mDocumentMgr; + // Edit cookie associated with the current editing context + DWORD mEditCookie; + // Editing context at the bottom of mDocumentMgr's context stack + RefPtr mContext; + // Currently installed notification sink + RefPtr mSink; + // TS_AS_* mask of what events to notify + DWORD mSinkMask; + // 0 if not locked, otherwise TS_LF_* indicating the current lock + DWORD mLock; + // 0 if no lock is queued, otherwise TS_LF_* indicating the queue lock + DWORD mLockQueued; + + uint32_t mHandlingKeyMessage; + void OnStartToHandleKeyMessage() { + // If we're starting to handle another key message during handling a + // key message, let's assume that the handling key message is handled by + // TIP and it sends another key message for hacking something. + // Let's try to dispatch a keyboard event now. + // FYI: All callers of this method grab this instance with local variable. + // So, even after calling MaybeDispatchKeyboardEventAsProcessedByIME(), + // we're safe to access any members. + if (!mDestroyed && sHandlingKeyMsg && !sIsKeyboardEventDispatched) { + MaybeDispatchKeyboardEventAsProcessedByIME(); + } + ++mHandlingKeyMessage; + } + void OnEndHandlingKeyMessage(bool aIsProcessedByTSF) { + // If sHandlingKeyMsg has been handled by TSF or TIP and we're still + // alive, but we haven't dispatch keyboard event for it, let's fire it now. + // FYI: All callers of this method grab this instance with local variable. + // So, even after calling MaybeDispatchKeyboardEventAsProcessedByIME(), + // we're safe to access any members. + if (!mDestroyed && sHandlingKeyMsg && aIsProcessedByTSF && + !sIsKeyboardEventDispatched) { + MaybeDispatchKeyboardEventAsProcessedByIME(); + } + MOZ_ASSERT(mHandlingKeyMessage); + if (--mHandlingKeyMessage) { + return; + } + // If TSFTextStore instance is destroyed during handling key message(s), + // release all TSF objects when all nested key messages have been handled. + if (mDestroyed) { + ReleaseTSFObjects(); + } + } + + /** + * MaybeDispatchKeyboardEventAsProcessedByIME() tries to dispatch eKeyDown + * event or eKeyUp event for sHandlingKeyMsg and marking the dispatching + * event as "processed by IME". Note that if the document is locked, this + * just adds a pending action into the queue and sets + * sIsKeyboardEventDispatched to true. + */ + void MaybeDispatchKeyboardEventAsProcessedByIME(); + + /** + * DispatchKeyboardEventAsProcessedByIME() dispatches an eKeyDown or + * eKeyUp event with NativeKey class and aMsg. + */ + void DispatchKeyboardEventAsProcessedByIME(const MSG& aMsg); + + // Composition class stores a copy of the active composition string. Only + // the data is updated during an InsertTextAtSelection call if we have a + // composition. The data acts as a buffer until OnUpdateComposition is + // called and the data is flushed to editor through eCompositionChange. + // This allows all changes to be updated in batches to avoid inconsistencies + // and artifacts. + class Composition final : public OffsetAndData { + public: + explicit Composition(ITfCompositionView* aCompositionView, + LONG aCompositionStartOffset, + const nsAString& aCompositionString) + : OffsetAndData(aCompositionStartOffset, aCompositionString), + mView(aCompositionView) {} + + ITfCompositionView* GetView() const { return mView; } + + friend std::ostream& operator<<(std::ostream& aStream, + const Composition& aComposition) { + aStream << "{ mView=0x" << aComposition.mView.get() + << ", OffsetAndData=" + << static_cast&>(aComposition) << " }"; + return aStream; + } + + private: + RefPtr const mView; + }; + // While the document is locked, we cannot dispatch any events which cause + // DOM events since the DOM events' handlers may modify the locked document. + // However, even while the document is locked, TSF may queries us. + // For that, TSFTextStore modifies mComposition even while the document is + // locked. With mComposition, query methods can returns the text content + // information. + Maybe mComposition; + + /** + * IsHandlingCompositionInParent() returns true if eCompositionStart is + * dispatched, but eCompositionCommit(AsIs) is not dispatched. This means + * that if composition is handled in a content process, this status indicates + * whether ContentCacheInParent has composition or not. On the other hand, + * if it's handled in the chrome process, this is exactly same as + * IsHandlingCompositionInContent(). + */ + bool IsHandlingCompositionInParent() const { + return mDispatcher && mDispatcher->IsComposing(); + } + + /** + * IsHandlingCompositionInContent() returns true if there is a composition in + * the focused editor which may be in a content process. + */ + bool IsHandlingCompositionInContent() const { + return mDispatcher && mDispatcher->IsHandlingComposition(); + } + + class Selection { + public: + static TS_SELECTION_ACP EmptyACP() { + return TS_SELECTION_ACP{ + .acpStart = 0, + .acpEnd = 0, + .style = {.ase = TS_AE_NONE, .fInterimChar = FALSE}}; + } + + bool HasRange() const { return mACP.isSome(); } + const TS_SELECTION_ACP& ACPRef() const { return mACP.ref(); } + + explicit Selection(const TS_SELECTION_ACP& aSelection) { + SetSelection(aSelection); + } + + explicit Selection(uint32_t aOffsetToCollapse) { + Collapse(aOffsetToCollapse); + } + + explicit Selection(const SelectionChangeDataBase& aSelectionChangeData) { + SetSelection(aSelectionChangeData); + } + + explicit Selection(const WidgetQueryContentEvent& aQuerySelectionEvent) { + SetSelection(aQuerySelectionEvent); + } + + Selection(uint32_t aStart, uint32_t aLength, bool aReversed, + const WritingMode& aWritingMode) { + SetSelection(aStart, aLength, aReversed, aWritingMode); + } + + void SetSelection(const TS_SELECTION_ACP& aSelection) { + mACP = Some(aSelection); + // Selection end must be active in our editor. + if (mACP->style.ase != TS_AE_START) { + mACP->style.ase = TS_AE_END; + } + // We're not support interim char selection for now. + // XXX Probably, this is necessary for supporting South Asian languages. + mACP->style.fInterimChar = FALSE; + } + + bool SetSelection(const SelectionChangeDataBase& aSelectionChangeData) { + MOZ_ASSERT(aSelectionChangeData.IsInitialized()); + if (!aSelectionChangeData.HasRange()) { + if (mACP.isNothing()) { + return false; + } + mACP.reset(); + // Let's keep the WritingMode because users don't want to change the UI + // of TIP temporarily since no selection case is created only by web + // apps, but they or TIP would restore selection at last point later. + return true; + } + return SetSelection(aSelectionChangeData.mOffset, + aSelectionChangeData.Length(), + aSelectionChangeData.mReversed, + aSelectionChangeData.GetWritingMode()); + } + + bool SetSelection(const WidgetQueryContentEvent& aQuerySelectionEvent) { + MOZ_ASSERT(aQuerySelectionEvent.mMessage == eQuerySelectedText); + MOZ_ASSERT(aQuerySelectionEvent.Succeeded()); + if (aQuerySelectionEvent.DidNotFindSelection()) { + if (mACP.isNothing()) { + return false; + } + mACP.reset(); + // Let's keep the WritingMode because users don't want to change the UI + // of TIP temporarily since no selection case is created only by web + // apps, but they or TIP would restore selection at last point later. + return true; + } + return SetSelection(aQuerySelectionEvent.mReply->StartOffset(), + aQuerySelectionEvent.mReply->DataLength(), + aQuerySelectionEvent.mReply->mReversed, + aQuerySelectionEvent.mReply->WritingModeRef()); + } + + bool SetSelection(uint32_t aStart, uint32_t aLength, bool aReversed, + const WritingMode& aWritingMode) { + const bool changed = mACP.isNothing() || + mACP->acpStart != static_cast(aStart) || + mACP->acpEnd != static_cast(aStart + aLength); + mACP = Some( + TS_SELECTION_ACP{.acpStart = static_cast(aStart), + .acpEnd = static_cast(aStart + aLength), + .style = {.ase = aReversed ? TS_AE_START : TS_AE_END, + .fInterimChar = FALSE}}); + mWritingMode = aWritingMode; + + return changed; + } + + bool Collapsed() const { + return mACP.isNothing() || mACP->acpStart == mACP->acpEnd; + } + + void Collapse(uint32_t aOffset) { + // XXX This does not update the selection's mWritingMode. + // If it is ever used to "collapse" to an entirely new location, + // we may need to fix that. + mACP = Some( + TS_SELECTION_ACP{.acpStart = static_cast(aOffset), + .acpEnd = static_cast(aOffset), + .style = {.ase = TS_AE_END, .fInterimChar = FALSE}}); + } + + LONG MinOffset() const { + MOZ_ASSERT(mACP.isSome()); + LONG min = std::min(mACP->acpStart, mACP->acpEnd); + MOZ_ASSERT(min >= 0); + return min; + } + + LONG MaxOffset() const { + MOZ_ASSERT(mACP.isSome()); + LONG max = std::max(mACP->acpStart, mACP->acpEnd); + MOZ_ASSERT(max >= 0); + return max; + } + + LONG StartOffset() const { + MOZ_ASSERT(mACP.isSome()); + MOZ_ASSERT(mACP->acpStart >= 0); + return mACP->acpStart; + } + + LONG EndOffset() const { + MOZ_ASSERT(mACP.isSome()); + MOZ_ASSERT(mACP->acpEnd >= 0); + return mACP->acpEnd; + } + + LONG Length() const { + MOZ_ASSERT_IF(mACP.isSome(), mACP->acpEnd >= mACP->acpStart); + return mACP.isSome() ? std::abs(mACP->acpEnd - mACP->acpStart) : 0; + } + + bool IsReversed() const { + return mACP.isSome() && mACP->style.ase == TS_AE_START; + } + + TsActiveSelEnd ActiveSelEnd() const { + return mACP.isSome() ? mACP->style.ase : TS_AE_NONE; + } + + bool IsInterimChar() const { + return mACP.isSome() && mACP->style.fInterimChar != FALSE; + } + + const WritingMode& WritingModeRef() const { return mWritingMode; } + + bool EqualsExceptDirection(const TS_SELECTION_ACP& aACP) const { + if (mACP.isNothing()) { + return false; + } + if (mACP->style.ase == aACP.style.ase) { + return mACP->acpStart == aACP.acpStart && mACP->acpEnd == aACP.acpEnd; + } + return mACP->acpStart == aACP.acpEnd && mACP->acpEnd == aACP.acpStart; + } + + bool EqualsExceptDirection( + const SelectionChangeDataBase& aChangedSelection) const { + MOZ_ASSERT(aChangedSelection.IsInitialized()); + if (mACP.isNothing()) { + return aChangedSelection.HasRange(); + } + return aChangedSelection.Length() == static_cast(Length()) && + aChangedSelection.mOffset == static_cast(StartOffset()); + } + + friend std::ostream& operator<<(std::ostream& aStream, + const Selection& aSelection) { + aStream << "{ mACP=" << ToString(aSelection.mACP).c_str() + << ", mWritingMode=" << ToString(aSelection.mWritingMode).c_str() + << ", Collapsed()=" + << (aSelection.Collapsed() ? "true" : "false") + << ", Length=" << aSelection.Length() << " }"; + return aStream; + } + + private: + Maybe mACP; // If Nothing, there is no selection + WritingMode mWritingMode; + }; + // Don't access mSelection directly. Instead, Use SelectionForTSFRef(). + // This is modified immediately when TSF requests to set selection and not + // updated by selection change in content until mContentForTSF is cleared. + Maybe mSelectionForTSF; + + /** + * Get the selection expected by TSF. If mSelectionForTSF is already valid, + * this just return the reference to it. Otherwise, this initializes it + * with eQuerySelectedText. Please check if the result is valid before + * actually using it. + * Note that this is also called by ContentForTSF(). + */ + Maybe& SelectionForTSF(); + + struct PendingAction final { + enum class Type : uint8_t { + eCompositionStart, + eCompositionUpdate, + eCompositionEnd, + eSetSelection, + eKeyboardEvent, + }; + Type mType; + // For eCompositionStart, eCompositionEnd and eSetSelection + LONG mSelectionStart; + // For eCompositionStart and eSetSelection + LONG mSelectionLength; + // For eCompositionStart, eCompositionUpdate and eCompositionEnd + nsString mData; + // For eCompositionUpdate + RefPtr mRanges; + // For eKeyboardEvent + MSG mKeyMsg; + // For eSetSelection + bool mSelectionReversed; + // For eCompositionUpdate + bool mIncomplete; + // For eCompositionStart + bool mAdjustSelection; + }; + // Items of mPendingActions are appended when TSF tells us to need to dispatch + // DOM composition events. However, we cannot dispatch while the document is + // locked because it can cause modifying the locked document. So, the pending + // actions should be performed when document lock is unlocked. + nsTArray mPendingActions; + + PendingAction* LastOrNewPendingCompositionUpdate() { + if (!mPendingActions.IsEmpty()) { + PendingAction& lastAction = mPendingActions.LastElement(); + if (lastAction.mType == PendingAction::Type::eCompositionUpdate) { + return &lastAction; + } + } + PendingAction* newAction = mPendingActions.AppendElement(); + newAction->mType = PendingAction::Type::eCompositionUpdate; + newAction->mRanges = new TextRangeArray(); + newAction->mIncomplete = true; + return newAction; + } + + /** + * IsLastPendingActionCompositionEndAt() checks whether the previous pending + * action is committing composition whose range starts from aStart and its + * length is aLength. In other words, this checks whether new composition + * which will replace same range as previous pending commit can be merged + * with the previous composition. + * + * @param aStart The inserted offset you expected. + * @param aLength The inserted text length you expected. + * @return true if the last pending action is + * eCompositionEnd and it inserted the text + * between aStart and aStart + aLength. + */ + bool IsLastPendingActionCompositionEndAt(LONG aStart, LONG aLength) const { + if (mPendingActions.IsEmpty()) { + return false; + } + const PendingAction& pendingLastAction = mPendingActions.LastElement(); + return pendingLastAction.mType == PendingAction::Type::eCompositionEnd && + pendingLastAction.mSelectionStart == aStart && + pendingLastAction.mData.Length() == static_cast(aLength); + } + + bool IsPendingCompositionUpdateIncomplete() const { + if (mPendingActions.IsEmpty()) { + return false; + } + const PendingAction& lastAction = mPendingActions.LastElement(); + return lastAction.mType == PendingAction::Type::eCompositionUpdate && + lastAction.mIncomplete; + } + + void CompleteLastActionIfStillIncomplete() { + if (!IsPendingCompositionUpdateIncomplete()) { + return; + } + RecordCompositionUpdateAction(); + } + + void RemoveLastCompositionUpdateActions() { + while (!mPendingActions.IsEmpty()) { + const PendingAction& lastAction = mPendingActions.LastElement(); + if (lastAction.mType != PendingAction::Type::eCompositionUpdate) { + break; + } + mPendingActions.RemoveLastElement(); + } + } + + // When On*Composition() is called without document lock, we need to flush + // the recorded actions at quitting the method. + // AutoPendingActionAndContentFlusher class is usedful for it. + class MOZ_STACK_CLASS AutoPendingActionAndContentFlusher final { + public: + explicit AutoPendingActionAndContentFlusher(TSFTextStore* aTextStore) + : mTextStore(aTextStore) { + MOZ_ASSERT(!mTextStore->mIsRecordingActionsWithoutLock); + if (!mTextStore->IsReadWriteLocked()) { + mTextStore->mIsRecordingActionsWithoutLock = true; + } + } + + ~AutoPendingActionAndContentFlusher() { + if (!mTextStore->mIsRecordingActionsWithoutLock) { + return; + } + mTextStore->FlushPendingActions(); + mTextStore->mIsRecordingActionsWithoutLock = false; + } + + private: + AutoPendingActionAndContentFlusher() {} + + RefPtr mTextStore; + }; + + class Content final { + public: + Content(TSFTextStore& aTSFTextStore, const nsAString& aText) + : mText(aText), + mLastComposition(aTSFTextStore.mComposition), + mComposition(aTSFTextStore.mComposition), + mSelection(aTSFTextStore.mSelectionForTSF) {} + + void OnLayoutChanged() { mMinModifiedOffset.reset(); } + + // OnCompositionEventsHandled() is called when all pending composition + // events are handled in the focused content which may be in a remote + // process. + void OnCompositionEventsHandled() { mLastComposition = mComposition; } + + const nsDependentSubstring GetSelectedText() const; + const nsDependentSubstring GetSubstring(uint32_t aStart, + uint32_t aLength) const; + void ReplaceSelectedTextWith(const nsAString& aString); + void ReplaceTextWith(LONG aStart, LONG aLength, const nsAString& aString); + + void StartComposition(ITfCompositionView* aCompositionView, + const PendingAction& aCompStart, + bool aPreserveSelection); + /** + * RestoreCommittedComposition() restores the committed string as + * composing string. If InsertTextAtSelection() or something is called + * before a call of OnStartComposition() or previous composition is + * committed and new composition is restarted to clean up the commited + * string, there is a pending compositionend. In this case, we need to + * cancel the pending compositionend and continue the composition. + * + * @param aCompositionView The composition view. + * @param aCanceledCompositionEnd The pending compositionend which is + * canceled for restarting the composition. + */ + void RestoreCommittedComposition( + ITfCompositionView* aCompositionView, + const PendingAction& aCanceledCompositionEnd); + void EndComposition(const PendingAction& aCompEnd); + + const nsString& TextRef() const { return mText; } + const Maybe>& LastComposition() const { + return mLastComposition; + } + const Maybe& MinModifiedOffset() const { + return mMinModifiedOffset; + } + const Maybe>& LatestCompositionRange() const { + return mLatestCompositionRange; + } + + // Returns true if layout of the character at the aOffset has not been + // calculated. + bool IsLayoutChangedAt(uint32_t aOffset) const { + return IsLayoutChanged() && (mMinModifiedOffset.value() <= aOffset); + } + // Returns true if layout of the content has been changed, i.e., the new + // layout has not been calculated. + bool IsLayoutChanged() const { return mMinModifiedOffset.isSome(); } + bool HasOrHadComposition() const { + return mLatestCompositionRange.isSome(); + } + + Maybe& Composition() { return mComposition; } + Maybe& Selection() { return mSelection; } + + friend std::ostream& operator<<(std::ostream& aStream, + const Content& aContent) { + aStream << "{ mText=" + << PrintStringDetail(aContent.mText, + PrintStringDetail::kMaxLengthForEditor) + .get() + << ", mLastComposition=" << aContent.mLastComposition + << ", mLatestCompositionRange=" + << aContent.mLatestCompositionRange + << ", mMinModifiedOffset=" << aContent.mMinModifiedOffset << " }"; + return aStream; + } + + private: + nsString mText; + + // mLastComposition may store the composition string and its start offset + // when the document is locked. This is necessary to compute + // mMinTextModifiedOffset. + Maybe> mLastComposition; + + Maybe& mComposition; + Maybe& mSelection; + + // The latest composition's start and end offset. + Maybe> mLatestCompositionRange; + + // The minimum offset of modified part of the text. + Maybe mMinModifiedOffset; + }; + // mContentForTSF is cache of content. The information is expected by TSF + // and TIP. Therefore, this is useful for answering the query from TSF or + // TIP. + // This is initialized by ContentForTSF() automatically (therefore, don't + // access this member directly except at calling Clear(), IsInitialized(), + // IsLayoutChangeAfter() or IsLayoutChanged()). + // This is cleared when: + // - When there is no composition, the document is unlocked. + // - When there is a composition, all dispatched events are handled by + // the focused editor which may be in a remote process. + // So, if two compositions are created very quickly, this cache may not be + // cleared between eCompositionCommit(AsIs) and eCompositionStart. + Maybe mContentForTSF; + + Maybe& ContentForTSF(); + + class MOZ_STACK_CLASS AutoNotifyingTSFBatch final { + public: + explicit AutoNotifyingTSFBatch(TSFTextStore& aTextStore) + : mTextStore(aTextStore), mOldValue(aTextStore.mDeferNotifyingTSF) { + mTextStore.mDeferNotifyingTSF = true; + } + ~AutoNotifyingTSFBatch() { + mTextStore.mDeferNotifyingTSF = mOldValue; + mTextStore.MaybeFlushPendingNotifications(); + } + + private: + TSFTextStore& mTextStore; + bool mOldValue; + }; + + // CanAccessActualContentDirectly() returns true when TSF/TIP can access + // actual content directly. In other words, mContentForTSF and/or + // mSelectionForTSF doesn't cache content or they matches with actual + // contents due to no pending text/selection change notifications. + bool CanAccessActualContentDirectly() const; + + // While mContentForTSF is valid, this returns the text stored by it. + // Otherwise, return the current text content retrieved by eQueryTextContent. + bool GetCurrentText(nsAString& aTextContent); + + class MouseTracker final { + public: + static const DWORD kInvalidCookie = static_cast(-1); + + MouseTracker(); + + HRESULT Init(TSFTextStore* aTextStore); + HRESULT AdviseSink(TSFTextStore* aTextStore, ITfRangeACP* aTextRange, + ITfMouseSink* aMouseSink); + void UnadviseSink(); + + bool IsUsing() const { return mSink != nullptr; } + DWORD Cookie() const { return mCookie; } + bool OnMouseButtonEvent(ULONG aEdge, ULONG aQuadrant, DWORD aButtonStatus); + const Maybe> Range() const { return mRange; } + + private: + RefPtr mSink; + Maybe> mRange; + DWORD mCookie; + }; + // mMouseTrackers is an array to store each information of installed + // ITfMouseSink instance. + nsTArray mMouseTrackers; + + // The input scopes for this context, defaults to IS_DEFAULT. + nsTArray mInputScopes; + + // The URL cache of the focused document. + nsString mDocumentURL; + + // Support retrieving attributes. + // TODO: We should support RightToLeft, perhaps. + enum { + // Used for result of GetRequestedAttrIndex() + eNotSupported = -1, + + // Supported attributes + eInputScope = 0, + eDocumentURL, + eTextVerticalWriting, + eTextOrientation, + + // Count of the supported attributes + NUM_OF_SUPPORTED_ATTRS + }; + bool mRequestedAttrs[NUM_OF_SUPPORTED_ATTRS] = {false}; + + int32_t GetRequestedAttrIndex(const TS_ATTRID& aAttrID); + TS_ATTRID GetAttrID(int32_t aIndex); + + bool mRequestedAttrValues = false; + + // If edit actions are being recorded without document lock, this is true. + // Otherwise, false. + bool mIsRecordingActionsWithoutLock = false; + // If GetTextExt() or GetACPFromPoint() is called and the layout hasn't been + // calculated yet, these methods return TS_E_NOLAYOUT. At that time, + // mHasReturnedNoLayoutError is set to true. + bool mHasReturnedNoLayoutError = false; + // Before calling ITextStoreACPSink::OnLayoutChange() and + // ITfContextOwnerServices::OnLayoutChange(), mWaitingQueryLayout is set to + // true. This is set to false when GetTextExt() or GetACPFromPoint() is + // called. + bool mWaitingQueryLayout = false; + // During the document is locked, we shouldn't destroy the instance. + // If this is true, the instance will be destroyed after unlocked. + bool mPendingDestroy = false; + // If this is false, MaybeFlushPendingNotifications() will clear the + // mContentForTSF. + bool mDeferClearingContentForTSF = false; + // While the instance is initializing content/selection cache, another + // initialization shouldn't run recursively. Therefore, while the + // initialization is running, this is set to true. Use AutoNotifyingTSFBatch + // to set this. + bool mDeferNotifyingTSF = false; + // While the instance is dispatching events, the event may not be handled + // synchronously when remote content has focus. In the case, we cannot + // return the latest layout/content information to TSF/TIP until we get next + // update notification from ContentCacheInParent. For preventing TSF/TIP + // retrieves the latest content/layout information while it becomes available, + // we should put off notifying TSF of any updates. + bool mDeferNotifyingTSFUntilNextUpdate = false; + // While the document is locked, committing composition always fails since + // TSF needs another document lock for modifying the composition, selection + // and etc. So, committing composition should be performed after the + // document is unlocked. + bool mDeferCommittingComposition = false; + bool mDeferCancellingComposition = false; + // Immediately after a call of Destroy(), mDestroyed becomes true. If this + // is true, the instance shouldn't grant any requests from the TIP anymore. + bool mDestroyed = false; + // While the instance is being destroyed, this is set to true for avoiding + // recursive Destroy() calls. + bool mBeingDestroyed = false; + // Whether we're in the private browsing mode. + bool mInPrivateBrowsing = true; + + // TSF thread manager object for the current application + static StaticRefPtr sThreadMgr; + static already_AddRefed GetThreadMgr(); + // sMessagePump is QI'ed from sThreadMgr + static StaticRefPtr sMessagePump; + + public: + // Expose GetMessagePump() for WinUtils. + static already_AddRefed GetMessagePump(); + + private: + // sKeystrokeMgr is QI'ed from sThreadMgr + static StaticRefPtr sKeystrokeMgr; + // TSF display attribute manager + static StaticRefPtr sDisplayAttrMgr; + static already_AddRefed GetDisplayAttributeMgr(); + // TSF category manager + static StaticRefPtr sCategoryMgr; + static already_AddRefed GetCategoryMgr(); + // Compartment for (Get|Set)IMEOpenState() + static StaticRefPtr sCompartmentForOpenClose; + static already_AddRefed GetCompartmentForOpenClose(); + + // Current text store which is managing a keyboard enabled editor (i.e., + // editable editor). Currently only ONE TSFTextStore instance is ever used, + // although Create is called when an editor is focused and Destroy called + // when the focused editor is blurred. + static StaticRefPtr sEnabledTextStore; + + // For IME (keyboard) disabled state: + static StaticRefPtr sDisabledDocumentMgr; + static StaticRefPtr sDisabledContext; + + static StaticRefPtr sInputProcessorProfiles; + static already_AddRefed + GetInputProcessorProfiles(); + + // Handling key message. + static const MSG* sHandlingKeyMsg; + + // TSF client ID for the current application + static DWORD sClientId; + + // true if an eKeyDown or eKeyUp event for sHandlingKeyMsg has already + // been dispatched. + static bool sIsKeyboardEventDispatched; +}; + +} // namespace widget +} // namespace mozilla + +#endif // #ifndef TSFTextStore_h_ diff --git a/widget/windows/TaskbarPreview.cpp b/widget/windows/TaskbarPreview.cpp new file mode 100644 index 0000000000..a5d2aeb540 --- /dev/null +++ b/widget/windows/TaskbarPreview.cpp @@ -0,0 +1,416 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- 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 "TaskbarPreview.h" +#include +#include + +#include +#include +#include +#include + +#include "nsUXThemeData.h" +#include "nsWindow.h" +#include "nsAppShell.h" +#include "TaskbarPreviewButton.h" +#include "WinUtils.h" + +#include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/Telemetry.h" + +// Defined in dwmapi in a header that needs a higher numbered _WINNT #define +#ifndef DWM_SIT_DISPLAYFRAME +# define DWM_SIT_DISPLAYFRAME 0x1 +#endif + +namespace mozilla { +namespace widget { + +/////////////////////////////////////////////////////////////////////////////// +// TaskbarPreview + +TaskbarPreview::TaskbarPreview(ITaskbarList4* aTaskbar, + nsITaskbarPreviewController* aController, + HWND aHWND, nsIDocShell* aShell) + : mTaskbar(aTaskbar), + mController(aController), + mWnd(aHWND), + mVisible(false), + mDocShell(do_GetWeakReference(aShell)) {} + +TaskbarPreview::~TaskbarPreview() { + // Avoid dangling pointer + if (sActivePreview == this) sActivePreview = nullptr; + + // Our subclass should have invoked DetachFromNSWindow already. + NS_ASSERTION( + !mWnd, + "TaskbarPreview::DetachFromNSWindow was not called before destruction"); + + // Make sure to release before potentially uninitializing COM + mTaskbar = nullptr; + + ::CoUninitialize(); +} + +nsresult TaskbarPreview::Init() { + // TaskbarPreview may outlive the WinTaskbar that created it + if (FAILED(::CoInitialize(nullptr))) { + return NS_ERROR_NOT_INITIALIZED; + } + + WindowHook* hook = GetWindowHook(); + if (!hook) { + return NS_ERROR_NOT_AVAILABLE; + } + return hook->AddMonitor(WM_DESTROY, MainWindowHook, this); +} + +NS_IMETHODIMP +TaskbarPreview::SetController(nsITaskbarPreviewController* aController) { + NS_ENSURE_ARG(aController); + + mController = aController; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreview::GetController(nsITaskbarPreviewController** aController) { + NS_ADDREF(*aController = mController); + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreview::GetTooltip(nsAString& aTooltip) { + aTooltip = mTooltip; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreview::SetTooltip(const nsAString& aTooltip) { + mTooltip = aTooltip; + return CanMakeTaskbarCalls() ? UpdateTooltip() : NS_OK; +} + +NS_IMETHODIMP +TaskbarPreview::SetVisible(bool visible) { + if (mVisible == visible) return NS_OK; + mVisible = visible; + + // If the nsWindow has already been destroyed but the caller is still trying + // to use it then just pretend that everything succeeded. The caller doesn't + // actually have a way to detect this since it's the same case as when we + // CanMakeTaskbarCalls returns false. + if (!IsWindowAvailable()) return NS_OK; + + return visible ? Enable() : Disable(); +} + +NS_IMETHODIMP +TaskbarPreview::GetVisible(bool* visible) { + *visible = mVisible; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreview::SetActive(bool active) { + if (active) + sActivePreview = this; + else if (sActivePreview == this) + sActivePreview = nullptr; + + return CanMakeTaskbarCalls() ? ShowActive(active) : NS_OK; +} + +NS_IMETHODIMP +TaskbarPreview::GetActive(bool* active) { + *active = sActivePreview == this; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreview::Invalidate() { + if (!mVisible) return NS_OK; + + // DWM Composition is required for previews + if (!gfxWindowsPlatform::GetPlatform()->DwmCompositionEnabled()) return NS_OK; + + HWND previewWindow = PreviewWindow(); + return FAILED(DwmInvalidateIconicBitmaps(previewWindow)) ? NS_ERROR_FAILURE + : NS_OK; +} + +nsresult TaskbarPreview::UpdateTaskbarProperties() { + nsresult rv = UpdateTooltip(); + + // If we are the active preview and our window is the active window, restore + // our active state - otherwise some other non-preview window is now active + // and should be displayed as so. + if (sActivePreview == this) { + if (mWnd == ::GetActiveWindow()) { + nsresult rvActive = ShowActive(true); + if (NS_FAILED(rvActive)) rv = rvActive; + } else { + sActivePreview = nullptr; + } + } + return rv; +} + +nsresult TaskbarPreview::Enable() { + nsresult rv = NS_OK; + if (CanMakeTaskbarCalls()) { + rv = UpdateTaskbarProperties(); + } else if (IsWindowAvailable()) { + WindowHook* hook = GetWindowHook(); + MOZ_ASSERT(hook, + "IsWindowAvailable() should have eliminated the null case."); + hook->AddMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(), + MainWindowHook, this); + } + return rv; +} + +nsresult TaskbarPreview::Disable() { + if (!IsWindowAvailable()) { + // Window is already destroyed + return NS_OK; + } + + WindowHook* hook = GetWindowHook(); + MOZ_ASSERT(hook, "IsWindowAvailable() should have eliminated the null case."); + (void)hook->RemoveMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(), + MainWindowHook, this); + + return NS_OK; +} + +bool TaskbarPreview::IsWindowAvailable() const { + if (mWnd) { + nsWindow* win = WinUtils::GetNSWindowPtr(mWnd); + if (win && !win->Destroyed()) { + return true; + } + } + return false; +} + +void TaskbarPreview::DetachFromNSWindow() { + if (WindowHook* hook = GetWindowHook()) { + hook->RemoveMonitor(WM_DESTROY, MainWindowHook, this); + } + mWnd = nullptr; +} + +LRESULT +TaskbarPreview::WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam) { + switch (nMsg) { + case WM_DWMSENDICONICTHUMBNAIL: { + uint32_t width = HIWORD(lParam); + uint32_t height = LOWORD(lParam); + float aspectRatio = width / float(height); + + nsresult rv; + float preferredAspectRatio; + rv = mController->GetThumbnailAspectRatio(&preferredAspectRatio); + if (NS_FAILED(rv)) break; + + uint32_t thumbnailWidth = width; + uint32_t thumbnailHeight = height; + + if (aspectRatio > preferredAspectRatio) { + thumbnailWidth = uint32_t(thumbnailHeight * preferredAspectRatio); + } else { + thumbnailHeight = uint32_t(thumbnailWidth / preferredAspectRatio); + } + + DrawBitmap(thumbnailWidth, thumbnailHeight, false); + } break; + case WM_DWMSENDICONICLIVEPREVIEWBITMAP: { + uint32_t width, height; + nsresult rv; + rv = mController->GetWidth(&width); + if (NS_FAILED(rv)) break; + rv = mController->GetHeight(&height); + if (NS_FAILED(rv)) break; + + double scale = StaticPrefs::layout_css_devPixelsPerPx(); + if (scale <= 0.0) { + scale = WinUtils::LogToPhysFactor(PreviewWindow()); + } + DrawBitmap(NSToIntRound(scale * width), NSToIntRound(scale * height), + true); + } break; + } + return ::DefWindowProcW(PreviewWindow(), nMsg, wParam, lParam); +} + +bool TaskbarPreview::CanMakeTaskbarCalls() { + // If the nsWindow has already been destroyed and we know it but our caller + // clearly doesn't so we can't make any calls. + if (!mWnd) return false; + // Certain functions like SetTabOrder seem to require a visible window. During + // window close, the window seems to be hidden before being destroyed. + if (!::IsWindowVisible(mWnd)) return false; + if (mVisible) { + nsWindow* window = WinUtils::GetNSWindowPtr(mWnd); + NS_ASSERTION(window, "Could not get nsWindow from HWND"); + return window ? window->HasTaskbarIconBeenCreated() : false; + } + return false; +} + +WindowHook* TaskbarPreview::GetWindowHook() { + nsWindow* window = WinUtils::GetNSWindowPtr(mWnd); + NS_ASSERTION(window, "Cannot use taskbar previews in an embedded context!"); + + return window ? &window->GetWindowHook() : nullptr; +} + +void TaskbarPreview::EnableCustomDrawing(HWND aHWND, bool aEnable) { + BOOL enabled = aEnable; + DwmSetWindowAttribute(aHWND, DWMWA_FORCE_ICONIC_REPRESENTATION, &enabled, + sizeof(enabled)); + + DwmSetWindowAttribute(aHWND, DWMWA_HAS_ICONIC_BITMAP, &enabled, + sizeof(enabled)); +} + +nsresult TaskbarPreview::UpdateTooltip() { + NS_ASSERTION(CanMakeTaskbarCalls() && mVisible, + "UpdateTooltip called on invisible tab preview"); + + if (FAILED(mTaskbar->SetThumbnailTooltip(PreviewWindow(), mTooltip.get()))) + return NS_ERROR_FAILURE; + return NS_OK; +} + +void TaskbarPreview::DrawBitmap(uint32_t width, uint32_t height, + bool isPreview) { + nsresult rv; + nsCOMPtr callback = + do_CreateInstance("@mozilla.org/widget/taskbar-preview-callback;1", &rv); + if (NS_FAILED(rv)) { + return; + } + + ((TaskbarPreviewCallback*)callback.get())->SetPreview(this); + + if (isPreview) { + ((TaskbarPreviewCallback*)callback.get())->SetIsPreview(); + mController->RequestPreview(callback); + } else { + mController->RequestThumbnail(callback, width, height); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// TaskbarPreviewCallback + +NS_IMPL_ISUPPORTS(TaskbarPreviewCallback, nsITaskbarPreviewCallback) + +/* void done (in nsISupports aCanvas, in boolean aDrawBorder); */ +NS_IMETHODIMP +TaskbarPreviewCallback::Done(nsISupports* aCanvas, bool aDrawBorder) { + // We create and destroy TaskbarTabPreviews from front end code in response + // to TabOpen and TabClose events. Each TaskbarTabPreview creates and owns a + // proxy HWND which it hands to Windows as a tab identifier. When a tab + // closes, TaskbarTabPreview Disable() method is called by front end, which + // destroys the proxy window and clears mProxyWindow which is the HWND + // returned from PreviewWindow(). So, since this is async, we should check to + // be sure the tab is still alive before doing all this gfx work and making + // dwm calls. To accomplish this we check the result of PreviewWindow(). + if (!aCanvas || !mPreview || !mPreview->PreviewWindow() || + !mPreview->IsWindowAvailable()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr content(do_QueryInterface(aCanvas)); + auto canvas = dom::HTMLCanvasElement::FromNodeOrNull(content); + if (!canvas) { + return NS_ERROR_FAILURE; + } + + RefPtr source = canvas->GetSurfaceSnapshot(); + if (!source) { + return NS_ERROR_FAILURE; + } + RefPtr target = new gfxWindowsSurface( + source->GetSize(), gfx::SurfaceFormat::A8R8G8B8_UINT32); + if (target->CairoStatus() != CAIRO_STATUS_SUCCESS) { + return NS_ERROR_FAILURE; + } + + using DataSrcSurf = gfx::DataSourceSurface; + RefPtr srcSurface = source->GetDataSurface(); + RefPtr imageSurface = target->GetAsImageSurface(); + if (!srcSurface || !imageSurface) { + return NS_ERROR_FAILURE; + } + + if (DataSrcSurf::ScopedMap const sourceMap(srcSurface, DataSrcSurf::READ); + sourceMap.IsMapped()) { + mozilla::gfx::CopySurfaceDataToPackedArray( + sourceMap.GetData(), imageSurface->Data(), srcSurface->GetSize(), + sourceMap.GetStride(), BytesPerPixel(srcSurface->GetFormat())); + } else if (source->GetSize().IsEmpty()) { + // A zero-size source-surface probably shouldn't happen, but is harmless + // here. Fall through. + } else { + return NS_ERROR_FAILURE; + } + + HDC hDC = target->GetDC(); + HBITMAP hBitmap = (HBITMAP)GetCurrentObject(hDC, OBJ_BITMAP); + + DWORD flags = aDrawBorder ? DWM_SIT_DISPLAYFRAME : 0; + HRESULT hr; + if (!mIsThumbnail) { + POINT pptClient = {0, 0}; + hr = DwmSetIconicLivePreviewBitmap(mPreview->PreviewWindow(), hBitmap, + &pptClient, flags); + } else { + hr = DwmSetIconicThumbnail(mPreview->PreviewWindow(), hBitmap, flags); + } + MOZ_ASSERT(SUCCEEDED(hr)); + mozilla::Unused << hr; + return NS_OK; +} + +/* static */ +bool TaskbarPreview::MainWindowHook(void* aContext, HWND hWnd, UINT nMsg, + WPARAM wParam, LPARAM lParam, + LRESULT* aResult) { + NS_ASSERTION(nMsg == nsAppShell::GetTaskbarButtonCreatedMessage() || + nMsg == WM_DESTROY, + "Window hook proc called with wrong message"); + NS_ASSERTION(aContext, "Null context in MainWindowHook"); + if (!aContext) return false; + TaskbarPreview* preview = reinterpret_cast(aContext); + if (nMsg == WM_DESTROY) { + // nsWindow is being destroyed + // We can't really do anything at this point including removing hooks + return false; + } else { + nsWindow* window = WinUtils::GetNSWindowPtr(preview->mWnd); + if (window) { + window->SetHasTaskbarIconBeenCreated(); + + if (preview->mVisible) preview->UpdateTaskbarProperties(); + } + } + return false; +} + +TaskbarPreview* TaskbarPreview::sActivePreview = nullptr; + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/TaskbarPreview.h b/widget/windows/TaskbarPreview.h new file mode 100644 index 0000000000..ca21e3eeb8 --- /dev/null +++ b/widget/windows/TaskbarPreview.h @@ -0,0 +1,132 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __mozilla_widget_TaskbarPreview_h__ +#define __mozilla_widget_TaskbarPreview_h__ + +#include +#include +#undef LogSeverity // SetupAPI.h #defines this as DWORD + +#include "mozilla/RefPtr.h" +#include +#include +#include +#include +#include +#include "WindowHook.h" + +namespace mozilla { +namespace widget { + +class TaskbarPreviewCallback; + +class TaskbarPreview : public nsITaskbarPreview { + public: + TaskbarPreview(ITaskbarList4* aTaskbar, + nsITaskbarPreviewController* aController, HWND aHWND, + nsIDocShell* aShell); + virtual nsresult Init(); + + friend class TaskbarPreviewCallback; + + NS_DECL_NSITASKBARPREVIEW + + protected: + virtual ~TaskbarPreview(); + + // Called to update ITaskbarList4 dependent properties + virtual nsresult UpdateTaskbarProperties(); + + // Invoked when the preview is made visible + virtual nsresult Enable(); + // Invoked when the preview is made invisible + virtual nsresult Disable(); + + // Detaches this preview from the nsWindow instance it's tied to + virtual void DetachFromNSWindow(); + + // Determines if the window is available and a destroy has not yet started + bool IsWindowAvailable() const; + + // Marks this preview as being active + virtual nsresult ShowActive(bool active) = 0; + // Gets a reference to the window used to handle the preview messages + virtual HWND& PreviewWindow() = 0; + + // Window procedure for the PreviewWindow (hooked for window previews) + virtual LRESULT WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam); + + // Returns whether or not the taskbar icon has been created for mWnd The + // ITaskbarList4 API requires that we wait until the icon has been created + // before we can call its methods. + bool CanMakeTaskbarCalls(); + + // Gets the WindowHook for the nsWindow + WindowHook* GetWindowHook(); + + // Enables/disables custom drawing for the given window + static void EnableCustomDrawing(HWND aHWND, bool aEnable); + + // MSCOM Taskbar interface + RefPtr mTaskbar; + // Controller for this preview + nsCOMPtr mController; + // The HWND to the nsWindow that this object previews + HWND mWnd; + // Whether or not this preview is visible + bool mVisible; + + private: + // Called when the tooltip should be updated + nsresult UpdateTooltip(); + + // Requests the controller to draw into a canvas of the given width and + // height. The resulting bitmap is sent to the DWM to display. + void DrawBitmap(uint32_t width, uint32_t height, bool isPreview); + + // WindowHook procedure for hooking mWnd + static bool MainWindowHook(void* aContext, HWND hWnd, UINT nMsg, + WPARAM wParam, LPARAM lParam, LRESULT* aResult); + + // Docshell corresponding to the the nsWindow contains + nsWeakPtr mDocShell; + nsString mTooltip; + + // The preview currently marked as active in the taskbar. nullptr if no + // preview is active (some other window is). + static TaskbarPreview* sActivePreview; +}; + +/* + * Callback object TaskbarPreview hands to preview controllers when we + * request async thumbnail or live preview images. Controllers invoke + * this interface once they have aquired the requested image. + */ +class TaskbarPreviewCallback : public nsITaskbarPreviewCallback { + public: + TaskbarPreviewCallback() : mIsThumbnail(true) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSITASKBARPREVIEWCALLBACK + + void SetPreview(TaskbarPreview* aPreview) { mPreview = aPreview; } + + void SetIsPreview() { mIsThumbnail = false; } + + protected: + virtual ~TaskbarPreviewCallback() {} + + private: + RefPtr mPreview; + bool mIsThumbnail; +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __mozilla_widget_TaskbarPreview_h__ */ diff --git a/widget/windows/TaskbarPreviewButton.cpp b/widget/windows/TaskbarPreviewButton.cpp new file mode 100644 index 0000000000..abbb1069a9 --- /dev/null +++ b/widget/windows/TaskbarPreviewButton.cpp @@ -0,0 +1,137 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- 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 +#include + +#include "TaskbarWindowPreview.h" +#include "TaskbarPreviewButton.h" +#include "nsWindowGfx.h" +#include + +namespace mozilla { +namespace widget { + +NS_IMPL_ISUPPORTS(TaskbarPreviewButton, nsITaskbarPreviewButton, + nsISupportsWeakReference) + +TaskbarPreviewButton::TaskbarPreviewButton(TaskbarWindowPreview* preview, + uint32_t index) + : mPreview(preview), mIndex(index) {} + +TaskbarPreviewButton::~TaskbarPreviewButton() { SetVisible(false); } + +NS_IMETHODIMP +TaskbarPreviewButton::GetTooltip(nsAString& aTooltip) { + aTooltip = mTooltip; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreviewButton::SetTooltip(const nsAString& aTooltip) { + mTooltip = aTooltip; + size_t destLength = sizeof Button().szTip / (sizeof Button().szTip[0]); + wchar_t* tooltip = &(Button().szTip[0]); + StringCchCopyNW(tooltip, destLength, mTooltip.get(), mTooltip.Length()); + return Update(); +} + +NS_IMETHODIMP +TaskbarPreviewButton::GetDismissOnClick(bool* dismiss) { + *dismiss = (Button().dwFlags & THBF_DISMISSONCLICK) == THBF_DISMISSONCLICK; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreviewButton::SetDismissOnClick(bool dismiss) { + if (dismiss) + Button().dwFlags |= THBF_DISMISSONCLICK; + else + Button().dwFlags &= ~THBF_DISMISSONCLICK; + return Update(); +} + +NS_IMETHODIMP +TaskbarPreviewButton::GetHasBorder(bool* hasBorder) { + *hasBorder = (Button().dwFlags & THBF_NOBACKGROUND) != THBF_NOBACKGROUND; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreviewButton::SetHasBorder(bool hasBorder) { + if (hasBorder) + Button().dwFlags &= ~THBF_NOBACKGROUND; + else + Button().dwFlags |= THBF_NOBACKGROUND; + return Update(); +} + +NS_IMETHODIMP +TaskbarPreviewButton::GetDisabled(bool* disabled) { + *disabled = (Button().dwFlags & THBF_DISABLED) == THBF_DISABLED; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreviewButton::SetDisabled(bool disabled) { + if (disabled) + Button().dwFlags |= THBF_DISABLED; + else + Button().dwFlags &= ~THBF_DISABLED; + return Update(); +} + +NS_IMETHODIMP +TaskbarPreviewButton::GetImage(imgIContainer** img) { + if (mImage) + NS_ADDREF(*img = mImage); + else + *img = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreviewButton::SetImage(imgIContainer* img) { + if (Button().hIcon) ::DestroyIcon(Button().hIcon); + if (img) { + nsresult rv; + rv = nsWindowGfx::CreateIcon( + img, false, LayoutDeviceIntPoint(), + nsWindowGfx::GetIconMetrics(nsWindowGfx::kRegularIcon), + &Button().hIcon); + NS_ENSURE_SUCCESS(rv, rv); + } else { + Button().hIcon = nullptr; + } + return Update(); +} + +NS_IMETHODIMP +TaskbarPreviewButton::GetVisible(bool* visible) { + *visible = (Button().dwFlags & THBF_HIDDEN) != THBF_HIDDEN; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarPreviewButton::SetVisible(bool visible) { + if (visible) + Button().dwFlags &= ~THBF_HIDDEN; + else + Button().dwFlags |= THBF_HIDDEN; + return Update(); +} + +THUMBBUTTON& TaskbarPreviewButton::Button() { + return mPreview->mThumbButtons[mIndex]; +} + +nsresult TaskbarPreviewButton::Update() { + return mPreview->UpdateButton(mIndex); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/TaskbarPreviewButton.h b/widget/windows/TaskbarPreviewButton.h new file mode 100644 index 0000000000..e13b2d6771 --- /dev/null +++ b/widget/windows/TaskbarPreviewButton.h @@ -0,0 +1,47 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __mozilla_widget_TaskbarPreviewButton_h__ +#define __mozilla_widget_TaskbarPreviewButton_h__ + +#include +#include +#undef LogSeverity // SetupAPI.h #defines this as DWORD + +#include "mozilla/RefPtr.h" +#include +#include +#include "nsWeakReference.h" + +namespace mozilla { +namespace widget { + +class TaskbarWindowPreview; +class TaskbarPreviewButton : public nsITaskbarPreviewButton, + public nsSupportsWeakReference { + virtual ~TaskbarPreviewButton(); + + public: + TaskbarPreviewButton(TaskbarWindowPreview* preview, uint32_t index); + + NS_DECL_ISUPPORTS + NS_DECL_NSITASKBARPREVIEWBUTTON + + private: + THUMBBUTTON& Button(); + nsresult Update(); + + RefPtr mPreview; + uint32_t mIndex; + nsString mTooltip; + nsCOMPtr mImage; +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __mozilla_widget_TaskbarPreviewButton_h__ */ diff --git a/widget/windows/TaskbarTabPreview.cpp b/widget/windows/TaskbarTabPreview.cpp new file mode 100644 index 0000000000..a19a6bf12e --- /dev/null +++ b/widget/windows/TaskbarTabPreview.cpp @@ -0,0 +1,345 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- 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 "TaskbarTabPreview.h" +#include "nsWindowGfx.h" +#include "nsUXThemeData.h" +#include "WinUtils.h" +#include + +#define TASKBARPREVIEW_HWNDID L"TaskbarTabPreviewHwnd" + +namespace mozilla { +namespace widget { + +NS_IMPL_ISUPPORTS(TaskbarTabPreview, nsITaskbarTabPreview) + +const wchar_t* const kWindowClass = L"MozillaTaskbarPreviewClass"; + +TaskbarTabPreview::TaskbarTabPreview(ITaskbarList4* aTaskbar, + nsITaskbarPreviewController* aController, + HWND aHWND, nsIDocShell* aShell) + : TaskbarPreview(aTaskbar, aController, aHWND, aShell), + mProxyWindow(nullptr), + mIcon(nullptr), + mRegistered(false) {} + +TaskbarTabPreview::~TaskbarTabPreview() { + if (mIcon) { + ::DestroyIcon(mIcon); + mIcon = nullptr; + } + + // We need to ensure that proxy window disappears or else Bad Things happen. + if (mProxyWindow) Disable(); + + NS_ASSERTION(!mProxyWindow, "Taskbar proxy window was not destroyed!"); + + if (IsWindowAvailable()) { + DetachFromNSWindow(); + } else { + mWnd = nullptr; + } +} + +nsresult TaskbarTabPreview::Init() { + nsresult rv = TaskbarPreview::Init(); + if (NS_FAILED(rv)) { + return rv; + } + + WindowHook* hook = GetWindowHook(); + if (!hook) { + return NS_ERROR_NOT_AVAILABLE; + } + + return hook->AddMonitor(WM_WINDOWPOSCHANGED, MainWindowHook, this); +} + +nsresult TaskbarTabPreview::ShowActive(bool active) { + NS_ASSERTION(mVisible && CanMakeTaskbarCalls(), + "ShowActive called on invisible window or before taskbar calls " + "can be made for this window"); + return FAILED( + mTaskbar->SetTabActive(active ? mProxyWindow : nullptr, mWnd, 0)) + ? NS_ERROR_FAILURE + : NS_OK; +} + +HWND& TaskbarTabPreview::PreviewWindow() { return mProxyWindow; } + +nativeWindow TaskbarTabPreview::GetHWND() { return mProxyWindow; } + +void TaskbarTabPreview::EnsureRegistration() { + NS_ASSERTION(mVisible && CanMakeTaskbarCalls(), + "EnsureRegistration called when it is not safe to do so"); + + (void)UpdateTaskbarProperties(); +} + +NS_IMETHODIMP +TaskbarTabPreview::GetTitle(nsAString& aTitle) { + aTitle = mTitle; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarTabPreview::SetTitle(const nsAString& aTitle) { + mTitle = aTitle; + return mVisible ? UpdateTitle() : NS_OK; +} + +NS_IMETHODIMP +TaskbarTabPreview::SetIcon(imgIContainer* icon) { + HICON hIcon = nullptr; + if (icon) { + nsresult rv; + rv = nsWindowGfx::CreateIcon( + icon, false, LayoutDeviceIntPoint(), + nsWindowGfx::GetIconMetrics(nsWindowGfx::kSmallIcon), &hIcon); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mIcon) ::DestroyIcon(mIcon); + mIcon = hIcon; + mIconImage = icon; + return mVisible ? UpdateIcon() : NS_OK; +} + +NS_IMETHODIMP +TaskbarTabPreview::GetIcon(imgIContainer** icon) { + NS_IF_ADDREF(*icon = mIconImage); + return NS_OK; +} + +NS_IMETHODIMP +TaskbarTabPreview::Move(nsITaskbarTabPreview* aNext) { + if (aNext == this) return NS_ERROR_INVALID_ARG; + mNext = aNext; + return CanMakeTaskbarCalls() ? UpdateNext() : NS_OK; +} + +nsresult TaskbarTabPreview::UpdateTaskbarProperties() { + if (mRegistered) return NS_OK; + + if (FAILED(mTaskbar->RegisterTab(mProxyWindow, mWnd))) + return NS_ERROR_FAILURE; + + nsresult rv = UpdateNext(); + NS_ENSURE_SUCCESS(rv, rv); + rv = TaskbarPreview::UpdateTaskbarProperties(); + mRegistered = true; + return rv; +} + +LRESULT +TaskbarTabPreview::WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam) { + RefPtr kungFuDeathGrip(this); + switch (nMsg) { + case WM_CREATE: + TaskbarPreview::EnableCustomDrawing(mProxyWindow, true); + return 0; + case WM_CLOSE: + mController->OnClose(); + return 0; + case WM_ACTIVATE: + if (LOWORD(wParam) == WA_ACTIVE) { + // Activate the tab the user selected then restore the main window, + // keeping normal/max window state intact. + bool activateWindow; + nsresult rv = mController->OnActivate(&activateWindow); + if (NS_SUCCEEDED(rv) && activateWindow) { + nsWindow* win = WinUtils::GetNSWindowPtr(mWnd); + if (win) { + nsWindow* parent = win->GetTopLevelWindow(true); + if (parent) { + parent->Show(true); + } + } + } + } + return 0; + case WM_GETICON: + return (LRESULT)mIcon; + case WM_SYSCOMMAND: + // Send activation events to the top level window and select the proper + // tab through the controller. + if (wParam == SC_RESTORE || wParam == SC_MAXIMIZE) { + bool activateWindow; + nsresult rv = mController->OnActivate(&activateWindow); + if (NS_SUCCEEDED(rv) && activateWindow) { + // Note, restoring an iconic, maximized window here will only + // activate the maximized window. This is not a bug, it's default + // windows behavior. + ::SendMessageW(mWnd, WM_SYSCOMMAND, wParam, lParam); + } + return 0; + } + // Forward everything else to the top level window. Do not forward + // close since that's intended for the tab. When the preview proxy + // closes, we'll close the tab above. + return wParam == SC_CLOSE + ? ::DefWindowProcW(mProxyWindow, WM_SYSCOMMAND, wParam, lParam) + : ::SendMessageW(mWnd, WM_SYSCOMMAND, wParam, lParam); + return 0; + } + return TaskbarPreview::WndProc(nMsg, wParam, lParam); +} + +/* static */ +LRESULT CALLBACK TaskbarTabPreview::GlobalWndProc(HWND hWnd, UINT nMsg, + WPARAM wParam, + LPARAM lParam) { + TaskbarTabPreview* preview(nullptr); + if (nMsg == WM_CREATE) { + CREATESTRUCT* cs = reinterpret_cast(lParam); + preview = reinterpret_cast(cs->lpCreateParams); + if (!::SetPropW(hWnd, TASKBARPREVIEW_HWNDID, preview)) + NS_ERROR("Could not associate native window with tab preview"); + preview->mProxyWindow = hWnd; + } else { + preview = reinterpret_cast( + ::GetPropW(hWnd, TASKBARPREVIEW_HWNDID)); + if (nMsg == WM_DESTROY) ::RemovePropW(hWnd, TASKBARPREVIEW_HWNDID); + } + + if (preview) return preview->WndProc(nMsg, wParam, lParam); + return ::DefWindowProcW(hWnd, nMsg, wParam, lParam); +} + +nsresult TaskbarTabPreview::Enable() { + WNDCLASSW wc; + HINSTANCE module = GetModuleHandle(nullptr); + + if (!GetClassInfoW(module, kWindowClass, &wc)) { + wc.style = 0; + wc.lpfnWndProc = GlobalWndProc; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hInstance = module; + wc.hIcon = nullptr; + wc.hCursor = nullptr; + wc.hbrBackground = (HBRUSH) nullptr; + wc.lpszMenuName = (LPCWSTR) nullptr; + wc.lpszClassName = kWindowClass; + RegisterClassW(&wc); + } + ::CreateWindowW(kWindowClass, L"TaskbarPreviewWindow", + WS_CAPTION | WS_SYSMENU, 0, 0, 200, 60, nullptr, nullptr, + module, this); + // GlobalWndProc will set mProxyWindow so that WM_CREATE can have a valid HWND + if (!mProxyWindow) return NS_ERROR_INVALID_ARG; + + UpdateProxyWindowStyle(); + + nsresult rv = TaskbarPreview::Enable(); + nsresult rvUpdate; + rvUpdate = UpdateTitle(); + if (NS_FAILED(rvUpdate)) rv = rvUpdate; + + rvUpdate = UpdateIcon(); + if (NS_FAILED(rvUpdate)) rv = rvUpdate; + + return rv; +} + +nsresult TaskbarTabPreview::Disable() { + // TaskbarPreview::Disable assumes that mWnd is valid but this method can be + // called when it is null iff the nsWindow has already been destroyed and we + // are still visible for some reason during object destruction. + if (mWnd) TaskbarPreview::Disable(); + + if (FAILED(mTaskbar->UnregisterTab(mProxyWindow))) return NS_ERROR_FAILURE; + mRegistered = false; + + // TaskbarPreview::WndProc will set mProxyWindow to null + if (!DestroyWindow(mProxyWindow)) return NS_ERROR_FAILURE; + mProxyWindow = nullptr; + return NS_OK; +} + +void TaskbarTabPreview::DetachFromNSWindow() { + (void)SetVisible(false); + if (WindowHook* hook = GetWindowHook()) { + hook->RemoveMonitor(WM_WINDOWPOSCHANGED, MainWindowHook, this); + } + TaskbarPreview::DetachFromNSWindow(); +} + +/* static */ +bool TaskbarTabPreview::MainWindowHook(void* aContext, HWND hWnd, UINT nMsg, + WPARAM wParam, LPARAM lParam, + LRESULT* aResult) { + if (nMsg == WM_WINDOWPOSCHANGED) { + TaskbarTabPreview* preview = reinterpret_cast(aContext); + WINDOWPOS* pos = reinterpret_cast(lParam); + if (SWP_FRAMECHANGED == (pos->flags & SWP_FRAMECHANGED)) + preview->UpdateProxyWindowStyle(); + } else { + MOZ_ASSERT_UNREACHABLE( + "Style changed hook fired on non-style changed " + "message"); + } + return false; +} + +void TaskbarTabPreview::UpdateProxyWindowStyle() { + if (!mProxyWindow) return; + + DWORD minMaxMask = WS_MINIMIZE | WS_MAXIMIZE; + DWORD windowStyle = GetWindowLongW(mWnd, GWL_STYLE); + + DWORD proxyStyle = GetWindowLongW(mProxyWindow, GWL_STYLE); + proxyStyle &= ~minMaxMask; + proxyStyle |= windowStyle & minMaxMask; + SetWindowLongW(mProxyWindow, GWL_STYLE, proxyStyle); + + DWORD exStyle = + (WS_MAXIMIZE == (windowStyle & WS_MAXIMIZE)) ? WS_EX_TOOLWINDOW : 0; + SetWindowLongW(mProxyWindow, GWL_EXSTYLE, exStyle); +} + +nsresult TaskbarTabPreview::UpdateTitle() { + NS_ASSERTION(mVisible, "UpdateTitle called on invisible preview"); + + if (!::SetWindowTextW(mProxyWindow, mTitle.get())) return NS_ERROR_FAILURE; + return NS_OK; +} + +nsresult TaskbarTabPreview::UpdateIcon() { + NS_ASSERTION(mVisible, "UpdateIcon called on invisible preview"); + + ::SendMessageW(mProxyWindow, WM_SETICON, ICON_SMALL, (LPARAM)mIcon); + + return NS_OK; +} + +nsresult TaskbarTabPreview::UpdateNext() { + NS_ASSERTION(CanMakeTaskbarCalls() && mVisible, + "UpdateNext called on invisible tab preview"); + HWND hNext = nullptr; + if (mNext) { + bool visible; + nsresult rv = mNext->GetVisible(&visible); + + NS_ENSURE_SUCCESS(rv, rv); + + // Can only move next to enabled previews + if (!visible) return NS_ERROR_FAILURE; + + hNext = (HWND)mNext->GetHWND(); + + // hNext must be registered with the taskbar if the call is to succeed + mNext->EnsureRegistration(); + } + if (FAILED(mTaskbar->SetTabOrder(mProxyWindow, hNext))) + return NS_ERROR_FAILURE; + return NS_OK; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/TaskbarTabPreview.h b/widget/windows/TaskbarTabPreview.h new file mode 100644 index 0000000000..9a15760557 --- /dev/null +++ b/widget/windows/TaskbarTabPreview.h @@ -0,0 +1,70 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __mozilla_widget_TaskbarTabPreview_h__ +#define __mozilla_widget_TaskbarTabPreview_h__ + +#include "nsITaskbarTabPreview.h" +#include "TaskbarPreview.h" + +namespace mozilla { +namespace widget { + +class TaskbarTabPreview : public nsITaskbarTabPreview, public TaskbarPreview { + virtual ~TaskbarTabPreview(); + + public: + TaskbarTabPreview(ITaskbarList4* aTaskbar, + nsITaskbarPreviewController* aController, HWND aHWND, + nsIDocShell* aShell); + virtual nsresult Init() override; + + NS_DECL_ISUPPORTS + NS_DECL_NSITASKBARTABPREVIEW + NS_FORWARD_NSITASKBARPREVIEW(TaskbarPreview::) + + private: + virtual nsresult ShowActive(bool active); + virtual HWND& PreviewWindow(); + virtual LRESULT WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam); + static LRESULT CALLBACK GlobalWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, + LPARAM lParam); + + virtual nsresult UpdateTaskbarProperties(); + virtual nsresult Enable(); + virtual nsresult Disable(); + virtual void DetachFromNSWindow(); + + // WindowHook procedure for hooking mWnd + static bool MainWindowHook(void* aContext, HWND hWnd, UINT nMsg, + WPARAM wParam, LPARAM lParam, LRESULT* aResult); + + // Bug 520807 - we need to update the proxy window style based on the main + // window's style to workaround a bug with the way the DWM displays the + // previews. + void UpdateProxyWindowStyle(); + + nsresult UpdateTitle(); + nsresult UpdateIcon(); + nsresult UpdateNext(); + + // Handle to the toplevel proxy window + HWND mProxyWindow; + nsString mTitle; + nsCOMPtr mIconImage; + // Cached Windows icon of mIconImage + HICON mIcon; + // Preview that follows this preview in the taskbar (left-to-right order) + nsCOMPtr mNext; + // True if this preview has been registered with the taskbar + bool mRegistered; +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __mozilla_widget_TaskbarTabPreview_h__ */ diff --git a/widget/windows/TaskbarWindowPreview.cpp b/widget/windows/TaskbarWindowPreview.cpp new file mode 100644 index 0000000000..212278be4a --- /dev/null +++ b/widget/windows/TaskbarWindowPreview.cpp @@ -0,0 +1,326 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- 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 "mozilla/ArrayUtils.h" + +#include +#include "TaskbarWindowPreview.h" +#include "WindowHook.h" +#include "nsUXThemeData.h" +#include "TaskbarPreviewButton.h" +#include "nsWindow.h" +#include "nsWindowGfx.h" + +namespace mozilla { +namespace widget { + +namespace { +bool WindowHookProc(void* aContext, HWND hWnd, UINT nMsg, WPARAM wParam, + LPARAM lParam, LRESULT* aResult) { + TaskbarWindowPreview* preview = + reinterpret_cast(aContext); + *aResult = preview->WndProc(nMsg, wParam, lParam); + return true; +} +} // namespace + +NS_IMPL_ISUPPORTS(TaskbarWindowPreview, nsITaskbarWindowPreview, + nsITaskbarProgress, nsITaskbarOverlayIconController, + nsISupportsWeakReference) + +/** + * These correspond directly to the states defined in nsITaskbarProgress.idl, so + * they should be kept in sync. + */ +static TBPFLAG sNativeStates[] = {TBPF_NOPROGRESS, TBPF_INDETERMINATE, + TBPF_NORMAL, TBPF_ERROR, TBPF_PAUSED}; + +TaskbarWindowPreview::TaskbarWindowPreview( + ITaskbarList4* aTaskbar, nsITaskbarPreviewController* aController, + HWND aHWND, nsIDocShell* aShell) + : TaskbarPreview(aTaskbar, aController, aHWND, aShell), + mCustomDrawing(false), + mHaveButtons(false), + mState(TBPF_NOPROGRESS), + mCurrentValue(0), + mMaxValue(0), + mOverlayIcon(nullptr) { + // Window previews are visible by default + (void)SetVisible(true); + + memset(mThumbButtons, 0, sizeof mThumbButtons); + for (int32_t i = 0; i < nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS; i++) { + mThumbButtons[i].dwMask = THB_FLAGS | THB_ICON | THB_TOOLTIP; + mThumbButtons[i].iId = i; + mThumbButtons[i].dwFlags = THBF_HIDDEN; + } +} + +TaskbarWindowPreview::~TaskbarWindowPreview() { + if (mOverlayIcon) { + ::DestroyIcon(mOverlayIcon); + mOverlayIcon = nullptr; + } + + // We need to clean up a hook associated with the "this" pointer. + SetVisible(false); + + if (IsWindowAvailable()) { + DetachFromNSWindow(); + } else { + mWnd = nullptr; + } +} + +nsresult TaskbarWindowPreview::Init() { + nsresult rv = TaskbarPreview::Init(); + if (NS_FAILED(rv)) { + return rv; + } + + if (CanMakeTaskbarCalls()) { + return NS_OK; + } + + WindowHook* hook = GetWindowHook(); + if (!hook) { + return NS_ERROR_NOT_AVAILABLE; + } + + return hook->AddMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(), + TaskbarWindowHook, this); +} + +nsresult TaskbarWindowPreview::ShowActive(bool active) { + return FAILED(mTaskbar->ActivateTab(active ? mWnd : nullptr)) + ? NS_ERROR_FAILURE + : NS_OK; +} + +HWND& TaskbarWindowPreview::PreviewWindow() { return mWnd; } + +nsresult TaskbarWindowPreview::GetButton(uint32_t index, + nsITaskbarPreviewButton** _retVal) { + if (index >= nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS) + return NS_ERROR_INVALID_ARG; + + nsCOMPtr button( + do_QueryReferent(mWeakButtons[index])); + + if (!button) { + // Lost reference + button = new TaskbarPreviewButton(this, index); + if (!button) { + return NS_ERROR_OUT_OF_MEMORY; + } + mWeakButtons[index] = do_GetWeakReference(button); + } + + if (!mHaveButtons) { + mHaveButtons = true; + + WindowHook* hook = GetWindowHook(); + if (!hook) { + return NS_ERROR_NOT_AVAILABLE; + } + (void)hook->AddHook(WM_COMMAND, WindowHookProc, this); + + if (mVisible && FAILED(mTaskbar->ThumbBarAddButtons( + mWnd, nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS, + mThumbButtons))) { + return NS_ERROR_FAILURE; + } + } + button.forget(_retVal); + return NS_OK; +} + +NS_IMETHODIMP +TaskbarWindowPreview::SetEnableCustomDrawing(bool aEnable) { + if (aEnable == mCustomDrawing) return NS_OK; + + WindowHook* hook = GetWindowHook(); + if (!hook) { + return NS_ERROR_NOT_AVAILABLE; + } + + mCustomDrawing = aEnable; + TaskbarPreview::EnableCustomDrawing(mWnd, aEnable); + + if (aEnable) { + (void)hook->AddHook(WM_DWMSENDICONICTHUMBNAIL, WindowHookProc, this); + (void)hook->AddHook(WM_DWMSENDICONICLIVEPREVIEWBITMAP, WindowHookProc, + this); + } else { + (void)hook->RemoveHook(WM_DWMSENDICONICLIVEPREVIEWBITMAP, WindowHookProc, + this); + (void)hook->RemoveHook(WM_DWMSENDICONICTHUMBNAIL, WindowHookProc, this); + } + return NS_OK; +} + +NS_IMETHODIMP +TaskbarWindowPreview::GetEnableCustomDrawing(bool* aEnable) { + *aEnable = mCustomDrawing; + return NS_OK; +} + +NS_IMETHODIMP +TaskbarWindowPreview::SetProgressState(nsTaskbarProgressState aState, + uint64_t aCurrentValue, + uint64_t aMaxValue) { + NS_ENSURE_ARG_RANGE(aState, nsTaskbarProgressState(0), + nsTaskbarProgressState(ArrayLength(sNativeStates) - 1)); + + TBPFLAG nativeState = sNativeStates[aState]; + if (nativeState == TBPF_NOPROGRESS || nativeState == TBPF_INDETERMINATE) { + NS_ENSURE_TRUE(aCurrentValue == 0, NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(aMaxValue == 0, NS_ERROR_INVALID_ARG); + } + + if (aCurrentValue > aMaxValue) return NS_ERROR_ILLEGAL_VALUE; + + mState = nativeState; + mCurrentValue = aCurrentValue; + mMaxValue = aMaxValue; + + // Only update if we can + return CanMakeTaskbarCalls() ? UpdateTaskbarProgress() : NS_OK; +} + +NS_IMETHODIMP +TaskbarWindowPreview::SetOverlayIcon(imgIContainer* aStatusIcon, + const nsAString& aStatusDescription) { + nsresult rv; + if (aStatusIcon) { + // The image shouldn't be animated + bool isAnimated; + rv = aStatusIcon->GetAnimated(&isAnimated); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_FALSE(isAnimated, NS_ERROR_INVALID_ARG); + } + + HICON hIcon = nullptr; + if (aStatusIcon) { + rv = nsWindowGfx::CreateIcon( + aStatusIcon, false, LayoutDeviceIntPoint(), + nsWindowGfx::GetIconMetrics(nsWindowGfx::kSmallIcon), &hIcon); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mOverlayIcon) ::DestroyIcon(mOverlayIcon); + mOverlayIcon = hIcon; + mIconDescription = aStatusDescription; + + // Only update if we can + return CanMakeTaskbarCalls() ? UpdateOverlayIcon() : NS_OK; +} + +nsresult TaskbarWindowPreview::UpdateTaskbarProperties() { + if (mHaveButtons) { + if (FAILED(mTaskbar->ThumbBarAddButtons( + mWnd, nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS, mThumbButtons))) + return NS_ERROR_FAILURE; + } + nsresult rv = UpdateTaskbarProgress(); + NS_ENSURE_SUCCESS(rv, rv); + rv = UpdateOverlayIcon(); + NS_ENSURE_SUCCESS(rv, rv); + return TaskbarPreview::UpdateTaskbarProperties(); +} + +nsresult TaskbarWindowPreview::UpdateTaskbarProgress() { + HRESULT hr = mTaskbar->SetProgressState(mWnd, mState); + if (SUCCEEDED(hr) && mState != TBPF_NOPROGRESS && + mState != TBPF_INDETERMINATE) + hr = mTaskbar->SetProgressValue(mWnd, mCurrentValue, mMaxValue); + + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult TaskbarWindowPreview::UpdateOverlayIcon() { + HRESULT hr = + mTaskbar->SetOverlayIcon(mWnd, mOverlayIcon, mIconDescription.get()); + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; +} + +LRESULT +TaskbarWindowPreview::WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam) { + RefPtr kungFuDeathGrip(this); + switch (nMsg) { + case WM_COMMAND: { + uint32_t id = LOWORD(wParam); + uint32_t index = id; + nsCOMPtr button; + nsresult rv = GetButton(index, getter_AddRefs(button)); + if (NS_SUCCEEDED(rv)) mController->OnClick(button); + } + return 0; + } + return TaskbarPreview::WndProc(nMsg, wParam, lParam); +} + +/* static */ +bool TaskbarWindowPreview::TaskbarWindowHook(void* aContext, HWND hWnd, + UINT nMsg, WPARAM wParam, + LPARAM lParam, LRESULT* aResult) { + NS_ASSERTION(nMsg == nsAppShell::GetTaskbarButtonCreatedMessage(), + "Window hook proc called with wrong message"); + TaskbarWindowPreview* preview = + reinterpret_cast(aContext); + // Now we can make all the calls to mTaskbar + preview->UpdateTaskbarProperties(); + return false; +} + +nsresult TaskbarWindowPreview::Enable() { + nsresult rv = TaskbarPreview::Enable(); + NS_ENSURE_SUCCESS(rv, rv); + + return FAILED(mTaskbar->AddTab(mWnd)) ? NS_ERROR_FAILURE : NS_OK; +} + +nsresult TaskbarWindowPreview::Disable() { + nsresult rv = TaskbarPreview::Disable(); + NS_ENSURE_SUCCESS(rv, rv); + + return FAILED(mTaskbar->DeleteTab(mWnd)) ? NS_ERROR_FAILURE : NS_OK; +} + +void TaskbarWindowPreview::DetachFromNSWindow() { + // Remove the hooks we have for drawing + SetEnableCustomDrawing(false); + + if (WindowHook* hook = GetWindowHook()) { + (void)hook->RemoveHook(WM_COMMAND, WindowHookProc, this); + (void)hook->RemoveMonitor(nsAppShell::GetTaskbarButtonCreatedMessage(), + TaskbarWindowHook, this); + } + TaskbarPreview::DetachFromNSWindow(); +} + +nsresult TaskbarWindowPreview::UpdateButtons() { + NS_ASSERTION(mVisible, "UpdateButtons called on invisible preview"); + + if (FAILED(mTaskbar->ThumbBarUpdateButtons( + mWnd, nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS, mThumbButtons))) + return NS_ERROR_FAILURE; + return NS_OK; +} + +nsresult TaskbarWindowPreview::UpdateButton(uint32_t index) { + if (index >= nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS) + return NS_ERROR_INVALID_ARG; + if (mVisible) { + if (FAILED(mTaskbar->ThumbBarUpdateButtons(mWnd, 1, &mThumbButtons[index]))) + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/TaskbarWindowPreview.h b/widget/windows/TaskbarWindowPreview.h new file mode 100644 index 0000000000..97c074197c --- /dev/null +++ b/widget/windows/TaskbarWindowPreview.h @@ -0,0 +1,85 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __mozilla_widget_TaskbarWindowPreview_h__ +#define __mozilla_widget_TaskbarWindowPreview_h__ + +#include "nsITaskbarWindowPreview.h" +#include "nsITaskbarProgress.h" +#include "nsITaskbarOverlayIconController.h" +#include "TaskbarPreview.h" +#include "nsWeakReference.h" + +namespace mozilla { +namespace widget { + +class TaskbarPreviewButton; +class TaskbarWindowPreview : public TaskbarPreview, + public nsITaskbarWindowPreview, + public nsITaskbarProgress, + public nsITaskbarOverlayIconController, + public nsSupportsWeakReference { + virtual ~TaskbarWindowPreview(); + + public: + TaskbarWindowPreview(ITaskbarList4* aTaskbar, + nsITaskbarPreviewController* aController, HWND aHWND, + nsIDocShell* aShell); + virtual nsresult Init() override; + + NS_DECL_ISUPPORTS + NS_DECL_NSITASKBARWINDOWPREVIEW + NS_DECL_NSITASKBARPROGRESS + NS_DECL_NSITASKBAROVERLAYICONCONTROLLER + NS_FORWARD_NSITASKBARPREVIEW(TaskbarPreview::) + + virtual LRESULT WndProc(UINT nMsg, WPARAM wParam, LPARAM lParam) override; + + private: + virtual nsresult ShowActive(bool active) override; + virtual HWND& PreviewWindow() override; + + virtual nsresult UpdateTaskbarProperties() override; + virtual nsresult Enable() override; + virtual nsresult Disable() override; + virtual void DetachFromNSWindow() override; + nsresult UpdateButton(uint32_t index); + nsresult UpdateButtons(); + + // Is custom drawing enabled? + bool mCustomDrawing; + // Have we made any buttons? + bool mHaveButtons; + // Windows button format + THUMBBUTTON mThumbButtons[nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS]; + // Pointers to our button class (cached instances) + nsWeakPtr mWeakButtons[nsITaskbarWindowPreview::NUM_TOOLBAR_BUTTONS]; + + // Called to update ITaskbarList4 dependent properties + nsresult UpdateTaskbarProgress(); + nsresult UpdateOverlayIcon(); + + // The taskbar progress + TBPFLAG mState; + ULONGLONG mCurrentValue; + ULONGLONG mMaxValue; + + // Taskbar overlay icon + HICON mOverlayIcon; + nsString mIconDescription; + + // WindowHook procedure for hooking mWnd for taskbar progress and icon stuff + static bool TaskbarWindowHook(void* aContext, HWND hWnd, UINT nMsg, + WPARAM wParam, LPARAM lParam, LRESULT* aResult); + + friend class TaskbarPreviewButton; +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __mozilla_widget_TaskbarWindowPreview_h__ */ diff --git a/widget/windows/ToastNotification.cpp b/widget/windows/ToastNotification.cpp new file mode 100644 index 0000000000..a906f322ed --- /dev/null +++ b/widget/windows/ToastNotification.cpp @@ -0,0 +1,891 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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 "ToastNotification.h" + +#include +#include +#include +#include +#include + +#include "ErrorList.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Buffer.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/mscom/COMWrappers.h" +#include "mozilla/mscom/Utils.h" +#include "mozilla/Logging.h" +#include "mozilla/Services.h" +#include "mozilla/WidgetUtils.h" +#include "mozilla/WindowsVersion.h" +#include "nsAppRunner.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIObserverService.h" +#include "nsIWindowMediator.h" +#include "nsPIDOMWindow.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nsWindowsHelpers.h" +#include "nsXREDirProvider.h" +#include "prenv.h" +#include "ToastNotificationHandler.h" +#include "ToastNotificationHeaderOnlyUtils.h" +#include "WinTaskbar.h" +#include "WinUtils.h" + +namespace mozilla { +namespace widget { + +using namespace toastnotification; + +using namespace ABI::Windows::Foundation; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +// Needed to disambiguate internal and Windows `ToastNotification` classes. +using namespace ABI::Windows::UI::Notifications; +using WinToastNotification = ABI::Windows::UI::Notifications::ToastNotification; +using IVectorView_ToastNotification = + ABI::Windows::Foundation::Collections::IVectorView; +using IVectorView_ScheduledToastNotification = + ABI::Windows::Foundation::Collections::IVectorView< + ScheduledToastNotification*>; + +LazyLogModule sWASLog("WindowsAlertsService"); + +NS_IMPL_ISUPPORTS(ToastNotification, nsIAlertsService, nsIWindowsAlertsService, + nsIAlertsDoNotDisturb, nsIObserver) + +ToastNotification::ToastNotification() = default; + +ToastNotification::~ToastNotification() = default; + +nsresult ToastNotification::Init() { + if (!IsWin8OrLater()) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + if (!PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR")) { + // Windows Toast Notification requires AppId. But allow `xpcshell` to + // create the service to test other functionality. + if (!EnsureAumidRegistered()) { + MOZ_LOG(sWASLog, LogLevel::Warning, ("Failed to register AUMID!")); + return NS_ERROR_NOT_IMPLEMENTED; + } + } else { + MOZ_LOG(sWASLog, LogLevel::Info, ("Using dummy AUMID in xpcshell test")); + mAumid.emplace(u"XpcshellTestToastAumid"_ns); + } + + MOZ_LOG(sWASLog, LogLevel::Info, + ("Using AUMID: '%s'", NS_ConvertUTF16toUTF8(mAumid.ref()).get())); + + nsCOMPtr obsServ = + mozilla::services::GetObserverService(); + if (obsServ) { + Unused << NS_WARN_IF( + NS_FAILED(obsServ->AddObserver(this, "last-pb-context-exited", false))); + Unused << NS_WARN_IF( + NS_FAILED(obsServ->AddObserver(this, "quit-application", false))); + } + + return NS_OK; +} + +bool ToastNotification::EnsureAumidRegistered() { + // Check if this is an MSIX install, app identity is provided by the package + // so no registration is necessary. + if (AssignIfMsixAumid(mAumid)) { + MOZ_LOG( + sWASLog, LogLevel::Info, + ("Found MSIX AUMID: '%s'", NS_ConvertUTF16toUTF8(mAumid.ref()).get())); + return true; + } + + nsAutoString installHash; + nsresult rv = gDirServiceProvider->GetInstallHash(installHash); + NS_ENSURE_SUCCESS(rv, false); + + // Check if toasts were registered during NSIS/MSI installation. + if (AssignIfNsisAumid(installHash, mAumid)) { + MOZ_LOG(sWASLog, LogLevel::Info, + ("Found AUMID from installer (with install hash '%s'): '%s'", + NS_ConvertUTF16toUTF8(installHash).get(), + NS_ConvertUTF16toUTF8(mAumid.ref()).get())); + return true; + } + + // No AUMID registered, fall through to runtime registration for development + // and portable builds. + if (RegisterRuntimeAumid(installHash, mAumid)) { + MOZ_LOG( + sWASLog, LogLevel::Info, + ("Updated AUMID registration at runtime (for install hash '%s'): '%s'", + NS_ConvertUTF16toUTF8(installHash).get(), + NS_ConvertUTF16toUTF8(mAumid.ref()).get())); + return true; + } + + MOZ_LOG(sWASLog, LogLevel::Warning, + ("Failed to register AUMID at runtime! (for install hash '%s')", + NS_ConvertUTF16toUTF8(installHash).get())); + return false; +} + +bool ToastNotification::AssignIfMsixAumid(Maybe& aAumid) { + // `GetCurrentApplicationUserModelId` added in Windows 8. + DynamicallyLinkedFunctionPtr + pGetCurrentApplicationUserModelId(L"kernel32.dll", + "GetCurrentApplicationUserModelId"); + if (!pGetCurrentApplicationUserModelId) { + return false; + } + + UINT32 len = 0; + // ERROR_INSUFFICIENT_BUFFER signals that we're in an MSIX package, and + // therefore should use the package's AUMID. + if (pGetCurrentApplicationUserModelId(&len, nullptr) != + ERROR_INSUFFICIENT_BUFFER) { + MOZ_LOG(sWASLog, LogLevel::Debug, ("Not an MSIX package")); + return false; + } + mozilla::Buffer buffer(len); + LONG success = pGetCurrentApplicationUserModelId(&len, buffer.Elements()); + NS_ENSURE_TRUE(success == ERROR_SUCCESS, false); + + aAumid.emplace(buffer.Elements()); + return true; +} + +bool ToastNotification::AssignIfNsisAumid(nsAutoString& aInstallHash, + Maybe& aAumid) { + nsAutoString nsisAumidName = + u""_ns MOZ_TOAST_APP_NAME u"Toast-"_ns + aInstallHash; + nsAutoString nsisAumidPath = u"AppUserModelId\\"_ns + nsisAumidName; + if (!WinUtils::HasRegistryKey(HKEY_CLASSES_ROOT, nsisAumidPath.get())) { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("No CustomActivator value from installer in key 'HKCR\\%s'", + NS_ConvertUTF16toUTF8(nsisAumidPath).get())); + return false; + } + + aAumid.emplace(nsisAumidName); + return true; +} + +bool ToastNotification::RegisterRuntimeAumid(nsAutoString& aInstallHash, + Maybe& aAumid) { + // Portable AUMID slightly differs from installed AUMID so we can + // differentiate installed to HKCU vs portable installs if necessary. + nsAutoString portableAumid = + u""_ns MOZ_TOAST_APP_NAME u"PortableToast-"_ns + aInstallHash; + + nsCOMPtr appdir; + nsresult rv = gDirServiceProvider->GetGREDir()->Clone(getter_AddRefs(appdir)); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr icon; + rv = appdir->Clone(getter_AddRefs(icon)); + NS_ENSURE_SUCCESS(rv, false); + + rv = icon->Append(u"browser"_ns); + NS_ENSURE_SUCCESS(rv, false); + + rv = icon->Append(u"VisualElements"_ns); + NS_ENSURE_SUCCESS(rv, false); + + rv = icon->Append(u"VisualElements_70.png"_ns); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoString iconPath; + rv = icon->GetPath(iconPath); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr comDll; + rv = appdir->Clone(getter_AddRefs(comDll)); + NS_ENSURE_SUCCESS(rv, false); + + rv = comDll->Append(u"notificationserver.dll"_ns); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoString dllPath; + rv = comDll->GetPath(dllPath); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoHandle txn; + // Manipulate the registry using a transaction so that any failures are + // rolled back. + wchar_t transactionName[] = L"" MOZ_TOAST_APP_NAME L" toast registration"; + txn.own(::CreateTransaction(nullptr, nullptr, TRANSACTION_DO_NOT_PROMOTE, 0, + 0, 0, transactionName)); + NS_ENSURE_TRUE(txn.get() != INVALID_HANDLE_VALUE, false); + + LSTATUS status; + + auto RegisterKey = [&](const nsAString& path, nsAutoRegKey& key) { + HKEY rawKey; + status = ::RegCreateKeyTransactedW( + HKEY_CURRENT_USER, PromiseFlatString(path).get(), 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr, &rawKey, nullptr, txn, + nullptr); + NS_ENSURE_TRUE(status == ERROR_SUCCESS, false); + + key.own(rawKey); + return true; + }; + auto RegisterValue = [&](nsAutoRegKey& key, const nsAString& name, + unsigned long type, const nsAString& data) { + status = ::RegSetValueExW( + key, PromiseFlatString(name).get(), 0, type, + static_cast(PromiseFlatString(data).get()), + (data.Length() + 1) * sizeof(wchar_t)); + + return status == ERROR_SUCCESS; + }; + + // clang-format off + /* Writes the following keys and values to the registry. + * HKEY_CURRENT_USER\Software\Classes\AppID\{GUID} DllSurrogate : REG_SZ = "" + * \AppUserModelId\{MOZ_TOAST_APP_NAME}PortableToast-{install hash} CustomActivator : REG_SZ = {GUID} + * DisplayName : REG_EXPAND_SZ = {display name} + * IconUri : REG_EXPAND_SZ = {icon path} + * \CLSID\{GUID} AppID : REG_SZ = {GUID} + * \InprocServer32 (Default) : REG_SZ = {notificationserver.dll path} + */ + // clang-format on + + constexpr nsLiteralString classes = u"Software\\Classes\\"_ns; + + nsAutoString aumid = classes + u"AppUserModelId\\"_ns + portableAumid; + nsAutoRegKey aumidKey; + NS_ENSURE_TRUE(RegisterKey(aumid, aumidKey), false); + + nsAutoString guidStr; + { + DWORD bufferSizeBytes = NSID_LENGTH * sizeof(wchar_t); + Buffer guidBuffer(bufferSizeBytes); + status = ::RegGetValueW(HKEY_CURRENT_USER, aumid.get(), L"CustomActivator", + RRF_RT_REG_SZ, 0, guidBuffer.Elements(), + &bufferSizeBytes); + + CLSID unused; + if (status == ERROR_SUCCESS && + SUCCEEDED(CLSIDFromString(guidBuffer.Elements(), &unused))) { + guidStr = guidBuffer.Elements(); + } else { + nsIDToCString uuidString(nsID::GenerateUUID()); + size_t len = strlen(uuidString.get()); + MOZ_ASSERT(len == NSID_LENGTH - 1); + CopyASCIItoUTF16(nsDependentCSubstring(uuidString.get(), len), guidStr); + } + + if (status == ERROR_SUCCESS) { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Existing CustomActivator guid found: '%s'", + NS_ConvertUTF16toUTF8(guidStr).get())); + } else { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("New CustomActivator guid generated: '%s'", + NS_ConvertUTF16toUTF8(guidStr).get())); + } + } + NS_ENSURE_TRUE( + RegisterValue(aumidKey, u"CustomActivator"_ns, REG_SZ, guidStr), false); + nsAutoString brandName; + WidgetUtils::GetBrandShortName(brandName); + NS_ENSURE_TRUE( + RegisterValue(aumidKey, u"DisplayName"_ns, REG_EXPAND_SZ, brandName), + false); + NS_ENSURE_TRUE( + RegisterValue(aumidKey, u"IconUri"_ns, REG_EXPAND_SZ, iconPath), false); + + nsAutoString appid = classes + u"AppID\\"_ns + guidStr; + nsAutoRegKey appidKey; + NS_ENSURE_TRUE(RegisterKey(appid, appidKey), false); + NS_ENSURE_TRUE(RegisterValue(appidKey, u"DllSurrogate"_ns, REG_SZ, u""_ns), + false); + + nsAutoString clsid = classes + u"CLSID\\"_ns + guidStr; + nsAutoRegKey clsidKey; + NS_ENSURE_TRUE(RegisterKey(clsid, clsidKey), false); + NS_ENSURE_TRUE(RegisterValue(clsidKey, u"AppID"_ns, REG_SZ, guidStr), false); + + nsAutoString inproc = clsid + u"\\InprocServer32"_ns; + nsAutoRegKey inprocKey; + NS_ENSURE_TRUE(RegisterKey(inproc, inprocKey), false); + // Set the component's path to this DLL + NS_ENSURE_TRUE(RegisterValue(inprocKey, u""_ns, REG_SZ, dllPath), false); + + NS_ENSURE_TRUE(::CommitTransaction(txn), false); + + MOZ_LOG( + sWASLog, LogLevel::Debug, + ("Updated registration for CustomActivator value in key 'HKCU\\%s': '%s'", + NS_ConvertUTF16toUTF8(aumid).get(), + NS_ConvertUTF16toUTF8(guidStr).get())); + aAumid.emplace(portableAumid); + return true; +} + +nsresult ToastNotification::BackgroundDispatch(nsIRunnable* runnable) { + return NS_DispatchBackgroundTask(runnable); +} + +NS_IMETHODIMP +ToastNotification::GetSuppressForScreenSharing(bool* aRetVal) { + *aRetVal = mSuppressForScreenSharing; + return NS_OK; +} + +NS_IMETHODIMP +ToastNotification::SetSuppressForScreenSharing(bool aSuppress) { + mSuppressForScreenSharing = aSuppress; + return NS_OK; +} + +NS_IMETHODIMP +ToastNotification::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + nsDependentCString topic(aTopic); + + for (auto iter = mActiveHandlers.Iter(); !iter.Done(); iter.Next()) { + RefPtr handler = iter.UserData(); + + auto removeNotification = [&]() { + // The handlers' destructors will do the right thing (de-register with + // Windows). + iter.Remove(); + + // Break the cycle between the handler and the MSCOM notification so the + // handler's destructor will be called. + handler->UnregisterHandler(); + }; + + if (topic == "last-pb-context-exited"_ns) { + if (handler->IsPrivate()) { + handler->HideAlert(); + removeNotification(); + } + } else if (topic == "quit-application"_ns) { + removeNotification(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +ToastNotification::ShowAlertNotification( + const nsAString& aImageUrl, const nsAString& aAlertTitle, + const nsAString& aAlertText, bool aAlertTextClickable, + const nsAString& aAlertCookie, nsIObserver* aAlertListener, + const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang, + const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing, + bool aRequireInteraction) { + nsCOMPtr alert = + do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); + if (NS_WARN_IF(!alert)) { + return NS_ERROR_FAILURE; + } + // vibrate is unused for now + nsTArray vibrate; + nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText, + aAlertTextClickable, aAlertCookie, aBidi, aLang, + aData, aPrincipal, aInPrivateBrowsing, + aRequireInteraction, false, vibrate); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return ShowAlert(alert, aAlertListener); +} + +NS_IMETHODIMP +ToastNotification::ShowPersistentNotification(const nsAString& aPersistentData, + nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) { + return ShowAlert(aAlert, aAlertListener); +} + +NS_IMETHODIMP +ToastNotification::SetManualDoNotDisturb(bool aDoNotDisturb) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ToastNotification::GetManualDoNotDisturb(bool* aRet) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ToastNotification::ShowAlert(nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) { + NS_ENSURE_ARG(aAlert); + + if (mSuppressForScreenSharing) { + return NS_OK; + } + + nsAutoString cookie; + MOZ_TRY(aAlert->GetCookie(cookie)); + + nsAutoString name; + MOZ_TRY(aAlert->GetName(name)); + + nsAutoString title; + MOZ_TRY(aAlert->GetTitle(title)); + + nsAutoString text; + MOZ_TRY(aAlert->GetText(text)); + + bool textClickable; + MOZ_TRY(aAlert->GetTextClickable(&textClickable)); + + bool isSilent; + MOZ_TRY(aAlert->GetSilent(&isSilent)); + + nsAutoString hostPort; + MOZ_TRY(aAlert->GetSource(hostPort)); + + nsAutoString launchUrl; + MOZ_TRY(aAlert->GetLaunchURL(launchUrl)); + + bool requireInteraction; + MOZ_TRY(aAlert->GetRequireInteraction(&requireInteraction)); + + bool inPrivateBrowsing; + MOZ_TRY(aAlert->GetInPrivateBrowsing(&inPrivateBrowsing)); + + nsTArray> actions; + MOZ_TRY(aAlert->GetActions(actions)); + + nsCOMPtr principal; + MOZ_TRY(aAlert->GetPrincipal(getter_AddRefs(principal))); + bool isSystemPrincipal = principal && principal->IsSystemPrincipal(); + + RefPtr oldHandler = mActiveHandlers.Get(name); + + NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED); + RefPtr handler = new ToastNotificationHandler( + this, mAumid.ref(), aAlertListener, name, cookie, title, text, hostPort, + textClickable, requireInteraction, actions, isSystemPrincipal, launchUrl, + inPrivateBrowsing, isSilent); + mActiveHandlers.InsertOrUpdate(name, RefPtr{handler}); + + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Adding handler '%s': [%p] (now %d handlers)", + NS_ConvertUTF16toUTF8(name).get(), handler.get(), + mActiveHandlers.Count())); + + nsresult rv = handler->InitAlertAsync(aAlert); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Failed to init alert, removing '%s'", + NS_ConvertUTF16toUTF8(name).get())); + mActiveHandlers.Remove(name); + handler->UnregisterHandler(); + return rv; + } + + // If there was a previous handler with the same name then unregister it. + if (oldHandler) { + oldHandler->UnregisterHandler(); + } + + return NS_OK; +} + +NS_IMETHODIMP +ToastNotification::GetXmlStringForWindowsAlert(nsIAlertNotification* aAlert, + const nsAString& aWindowsTag, + nsAString& aString) { + NS_ENSURE_ARG(aAlert); + + nsAutoString cookie; + MOZ_TRY(aAlert->GetCookie(cookie)); + + nsAutoString name; + MOZ_TRY(aAlert->GetName(name)); + + nsAutoString title; + MOZ_TRY(aAlert->GetTitle(title)); + + nsAutoString text; + MOZ_TRY(aAlert->GetText(text)); + + bool textClickable; + MOZ_TRY(aAlert->GetTextClickable(&textClickable)); + + bool isSilent; + MOZ_TRY(aAlert->GetSilent(&isSilent)); + + nsAutoString hostPort; + MOZ_TRY(aAlert->GetSource(hostPort)); + + nsAutoString launchUrl; + MOZ_TRY(aAlert->GetLaunchURL(launchUrl)); + + bool requireInteraction; + MOZ_TRY(aAlert->GetRequireInteraction(&requireInteraction)); + + bool inPrivateBrowsing; + MOZ_TRY(aAlert->GetInPrivateBrowsing(&inPrivateBrowsing)); + + nsTArray> actions; + MOZ_TRY(aAlert->GetActions(actions)); + + nsCOMPtr principal; + MOZ_TRY(aAlert->GetPrincipal(getter_AddRefs(principal))); + bool isSystemPrincipal = principal && principal->IsSystemPrincipal(); + + NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED); + RefPtr handler = new ToastNotificationHandler( + this, mAumid.ref(), nullptr /* aAlertListener */, name, cookie, title, + text, hostPort, textClickable, requireInteraction, actions, + isSystemPrincipal, launchUrl, inPrivateBrowsing, isSilent); + + // Usually, this will be empty during testing, making test output + // deterministic. + MOZ_TRY(handler->SetWindowsTag(aWindowsTag)); + + nsAutoString imageURL; + MOZ_TRY(aAlert->GetImageURL(imageURL)); + + return handler->CreateToastXmlString(imageURL, aString); +} + +// Verifies that the tag recieved associates to a notification created during +// this application's session, or handles fallback behavior. +RefPtr ToastNotification::VerifyTagPresentOrFallback( + const nsAString& aWindowsTag) { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Iterating %d handlers", mActiveHandlers.Count())); + + for (auto iter = mActiveHandlers.Iter(); !iter.Done(); iter.Next()) { + RefPtr handler = iter.UserData(); + nsAutoString tag; + nsresult rv = handler->GetWindowsTag(tag); + + if (NS_SUCCEEDED(rv)) { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Comparing external windowsTag '%s' to handled windowsTag '%s'", + NS_ConvertUTF16toUTF8(aWindowsTag).get(), + NS_ConvertUTF16toUTF8(tag).get())); + if (aWindowsTag.Equals(tag)) { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("External windowsTag '%s' is handled by handler [%p]", + NS_ConvertUTF16toUTF8(aWindowsTag).get(), handler.get())); + ToastHandledResolve handled{u""_ns, u""_ns}; + return ToastHandledPromise::CreateAndResolve(handled, __func__); + } + } else { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Failed to get windowsTag for handler [%p]", handler.get())); + } + } + + // Fallback handling + + RefPtr fallbackPromise = + new ToastHandledPromise::Private(__func__); + + // TODO: Bug 1806005 - At time of writing this function is called in a call + // stack containing `WndProc` callback on an STA thread. As a result attempts + // to create a `ToastNotificationManager` instance results an an + // `RPC_E_CANTCALLOUT_ININPUTSYNCCALL` error. We can simplify the the XPCOM + // interface and synchronize the COM interactions if notification fallback + // handling were no longer handled in a `WndProc` context. + NS_DispatchBackgroundTask(NS_NewRunnableFunction( + "VerifyTagPresentOrFallback fallback background task", + [fallbackPromise, aWindowsTag = nsString(aWindowsTag), + aAumid = nsString(mAumid.ref())]() { + MOZ_ASSERT(mscom::IsCOMInitializedOnCurrentThread()); + + bool foundTag; + nsAutoString launchUrl; + nsAutoString privilegedName; + + nsresult rv = ToastNotificationHandler:: + FindLaunchURLAndPrivilegedNameForWindowsTag( + aWindowsTag, aAumid, foundTag, launchUrl, privilegedName); + + if (NS_FAILED(rv) || !foundTag) { + MOZ_LOG(sWASLog, LogLevel::Error, + ("Failed to get launch URL and privileged name for " + "notification tag '%s'", + NS_ConvertUTF16toUTF8(aWindowsTag).get())); + + fallbackPromise->Reject(false, __func__); + return; + } + + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Found launch URL '%s' and privileged name '%s' for " + "windowsTag '%s'", + NS_ConvertUTF16toUTF8(launchUrl).get(), + NS_ConvertUTF16toUTF8(privilegedName).get(), + NS_ConvertUTF16toUTF8(aWindowsTag).get())); + + ToastHandledResolve handled{launchUrl, privilegedName}; + fallbackPromise->Resolve(handled, __func__); + })); + + return fallbackPromise; +} + +// Send our window's PID to the notification server so that it can grant us +// `SetForegroundWindow` permissions. PID 0 is sent to signal no window PID. +// Absense of PID which may occur when we are yet unable to retrieve the +// window during startup, which is not a problem in practice as new windows +// receive focus by default. +void ToastNotification::SignalComNotificationHandled( + const nsAString& aWindowsTag) { + DWORD pid = 0; + + nsCOMPtr winMediator( + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID)); + if (winMediator) { + nsCOMPtr navWin; + winMediator->GetMostRecentWindow(u"navigator:browser", + getter_AddRefs(navWin)); + if (navWin) { + nsCOMPtr widget = + WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin)); + if (widget) { + HWND hwnd = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW); + GetWindowThreadProcessId(hwnd, &pid); + } else { + MOZ_LOG(sWASLog, LogLevel::Debug, ("Failed to get widget")); + } + } else { + MOZ_LOG(sWASLog, LogLevel::Debug, ("Failed to get navWin")); + } + } else { + MOZ_LOG(sWASLog, LogLevel::Debug, ("Failed to get WinMediator")); + } + + // Run pipe communication off the main thread to prevent UI jank from + // blocking. Nothing relies on the COM server's response or that it has + // responded at time of commit. + NS_DispatchBackgroundTask( + NS_NewRunnableFunction( + "SignalComNotificationHandled background task", + [pid, aWindowsTag = nsString{aWindowsTag}]() mutable { + std::wstring pipeName = GetNotificationPipeName(aWindowsTag.get()); + + nsAutoHandle pipe; + pipe.own(CreateFileW(pipeName.c_str(), GENERIC_READ | GENERIC_WRITE, + 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, nullptr)); + if (pipe.get() == INVALID_HANDLE_VALUE) { + MOZ_LOG(sWASLog, LogLevel::Error, + ("Unable to open notification server pipe.")); + return; + } + + DWORD pipeFlags = PIPE_READMODE_MESSAGE; + if (!SetNamedPipeHandleState(pipe.get(), &pipeFlags, nullptr, + nullptr)) { + MOZ_LOG(sWASLog, LogLevel::Error, + ("Error setting pipe handle state, error %lu", + GetLastError())); + return; + } + + // Pass our window's PID to the COM server receive + // `SetForegroundWindow` permissions, and wait for a message + // acknowledging the permission has been granted. + ToastNotificationPidMessage in{}; + in.pid = pid; + ToastNotificationPermissionMessage out{}; + auto transact = [&](OVERLAPPED& overlapped) { + return TransactNamedPipe(pipe.get(), &in, sizeof(in), &out, + sizeof(out), nullptr, &overlapped); + }; + bool result = + SyncDoOverlappedIOWithTimeout(pipe, sizeof(out), transact); + + if (result && out.setForegroundPermissionGranted && pid != 0) { + MOZ_LOG( + sWASLog, LogLevel::Info, + ("SetForegroundWindow permission granted to our window.")); + } else { + MOZ_LOG(sWASLog, LogLevel::Error, + ("SetForegroundWindow permission not granted to our " + "window.")); + } + }), + NS_DISPATCH_EVENT_MAY_BLOCK); +} + +NS_IMETHODIMP +ToastNotification::HandleWindowsTag(const nsAString& aWindowsTag, + JSContext* aCx, dom::Promise** aPromise) { + NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED); + NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD); + + ErrorResult rv; + RefPtr promise = + dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv); + ENSURE_SUCCESS(rv, rv.StealNSResult()); + + this->VerifyTagPresentOrFallback(aWindowsTag) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [aWindowsTag = nsString(aWindowsTag), + promise](const ToastHandledResolve& aResolved) { + // We no longer need to query toast information from OS and can + // allow the COM server to proceed (toast information is lost once + // the COM server's `Activate` callback returns). + SignalComNotificationHandled(aWindowsTag); + + dom::AutoJSAPI js; + if (NS_WARN_IF(!js.Init(promise->GetGlobalObject()))) { + promise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + // Resolve the DOM Promise with a JS object. Set `launchUrl` and/or + // `privilegedName` properties if fallback handling is necessary. + + JSContext* cx = js.cx(); + JS::Rooted obj(cx, JS_NewPlainObject(cx)); + + auto setProperty = [&](const char* name, const nsString& value) { + JS::Rooted title(cx, + JS_NewUCStringCopyZ(cx, value.get())); + JS::Rooted attVal(cx, JS::StringValue(title)); + Unused << NS_WARN_IF(!JS_SetProperty(cx, obj, name, attVal)); + }; + + if (!aResolved.launchUrl.IsEmpty()) { + setProperty("launchUrl", aResolved.launchUrl); + } + if (!aResolved.privilegedName.IsEmpty()) { + setProperty("privilegedName", aResolved.privilegedName); + } + + promise->MaybeResolve(obj); + }, + [aWindowsTag = nsString(aWindowsTag), promise]() { + // We no longer need to query toast information from OS and can + // allow the COM server to proceed (toast information is lost once + // the COM server's `Activate` callback returns). + SignalComNotificationHandled(aWindowsTag); + + promise->MaybeReject(NS_ERROR_FAILURE); + }); + + promise.forget(aPromise); + return NS_OK; +} + +NS_IMETHODIMP +ToastNotification::CloseAlert(const nsAString& aAlertName, + bool aContextClosed) { + RefPtr handler; + if (NS_WARN_IF(!mActiveHandlers.Get(aAlertName, getter_AddRefs(handler)))) { + return NS_OK; + } + + if (!aContextClosed || handler->IsPrivate()) { + // Hide the alert when not implicitly closed by tab/window closing or when + // notification originated from a private tab. + handler->HideAlert(); + } + + mActiveHandlers.Remove(aAlertName); + handler->UnregisterHandler(); + + return NS_OK; +} + +bool ToastNotification::IsActiveHandler(const nsAString& aAlertName, + ToastNotificationHandler* aHandler) { + RefPtr handler; + if (NS_WARN_IF(!mActiveHandlers.Get(aAlertName, getter_AddRefs(handler)))) { + return false; + } + return handler == aHandler; +} + +void ToastNotification::RemoveHandler(const nsAString& aAlertName, + ToastNotificationHandler* aHandler) { + // The alert may have been replaced; only remove it from the active + // handler's map if it's the same. + if (IsActiveHandler(aAlertName, aHandler)) { + // Terrible things happen if the destructor of a handler is called inside + // the hashtable .Remove() method. Wait until we have returned from there. + RefPtr kungFuDeathGrip(aHandler); + mActiveHandlers.Remove(aAlertName); + aHandler->UnregisterHandler(); + } +} + +NS_IMETHODIMP +ToastNotification::RemoveAllNotificationsForInstall() { + HRESULT hr = S_OK; + + ComPtr manager; + hr = GetActivationFactory( + HStringReference( + RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) + .Get(), + &manager); + NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE); + + HString aumid; + MOZ_ASSERT(mAumid.isSome()); + hr = aumid.Set(mAumid.ref().get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE); + + // Hide toasts in action center. + [&]() { + ComPtr manager2; + hr = manager.As(&manager2); + NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); + + ComPtr history; + hr = manager2->get_History(&history); + NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); + + hr = history->ClearWithId(aumid.Get()); + NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); + }(); + + // Hide scheduled toasts. + [&]() { + ComPtr notifier; + hr = manager->CreateToastNotifierWithId(aumid.Get(), ¬ifier); + NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); + + ComPtr scheduledToasts; + hr = notifier->GetScheduledToastNotifications(&scheduledToasts); + NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); + + unsigned int schedSize; + hr = scheduledToasts->get_Size(&schedSize); + NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); + + for (unsigned int i = 0; i < schedSize; i++) { + ComPtr schedToast; + hr = scheduledToasts->GetAt(i, &schedToast); + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + + hr = notifier->RemoveFromSchedule(schedToast.Get()); + Unused << NS_WARN_IF(FAILED(hr)); + } + }(); + + return NS_OK; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/ToastNotification.h b/widget/windows/ToastNotification.h new file mode 100644 index 0000000000..23f4941125 --- /dev/null +++ b/widget/windows/ToastNotification.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ToastNotification_h__ +#define ToastNotification_h__ + +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "nsIAlertsService.h" +#include "nsIObserver.h" +#include "nsIThread.h" +#include "nsIWindowsAlertsService.h" +#include "nsRefPtrHashtable.h" + +namespace mozilla { +namespace widget { + +struct ToastHandledResolve { + const nsString launchUrl; + const nsString privilegedName; +}; +using ToastHandledPromise = MozPromise; + +class ToastNotificationHandler; + +class ToastNotification final : public nsIWindowsAlertsService, + public nsIAlertsDoNotDisturb, + public nsIObserver { + public: + NS_DECL_NSIALERTSSERVICE + NS_DECL_NSIWINDOWSALERTSSERVICE + NS_DECL_NSIALERTSDONOTDISTURB + NS_DECL_NSIOBSERVER + NS_DECL_ISUPPORTS + + ToastNotification(); + + nsresult Init(); + + bool IsActiveHandler(const nsAString& aAlertName, + ToastNotificationHandler* aHandler); + void RemoveHandler(const nsAString& aAlertName, + ToastNotificationHandler* aHandler); + + nsresult BackgroundDispatch(nsIRunnable* runnable); + + protected: + virtual ~ToastNotification(); + bool EnsureAumidRegistered(); + + static bool AssignIfMsixAumid(Maybe& aAumid); + static bool AssignIfNsisAumid(nsAutoString& aInstallHash, + Maybe& aAumid); + static bool RegisterRuntimeAumid(nsAutoString& aInstallHash, + Maybe& aAumid); + + RefPtr VerifyTagPresentOrFallback( + const nsAString& aWindowsTag); + static void SignalComNotificationHandled(const nsAString& aWindowsTag); + + nsRefPtrHashtable mActiveHandlers; + Maybe mAumid; + bool mSuppressForScreenSharing = false; +}; + +} // namespace widget +} // namespace mozilla + +#endif diff --git a/widget/windows/ToastNotificationHandler.cpp b/widget/windows/ToastNotificationHandler.cpp new file mode 100644 index 0000000000..3250a6837c --- /dev/null +++ b/widget/windows/ToastNotificationHandler.cpp @@ -0,0 +1,1089 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* 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 "ToastNotificationHandler.h" + +#include + +#include "imgIContainer.h" +#include "imgIRequest.h" +#include "mozilla/gfx/2D.h" +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +#endif +#include "mozilla/HashFunctions.h" +#include "mozilla/Result.h" +#include "mozilla/Logging.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/WindowsVersion.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsAppRunner.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIDUtils.h" +#include "nsIStringBundle.h" +#include "nsIToolkitProfile.h" +#include "nsIToolkitProfileService.h" +#include "nsIURI.h" +#include "nsIWidget.h" +#include "nsIWindowMediator.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsProxyRelease.h" +#include "nsXREDirProvider.h" +#include "ToastNotificationHeaderOnlyUtils.h" +#include "WidgetUtils.h" +#include "WinUtils.h" + +#include "ToastNotification.h" + +namespace mozilla { +namespace widget { + +extern LazyLogModule sWASLog; + +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::UI::Notifications; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace toastnotification; + +// Needed to disambiguate internal and Windows `ToastNotification` classes. +using WinToastNotification = ABI::Windows::UI::Notifications::ToastNotification; +using ToastActivationHandler = + ITypedEventHandler; +using ToastDismissedHandler = + ITypedEventHandler; +using ToastFailedHandler = + ITypedEventHandler; +using IVectorView_ToastNotification = + Collections::IVectorView; + +NS_IMPL_ISUPPORTS(ToastNotificationHandler, nsIAlertNotificationImageListener) + +static bool SetNodeValueString(const nsString& aString, IXmlNode* node, + IXmlDocument* xml) { + ComPtr inputText; + HRESULT hr; + hr = xml->CreateTextNode(HStringReference(aString.get()).Get(), &inputText); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr inputTextNode; + hr = inputText.As(&inputTextNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr appendedChild; + hr = node->AppendChild(inputTextNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +static bool SetAttribute(ComPtr& element, + const HStringReference& name, const nsAString& value) { + HString valueStr; + valueStr.Set(PromiseFlatString(value).get()); + + HRESULT hr = element->SetAttribute(name.Get(), valueStr.Get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +static bool AddActionNode(ComPtr& toastXml, + ComPtr& actionsNode, + const nsAString& actionTitle, + const nsAString& launchArg, + const nsAString& actionArgs, + const nsAString& actionPlacement = u""_ns, + const nsAString& activationType = u""_ns) { + ComPtr action; + HRESULT hr = + toastXml->CreateElement(HStringReference(L"action").Get(), &action); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + bool success = + SetAttribute(action, HStringReference(L"content"), actionTitle); + NS_ENSURE_TRUE(success, false); + + // Action arguments overwrite the toast's launch arguments, so we need to + // prepend the launch arguments necessary for the Notification Server to + // reconstruct the toast's origin. + // + // Web Notification actions are arbitrary strings; to prevent breaking launch + // argument parsing the action argument must be last. All delimiters after + // `action` are part of the action arugment. + nsAutoString args = launchArg + u"\n"_ns + + nsDependentString(kLaunchArgAction) + u"\n"_ns + + actionArgs; + success = SetAttribute(action, HStringReference(L"arguments"), args); + NS_ENSURE_TRUE(success, false); + + if (!actionPlacement.IsEmpty()) { + success = + SetAttribute(action, HStringReference(L"placement"), actionPlacement); + NS_ENSURE_TRUE(success, false); + } + + if (!activationType.IsEmpty()) { + success = SetAttribute(action, HStringReference(L"activationType"), + activationType); + NS_ENSURE_TRUE(success, false); + + // No special argument handling: when `activationType="system"`, `action` is + // a Windows-specific keyword, generally "dismiss" or "snooze". + success = SetAttribute(action, HStringReference(L"arguments"), actionArgs); + NS_ENSURE_TRUE(success, false); + } + + // Add to + ComPtr actionNode; + hr = action.As(&actionNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr appendedChild; + hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +nsresult ToastNotificationHandler::GetWindowsTag(nsAString& aWindowsTag) { + aWindowsTag.Assign(mWindowsTag); + return NS_OK; +} + +nsresult ToastNotificationHandler::SetWindowsTag(const nsAString& aWindowsTag) { + mWindowsTag.Assign(aWindowsTag); + return NS_OK; +} + +// clang - format off +/* Populate the launch argument so the COM server can reconstruct the toast + * origin. + * + * program + * {MOZ_APP_NAME} + * profile + * {path to profile} + */ +// clang-format on +Result ToastNotificationHandler::GetLaunchArgument() { + nsString launchArg; + + // When the preference is false, the COM notification server will be invoked, + // discover that there is no `program`, and exit (successfully), after which + // Windows will invoke the in-product Windows 8-style callbacks. When true, + // the COM notification server will launch Firefox with sufficient arguments + // for Firefox to handle the notification. + if (!Preferences::GetBool( + "alerts.useSystemBackend.windows.notificationserver.enabled", + false)) { + // Include dummy key/value so that newline appended arguments aren't off by + // one line. + launchArg += u"invalid key\ninvalid value"_ns; + return launchArg; + } + + // `program` argument. + launchArg += nsDependentString(kLaunchArgProgram) + u"\n"_ns MOZ_APP_NAME; + + // `profile` argument. + nsCOMPtr profDir; + bool wantCurrentProfile = true; +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // Notifications popped from a background task want to invoke Firefox with a + // different profile -- the default browsing profile. We'd prefer to not + // specify a profile, so that the Firefox invoked by the notification server + // chooses its default profile, but this might pop the profile chooser in + // some configurations. + wantCurrentProfile = false; + + nsCOMPtr profileSvc = + do_GetService(NS_PROFILESERVICE_CONTRACTID); + if (profileSvc) { + nsCOMPtr defaultProfile; + nsresult rv = + profileSvc->GetDefaultProfile(getter_AddRefs(defaultProfile)); + if (NS_SUCCEEDED(rv) && defaultProfile) { + // Not all installations have a default profile. But if one is set, + // then it should have a profile directory. + MOZ_TRY(defaultProfile->GetRootDir(getter_AddRefs(profDir))); + } + } + } +#endif + if (wantCurrentProfile) { + MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profDir))); + } + + if (profDir) { + nsAutoString profilePath; + MOZ_TRY(profDir->GetPath(profilePath)); + launchArg += u"\n"_ns + nsDependentString(kLaunchArgProfile) + u"\n"_ns + + profilePath; + } + + if (!mLaunchUrl.IsEmpty()) { + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgUrl) + u"\n"_ns + mLaunchUrl; + } else if (!mHostPort.IsEmpty()) { + // Fall back to the origin domain if no explicit launch url is provided. + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgUrl) + u"\n"_ns + mHostPort; + } + + if (mIsSystemPrincipal && !mName.IsEmpty()) { + // Privileged alerts include any provided name for metrics. + launchArg += u"\n"_ns + nsDependentString(kLaunchArgPrivilegedName) + + u"\n"_ns + mName; + } + + // `windowsTag` argument. + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgTag) + u"\n"_ns + mWindowsTag; + + return launchArg; +} + +static ComPtr +GetToastNotificationManagerStatics() { + ComPtr toastNotificationManagerStatics; + HRESULT hr = GetActivationFactory( + HStringReference( + RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) + .Get(), + &toastNotificationManagerStatics); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + return toastNotificationManagerStatics; +} + +ToastNotificationHandler::~ToastNotificationHandler() { + if (mImageRequest) { + mImageRequest->Cancel(NS_BINDING_ABORTED); + mImageRequest = nullptr; + } + + if (mHasImage && mImageFile) { + DebugOnly rv = mImageFile->Remove(false); + NS_ASSERTION(NS_SUCCEEDED(rv), "Cannot remove temporary image file"); + } + + UnregisterHandler(); +} + +void ToastNotificationHandler::UnregisterHandler() { + if (mNotification) { + mNotification->remove_Dismissed(mDismissedToken); + mNotification->remove_Activated(mActivatedToken); + mNotification->remove_Failed(mFailedToken); + } + + mNotification = nullptr; + mNotifier = nullptr; + + SendFinished(); +} + +nsresult ToastNotificationHandler::InitAlertAsync( + nsIAlertNotification* aAlert) { + MOZ_TRY(InitWindowsTag()); + + return aAlert->LoadImage(/* aTimeout = */ 0, this, /* aUserData = */ nullptr, + getter_AddRefs(mImageRequest)); +} + +// Uniquely identify this toast to Windows. Existing names and cookies are not +// suitable: we want something generated and unique. This is needed to check if +// toast is still present in the Windows Action Center when we receive a dismiss +// timeout. +// +// Local testing reveals that the space of tags is not global but instead is per +// AUMID. Since an installation uses a unique AUMID incorporating the install +// directory hash, it should not witness another installation's tag. +nsresult ToastNotificationHandler::InitWindowsTag() { + mWindowsTag.Truncate(); + + nsAutoString tag; + + // Multiple profiles might overwrite each other's toast messages when a + // common name is used for a given host port. We prevent this by including + // the profile directory as part of the toast hash. + nsCOMPtr profDir; + MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profDir))); + MOZ_TRY(profDir->GetPath(tag)); + + if (!mHostPort.IsEmpty()) { + // Notification originated from a web notification. + // `mName` will be in the form `{mHostPort}#tag:{tag}` if the notification + // was created with a tag and `{mHostPort}#notag:{uuid}` otherwise. + tag += mName; + } else { + // Notification originated from the browser chrome. + if (!mName.IsEmpty()) { + tag += u"chrome#tag:"_ns; + // Browser chrome notifications don't follow any convention for naming. + tag += mName; + } else { + // No associated name, append a UUID to prevent reuse of the same tag. + nsIDToCString uuidString(nsID::GenerateUUID()); + size_t len = strlen(uuidString.get()); + MOZ_ASSERT(len == NSID_LENGTH - 1); + nsAutoString uuid; + CopyASCIItoUTF16(nsDependentCSubstring(uuidString.get(), len), uuid); + + tag += u"chrome#notag:"_ns; + tag += uuid; + } + } + + // Windows notification tags are limited to 16 characters, or 64 characters + // after the Creators Update; therefore we hash the tag to fit the minimum + // range. + HashNumber hash = HashString(tag); + mWindowsTag.AppendPrintf("%010u", hash); + + return NS_OK; +} + +ComPtr ToastNotificationHandler::CreateToastXmlDocument() { + ComPtr toastNotificationManagerStatics = + GetToastNotificationManagerStatics(); + NS_ENSURE_TRUE(toastNotificationManagerStatics, nullptr); + + ToastTemplateType toastTemplate; + if (mHostPort.IsEmpty()) { + toastTemplate = + mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText03 + : ToastTemplateType::ToastTemplateType_ToastText03; + } else { + toastTemplate = + mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText04 + : ToastTemplateType::ToastTemplateType_ToastText04; + } + + ComPtr toastXml; + toastNotificationManagerStatics->GetTemplateContent(toastTemplate, &toastXml); + + if (!toastXml) { + return nullptr; + } + + nsresult ns; + HRESULT hr; + bool success; + + if (mHasImage) { + ComPtr toastImageElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"image").Get(), + &toastImageElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr imageNode; + hr = toastImageElements->Item(0, &imageNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr image; + hr = imageNode.As(&image); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetAttribute(image, HStringReference(L"src"), mImageUri); + NS_ENSURE_TRUE(success, nullptr); + } + + ComPtr toastTextElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"text").Get(), + &toastTextElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr titleTextNodeRoot; + hr = toastTextElements->Item(0, &titleTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr msgTextNodeRoot; + hr = toastTextElements->Item(1, &msgTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetNodeValueString(mTitle, titleTextNodeRoot.Get(), toastXml.Get()); + NS_ENSURE_TRUE(success, nullptr); + + success = SetNodeValueString(mMsg, msgTextNodeRoot.Get(), toastXml.Get()); + NS_ENSURE_TRUE(success, nullptr); + + ComPtr toastElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"toast").Get(), + &toastElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr toastNodeRoot; + hr = toastElements->Item(0, &toastNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr toastElement; + hr = toastNodeRoot.As(&toastElement); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + if (mRequireInteraction) { + success = SetAttribute(toastElement, HStringReference(L"scenario"), + u"reminder"_ns); + NS_ENSURE_TRUE(success, nullptr); + } + + auto maybeLaunchArg = GetLaunchArgument(); + NS_ENSURE_TRUE(maybeLaunchArg.isOk(), nullptr); + nsString launchArg = maybeLaunchArg.unwrap(); + + success = SetAttribute(toastElement, HStringReference(L"launch"), launchArg); + NS_ENSURE_TRUE(success, nullptr); + + MOZ_LOG(sWASLog, LogLevel::Debug, + ("launchArg: '%s'", NS_ConvertUTF16toUTF8(launchArg).get())); + + // On modern Windows (10+), use newer toast layout, which makes images larger, + // for system (chrome-privileged) toasts. + if (IsWin10OrLater() && mIsSystemPrincipal) { + ComPtr bindingElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"binding").Get(), + &bindingElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr bindingNodeRoot; + hr = bindingElements->Item(0, &bindingNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr bindingElement; + hr = bindingNodeRoot.As(&bindingElement); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetAttribute(bindingElement, HStringReference(L"template"), + u"ToastGeneric"_ns); + NS_ENSURE_TRUE(success, nullptr); + } + + ComPtr actions; + hr = toastXml->CreateElement(HStringReference(L"actions").Get(), &actions); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr actionsNode; + hr = actions.As(&actionsNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + nsCOMPtr sbs = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + NS_ENSURE_TRUE(sbs, nullptr); + + nsCOMPtr bundle; + sbs->CreateBundle("chrome://alerts/locale/alert.properties", + getter_AddRefs(bundle)); + NS_ENSURE_TRUE(bundle, nullptr); + + if (!mHostPort.IsEmpty()) { + AutoTArray formatStrings = {mHostPort}; + + ComPtr urlTextNodeRoot; + hr = toastTextElements->Item(2, &urlTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + nsAutoString urlReference; + bundle->FormatStringFromName("source.label", formatStrings, urlReference); + + success = + SetNodeValueString(urlReference, urlTextNodeRoot.Get(), toastXml.Get()); + NS_ENSURE_TRUE(success, nullptr); + + if (IsWin10AnniversaryUpdateOrLater()) { + ComPtr placementText; + hr = urlTextNodeRoot.As(&placementText); + if (SUCCEEDED(hr)) { + // placement is supported on Windows 10 Anniversary Update or later + SetAttribute(placementText, HStringReference(L"placement"), + u"attribution"_ns); + } + } + + nsAutoString disableButtonTitle; + ns = bundle->FormatStringFromName("webActions.disableForOrigin.label", + formatStrings, disableButtonTitle); + NS_ENSURE_SUCCESS(ns, nullptr); + + AddActionNode(toastXml, actionsNode, disableButtonTitle, launchArg, + u"snooze"_ns, u"contextmenu"_ns); + } + + bool wantSettings = true; +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // Notifications popped from a background task want to invoke Firefox with a + // different profile -- the default browsing profile. Don't link to Firefox + // settings in some different profile: the relevant Firefox settings won't + // take effect. + wantSettings = false; + } +#endif + if (MOZ_LIKELY(wantSettings)) { + nsAutoString settingsButtonTitle; + bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle); + success = AddActionNode(toastXml, actionsNode, settingsButtonTitle, + launchArg, u"settings"_ns, u"contextmenu"_ns); + NS_ENSURE_TRUE(success, nullptr); + } + + for (const auto& action : mActions) { + // Bug 1778596: include per-action icon from image URL. + nsString title; + ns = action->GetTitle(title); + NS_ENSURE_SUCCESS(ns, nullptr); + + nsString actionString; + ns = action->GetAction(actionString); + NS_ENSURE_SUCCESS(ns, nullptr); + + // Privileged/chrome alerts can have actions that are activated by Windows. + // Recognize these actions and enable these activations. + bool activationType(false); + ns = action->GetWindowsSystemActivationType(&activationType); + NS_ENSURE_SUCCESS(ns, nullptr); + + nsString activationTypeString( + (mIsSystemPrincipal && activationType) ? u"system"_ns : u""_ns); + success = AddActionNode(toastXml, actionsNode, title, launchArg, + actionString, u""_ns, activationTypeString); + NS_ENSURE_TRUE(success, nullptr); + } + + ComPtr appendedChild; + hr = toastNodeRoot->AppendChild(actionsNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + if (mIsSilent) { + ComPtr audioNode; + // Create