diff options
Diffstat (limited to 'widget/windows')
207 files changed, 81182 insertions, 0 deletions
diff --git a/widget/windows/AudioSession.cpp b/widget/windows/AudioSession.cpp new file mode 100644 index 0000000000..c14278f56c --- /dev/null +++ b/widget/windows/AudioSession.cpp @@ -0,0 +1,346 @@ +/* -*- 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 <atomic> +#include <audiopolicy.h> +#include <windows.h> +#include <mmdeviceapi.h> + +#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<IAudioSessionControl> mAudioSessionControl; + nsString mDisplayName; + nsString mIconPath; + nsID mSessionGroupingParameter; + // Guards the IAudioSessionControl + mozilla::Mutex mMutex MOZ_UNANNOTATED; + + ThreadSafeAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD +}; + +StaticRefPtr<AudioSession> 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<IAudioSessionEvents*>(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<nsIStringBundleService> bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + MOZ_ASSERT(bundleService); + + nsCOMPtr<nsIStringBundle> 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<IMMDeviceEnumerator> enumerator; + HRESULT hr = + ::CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, + IID_IMMDeviceEnumerator, getter_AddRefs(enumerator)); + if (FAILED(hr)) { + return; + } + + RefPtr<IMMDevice> device; + hr = enumerator->GetDefaultAudioEndpoint( + EDataFlow::eRender, ERole::eMultimedia, getter_AddRefs(device)); + if (FAILED(hr)) { + return; + } + + RefPtr<IAudioSessionManager> 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. + mscom::AgileReference agileAsc(mAudioSessionControl); + mAudioSessionControl = nullptr; + NS_DispatchToMainThread(NS_NewRunnableFunction( + "FreeAudioSession", + [agileAsc = std::move(agileAsc), shouldRestart]() mutable { + // Now release the AgileReference which holds our only reference to the + // IAudioSessionControl, then maybe restart. + agileAsc = nullptr; + 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 <utility> + +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 <typename T> +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 <typename... Args> + explicit CheckInvariantWrapper(std::in_place_t, Args&&... args) + : mObject(std::forward<Args>(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..b294efef51 --- /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<CompositorVsyncDispatcher> aVsyncDispatcher, + RefPtr<CompositorWidgetVsyncObserver> aVsyncObserver, + const CompositorWidgetInitData& aInitData) + : mVsyncDispatcher(aVsyncDispatcher), + mVsyncObserver(aVsyncObserver), + mCompositorWnd(nullptr), + mWnd(reinterpret_cast<HWND>( + 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<remote_backbuffer::Provider>(); + 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(TransparencyMode 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<HWND>(aParentWnd); + if (mWnd == parentWnd) { + mCompositorWnd = reinterpret_cast<HWND>(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..bec8ead98e --- /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<CompositorVsyncDispatcher> aVsyncDispatcher, + RefPtr<CompositorWidgetVsyncObserver> 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(TransparencyMode 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<CompositorVsyncDispatcher> mVsyncDispatcher; + RefPtr<CompositorWidgetVsyncObserver> mVsyncObserver; + HWND mCompositorWnd; + + HWND mWnd; + TransparencyMode mTransparencyMode; + + std::unique_ptr<remote_backbuffer::Provider> 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..b25d30d9d5 --- /dev/null +++ b/widget/windows/CompositorWidgetParent.cpp @@ -0,0 +1,232 @@ +/* -*- 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 <ddraw.h> + +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<HWND>( + aInitData.get_WinCompositorWidgetInitData().hWnd())), + mTransparencyMode(uint32_t( + 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<remote_backbuffer::Client>(); + 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<gfx::DrawTarget> +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<gfx::DrawTarget> +CompositorWidgetParent::GetBackBufferDrawTarget(gfx::DrawTarget* aScreenTarget, + const gfx::IntRect& aRect, + bool* aOutIsCleared) { + MOZ_CRASH( + "Unexpected call to GetBackBufferDrawTarget() with remote " + "backbuffering in use"); +} + +already_AddRefed<gfx::SourceSurface> +CompositorWidgetParent::EndBackBufferDrawing() { + MOZ_CRASH( + "Unexpected call to EndBackBufferDrawing() with remote " + "backbuffering in use"); +} + +bool CompositorWidgetParent::InitCompositor(layers::Compositor* aCompositor) { + return true; +} + +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 TransparencyMode& aMode) { + mTransparencyMode = uint32_t(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> 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<VsyncObserver> 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<CompositorWidgetParent> self = this; + SendUpdateCompositorWnd(reinterpret_cast<WindowsHandle>(aCompositorWnd), + reinterpret_cast<WindowsHandle>(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..0f91fa7ccb --- /dev/null +++ b/widget/windows/CompositorWidgetParent.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_CompositorWidgetParent_h +#define widget_windows_CompositorWidgetParent_h + +#include "WinCompositorWidget.h" +#include "mozilla/Maybe.h" +#include "mozilla/widget/PCompositorWidgetParent.h" + +namespace mozilla { +namespace widget { + +enum class TransparencyMode : uint8_t; + +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<gfx::DrawTarget> 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<gfx::DrawTarget> GetBackBufferDrawTarget( + gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect, + bool* aOutIsCleared) override; + already_AddRefed<gfx::SourceSurface> EndBackBufferDrawing() override; + bool InitCompositor(layers::Compositor* aCompositor) override; + bool IsHidden() 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 TransparencyMode& 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<VsyncObserver> GetVsyncObserver() const override; + + // PlatformCompositorWidgetDelegate Overrides + void UpdateCompositorWnd(const HWND aCompositorWnd, + const HWND aParentWnd) override; + void SetRootLayerTreeID(const layers::LayersId& aRootLayerTreeId) override; + + private: + RefPtr<VsyncObserver> mVsyncObserver; + Maybe<layers::LayersId> mRootLayerTreeID; + + HWND mWnd; + + gfx::CriticalSection mPresentLock; + + // Transparency handling. + mozilla::Atomic<uint32_t, MemoryOrdering::Relaxed> mTransparencyMode; + + // Visibility handling. + mozilla::Atomic<nsSizeMode, MemoryOrdering::Relaxed> mSizeMode; + mozilla::Atomic<bool, MemoryOrdering::Relaxed> mIsFullyOccluded; + + std::unique_ptr<remote_backbuffer::Client> 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..569dd4e189 --- /dev/null +++ b/widget/windows/DirectManipulationOwner.cpp @@ -0,0 +1,722 @@ +/* -*- 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" + +#include "directmanipulation.h" + +namespace mozilla { +namespace widget { + +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<VObserver> 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<IDirectManipulationViewportEventHandler*>(this); + AddRef(); + return S_OK; + } + if (IID_IDirectManipulationInteractionEventHandler == iid) { + *ppv = static_cast<IDirectManipulationInteractionEventHandler*>(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(); + } +} + +void DirectManipulationOwner::Update() { + if (mDmUpdateManager) { + mDmUpdateManager->Update(nullptr); + } +} + +DirectManipulationOwner::DirectManipulationOwner(nsWindow* aWindow) + : mWindow(aWindow) {} + +DirectManipulationOwner::~DirectManipulationOwner() { Destroy(); } + +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<ExternalPixel>( + mWindow->WidgetToScreenOffset(), + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); + + POINT cursor_pos; + ::GetCursorPos(&cursor_pos); + HWND wnd = static_cast<HWND>(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<HWND>(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); +} + +void DirectManipulationOwner::Init(const LayoutDeviceIntRect& aBounds) { + 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<HWND>(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; + } +} + +void DirectManipulationOwner::ResizeViewport( + const LayoutDeviceIntRect& aBounds) { + 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"); + } + } +} + +void DirectManipulationOwner::Destroy() { + 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<HWND>(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; + mWindow = nullptr; +} + +void DirectManipulationOwner::SetContact(UINT aContactId) { + if (mDmViewport) { + mDmViewport->SetContact(aContactId); + } +} + +/*static */ void DirectManipulationOwner::SynthesizeNativeTouchpadPan( + nsWindow* aWindow, nsIWidget::TouchpadGesturePhase aEventPhase, + LayoutDeviceIntPoint aPoint, double aDeltaX, double aDeltaY, + int32_t aModifierFlags) { + DManipEventHandler::SynthesizeNativeTouchpadPan( + aWindow, aEventPhase, aPoint, aDeltaX, aDeltaY, aModifierFlags); +} + +/*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); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/DirectManipulationOwner.h b/widget/windows/DirectManipulationOwner.h new file mode 100644 index 0000000000..b4d5877d70 --- /dev/null +++ b/widget/windows/DirectManipulationOwner.h @@ -0,0 +1,54 @@ +/* -*- 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 <windows.h> +#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; + DWORD mDmViewportHandlerCookie; + RefPtr<IDirectManipulationManager> mDmManager; + RefPtr<IDirectManipulationUpdateManager> mDmUpdateManager; + RefPtr<IDirectManipulationViewport> mDmViewport; + RefPtr<DManipEventHandler> mDmHandler; +}; + +} // 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..6f033785f5 --- /dev/null +++ b/widget/windows/GfxInfo.cpp @@ -0,0 +1,2082 @@ +/* -*- 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 <intrin.h> +#include <windows.h> +#include <devguid.h> // for GUID_DEVCLASS_BATTERY +#include <setupapi.h> // for SetupDi* +#include <winioctl.h> // for IOCTL_* +#include <batclass.h> // 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<ClearTypeParameterInfo> 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<const wchar_t*>(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::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<nsString>& 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<uint8_t[]> buffer(new (std::nothrow) uint8_t[bufferSize]); + if (!buffer) { + return true; + } + + PSP_DEVICE_INTERFACE_DETAIL_DATA pdidd = + reinterpret_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA>(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<IDXGIFactory> factory = nullptr; + createDXGIFactory(__uuidof(IDXGIFactory), (void**)(&factory)); + if (factory) { + RefPtr<IDXGIAdapter> 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<nsString> 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; + } +} + +// 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<GfxDriverInfo>& 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::optionalFeatures, + 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::optionalFeatures, + 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::optionalFeatures, + 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::optionalFeatures, + 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::optionalFeatures, + 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::optionalFeatures, + 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::optionalFeatures, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL, + V(22, 19, 162, 4), "FEATURE_FAILURE_BUG_1587155"); + + // Bug 1829487 - Work around a gen6 driver bug that miscompiles shaders + // resulting + // in black squares. Disabling shader optimization pass + // appears to work around this for now. + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::IntelSandyBridge, + nsIGfxInfo::FEATURE_WEBRENDER_OPTIMIZED_SHADERS, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_LESS_THAN, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1829487"); + + // 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::optionalFeatures, + 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::optionalFeatures, \ + 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::optionalFeatures, + 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::optionalFeatures, + 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::optionalFeatures, + 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::optionalFeatures, + 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::optionalFeatures, + 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::optionalFeatures, 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::optionalFeatures, 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::optionalFeatures, 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"); + APPEND_TO_DRIVER_BLOCKLIST2( + OperatingSystem::Windows, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_VIDEO_OVERLAY, nsIGfxInfo::FEATURE_ALLOW_ALWAYS, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_ROLLOUT_NVIDIA"); +#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, 4390), "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::Windows10, DeviceFamily::IntelKabyLake, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + GfxDriverInfo::allDriverVersions, "FEATURE_FAILURE_BUG_1802357"); + + 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_REUSE_DECODER_DEVICE + + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows10, DeviceFamily::IntelSkylake, + nsIGfxInfo::FEATURE_REUSE_DECODER_DEVICE, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(20, 19, 15, 4285), V(20, 19, 15, 4390), "FEATURE_FAILURE_BUG_1833809", + "Intel driver 20.19.15.*"); + + APPEND_TO_DRIVER_BLOCKLIST_RANGE( + OperatingSystem::Windows10, DeviceFamily::IntelSkylake, + nsIGfxInfo::FEATURE_REUSE_DECODER_DEVICE, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_BETWEEN_INCLUSIVE, + V(10, 18, 15, 4256), V(10, 18, 15, 4293), "FEATURE_FAILURE_BUG_1833809", + "Intel driver 10.18.15.*"); + + //////////////////////////////////// + // 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<GfxDriverInfo>& 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))) { + if (OnlyAllowFeatureOnKnownConfig(aFeature)) { + aFailureId = "FEATURE_FAILURE_GET_ADAPTER"; + *aStatus = FEATURE_BLOCKED_DEVICE; + } else { + *aStatus = FEATURE_STATUS_OK; + } + return NS_OK; + } + + if (OnlyAllowFeatureOnKnownConfig(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; + } + + if (adapterDriverVersionString.Length() == 0) { + if (OnlyAllowFeatureOnKnownConfig(aFeature)) { + aFailureId = "FEATURE_FAILURE_EMPTY_DRIVER_VERSION"; + *aStatus = FEATURE_BLOCKED_DRIVER_VERSION; + } else { + *aStatus = FEATURE_STATUS_OK; + } + return NS_OK; + } + + uint64_t driverVersion; + if (!ParseDriverVersion(adapterDriverVersionString, &driverVersion)) { + if (OnlyAllowFeatureOnKnownConfig(aFeature)) { + aFailureId = "FEATURE_FAILURE_PARSE_DRIVER"; + *aStatus = FEATURE_BLOCKED_DRIVER_VERSION; + } else { + *aStatus = FEATURE_STATUS_OK; + } + return NS_OK; + } + } + + return GfxInfoBase::GetFeatureStatusImpl( + aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aFailureId, &os); +} + +void GfxInfo::DescribeFeatures(JSContext* aCx, JS::Handle<JSObject*> aObj) { + // Add the platform neutral features + GfxInfoBase::DescribeFeatures(aCx, aObj); + + JS::Rooted<JSObject*> 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<JS::Value> 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<nsIGfxInfo> 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<JSString*> str(aCx, JS_NewStringCopyZ(aCx, version)); + JS::Rooted<JS::Value> 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; +} + +#endif diff --git a/widget/windows/GfxInfo.h b/widget/windows/GfxInfo.h new file mode 100644 index 0000000000..f2549afa4e --- /dev/null +++ b/widget/windows/GfxInfo.h @@ -0,0 +1,105 @@ +/* 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 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<GfxDriverInfo>& aDriverInfo, + nsACString& aFailureId, + OperatingSystem* aOS = nullptr) override; + const nsTArray<GfxDriverInfo>& GetGfxDriverInfo() override; + + void DescribeFeatures(JSContext* cx, JS::Handle<JSObject*> 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 <algorithm> + +CEnumFormatEtc::CEnumFormatEtc() : mRefCnt(0), mCurrentIdx(0) {} + +// Constructor used by Clone() +CEnumFormatEtc::CEnumFormatEtc(nsTArray<FormatEtc>& 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<uint32_t>(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 <ole2.h> + +#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<FormatEtc>& 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<FormatEtc> 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..2f64f93922 --- /dev/null +++ b/widget/windows/IMMHandler.cpp @@ -0,0 +1,2408 @@ +/* -*- 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 <algorithm> + +#include "mozilla/CheckedInt.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/TextEvents.h" +#include "mozilla/ToString.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<uint32_t>(aReconv->dwSize)); + AppendLiteral(", dwVersion="); + AppendInt(static_cast<uint32_t>(aReconv->dwVersion)); + AppendLiteral(", dwStrLen="); + AppendInt(static_cast<uint32_t>(aReconv->dwStrLen)); + AppendLiteral(", dwStrOffset="); + AppendInt(static_cast<uint32_t>(aReconv->dwStrOffset)); + AppendLiteral(", dwCompStrLen="); + AppendInt(static_cast<uint32_t>(aReconv->dwCompStrLen)); + AppendLiteral(", dwCompStrOffset="); + AppendInt(static_cast<uint32_t>(aReconv->dwCompStrOffset)); + AppendLiteral(", dwTargetStrLen="); + AppendInt(static_cast<uint32_t>(aReconv->dwTargetStrLen)); + AppendLiteral(", dwTargetStrOffset="); + AppendInt(static_cast<uint32_t>(aReconv->dwTargetStrOffset)); + AppendLiteral(", result str=\""); + if (aReconv->dwStrLen) { + char16_t* strStart = reinterpret_cast<char16_t*>( + reinterpret_cast<char*>(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 ((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<HKL>(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>& 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<TextEventDispatcher> 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<long>(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<long>(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<TextEventDispatcher> 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<RECONVERTSTRING*>(lParam); + + const Maybe<ContentSelection>& 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<LPVOID>(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<IMECHARPOSITION*>(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<nsWindow*>( + 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<RECONVERTSTRING*>(lParam); + + LayoutDeviceIntPoint point(0, 0); + + bool hasCompositionString = + mIsComposing && ShouldDrawCompositionStringOurselves(); + + int32_t targetOffset, targetLength; + if (!hasCompositionString) { + const Maybe<ContentSelection>& 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<int32_t>( + contentSelection->OffsetAndDataRef().StartOffset()); + targetLength = + static_cast<int32_t>(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<int32_t>(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<LPVOID>(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<nsWindow> kungFuDeathGrip(aWindow); + RefPtr<TextEventDispatcher> 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<uint32_t>(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>& 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<ContentSelection> 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..e012541fae --- /dev/null +++ b/widget/windows/IMMHandler.h @@ -0,0 +1,427 @@ +/* -*- 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 "windef.h" +#include "winnetwk.h" +#include "npapi.h" + +#include "nsCOMPtr.h" +#include "nsIWidget.h" +#include "nsRect.h" +#include "nsString.h" +#include "nsTArray.h" + +#include <windows.h> + +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<MSG> 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<TextEventDispatcher> mDispatcher; + nsString mCompositionString; + nsTArray<uint32_t> mClauseArray; + nsTArray<uint8_t> 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<ContentSelection> mContentSelection; + + const Maybe<ContentSelection>& 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<ContentSelection> sTempContentSelection; + sTempContentSelection = QueryContentSelection(aWindow); + return sTempContentSelection; + } + + /** + * Query content selection on aWindow with WidgetQueryContent event. + */ + static Maybe<ContentSelection> 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..de491b734b --- /dev/null +++ b/widget/windows/InProcessWinCompositorWidget.cpp @@ -0,0 +1,368 @@ +/* -*- 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 "nsIWidget.h" +#include "nsWindow.h" +#include "VsyncDispatcher.h" +#include "WinCompositorWindowThread.h" +#include "VRShMem.h" + +#include <ddraw.h> + +namespace mozilla::widget { + +using namespace mozilla::gfx; +using namespace mozilla; + +/* static */ +RefPtr<CompositorWidget> 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<HeadlessWidget*>(aWidget)); + } else { + return new InProcessWinCompositorWidget( + aInitData.get_WinCompositorWidgetInitData(), aOptions, + static_cast<nsWindow*>(aWidget)); + } +} + +InProcessWinCompositorWidget::InProcessWinCompositorWidget( + const WinCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, nsWindow* aWindow) + : WinCompositorWidget(aInitData, aOptions), + mWindow(aWindow), + mWnd(reinterpret_cast<HWND>(aInitData.hWnd())), + mTransparentSurfaceLock("mTransparentSurfaceLock"), + mTransparencyMode(uint32_t(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<gfx::DrawTarget> +InProcessWinCompositorWidget::StartRemoteDrawing() { + MutexAutoLock lock(mTransparentSurfaceLock); + + MOZ_ASSERT(!mCompositeDC); + + RefPtr<gfxASurface> surf; + if (TransparencyModeIs(TransparencyMode::Transparent)) { + 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 = TransparencyModeIs(TransparencyMode::Opaque) + ? 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<DrawTarget> 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 (TransparencyModeIs(TransparencyMode::Transparent)) { + 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<int>(scanLine) > height / 2) { + // No need to defer. + return false; + } + + return true; +} + +already_AddRefed<gfx::DrawTarget> +InProcessWinCompositorWidget::GetBackBufferDrawTarget( + gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect, + bool* aOutIsCleared) { + MOZ_ASSERT(!mLockedBackBufferData); + + RefPtr<gfx::DrawTarget> 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<gfx::DrawTarget> dataTarget = Factory::CreateDrawTargetForData( + BackendType::CAIRO, destData, destSize, destStride, destFormat); + mLockedBackBufferData = destData; + + return dataTarget.forget(); +} + +already_AddRefed<gfx::SourceSurface> +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<gfxASurface> InProcessWinCompositorWidget::EnsureTransparentSurface() { + mTransparentSurfaceLock.AssertCurrentThreadOwns(); + MOZ_ASSERT(TransparencyModeIs(TransparencyMode::Transparent)); + + IntSize size = GetClientSize().ToUnknownSize(); + if (!mTransparentSurface || mTransparentSurface->GetSize() != size) { + mTransparentSurface = nullptr; + mMemoryDC = nullptr; + CreateTransparentSurface(size); + } + + RefPtr<gfxASurface> surface = mTransparentSurface; + return surface.forget(); +} + +void InProcessWinCompositorWidget::CreateTransparentSurface( + const gfx::IntSize& aSize) { + mTransparentSurfaceLock.AssertCurrentThreadOwns(); + MOZ_ASSERT(!mTransparentSurface && !mMemoryDC); + RefPtr<gfxWindowsSurface> surface = + new gfxWindowsSurface(aSize, SurfaceFormat::A8R8G8B8_UINT32); + mTransparentSurface = surface; + mMemoryDC = surface->GetDC(); +} + +void InProcessWinCompositorWidget::UpdateTransparency(TransparencyMode aMode) { + gfx::CriticalSectionAutoEnter presentLock(&mPresentLock); + MutexAutoLock lock(mTransparentSurfaceLock); + if (TransparencyModeIs(aMode)) { + return; + } + + mTransparencyMode = uint32_t(aMode); + mTransparentSurface = nullptr; + mMemoryDC = nullptr; + + if (aMode == TransparencyMode::Transparent) { + 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; +} + +void InProcessWinCompositorWidget::ClearTransparentWindow() { + gfx::CriticalSectionAutoEnter presentLock(&mPresentLock); + MutexAutoLock lock(mTransparentSurfaceLock); + if (!mTransparentSurface) { + return; + } + + EnsureTransparentSurface(); + + IntSize size = mTransparentSurface->GetSize(); + if (!size.IsEmpty()) { + RefPtr<DrawTarget> drawTarget = + gfxPlatform::CreateDrawTargetForSurface(mTransparentSurface, size); + if (!drawTarget) { + return; + } + drawTarget->ClearRect(Rect(0, 0, size.width, size.height)); + RedrawTransparentWindow(); + } +} + +bool InProcessWinCompositorWidget::RedrawTransparentWindow() { + MOZ_ASSERT(TransparencyModeIs(TransparencyMode::Transparent)); + + 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 TransparencyModeIs(TransparencyMode::Transparent) ? mMemoryDC + : ::GetDC(mWnd); +} + +void InProcessWinCompositorWidget::FreeWindowSurface(HDC dc) { + if (!TransparencyModeIs(TransparencyMode::Transparent)) { + ::ReleaseDC(mWnd, dc); + } +} + +bool InProcessWinCompositorWidget::IsHidden() const { return ::IsIconic(mWnd); } + +nsIWidget* InProcessWinCompositorWidget::RealWidget() { return mWindow; } + +void InProcessWinCompositorWidget::ObserveVsync(VsyncObserver* aObserver) { + if (RefPtr<CompositorVsyncDispatcher> cvd = + mWindow->GetCompositorVsyncDispatcher()) { + cvd->SetCompositorVsyncObserver(aObserver); + } +} + +void InProcessWinCompositorWidget::UpdateCompositorWnd( + const HWND aCompositorWnd, const HWND aParentWnd) { + MOZ_ASSERT(layers::CompositorThreadHolder::IsInCompositorThread()); + MOZ_ASSERT(aCompositorWnd && aParentWnd); + MOZ_ASSERT(aParentWnd == mWnd); + + // Since we're in the parent process anyway, we can just call SetParent + // directly. + ::SetParent(aCompositorWnd, aParentWnd); + mSetParentCompleted = true; +} +} // namespace mozilla::widget diff --git a/widget/windows/InProcessWinCompositorWidget.h b/widget/windows/InProcessWinCompositorWidget.h new file mode 100644 index 0000000000..e287204461 --- /dev/null +++ b/widget/windows/InProcessWinCompositorWidget.h @@ -0,0 +1,111 @@ +/* -*- 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::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<gfx::DrawTarget> StartRemoteDrawing() override; + void EndRemoteDrawing() override; + bool NeedsToDeferEndRemoteDrawing() override; + LayoutDeviceIntSize GetClientSize() override; + already_AddRefed<gfx::DrawTarget> GetBackBufferDrawTarget( + gfx::DrawTarget* aScreenTarget, const gfx::IntRect& aRect, + bool* aOutIsCleared) override; + already_AddRefed<gfx::SourceSurface> 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(TransparencyMode aMode) override; + void NotifyVisibilityUpdated(nsSizeMode aSizeMode, + bool aIsFullyOccluded) override; + void ClearTransparentWindow() override; + + bool RedrawTransparentWindow(); + + // Ensure that a transparent surface exists, then return it. + RefPtr<gfxASurface> EnsureTransparentSurface(); + + HDC GetTransparentDC() const { return mMemoryDC; } + + mozilla::Mutex& GetTransparentSurfaceLock() { + return mTransparentSurfaceLock; + } + + 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<uint32_t, MemoryOrdering::Relaxed> mTransparencyMode; + + bool TransparencyModeIs(TransparencyMode aMode) const { + return TransparencyMode(uint32_t(mTransparencyMode)) == aMode; + } + + // Visibility handling. + mozilla::Atomic<nsSizeMode, MemoryOrdering::Relaxed> mSizeMode; + mozilla::Atomic<bool, MemoryOrdering::Relaxed> mIsFullyOccluded; + + RefPtr<gfxASurface> mTransparentSurface; + HDC mMemoryDC; + HDC mCompositeDC; + + // Locked back buffer of BasicCompositor + uint8_t* mLockedBackBufferData; + + bool mNotDeferEndRemoteDrawing; +}; + +} // namespace mozilla::widget + +#endif // widget_windows_InProcessWinCompositorWidget_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 <dbt.h> +#include <hidclass.h> +#include <ntddmou.h> +#include <setupapi.h> + +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 <windows.h> + +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..80b1c29aa7 --- /dev/null +++ b/widget/windows/JumpListBuilder.cpp @@ -0,0 +1,818 @@ +/* -*- 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 <windows.h> +#include <shobjidl.h> +#include <propkey.h> +#include <propvarutil.h> +#include <shellapi.h> +#include "JumpListBuilder.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/WindowsJumpListShortcutDescriptionBinding.h" +#include "mozilla/mscom/EnsureMTA.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsServiceManagerUtils.h" +#include "WinUtils.h" + +using mozilla::dom::Promise; +using mozilla::dom::WindowsJumpListShortcutDescription; + +namespace mozilla { +namespace widget { + +NS_IMPL_ISUPPORTS(JumpListBuilder, nsIJumpListBuilder, nsIObserver) + +#define TOPIC_PROFILE_BEFORE_CHANGE "profile-before-change" +#define TOPIC_CLEAR_PRIVATE_DATA "clear-private-data" + +// 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 + +const char kPrefTaskbarEnabled[] = "browser.taskbar.lists.enabled"; + +/** + * A wrapper around a ICustomDestinationList that implements the JumpListBackend + * interface. This is an implementation of JumpListBackend that actually causes + * items to appear in a Windows jump list. + */ +class NativeJumpListBackend : public JumpListBackend { + // We use NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET because this + // class might be destroyed on a different thread than the one it + // was created on, since it's maintained by a LazyIdleThread. + // + // This is a workaround for bug 1648031. + NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(JumpListBackend, override) + + NativeJumpListBackend() { + MOZ_ASSERT(!NS_IsMainThread()); + + mscom::EnsureMTA([&]() { + RefPtr<ICustomDestinationList> destList; + HRESULT hr = ::CoCreateInstance( + CLSID_DestinationList, nullptr, CLSCTX_INPROC_SERVER, + IID_ICustomDestinationList, getter_AddRefs(destList)); + if (FAILED(hr)) { + return; + } + + mWindowsDestList = destList; + }); + } + + virtual bool IsAvailable() override { + MOZ_ASSERT(!NS_IsMainThread()); + return mWindowsDestList != nullptr; + } + + virtual HRESULT SetAppID(LPCWSTR pszAppID) override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mWindowsDestList); + + return mWindowsDestList->SetAppID(pszAppID); + } + + virtual HRESULT BeginList(UINT* pcMinSlots, REFIID riid, + void** ppv) override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mWindowsDestList); + + return mWindowsDestList->BeginList(pcMinSlots, riid, ppv); + } + + virtual HRESULT AddUserTasks(IObjectArray* poa) override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mWindowsDestList); + + return mWindowsDestList->AddUserTasks(poa); + } + + virtual HRESULT AppendCategory(LPCWSTR pszCategory, + IObjectArray* poa) override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mWindowsDestList); + + return mWindowsDestList->AppendCategory(pszCategory, poa); + } + + virtual HRESULT CommitList() override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mWindowsDestList); + + return mWindowsDestList->CommitList(); + } + + virtual HRESULT AbortList() override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mWindowsDestList); + + return mWindowsDestList->AbortList(); + } + + virtual HRESULT DeleteList(LPCWSTR pszAppID) override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mWindowsDestList); + + return mWindowsDestList->DeleteList(pszAppID); + } + + virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mWindowsDestList); + + return mWindowsDestList->AppendKnownCategory(category); + } + + protected: + virtual ~NativeJumpListBackend() override{}; + + private: + RefPtr<ICustomDestinationList> mWindowsDestList; +}; + +JumpListBuilder::JumpListBuilder(const nsAString& aAppUserModelId, + RefPtr<JumpListBackend> aTestingBackend) { + MOZ_ASSERT(NS_IsMainThread()); + + mAppUserModelId.Assign(aAppUserModelId); + + Preferences::AddStrongObserver(this, kPrefTaskbarEnabled); + + // Make a lazy thread for any IO. + mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, "Jump List", + LazyIdleThread::ManualShutdown); + + nsCOMPtr<nsIObserverService> 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); + } + + nsCOMPtr<nsIRunnable> runnable; + + if (aTestingBackend) { + // Dispatch a task that hands a reference to the testing backend + // to the background thread. The testing backend was probably + // constructed on the main thread, and is responsible for doing + // any locking as well as cleanup. + runnable = NewRunnableMethod<RefPtr<JumpListBackend>>( + "SetupTestingBackend", this, &JumpListBuilder::DoSetupTestingBackend, + aTestingBackend); + + } else { + // Dispatch a task that constructs the native jump list backend. + runnable = NewRunnableMethod("SetupBackend", this, + &JumpListBuilder::DoSetupBackend); + } + + mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + + // 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()) { + mIOThread->Dispatch( + NewRunnableMethod<nsString>( + "SetAppID", this, &JumpListBuilder::DoSetAppID, aAppUserModelId), + NS_DISPATCH_NORMAL); + } +} + +JumpListBuilder::~JumpListBuilder() { + Preferences::RemoveObserver(this, kPrefTaskbarEnabled); +} + +void JumpListBuilder::DoSetupTestingBackend( + RefPtr<JumpListBackend> aTestingBackend) { + MOZ_ASSERT(!NS_IsMainThread()); + mJumpListBackend = aTestingBackend; +} + +void JumpListBuilder::DoSetupBackend() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!mJumpListBackend); + mJumpListBackend = new NativeJumpListBackend(); +} + +void JumpListBuilder::DoShutdownBackend() { + MOZ_ASSERT(!NS_IsMainThread()); + mJumpListBackend = nullptr; +} + +void JumpListBuilder::DoSetAppID(nsString aAppUserModelID) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mJumpListBackend); + mJumpListBackend->SetAppID(aAppUserModelID.get()); +} + +NS_IMETHODIMP +JumpListBuilder::ObtainAndCacheFavicon(nsIURI* aFaviconURI, + nsAString& aCachedIconPath) { + MOZ_ASSERT(NS_IsMainThread()); + nsString iconFilePath; + nsresult rv = mozilla::widget::FaviconHelper::ObtainCachedIconFile( + aFaviconURI, iconFilePath, mIOThread, false); + NS_ENSURE_SUCCESS(rv, rv); + + aCachedIconPath = iconFilePath; + return NS_OK; +} + +NS_IMETHODIMP +JumpListBuilder::IsAvailable(JSContext* aCx, Promise** aPromise) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPromise); + MOZ_ASSERT(mIOThread); + + ErrorResult result; + RefPtr<Promise> promise = + Promise::Create(xpc::CurrentNativeGlobal(aCx), result); + + if (MOZ_UNLIKELY(result.Failed())) { + return result.StealNSResult(); + } + + nsMainThreadPtrHandle<Promise> promiseHolder( + new nsMainThreadPtrHolder<Promise>("JumpListBuilder::IsAvailable promise", + promise)); + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod<nsMainThreadPtrHandle<Promise>>( + "IsAvailable", this, &JumpListBuilder::DoIsAvailable, + std::move(promiseHolder)); + nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + promise.forget(aPromise); + return NS_OK; +} + +NS_IMETHODIMP +JumpListBuilder::CheckForRemovals(JSContext* aCx, Promise** aPromise) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPromise); + MOZ_ASSERT(mIOThread); + + ErrorResult result; + RefPtr<Promise> promise = + Promise::Create(xpc::CurrentNativeGlobal(aCx), result); + + if (MOZ_UNLIKELY(result.Failed())) { + return result.StealNSResult(); + } + + nsMainThreadPtrHandle<Promise> promiseHolder( + new nsMainThreadPtrHolder<Promise>( + "JumpListBuilder::CheckForRemovals promise", promise)); + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod<nsMainThreadPtrHandle<Promise>>( + "CheckForRemovals", this, &JumpListBuilder::DoCheckForRemovals, + std::move(promiseHolder)); + + nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + promise.forget(aPromise); + + return NS_OK; +} + +NS_IMETHODIMP +JumpListBuilder::PopulateJumpList( + const nsTArray<JS::Value>& aTaskDescriptions, const nsAString& aCustomTitle, + const nsTArray<JS::Value>& aCustomDescriptions, JSContext* aCx, + Promise** aPromise) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPromise); + MOZ_ASSERT(mIOThread); + + if (aCustomDescriptions.Length() && aCustomTitle.IsEmpty()) { + return NS_ERROR_INVALID_ARG; + } + + // Get rid of the old icons + nsCOMPtr<nsIRunnable> event = + new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(true); + mIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + + nsTArray<WindowsJumpListShortcutDescription> taskDescs; + for (auto& jsval : aTaskDescriptions) { + JS::Rooted<JS::Value> rootedVal(aCx); + if (NS_WARN_IF(!dom::ToJSValue(aCx, jsval, &rootedVal))) { + return NS_ERROR_INVALID_ARG; + } + + WindowsJumpListShortcutDescription desc; + if (!desc.Init(aCx, rootedVal)) { + return NS_ERROR_INVALID_ARG; + } + + taskDescs.AppendElement(std::move(desc)); + } + + nsTArray<WindowsJumpListShortcutDescription> customDescs; + for (auto& jsval : aCustomDescriptions) { + JS::Rooted<JS::Value> rootedVal(aCx); + if (NS_WARN_IF(!dom::ToJSValue(aCx, jsval, &rootedVal))) { + return NS_ERROR_INVALID_ARG; + } + + WindowsJumpListShortcutDescription desc; + if (!desc.Init(aCx, rootedVal)) { + return NS_ERROR_INVALID_ARG; + } + + customDescs.AppendElement(std::move(desc)); + } + + ErrorResult result; + RefPtr<Promise> promise = + Promise::Create(xpc::CurrentNativeGlobal(aCx), result); + + if (MOZ_UNLIKELY(result.Failed())) { + return result.StealNSResult(); + } + + nsMainThreadPtrHandle<Promise> promiseHolder( + new nsMainThreadPtrHolder<Promise>( + "JumpListBuilder::PopulateJumpList promise", promise)); + + nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod< + StoreCopyPassByRRef<nsTArray<WindowsJumpListShortcutDescription>>, + nsString, + StoreCopyPassByRRef<nsTArray<WindowsJumpListShortcutDescription>>, + nsMainThreadPtrHandle<Promise>>( + "PopulateJumpList", this, &JumpListBuilder::DoPopulateJumpList, + std::move(taskDescs), aCustomTitle, std::move(customDescs), + std::move(promiseHolder)); + nsresult rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + promise.forget(aPromise); + + return NS_OK; +} + +NS_IMETHODIMP +JumpListBuilder::ClearJumpList(JSContext* aCx, Promise** aPromise) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPromise); + MOZ_ASSERT(mIOThread); + + ErrorResult result; + RefPtr<Promise> promise = + Promise::Create(xpc::CurrentNativeGlobal(aCx), result); + + if (MOZ_UNLIKELY(result.Failed())) { + return result.StealNSResult(); + } + + nsMainThreadPtrHandle<Promise> promiseHolder( + new nsMainThreadPtrHolder<Promise>( + "JumpListBuilder::ClearJumpList promise", promise)); + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod<nsMainThreadPtrHandle<Promise>>( + "ClearJumpList", this, &JumpListBuilder::DoClearJumpList, + std::move(promiseHolder)); + 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::DoIsAvailable( + const nsMainThreadPtrHandle<Promise>& aPromiseHolder) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPromiseHolder); + + if (!mJumpListBackend) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "DoIsAvailable", [promiseHolder = std::move(aPromiseHolder)]() { + promiseHolder.get()->MaybeResolve(false); + })); + return; + } + + bool isAvailable = mJumpListBackend->IsAvailable(); + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "DoIsAvailable", + [promiseHolder = std::move(aPromiseHolder), isAvailable]() { + promiseHolder.get()->MaybeResolve(isAvailable); + })); +} + +void JumpListBuilder::DoCheckForRemovals( + const nsMainThreadPtrHandle<Promise>& aPromiseHolder) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPromiseHolder); + + nsresult rv = NS_ERROR_UNEXPECTED; + + auto errorHandler = MakeScopeExit([&aPromiseHolder, &rv]() { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "DoCheckForRemovals", + [promiseHolder = std::move(aPromiseHolder), rv]() { + promiseHolder.get()->MaybeReject(rv); + })); + }); + + MOZ_ASSERT(mJumpListBackend); + if (!mJumpListBackend) { + return; + } + + // Abort any ongoing list building that might not have been committed, + // otherwise BeginList will give us problems. + Unused << mJumpListBackend->AbortList(); + + nsTArray<nsString> urisToRemove; + RefPtr<IObjectArray> objArray; + uint32_t maxItems = 0; + + HRESULT hr = mJumpListBackend->BeginList( + &maxItems, + IID_PPV_ARGS(static_cast<IObjectArray**>(getter_AddRefs(objArray)))); + + if (FAILED(hr)) { + rv = NS_ERROR_UNEXPECTED; + return; + } + + RemoveIconCacheAndGetJumplistShortcutURIs(objArray, urisToRemove); + + // We began a list in order to get the removals, which we can now abort. + Unused << mJumpListBackend->AbortList(); + + errorHandler.release(); + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "DoCheckForRemovals", [urisToRemove = std::move(urisToRemove), + promiseHolder = std::move(aPromiseHolder)]() { + promiseHolder.get()->MaybeResolve(urisToRemove); + })); +} + +void JumpListBuilder::DoPopulateJumpList( + const nsTArray<WindowsJumpListShortcutDescription>&& aTaskDescriptions, + const nsAString& aCustomTitle, + const nsTArray<WindowsJumpListShortcutDescription>&& aCustomDescriptions, + const nsMainThreadPtrHandle<Promise>& aPromiseHolder) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPromiseHolder); + + nsresult rv = NS_ERROR_UNEXPECTED; + + auto errorHandler = MakeScopeExit([&aPromiseHolder, &rv]() { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "DoPopulateJumpList", + [promiseHolder = std::move(aPromiseHolder), rv]() { + promiseHolder.get()->MaybeReject(rv); + })); + }); + + MOZ_ASSERT(mJumpListBackend); + if (!mJumpListBackend) { + return; + } + + // Abort any ongoing list building that might not have been committed, + // otherwise BeginList will give us problems. + Unused << mJumpListBackend->AbortList(); + + nsTArray<nsString> urisToRemove; + RefPtr<IObjectArray> objArray; + uint32_t maxItems = 0; + + HRESULT hr = mJumpListBackend->BeginList( + &maxItems, + IID_PPV_ARGS(static_cast<IObjectArray**>(getter_AddRefs(objArray)))); + + if (FAILED(hr)) { + rv = NS_ERROR_UNEXPECTED; + return; + } + + if (urisToRemove.Length()) { + // It'd be nice if we could return a more descriptive error here so that + // the caller knows that they should have called checkForRemovals first. + rv = NS_ERROR_UNEXPECTED; + return; + } + + if (aTaskDescriptions.Length()) { + RefPtr<IObjectCollection> taskCollection; + hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr, + CLSCTX_INPROC_SERVER, IID_IObjectCollection, + getter_AddRefs(taskCollection)); + if (FAILED(hr)) { + rv = NS_ERROR_UNEXPECTED; + return; + } + + // Start by building the task list + for (auto& desc : aTaskDescriptions) { + // These should all be ShellLinks + RefPtr<IShellLinkW> link; + rv = GetShellLinkFromDescription(desc, link); + if (NS_FAILED(rv)) { + // Let the errorHandler deal with this. + return; + } + taskCollection->AddObject(link); + } + + RefPtr<IObjectArray> pTaskArray; + hr = taskCollection->QueryInterface(IID_IObjectArray, + getter_AddRefs(pTaskArray)); + + if (FAILED(hr)) { + rv = NS_ERROR_UNEXPECTED; + return; + } + + hr = mJumpListBackend->AddUserTasks(pTaskArray); + + if (FAILED(hr)) { + rv = NS_ERROR_FAILURE; + return; + } + } + + if (aCustomDescriptions.Length()) { + // Then build the custom list + RefPtr<IObjectCollection> customCollection; + hr = CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr, + CLSCTX_INPROC_SERVER, IID_IObjectCollection, + getter_AddRefs(customCollection)); + if (FAILED(hr)) { + rv = NS_ERROR_UNEXPECTED; + return; + } + + for (auto& desc : aCustomDescriptions) { + // These should all be ShellLinks + RefPtr<IShellLinkW> link; + rv = GetShellLinkFromDescription(desc, link); + if (NS_FAILED(rv)) { + // Let the errorHandler deal with this. + return; + } + customCollection->AddObject(link); + } + + RefPtr<IObjectArray> pCustomArray; + hr = customCollection->QueryInterface(IID_IObjectArray, + getter_AddRefs(pCustomArray)); + if (FAILED(hr)) { + rv = NS_ERROR_UNEXPECTED; + return; + } + + hr = mJumpListBackend->AppendCategory( + reinterpret_cast<const wchar_t*>(aCustomTitle.BeginReading()), + pCustomArray); + + if (FAILED(hr)) { + rv = NS_ERROR_UNEXPECTED; + return; + } + } + + hr = mJumpListBackend->CommitList(); + if (FAILED(hr)) { + rv = NS_ERROR_FAILURE; + return; + } + + errorHandler.release(); + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "DoPopulateJumpList", [promiseHolder = std::move(aPromiseHolder)]() { + promiseHolder.get()->MaybeResolveWithUndefined(); + })); +} + +void JumpListBuilder::DoClearJumpList( + const nsMainThreadPtrHandle<Promise>& aPromiseHolder) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPromiseHolder); + + if (!mJumpListBackend) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "DoClearJumpList", [promiseHolder = std::move(aPromiseHolder)]() { + promiseHolder.get()->MaybeReject(NS_ERROR_UNEXPECTED); + })); + return; + } + + if (SUCCEEDED(mJumpListBackend->DeleteList(mAppUserModelId.get()))) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "DoClearJumpList", [promiseHolder = std::move(aPromiseHolder)]() { + promiseHolder.get()->MaybeResolveWithUndefined(); + })); + } else { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "DoClearJumpList", [promiseHolder = std::move(aPromiseHolder)]() { + promiseHolder.get()->MaybeReject(NS_ERROR_FAILURE); + })); + } +} + +// 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<nsString>& 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<IShellLinkW> pLink; + + if (FAILED(aObjArray->GetAt(idx, IID_IShellLinkW, + static_cast<void**>(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<nsIFile> icoFile; + nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(icoFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + icoFile->Remove(false); + } +} + +// Converts a WindowsJumpListShortcutDescription into a IShellLinkW +nsresult JumpListBuilder::GetShellLinkFromDescription( + const WindowsJumpListShortcutDescription& aDesc, + RefPtr<IShellLinkW>& aShellLink) { + MOZ_ASSERT(!NS_IsMainThread()); + + HRESULT hr; + IShellLinkW* psl; + + // Shell links: + // http://msdn.microsoft.com/en-us/library/bb776891(VS.85).aspx + // http://msdn.microsoft.com/en-us/library/bb774950(VS.85).aspx + + // Create a IShellLink + hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLinkW, (LPVOID*)&psl); + if (FAILED(hr)) { + return NS_ERROR_UNEXPECTED; + } + + // Store the title of the app + if (!aDesc.mTitle.IsEmpty()) { + IPropertyStore* pPropStore = nullptr; + hr = psl->QueryInterface(IID_IPropertyStore, (LPVOID*)&pPropStore); + if (FAILED(hr)) { + return NS_ERROR_UNEXPECTED; + } + + PROPVARIANT pv; + ::InitPropVariantFromString(aDesc.mTitle.get(), &pv); + + pPropStore->SetValue(PKEY_Title, pv); + pPropStore->Commit(); + pPropStore->Release(); + + PropVariantClear(&pv); + } + + // Store the rest of the params + hr = psl->SetPath(aDesc.mPath.get()); + + // According to the documentation at [1], the maximum description length is + // INFOTIPSIZE, so we copy the const string from the description into a buffer + // of that maximum size. However, testing reveals that INFOTIPSIZE is still + // sometimes too large. MAX_PATH seems to work instead. + // + // We truncate to MAX_PATH - 1, since nsAutoString's don't include the null + // character in their capacity calculations, but the string for IShellLinkW + // description does. So by truncating to MAX_PATH - 1, the full contents of + // the truncated nsAutoString will be copied into the IShellLink description + // buffer. + // + // [1]: + // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinka-setdescription + nsAutoString descriptionCopy(aDesc.mDescription.get()); + if (descriptionCopy.Length() >= MAX_PATH) { + descriptionCopy.Truncate(MAX_PATH - 1); + } + + hr = psl->SetDescription(descriptionCopy.get()); + + if (aDesc.mArguments.WasPassed() && !aDesc.mArguments.Value().IsEmpty()) { + hr = psl->SetArguments(aDesc.mArguments.Value().get()); + } else { + hr = psl->SetArguments(L""); + } + + // Set up the fallback icon in the event that a valid icon URI has + // not been supplied. + hr = psl->SetIconLocation(aDesc.mPath.get(), aDesc.mFallbackIconIndex); + + if (aDesc.mIconPath.WasPassed() && !aDesc.mIconPath.Value().IsEmpty()) { + // Always use the first icon in the ICO file, as our encoded icon only has 1 + // resource + hr = psl->SetIconLocation(aDesc.mIconPath.Value().get(), 0); + } + + aShellLink = dont_AddRef(psl); + + return NS_OK; +} + +NS_IMETHODIMP +JumpListBuilder::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG_POINTER(aTopic); + if (strcmp(aTopic, TOPIC_PROFILE_BEFORE_CHANGE) == 0) { + nsCOMPtr<nsIObserverService> observerService = + do_GetService("@mozilla.org/observer-service;1"); + if (observerService) { + observerService->RemoveObserver(this, TOPIC_PROFILE_BEFORE_CHANGE); + observerService->RemoveObserver(this, TOPIC_CLEAR_PRIVATE_DATA); + } + + mIOThread->Dispatch(NewRunnableMethod("ShutdownBackend", this, + &JumpListBuilder::DoShutdownBackend), + NS_DISPATCH_NORMAL); + + mIOThread->Shutdown(); + } else if (strcmp(aTopic, "nsPref:changed") == 0 && + nsDependentString(aData).EqualsASCII(kPrefTaskbarEnabled)) { + bool enabled = Preferences::GetBool(kPrefTaskbarEnabled, true); + if (!enabled) { + nsCOMPtr<nsIRunnable> 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<nsIRunnable> 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..e228669f11 --- /dev/null +++ b/widget/windows/JumpListBuilder.h @@ -0,0 +1,102 @@ +/* -*- 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 "nsIJumpListBuilder.h" + +#include "nsIObserver.h" +#include "nsProxyRelease.h" +#include "mozilla/LazyIdleThread.h" + +namespace mozilla { + +namespace dom { +struct WindowsJumpListShortcutDescription; +} // namespace dom + +namespace widget { + +/** + * This is an abstract class for a backend to write to the Windows Jump List. + * + * It has a 1-to-1 method mapping with ICustomDestinationList. The abtract + * class allows us to implement a "fake" backend for automated testing. + */ +class JumpListBackend { + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual bool IsAvailable() = 0; + + virtual HRESULT SetAppID(LPCWSTR pszAppID) = 0; + virtual HRESULT BeginList(UINT* pcMinSlots, REFIID riid, void** ppv) = 0; + virtual HRESULT AddUserTasks(IObjectArray* poa) = 0; + virtual HRESULT AppendCategory(LPCWSTR pszCategory, IObjectArray* poa) = 0; + virtual HRESULT CommitList() = 0; + virtual HRESULT AbortList() = 0; + virtual HRESULT DeleteList(LPCWSTR pszAppID) = 0; + virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) = 0; + + protected: + virtual ~JumpListBackend() {} +}; + +/** + * JumpListBuilder is a component that can be used to manage the Windows + * Jump List off of the main thread. + */ +class JumpListBuilder : public nsIJumpListBuilder, public nsIObserver { + virtual ~JumpListBuilder(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIJUMPLISTBUILDER + NS_DECL_NSIOBSERVER + + explicit JumpListBuilder(const nsAString& aAppUserModelId, + RefPtr<JumpListBackend> aTestingBackend = nullptr); + + private: + // These all run on the lazy helper thread. + void DoSetupBackend(); + void DoSetupTestingBackend(RefPtr<JumpListBackend> aTestingBackend); + void DoShutdownBackend(); + void DoSetAppID(nsString aAppUserModelID); + void DoIsAvailable(const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder); + void DoCheckForRemovals( + const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder); + void DoPopulateJumpList( + const nsTArray<dom::WindowsJumpListShortcutDescription>&& + aTaskDescriptions, + const nsAString& aCustomTitle, + const nsTArray<dom::WindowsJumpListShortcutDescription>&& + aCustomDescriptions, + const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder); + void DoClearJumpList( + const nsMainThreadPtrHandle<dom::Promise>& aPromiseHolder); + void RemoveIconCacheAndGetJumplistShortcutURIs(IObjectArray* aObjArray, + nsTArray<nsString>& aURISpecs); + void DeleteIconFromDisk(const nsAString& aPath); + nsresult GetShellLinkFromDescription( + const dom::WindowsJumpListShortcutDescription& aDesc, + RefPtr<IShellLinkW>& aShellLink); + + // This is written to once during construction on the main thread before the + // lazy helper thread is created. After that, the lazy helper thread might + // read from it. + nsString mAppUserModelId; + + // This is only accessed by the lazy helper thread. + RefPtr<JumpListBackend> mJumpListBackend; + + // This is only accessed by the main thread. + RefPtr<LazyIdleThread> mIOThread; +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __JumpListBuilder_h__ */ diff --git a/widget/windows/KeyboardLayout.cpp b/widget/windows/KeyboardLayout.cpp new file mode 100644 index 0000000000..3b59aa9319 --- /dev/null +++ b/widget/windows/KeyboardLayout.cpp @@ -0,0 +1,5442 @@ +/* -*- 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/MouseEvents.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/TextEvents.h" +#include "mozilla/widget/WinRegistry.h" + +#include "nsExceptionHandler.h" +#include "nsGkAtoms.h" +#include "nsIUserIdleServiceInternal.h" +#include "nsIWindowsRegKey.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsTArray.h" +#include "nsUnicharUtils.h" +#include "nsWindowDbg.h" + +#include "KeyboardLayout.h" +#include "WidgetUtils.h" +#include "WinUtils.h" + +#include "npapi.h" + +#include <windows.h> +#include <winnls.h> +#include <winuser.h> +#include <algorithm> + +#ifndef WINABLEAPI +# include <winable.h> +#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 (IS_VK_DOWN(VK_RMENU) && KeyboardLayout::GetInstance()->HasAltGr()) { + 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_META; + } + 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_META) != 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: + // <https://github.com/w3c/uievents/issues/147> + 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 = {}; +char16_t NativeKey::sPendingHighSurrogate = 0; + +NativeKey::NativeKey(nsWindow* aWidget, const MSG& aMessage, + const ModifierKeyState& aModKeyState, + HKL aOverrideKeyboardLayout, + nsTArray<FakeCharMsg>* 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->GetLoadedLayout(); + 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; + } + 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; + char16_t pendingHighSurrogate = sPendingHighSurrogate; + mScanCode = WinUtils::GetScanCode(mMsg.lParam); + mIsExtended = WinUtils::IsExtendedScanCode(mMsg.lParam); + switch (mMsg.message) { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + sPendingHighSurrogate = 0; + [[fallthrough]]; + 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<uint8_t>(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<uint8_t>(scanCodeEx & 0xFF); + uint8_t extended = static_cast<uint8_t>((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: + sPendingHighSurrogate = 0; + // 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); + } + if (mFollowingCharMsgs.Length() == 1) { + // If we receive a keydown message for a high-surrogate, a low-surrogate + // keydown message **will** and should follow it. We cannot translate the + // following WM_KEYDOWN message for the low-surrogate right now since + // it's not yet queued into the message queue yet. Therefore, we need to + // wait next one to dispatch keypress event with setting its `.key` value + // to a surrogate pair rather than setting it to a lone surrogate. + // FYI: This may happen with typing a non-BMP character on the touch + // keyboard on Windows 10 or later except when an IME is installed. (If + // IME is installed, composition is used instead.) + if (IS_HIGH_SURROGATE(mFollowingCharMsgs[0].wParam)) { + if (pendingHighSurrogate) { + MOZ_LOG(gKeyLog, LogLevel::Warning, + ("%p NativeKey::InitWithKeyOrChar(), there is pending " + "high surrogate input, but received another high surrogate " + "input. The previous one is discarded", + this)); + } + sPendingHighSurrogate = mFollowingCharMsgs[0].wParam; + mFollowingCharMsgs.Clear(); + } else if (IS_LOW_SURROGATE(mFollowingCharMsgs[0].wParam)) { + // If we stopped dispathing a keypress event for a preceding + // high-surrogate, treat this keydown (for a low-surrogate) as + // introducing both the high surrogate and the low surrogate. + if (pendingHighSurrogate) { + MSG charMsg = mFollowingCharMsgs[0]; + mFollowingCharMsgs[0].wParam = pendingHighSurrogate; + mFollowingCharMsgs.AppendElement(std::move(charMsg)); + } else { + MOZ_LOG( + gKeyLog, LogLevel::Warning, + ("%p NativeKey::InitWithKeyOrChar(), there is no pending high " + "surrogate input, but received lone low surrogate input", + this)); + } + } else { + MOZ_LOG(gKeyLog, LogLevel::Warning, + ("%p NativeKey::InitWithKeyOrChar(), there is pending " + "high surrogate input, but received non-surrogate input. " + "The high surrogate input is discarded", + this)); + } + } else if (!mFollowingCharMsgs.IsEmpty()) { + MOZ_LOG(gKeyLog, LogLevel::Warning, + ("%p NativeKey::InitWithKeyOrChar(), there is pending " + "high surrogate input, but received 2 or more character input. " + "The high surrogate input is discarded", + this)); + } + } + + 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<char16_t>(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<uint8_t>(scanCodeEx & 0xFF); + uint8_t extended = static_cast<uint8_t>((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<uint8_t>( + ::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<uint8_t>(::MapVirtualKeyEx( + GetScanCodeWithExtendedFlag(), MAPVK_VSC_TO_VK_EX, mKeyboardLayout)); +} + +uint16_t NativeKey::ComputeScanCodeExFromVirtualKeyCode( + UINT aVirtualKeyCode) const { + return static_cast<uint16_t>( + ::MapVirtualKeyEx(aVirtualKeyCode, MAPVK_VK_TO_VSC_EX, mKeyboardLayout)); +} + +char16_t NativeKey::ComputeUnicharFromScanCode() const { + return static_cast<char16_t>(::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<nsAtom> 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<HWND>(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<NativeKey*>(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<NativeKey*>(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 (sPendingHighSurrogate) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyDownMessage(), doesn't dispatch keydown " + "event because the key introduced only a high surrotate, so we " + "should wait the following low surrogate input", + 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<NativeKey*>(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<uint32_t>(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<NativeKey*>(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 (sPendingHighSurrogate) { + MOZ_LOG(gKeyLog, LogLevel::Info, + ("%p NativeKey::HandleKeyUpMessage(), doesn't dispatch keyup " + "event because the key introduced only a high surrotate, so we " + "should wait the following low surrogate input", + 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<NativeKey*>(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<MSG> saveLastRemovingMsg(mRemovingMsg); + mRemovingMsg = nextKeyMsg; + + mReceivedMsg = sEmptyMSG; + AutoRestore<MSG> 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::GetInstance()->GetLoadedLayout(), + KeyboardLayout::GetInstance()->GetLoadedLayoutName().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::GetInstance()->GetLoadedLayout(), + KeyboardLayout::GetInstance()->GetLoadedLayoutName().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::GetInstance()->GetLoadedLayout(), + KeyboardLayout::GetInstance()->GetLoadedLayoutName().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<NativeKey*>(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<NativeKey*>(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<AlternativeCharCode>& 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; +StaticRefPtr<nsIUserIdleServiceInternal> KeyboardLayout::sIdleService; + +// static +KeyboardLayout* KeyboardLayout::GetInstance() { + if (!sInstance) { + sInstance = new KeyboardLayout(); + } + return sInstance; +} + +// static +void KeyboardLayout::Shutdown() { + delete sInstance; + sInstance = nullptr; + sIdleService = nullptr; +} + +// static +void KeyboardLayout::NotifyIdleServiceOfUserActivity() { + if (!sIdleService) { + sIdleService = nsCOMPtr<nsIUserIdleServiceInternal>( + do_GetService("@mozilla.org/widget/useridleservice;1")) + .forget(); + if (NS_WARN_IF(!sIdleService)) { + return; + } + } + sIdleService->ResetIdleTimeOut(0); +} + +KeyboardLayout::KeyboardLayout() { + 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); + + // If we put it off to load active keyboard layout when first needed, we need + // to load it at instanciation. That makes us save the cost of a call of + // GetKeyboardLayout() API. + if (StaticPrefs::ui_key_layout_load_when_first_needed()) { + OnLayoutChange(::GetKeyboardLayout(0)); + } +} + +KeyboardLayout::~KeyboardLayout() { ReleaseDeadKeyTables(); } + +bool KeyboardLayout::IsPrintableCharKey(uint8_t aVirtualKey) { + return GetKeyIndex(aVirtualKey) >= 0; +} + +WORD KeyboardLayout::ComputeScanCodeForVirtualKeyCode( + uint8_t aVirtualKeyCode) const { + return static_cast<WORD>(::MapVirtualKeyEx(aVirtualKeyCode, MAPVK_VK_TO_VSC, + KeyboardLayout::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<char16_t>(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 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; +} + +// static +nsCString KeyboardLayout::GetLayoutName(HKL aLayout) { + constexpr auto kKeyboardLayouts = + u"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\"_ns; + uint16_t language = reinterpret_cast<uintptr_t>(aLayout) & 0xFFFF; + uint16_t layout = (reinterpret_cast<uintptr_t>(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<uintptr_t>(aLayout)); + wchar_t buf[256]; + if (NS_WARN_IF(!WinRegistry::GetString( + HKEY_LOCAL_MACHINE, key, u"Layout Text"_ns, buf, + WinRegistry::kLegacyWinUtilsStringFlags))) { + 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<uintptr_t>(aLayout)); + return result; + } + + // Otherwise, we need to walk the registry under "Keyboard Layouts". + WinRegistry::Key regKey(HKEY_LOCAL_MACHINE, kKeyboardLayouts, + WinRegistry::KeyMode::Read); + if (NS_WARN_IF(!regKey)) { + return ""_ns; + } + uint32_t childCount = regKey.GetChildCount(); + if (NS_WARN_IF(!childCount)) { + return ""_ns; + } + for (uint32_t i = 0; i < childCount; i++) { + nsAutoString childName; + if (NS_WARN_IF(!regKey.GetChildName(i, childName)) || + !IsValidKeyboardLayoutsChild(childName)) { + continue; + } + nsresult rv = NS_OK; + uint32_t childNum = static_cast<uint32_t>(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; + WinRegistry::Key regKey(HKEY_LOCAL_MACHINE, key, + WinRegistry::KeyMode::QueryValue); + if (NS_WARN_IF(!regKey)) { + continue; + } + wchar_t buf[256]; + if (NS_WARN_IF(!regKey.GetValueAsString( + u"Layout Id"_ns, buf, WinRegistry::kLegacyWinUtilsStringFlags))) { + continue; + } + uint16_t layoutId = wcstol(buf, nullptr, 16); + if (layoutId != (layout & 0x0FFF)) { + continue; + } + if (NS_WARN_IF(!regKey.GetValueAsString( + u"Layout Text"_ns, buf, WinRegistry::kLegacyWinUtilsStringFlags))) { + 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)) { + AutoTArray<DeadKeyEntry, 256> deadKeyArray; + uint32_t n = GetDeadKeyCombinations( + virtualKey, kbdState, shiftStatesWithBaseChars, deadKeyArray); + const DeadKeyTable* dkt = + mVirtualKeys[vki].MatchingDeadKeyTable(deadKeyArray.Elements(), n); + if (!dkt) { + dkt = AddDeadKeyTable(deadKeyArray.Elements(), 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]; +} + +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<DeadKeyTableListEntry*>(p); + mDeadKeyTableListHead->next = next; + + DeadKeyTable* dkt = + reinterpret_cast<DeadKeyTable*>(mDeadKeyTableListHead->data); + + dkt->Init(aDeadKeyArray, aEntries); + + return dkt; +} + +void KeyboardLayout::ReleaseDeadKeyTables() { + while (mDeadKeyTableListHead) { + uint8_t* p = reinterpret_cast<uint8_t*>(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, + nsTArray<DeadKeyEntry>& aDeadKeyArray) { + auto dke = DeadKeyEntry(aBaseChar, aCompositeChar); + for (uint32_t index = 0; index < aDeadKeyArray.Length(); index++) { + if (aDeadKeyArray[index] == dke) { + return false; + } + } + + aDeadKeyArray.AppendElement(dke); + + return true; +} + +uint32_t KeyboardLayout::GetDeadKeyCombinations( + uint8_t aDeadKey, const PBYTE aDeadKeyKbdState, + uint16_t aShiftStatesWithBaseChars, nsTArray<DeadKeyEntry>& aDeadKeyArray) { + 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 < aDeadKeyArray.Capacity()) { + 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++; + } + 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++; + } + // 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); + } + + aDeadKeyArray.Sort(); + + 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<uint32_t>(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 = KeyboardLayout::GetLayout(); + WORD langID = LOWORD(static_cast<HKL>(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<KeyPair, 10> 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<LPARAM>(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<NativeKey::FakeCharMsg, 10> 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<LPARAM>(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..5d430dbea0 --- /dev/null +++ b/widget/windows/KeyboardLayout.h @@ -0,0 +1,1156 @@ +/* -*- 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 "nsTArray.h" +#include "nsWindow.h" +#include "nsWindowDefs.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/widget/WinMessages.h" +#include "mozilla/widget/WinModifierKeyState.h" +#include <windows.h> + +#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<Modifiers, 5> mModifiers; +}; + +struct DeadKeyEntry { + char16_t BaseChar; + char16_t CompositeChar; + + DeadKeyEntry(char16_t aBaseChar, char16_t aCompositeChar) + : BaseChar(aBaseChar), CompositeChar(aCompositeChar) {} + + bool operator<(const DeadKeyEntry& aOther) const { + return this->BaseChar < aOther.BaseChar; + } + + bool operator==(const DeadKeyEntry& aOther) const { + return this->BaseChar == aOther.BaseChar; + } +}; + +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<uint8_t>(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<WPARAM>(mCharCode); + msg.lParam = static_cast<LPARAM>(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<FakeCharMsg>* 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<nsWindow> mWidget; + RefPtr<TextEventDispatcher> 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<MSG, 5> 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<MSG> 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<FakeCharMsg>* 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<char16_t>(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<char16_t>(aMSG.wParam)); + } + bool IsControlCharMessage(const MSG& aMSG) const { + return IsCharMessage(aMSG.message) && + IsControlChar(static_cast<char16_t>(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; + + // Set to non-zero if we receive a WM_KEYDOWN message which introduces only + // a high surrogate. Then, it'll be cleared when next keydown or char message + // is received. + static char16_t sPendingHighSurrogate; + + 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(); + + /** + * GetLayout() returns a keyboard layout which has already been loaded in the + * singleton instance or active keyboard layout. + */ + static HKL GetLayout() { + if (!sInstance || sInstance->mIsPendingToRestoreKeyboardLayout) { + return ::GetKeyboardLayout(0); + } + return sInstance->mKeyboardLayout; + } + + /** + * GetLoadedLayout() returns a keyboard layout which was loaded in the + * singleton instance. This may be different from the active keyboard layout + * on the system if we override the keyboard layout for synthesizing native + * key events for tests. + */ + HKL GetLoadedLayout() { return mKeyboardLayout; } + + /** + * GetLoadedLayoutName() returns the name of the loaded keyboard layout in the + * singleton instance. + */ + nsCString GetLoadedLayoutName() { + return KeyboardLayout::GetLayoutName(mKeyboardLayout); + } + + 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); + + /** + * 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 StaticRefPtr<nsIUserIdleServiceInternal> sIdleService; + + struct DeadKeyTableListEntry { + DeadKeyTableListEntry* next; + uint8_t data[1]; + }; + + HKL mKeyboardLayout = nullptr; + + VirtualKey mVirtualKeys[NS_NUM_OF_KEYS] = {}; + DeadKeyTableListEntry* mDeadKeyTableListHead = nullptr; + // 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<uint8_t> mActiveDeadKeys; + // mDeadKeyShiftStates is always same length as mActiveDeadKeys. + // This stores shift states at pressing each dead key stored in + // mActiveDeadKeys. + nsTArray<VirtualKey::ShiftState> mDeadKeyShiftStates; + + bool mIsOverridden = false; + bool mIsPendingToRestoreKeyboardLayout = false; + bool mHasAltGr = false; + + static inline int32_t GetKeyIndex(uint8_t aVirtualKey); + static bool AddDeadKeyEntry(char16_t aBaseChar, char16_t aCompositeChar, + nsTArray<DeadKeyEntry>& aDeadKeyArray); + bool EnsureDeadKeyActive(bool aIsActive, uint8_t aDeadKey, + const PBYTE aDeadKeyKbdState); + uint32_t GetDeadKeyCombinations(uint8_t aDeadKey, + const PBYTE aDeadKeyKbdState, + uint16_t aShiftStatesWithBaseChars, + nsTArray<DeadKeyEntry>& aDeadKeyArray); + /** + * 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. + */ + static nsCString GetLayoutName(HKL aLayout); + + /** + * 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<nsWindow> 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 <windows.h> +#include <rpc.h> +#include <ws2spi.h> + +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<nsICrashReporter> 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<char[]>(size); + WSAPROTOCOL_INFOW* providers = + reinterpret_cast<WSAPROTOCOL_INFOW*>(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<char*>(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<nsIRunnable> runnable(new LSPAnnotationGatherer()); + NS_DispatchBackgroundTask(runnable.forget()); +} + +} // namespace crashreporter +} // namespace mozilla diff --git a/widget/windows/LegacyJumpListBuilder.cpp b/widget/windows/LegacyJumpListBuilder.cpp new file mode 100644 index 0000000000..fbfe10f64b --- /dev/null +++ b/widget/windows/LegacyJumpListBuilder.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 "LegacyJumpListBuilder.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 <shellapi.h> +#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<bool> LegacyJumpListBuilder::sBuildingList(false); +const char kPrefTaskbarEnabled[] = "browser.taskbar.lists.enabled"; + +NS_IMPL_ISUPPORTS(LegacyJumpListBuilder, nsILegacyJumpListBuilder, 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(nsILegacyJumpListCommittedCallback* aCallback, + LegacyJumpListBuilder* 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<nsILegacyJumpListCommittedCallback> mCallback; + RefPtr<LegacyJumpListBuilder> mBuilder; + bool mResult; +}; + +NS_IMPL_ISUPPORTS(DoneCommitListBuildCallback, nsIRunnable); + +} // namespace detail + +LegacyJumpListBuilder::LegacyJumpListBuilder() + : mMaxItems(0), + mHasCommit(false), + mMonitor("LegacyJumpListBuilderMonitor") { + 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<ICustomDestinationList> 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 = mscom::AgileReference(jumpListMgr); + }); + + if (!mJumpListMgr) { + return; + } + + // Make a lazy thread for any IO + mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, "Jump List", + LazyIdleThread::ManualShutdown); + Preferences::AddStrongObserver(this, kPrefTaskbarEnabled); + + nsCOMPtr<nsIObserverService> 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); + } +} + +LegacyJumpListBuilder::~LegacyJumpListBuilder() { + Preferences::RemoveObserver(this, kPrefTaskbarEnabled); +} + +NS_IMETHODIMP LegacyJumpListBuilder::SetAppUserModelID( + const nsAString& aAppUserModelId) { + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve(); + 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 LegacyJumpListBuilder::GetAvailable(int16_t* aAvailable) { + *aAvailable = false; + + ReentrantMonitorAutoEnter lock(mMonitor); + if (mJumpListMgr) *aAvailable = true; + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListBuilder::GetIsListCommitted(bool* aCommit) { + *aCommit = mHasCommit; + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListBuilder::GetMaxListItems(int16_t* aMaxItems) { + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + *aMaxItems = 0; + + if (sBuildingList) { + *aMaxItems = mMaxItems; + return NS_OK; + } + + RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve(); + 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 LegacyJumpListBuilder::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 = Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod<StoreCopyPassByRRef<RefPtr<Promise>>>( + "InitListBuild", this, &LegacyJumpListBuilder::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 LegacyJumpListBuilder::DoInitListBuild(RefPtr<Promise>&& 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<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve(); + if (!jumpListMgr) { + return; + } + + nsTArray<nsString> urisToRemove; + RefPtr<IObjectArray> objArray; + hr = jumpListMgr->BeginList( + &mMaxItems, + IID_PPV_ARGS(static_cast<IObjectArray**>(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 LegacyJumpListBuilder::RemoveIconCacheForAllItems() { + // Construct the path of our jump list cache + nsCOMPtr<nsIFile> 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<nsIDirectoryEnumerator> 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<nsIFile> 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 LegacyJumpListBuilder::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<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve(); + if (!jumpListMgr) { + return NS_ERROR_UNEXPECTED; + } + + switch (aCatType) { + case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_TASKS: { + NS_ENSURE_ARG_POINTER(items); + + HRESULT hr; + RefPtr<IObjectCollection> 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<nsILegacyJumpListItem> item = do_QueryElementAt(items, i); + if (!item) continue; + // Check for separators + if (IsSeparator(item)) { + RefPtr<IShellLinkW> link; + rv = LegacyJumpListSeparator::GetSeparator(link); + if (NS_FAILED(rv)) return rv; + collection->AddObject(link); + continue; + } + // These should all be ShellLinks + RefPtr<IShellLinkW> link; + rv = LegacyJumpListShortcut::GetShellLink(item, link, mIOThread); + if (NS_FAILED(rv)) return rv; + collection->AddObject(link); + } + + // We need IObjectArray to submit + RefPtr<IObjectArray> 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 nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_RECENT: { + if (SUCCEEDED(jumpListMgr->AppendKnownCategory(KDC_RECENT))) + *_retval = true; + return NS_OK; + } break; + case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_FREQUENT: { + if (SUCCEEDED(jumpListMgr->AppendKnownCategory(KDC_FREQUENT))) + *_retval = true; + return NS_OK; + } break; + case nsILegacyJumpListBuilder::JUMPLIST_CATEGORY_CUSTOMLIST: { + NS_ENSURE_ARG_POINTER(items); + + if (catName.IsEmpty()) return NS_ERROR_INVALID_ARG; + + HRESULT hr; + RefPtr<IObjectCollection> 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<nsILegacyJumpListItem> item = do_QueryElementAt(items, i); + if (!item) continue; + int16_t type; + if (NS_FAILED(item->GetType(&type))) continue; + switch (type) { + case nsILegacyJumpListItem::JUMPLIST_ITEM_SEPARATOR: { + RefPtr<IShellLinkW> shellItem; + rv = LegacyJumpListSeparator::GetSeparator(shellItem); + if (NS_FAILED(rv)) return rv; + collection->AddObject(shellItem); + } break; + case nsILegacyJumpListItem::JUMPLIST_ITEM_LINK: { + RefPtr<IShellItem2> shellItem; + rv = LegacyJumpListLink::GetShellItem(item, shellItem); + if (NS_FAILED(rv)) return rv; + collection->AddObject(shellItem); + } break; + case nsILegacyJumpListItem::JUMPLIST_ITEM_SHORTCUT: { + RefPtr<IShellLinkW> shellItem; + rv = LegacyJumpListShortcut::GetShellLink(item, shellItem, + mIOThread); + if (NS_FAILED(rv)) return rv; + collection->AddObject(shellItem); + } break; + } + } + + // We need IObjectArray to submit + RefPtr<IObjectArray> pArray; + hr = collection->QueryInterface(IID_IObjectArray, (LPVOID*)&pArray); + if (FAILED(hr)) return NS_ERROR_UNEXPECTED; + + // Add the tasks + hr = jumpListMgr->AppendCategory( + reinterpret_cast<const wchar_t*>(catName.BeginReading()), pArray); + if (SUCCEEDED(hr)) *_retval = true; + + // Get rid of the old icons + nsCOMPtr<nsIRunnable> event = + new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(true); + mIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + + return NS_OK; + } break; + } + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListBuilder::AbortListBuild() { + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve(); + if (!jumpListMgr) { + return NS_ERROR_UNEXPECTED; + } + + jumpListMgr->AbortList(); + sBuildingList = false; + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListBuilder::CommitListBuild( + nsILegacyJumpListCommittedCallback* aCallback) { + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + // Also holds a strong reference to this to prevent use-after-free. + RefPtr<detail::DoneCommitListBuildCallback> 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<nsIRunnable> event = + NewNonOwningRunnableMethod<RefPtr<detail::DoneCommitListBuildCallback>>( + "LegacyJumpListBuilder::DoCommitListBuild", this, + &LegacyJumpListBuilder::DoCommitListBuild, std::move(callback)); + Unused << mIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + + return NS_OK; +} + +void LegacyJumpListBuilder::DoCommitListBuild( + RefPtr<detail::DoneCommitListBuildCallback> 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<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve(); + if (!jumpListMgr) { + return; + } + + hr = jumpListMgr->CommitList(); + sBuildingList = false; + + if (SUCCEEDED(hr)) { + mHasCommit = true; + } +} + +NS_IMETHODIMP LegacyJumpListBuilder::DeleteActiveList(bool* _retval) { + *_retval = false; + + ReentrantMonitorAutoEnter lock(mMonitor); + if (!mJumpListMgr) return NS_ERROR_NOT_AVAILABLE; + + if (sBuildingList) { + AbortListBuild(); + } + + RefPtr<ICustomDestinationList> jumpListMgr = mJumpListMgr.Resolve(); + if (!jumpListMgr) { + return NS_ERROR_UNEXPECTED; + } + + if (SUCCEEDED(jumpListMgr->DeleteList(mAppUserModelId.get()))) { + *_retval = true; + } + + return NS_OK; +} + +/* internal */ + +bool LegacyJumpListBuilder::IsSeparator(nsCOMPtr<nsILegacyJumpListItem>& item) { + int16_t type; + item->GetType(&type); + if (NS_FAILED(item->GetType(&type))) return false; + + if (type == nsILegacyJumpListItem::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 LegacyJumpListBuilder::RemoveIconCacheAndGetJumplistShortcutURIs( + IObjectArray* aObjArray, nsTArray<nsString>& 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<IShellLinkW> pLink; + + if (FAILED(aObjArray->GetAt(idx, IID_IShellLinkW, + static_cast<void**>(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 LegacyJumpListBuilder::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<nsIFile> icoFile; + nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(icoFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + icoFile->Remove(false); + } +} + +NS_IMETHODIMP LegacyJumpListBuilder::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + NS_ENSURE_ARG_POINTER(aTopic); + if (strcmp(aTopic, TOPIC_PROFILE_BEFORE_CHANGE) == 0) { + nsCOMPtr<nsIObserverService> 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<nsIRunnable> 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<nsIRunnable> event = + new mozilla::widget::AsyncDeleteAllFaviconsFromDisk(false); + mIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + } + return NS_OK; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/LegacyJumpListBuilder.h b/widget/windows/LegacyJumpListBuilder.h new file mode 100644 index 0000000000..1d96773c47 --- /dev/null +++ b/widget/windows/LegacyJumpListBuilder.h @@ -0,0 +1,71 @@ +/* -*- 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 __LegacyJumpListBuilder_h__ +#define __LegacyJumpListBuilder_h__ + +#include <windows.h> + +// Needed for various com interfaces +#include <shobjidl.h> +#undef LogSeverity // SetupAPI.h #defines this as DWORD + +#include "nsString.h" + +#include "nsILegacyJumpListBuilder.h" +#include "nsILegacyJumpListItem.h" +#include "LegacyJumpListItem.h" +#include "nsIObserver.h" +#include "nsTArray.h" +#include "mozilla/Attributes.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/mscom/AgileReference.h" +#include "mozilla/ReentrantMonitor.h" + +namespace mozilla { +namespace widget { + +namespace detail { +class DoneCommitListBuildCallback; +} // namespace detail + +class LegacyJumpListBuilder : public nsILegacyJumpListBuilder, + public nsIObserver { + virtual ~LegacyJumpListBuilder(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSILEGACYJUMPLISTBUILDER + NS_DECL_NSIOBSERVER + + LegacyJumpListBuilder(); + + protected: + static Atomic<bool> sBuildingList; + + private: + mscom::AgileReference<ICustomDestinationList> mJumpListMgr + MOZ_GUARDED_BY(mMonitor); + uint32_t mMaxItems MOZ_GUARDED_BY(mMonitor); + bool mHasCommit; + RefPtr<LazyIdleThread> mIOThread; + ReentrantMonitor mMonitor; + nsString mAppUserModelId; + + bool IsSeparator(nsCOMPtr<nsILegacyJumpListItem>& item); + void RemoveIconCacheAndGetJumplistShortcutURIs(IObjectArray* aObjArray, + nsTArray<nsString>& aURISpecs); + void DeleteIconFromDisk(const nsAString& aPath); + nsresult RemoveIconCacheForAllItems(); + void DoCommitListBuild(RefPtr<detail::DoneCommitListBuildCallback> aCallback); + void DoInitListBuild(RefPtr<dom::Promise>&& aPromise); + + friend class WinTaskbar; +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __LegacyJumpListBuilder_h__ */ diff --git a/widget/windows/LegacyJumpListItem.cpp b/widget/windows/LegacyJumpListItem.cpp new file mode 100644 index 0000000000..3ffddf11f9 --- /dev/null +++ b/widget/windows/LegacyJumpListItem.cpp @@ -0,0 +1,559 @@ +/* -*- 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 "LegacyJumpListItem.h" + +#include <shellapi.h> +#include <propvarutil.h> +#include <propkey.h> + +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsCRT.h" +#include "nsNetCID.h" +#include "nsCExternalHandlerService.h" +#include "nsComponentManagerUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/Preferences.h" +#include "LegacyJumpListBuilder.h" +#include "WinUtils.h" + +namespace mozilla { +namespace widget { + +// ISUPPORTS Impl's +NS_IMPL_ISUPPORTS(LegacyJumpListItem, nsILegacyJumpListItem) + +NS_INTERFACE_MAP_BEGIN(LegacyJumpListSeparator) + NS_INTERFACE_MAP_ENTRY(nsILegacyJumpListSeparator) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsILegacyJumpListItem, + LegacyJumpListItemBase) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, LegacyJumpListItemBase) +NS_INTERFACE_MAP_END +NS_IMPL_ADDREF(LegacyJumpListSeparator) +NS_IMPL_RELEASE(LegacyJumpListSeparator) + +NS_INTERFACE_MAP_BEGIN(LegacyJumpListLink) + NS_INTERFACE_MAP_ENTRY(nsILegacyJumpListLink) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsILegacyJumpListItem, + LegacyJumpListItemBase) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, LegacyJumpListItemBase) +NS_INTERFACE_MAP_END +NS_IMPL_ADDREF(LegacyJumpListLink) +NS_IMPL_RELEASE(LegacyJumpListLink) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LegacyJumpListShortcut) + NS_INTERFACE_MAP_ENTRY(nsILegacyJumpListShortcut) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsILegacyJumpListItem, + LegacyJumpListItemBase) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsILegacyJumpListShortcut) +NS_INTERFACE_MAP_END +NS_IMPL_CYCLE_COLLECTING_ADDREF(LegacyJumpListShortcut) +NS_IMPL_CYCLE_COLLECTING_RELEASE(LegacyJumpListShortcut) +NS_IMPL_CYCLE_COLLECTION(LegacyJumpListShortcut, mHandlerApp) + +NS_IMETHODIMP LegacyJumpListItemBase::GetType(int16_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + + *aType = mItemType; + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListItemBase::Equals(nsILegacyJumpListItem* aItem, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aItem); + + *aResult = false; + + int16_t theType = nsILegacyJumpListItem::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 LegacyJumpListLink::GetUri(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = mURI); + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListLink::SetUri(nsIURI* aURI) { + mURI = aURI; + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListLink::SetUriTitle(const nsAString& aUriTitle) { + mUriTitle.Assign(aUriTitle); + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListLink::GetUriTitle(nsAString& aUriTitle) { + aUriTitle.Assign(mUriTitle); + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListLink::Equals(nsILegacyJumpListItem* aItem, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aItem); + + nsresult rv; + + *aResult = false; + + int16_t theType = nsILegacyJumpListItem::JUMPLIST_ITEM_EMPTY; + if (NS_FAILED(aItem->GetType(&theType))) return NS_OK; + + // Make sure the types match. + if (Type() != theType) return NS_OK; + + nsCOMPtr<nsILegacyJumpListLink> 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<nsIURI> 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 LegacyJumpListShortcut::GetApp(nsILocalHandlerApp** aApp) { + NS_IF_ADDREF(*aApp = mHandlerApp); + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListShortcut::SetApp(nsILocalHandlerApp* aApp) { + mHandlerApp = aApp; + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListShortcut::GetIconIndex(int32_t* aIconIndex) { + NS_ENSURE_ARG_POINTER(aIconIndex); + + *aIconIndex = mIconIndex; + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListShortcut::SetIconIndex(int32_t aIconIndex) { + mIconIndex = aIconIndex; + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListShortcut::GetFaviconPageUri( + nsIURI** aFaviconPageURI) { + NS_IF_ADDREF(*aFaviconPageURI = mFaviconPageURI); + + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListShortcut::SetFaviconPageUri( + nsIURI* aFaviconPageURI) { + mFaviconPageURI = aFaviconPageURI; + return NS_OK; +} + +NS_IMETHODIMP LegacyJumpListShortcut::Equals(nsILegacyJumpListItem* aItem, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aItem); + + nsresult rv; + + *aResult = false; + + int16_t theType = nsILegacyJumpListItem::JUMPLIST_ITEM_EMPTY; + if (NS_FAILED(aItem->GetType(&theType))) return NS_OK; + + // Make sure the types match. + if (Type() != theType) return NS_OK; + + nsCOMPtr<nsILegacyJumpListShortcut> 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<nsILocalHandlerApp> 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 LegacyJumpListSeparator::GetSeparator( + RefPtr<IShellLinkW>& 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 LegacyJumpListShortcut::GetShellLink( + nsCOMPtr<nsILegacyJumpListItem>& item, RefPtr<IShellLinkW>& aShellLink, + RefPtr<LazyIdleThread>& 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 != nsILegacyJumpListItem::JUMPLIST_ITEM_SHORTCUT) + return NS_ERROR_INVALID_ARG; + + nsCOMPtr<nsILegacyJumpListShortcut> shortcut = do_QueryInterface(item, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILocalHandlerApp> 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<nsIFile> 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<nsIURI> 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<nsILegacyJumpListShortcut>& 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<nsIFile> 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<nsIFile> passedInFile = + do_CreateInstance("@mozilla.org/file/local;1"); + NS_ENSURE_TRUE(passedInFile, NS_ERROR_FAILURE); + nsAutoString passedInPath(aPath); + rv = passedInFile->InitWithPath(passedInPath); + nsCOMPtr<nsIFile> 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 +// nsILegacyJumpListShortcut. +nsresult LegacyJumpListShortcut::GetJumpListShortcut( + IShellLinkW* pLink, nsCOMPtr<nsILegacyJumpListShortcut>& aShortcut) { + NS_ENSURE_ARG_POINTER(pLink); + + nsresult rv; + HRESULT hres; + + nsCOMPtr<nsILocalHandlerApp> 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<nsIFile> 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<nsIURI> 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 LegacyJumpListLink::GetShellItem(nsCOMPtr<nsILegacyJumpListItem>& item, + RefPtr<IShellItem2>& aShellItem) { + IShellItem2* psi = nullptr; + nsresult rv; + + int16_t type; + if (NS_FAILED(item->GetType(&type))) return NS_ERROR_INVALID_ARG; + + if (type != nsILegacyJumpListItem::JUMPLIST_ITEM_LINK) + return NS_ERROR_INVALID_ARG; + + nsCOMPtr<nsILegacyJumpListLink> link = do_QueryInterface(item, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> 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 +// nsILegacyJumpListLink. +nsresult LegacyJumpListLink::GetJumpListLink( + IShellItem* pItem, nsCOMPtr<nsILegacyJumpListLink>& 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<nsIURI> 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/LegacyJumpListItem.h b/widget/windows/LegacyJumpListItem.h new file mode 100644 index 0000000000..bcef82d349 --- /dev/null +++ b/widget/windows/LegacyJumpListItem.h @@ -0,0 +1,133 @@ +/* -*- 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 __LegacyJumpListItem_h__ +#define __LegacyJumpListItem_h__ + +#include <windows.h> +#include <shobjidl.h> +#undef LogSeverity // SetupAPI.h #defines this as DWORD + +#include "mozilla/RefPtr.h" +#include "mozilla/LazyIdleThread.h" +#include "nsILegacyJumpListItem.h" // defines nsILegacyJumpListItem +#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 LegacyJumpListItemBase : public nsILegacyJumpListItem { + public: + LegacyJumpListItemBase() + : mItemType(nsILegacyJumpListItem::JUMPLIST_ITEM_EMPTY) {} + + explicit LegacyJumpListItemBase(int32_t type) : mItemType(type) {} + + NS_DECL_NSILEGACYJUMPLISTITEM + + static const char kJumpListCacheDir[]; + + protected: + virtual ~LegacyJumpListItemBase() {} + + short Type() { return mItemType; } + short mItemType; +}; + +class LegacyJumpListItem : public LegacyJumpListItemBase { + ~LegacyJumpListItem() {} + + public: + using LegacyJumpListItemBase::LegacyJumpListItemBase; + + NS_DECL_ISUPPORTS +}; + +class LegacyJumpListSeparator : public LegacyJumpListItemBase, + public nsILegacyJumpListSeparator { + ~LegacyJumpListSeparator() {} + + public: + LegacyJumpListSeparator() + : LegacyJumpListItemBase(nsILegacyJumpListItem::JUMPLIST_ITEM_SEPARATOR) { + } + + NS_DECL_ISUPPORTS + NS_FORWARD_NSILEGACYJUMPLISTITEM(LegacyJumpListItemBase::) + + static nsresult GetSeparator(RefPtr<IShellLinkW>& aShellLink); +}; + +class LegacyJumpListLink : public LegacyJumpListItemBase, + public nsILegacyJumpListLink { + ~LegacyJumpListLink() {} + + public: + LegacyJumpListLink() + : LegacyJumpListItemBase(nsILegacyJumpListItem::JUMPLIST_ITEM_LINK) {} + + NS_DECL_ISUPPORTS + NS_IMETHOD GetType(int16_t* aType) override { + return LegacyJumpListItemBase::GetType(aType); + } + NS_IMETHOD Equals(nsILegacyJumpListItem* item, bool* _retval) override; + NS_DECL_NSILEGACYJUMPLISTLINK + + static nsresult GetShellItem(nsCOMPtr<nsILegacyJumpListItem>& item, + RefPtr<IShellItem2>& aShellItem); + static nsresult GetJumpListLink(IShellItem* pItem, + nsCOMPtr<nsILegacyJumpListLink>& aLink); + + protected: + nsString mUriTitle; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsICryptoHash> mCryptoHash; +}; + +class LegacyJumpListShortcut : public LegacyJumpListItemBase, + public nsILegacyJumpListShortcut { + ~LegacyJumpListShortcut() {} + + public: + LegacyJumpListShortcut() + : LegacyJumpListItemBase(nsILegacyJumpListItem::JUMPLIST_ITEM_SHORTCUT) {} + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(LegacyJumpListShortcut, + LegacyJumpListItemBase) + NS_IMETHOD GetType(int16_t* aType) override { + return LegacyJumpListItemBase::GetType(aType); + } + NS_IMETHOD Equals(nsILegacyJumpListItem* item, bool* _retval) override; + NS_DECL_NSILEGACYJUMPLISTSHORTCUT + + static nsresult GetShellLink(nsCOMPtr<nsILegacyJumpListItem>& item, + RefPtr<IShellLinkW>& aShellLink, + RefPtr<LazyIdleThread>& aIOThread); + static nsresult GetJumpListShortcut( + IShellLinkW* pLink, nsCOMPtr<nsILegacyJumpListShortcut>& aShortcut); + static nsresult GetOutputIconPath(nsCOMPtr<nsIURI> aFaviconPageURI, + nsCOMPtr<nsIFile>& aICOFile); + + protected: + int32_t mIconIndex; + nsCOMPtr<nsIURI> mFaviconPageURI; + nsCOMPtr<nsILocalHandlerApp> mHandlerApp; + + bool ExecutableExists(nsCOMPtr<nsILocalHandlerApp>& handlerApp); +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __LegacyJumpListItem_h__ */ 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 <inputpaneinterop.h> +# include <windows.ui.viewmanagement.h> +# include <wrl.h> + +using namespace ABI::Windows::UI::ViewManagement; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +#endif + +namespace mozilla { +namespace widget { + +#ifndef __MINGW32__ +static ComPtr<IInputPane2> GetInputPane(HWND aHwnd) { + ComPtr<IInputPaneInterop> inputPaneInterop; + HRESULT hr = RoGetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_ViewManagement_InputPane).Get(), + IID_PPV_ARGS(&inputPaneInterop)); + if (NS_WARN_IF(FAILED(hr))) { + return nullptr; + } + + ComPtr<IInputPane> inputPane; + hr = inputPaneInterop->GetForWindow(aHwnd, IID_PPV_ARGS(&inputPane)); + if (NS_WARN_IF(FAILED(hr))) { + return nullptr; + } + + ComPtr<IInputPane2> inputPane2; + inputPane.As(&inputPane2); + return inputPane2; +} + +# ifdef DEBUG +static bool IsInputPaneVisible(ComPtr<IInputPane2>& aInputPane2) { + ComPtr<IInputPaneControl> 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<IInputPane2> 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<IInputPane2> 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 <windows.h> + +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..b7b06c08d1 --- /dev/null +++ b/widget/windows/OSKTabTipManager.cpp @@ -0,0 +1,112 @@ +/* -*- 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 "mozilla/widget/WinRegistry.h" + +#include <shellapi.h> +#include <shlobj.h> +#include <windows.h> + +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. + constexpr auto kRegKeyName = + u"Software\\Classes\\CLSID\\{054AAE20-4BEA-4347-8A35-64A533254A9D}\\LocalServer32"_ns; + if (!WinRegistry::GetString(HKEY_LOCAL_MACHINE, kRegKeyName, u""_ns, path, + WinRegistry::kLegacyWinUtilsStringFlags)) { + return; + } + + std::wstring wstrpath(path); + // The path provided by the registry will often contain + // %CommonProgramFiles%, which will need to be replaced if it is present. + size_t commonProgramFilesOffset = wstrpath.find(L"%CommonProgramFiles%"); + if (commonProgramFilesOffset != std::wstring::npos) { + // The path read from the registry contains the %CommonProgramFiles% + // environment variable prefix. On 64 bit Windows the + // SHGetKnownFolderPath function returns the common program files path + // with the X86 suffix for the FOLDERID_ProgramFilesCommon value. + // To get the correct path to TabTip.exe we first read the environment + // variable CommonProgramW6432 which points to the desired common + // files path. Failing that we fallback to the SHGetKnownFolderPath API. + // We then replace the %CommonProgramFiles% value with the actual common + // files path found in the process. + std::wstring commonProgramFilesPath; + std::vector<wchar_t> commonProgramFilesPathW6432; + DWORD bufferSize = + ::GetEnvironmentVariableW(L"CommonProgramW6432", nullptr, 0); + if (bufferSize) { + commonProgramFilesPathW6432.resize(bufferSize); + ::GetEnvironmentVariableW(L"CommonProgramW6432", + commonProgramFilesPathW6432.data(), + bufferSize); + commonProgramFilesPath = + std::wstring(commonProgramFilesPathW6432.data()); + } else { + PWSTR path = nullptr; + HRESULT hres = SHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, 0, + nullptr, &path); + if (FAILED(hres) || !path) { + return; + } + commonProgramFilesPath = + static_cast<const wchar_t*>(nsDependentString(path).get()); + ::CoTaskMemFree(path); + } + wstrpath.replace(commonProgramFilesOffset, + wcslen(L"%CommonProgramFiles%"), commonProgramFilesPath); + } + + cachedPath.Assign(wstrpath.data()); + Preferences::SetString(kOskPathPrefName, cachedPath); + } + + const char16_t* cachedPathPtr; + cachedPath.GetData(&cachedPathPtr); + ShellExecuteW(nullptr, L"", char16ptr_t(cachedPathPtr), nullptr, nullptr, + SW_SHOW); +} + +// 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..89ea575a47 --- /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 mozilla::widget::TransparencyMode 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(TransparencyMode 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..67dd92b802 --- /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 mozilla::widget::TransparencyMode from "nsIWidget.h"; + +namespace mozilla { +namespace widget { + +struct WinCompositorWidgetInitData +{ + WindowsHandle hWnd; + uintptr_t widgetKey; + TransparencyMode 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..56a8bae0fe --- /dev/null +++ b/widget/windows/RemoteBackbuffer.cpp @@ -0,0 +1,713 @@ +/* -*- 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 "mozilla/gfx/Point.h" +#include "WinUtils.h" +#include <algorithm> +#include <type_traits> + +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<SharedData>::value && + std::is_standard_layout<SharedData>::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<DWORD>(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<unsigned char*>(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<unsigned char*>(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<gfx::DrawTarget> CreateDrawTarget() { + return gfx::Factory::CreateDrawTargetForData( + gfx::BackendType::CAIRO, mPixelData, gfx::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<uint32_t>(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, TransparencyMode aTransparencyMode, + Span<const IpcSafeRect> aDirtyRects) { + if (aTransparencyMode == TransparencyMode::Transparent) { + // 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); + } + + gfx::IntRect sharedImageRect{0, 0, mSharedImage.GetWidth(), + mSharedImage.GetHeight()}; + + bool result = true; + + HDC windowDC = ::GetDC(aWindowHandle); + if (!windowDC) { + return false; + } + + for (auto& ipcDirtyRect : aDirtyRects) { + gfx::IntRect dirtyRect{ipcDirtyRect.x, ipcDirtyRect.y, ipcDirtyRect.width, + ipcDirtyRect.height}; + gfx::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<gfx::DrawTarget> 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, + TransparencyMode 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<DWORD>(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<SharedData*>(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<Provider*>(p)->ThreadMain(); }, + this, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, + 0 /*default stack size*/); + if (!mServiceThread) { + return false; + } + + mTransparencyMode = uint32_t(aTransparencyMode); + + return true; +} + +Maybe<RemoteBackbufferHandles> Provider::CreateRemoteHandles() { + return Some( + RemoteBackbufferHandles(ipc::FileDescriptor(mFileMapping), + ipc::FileDescriptor(mRequestReadyEvent), + ipc::FileDescriptor(mResponseReadyEvent))); +} + +void Provider::UpdateTransparencyMode(TransparencyMode aTransparencyMode) { + mTransparencyMode = uint32_t(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<PresentableSharedImage>(); + 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, GetTransparencyMode(), + 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<SharedData*>(mappedFilePtr); + + return true; +} + +already_AddRefed<gfx::DrawTarget> 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<SharedImage>(); + 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..5899e80984 --- /dev/null +++ b/widget/windows/RemoteBackbuffer.h @@ -0,0 +1,95 @@ +/* -*- 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 "mozilla/gfx/2D.h" +#include "prthread.h" +#include <windows.h> + +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, + TransparencyMode aTransparencyMode); + + Maybe<RemoteBackbufferHandles> CreateRemoteHandles(); + + void UpdateTransparencyMode(TransparencyMode 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<PresentableSharedImage> mBackbuffer; + mozilla::Atomic<uint32_t, MemoryOrdering::Relaxed> mTransparencyMode; + TransparencyMode GetTransparencyMode() const { + return TransparencyMode(uint32_t(mTransparencyMode)); + } +}; + +class Client { + public: + Client(); + ~Client(); + + bool Initialize(const RemoteBackbufferHandles& aRemoteHandles); + + already_AddRefed<gfx::DrawTarget> 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<SharedImage> 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..8a0ec3b608 --- /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<nsTArray<RefPtr<Screen>>*>(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, uint32_t(orientation), angle)); + auto screen = MakeRefPtr<Screen>( + 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<RefPtr<Screen>, 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..ea95077fb3 --- /dev/null +++ b/widget/windows/ShellHeaderOnlyUtils.h @@ -0,0 +1,183 @@ +/* -*- 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 + +#if defined(LIBXUL) && !defined(UNICODE) +# error \ + "UNICODE not set - must be set to prevent compile failure in `comdef.h` due to us deleting `FormatMessage` when absent." +#endif + +#include "mozilla/WinHeaderOnlyUtils.h" + +#include <objbase.h> + +#include <exdisp.h> +#include <shldisp.h> +#include <shlobj.h> +#include <shlwapi.h> +#include <shobjidl.h> +#include <shtypes.h> +// NB: include this after shldisp.h so its macros do not conflict with COM +// interfaces defined by shldisp.h +#include <shellapi.h> +#include <type_traits> + +#include <comdef.h> +#include <comutil.h> + +#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<IShellWindows> 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<IDispatch> 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<IServiceProvider> servProv; + hr = dispDesktop->QueryInterface(IID_IServiceProvider, + getter_AddRefs(servProv)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + RefPtr<IShellBrowser> browser; + hr = servProv->QueryService(SID_STopLevelBrowser, IID_IShellBrowser, + getter_AddRefs(browser)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + RefPtr<IShellView> 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<IDispatch> dispView; + hr = activeShellView->GetItemObject(SVGIO_BACKGROUND, IID_IDispatch, + getter_AddRefs(dispView)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + RefPtr<IShellFolderViewDual> 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<IDispatch> dispShell; + hr = folderView->get_Application(getter_AddRefs(dispShell)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + RefPtr<IShellDispatch2> 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<std::remove_pointer_t<PIDLIST_ABSOLUTE>, CoTaskMemFreeDeleter>; + +inline LauncherResult<UniqueAbsolutePidl> 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..6cc5668bbe --- /dev/null +++ b/widget/windows/SystemStatusBar.cpp @@ -0,0 +1,339 @@ +/* -*- 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 <strsafe.h> +#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" + +#include "shellapi.h" + +namespace mozilla::widget { + +using mozilla::LinkedListElement; +using mozilla::dom::Element; + +class StatusBarEntry final : public LinkedListElement<RefPtr<StatusBarEntry>>, + 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<mozilla::widget::IconLoader> mIconLoader; + // Effectively const but is cycle collected + MOZ_KNOWN_LIVE RefPtr<Element> 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<nsIURI> iconURI; + if (!hasImageAttr) { + // If the content node has no "image" attribute, get the + // "list-style-image" property from CSS. + RefPtr<mozilla::dom::Document> document = mMenu->GetComposedDoc(); + if (!document) { + return NS_ERROR_FAILURE; + } + + RefPtr<const ComputedStyle> sc = + nsComputedDOMStyle::GetComputedStyle(mMenu); + if (!sc) { + return NS_ERROR_FAILURE; + } + + iconURI = sc->StyleList()->GetListStyleImageURI(); + } else { + uint64_t dummy = 0; + nsContentPolicyType policyType; + nsCOMPtr<nsIPrincipal> 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(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<StatusBarEntry> 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<HWND>(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(nsGkAtoms::contextmenu)) { + ::SetForegroundWindow(win); + nsEventStatus status = nsEventStatus_eIgnore; + WidgetMouseEvent event(true, eXULSystemStatusBarClick, nullptr, + WidgetMouseEvent::eReal); + RefPtr<nsPresContext> 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 <xul:menu>, and the popup code + // that manages it expects that the window that the <xul:menu> 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()->AsElement(), 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<StatusBarEntry> entry = + (StatusBarEntry*)GetWindowLongPtr(hWnd, GWLP_USERDATA)) { + return entry->OnMessage(hWnd, msg, wp, lp); + } + return TRUE; +} + +static StaticRefPtr<SystemStatusBar> sSingleton; + +SystemStatusBar& SystemStatusBar::GetSingleton() { + if (!sSingleton) { + sSingleton = new SystemStatusBar(); + ClearOnShutdown(&sSingleton); + } + return *sSingleton; +} + +already_AddRefed<SystemStatusBar> SystemStatusBar::GetAddRefedSingleton() { + RefPtr<SystemStatusBar> 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<StatusBarEntry> 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..6afafd9d80 --- /dev/null +++ b/widget/windows/SystemStatusBar.h @@ -0,0 +1,33 @@ +/* -*- 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" +#include "mozilla/LinkedList.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<SystemStatusBar> GetAddRefedSingleton(); + + nsresult Init(); + + private: + ~SystemStatusBar() = default; + mozilla::LinkedList<RefPtr<StatusBarEntry>> 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..b3e3a164ae --- /dev/null +++ b/widget/windows/TSFTextStore.cpp @@ -0,0 +1,7513 @@ +/* -*- 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 <algorithm> +#include <comutil.h> // for _bstr_t +#include <oleauto.h> // for SysAllocString +#include <olectl.h> +#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/StaticPrefs_intl.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/ToString.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/widget/WinRegistry.h" +#include "nsWindow.h" +#include "nsPrintfCString.h" + +// 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<uint32_t>(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<uint32_t>(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 (WinRegistry::GetString(HKEY_CLASSES_ROOT, key, u""_ns, buf, + WinRegistry::kLegacyWinUtilsStringFlags)) { + 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<int32_t>(aColor.nIndex)); + case TF_CT_COLORREF: + return nsPrintfCString("TF_CT_COLORREF, cr:0x%08X", + static_cast<int32_t>(aColor.cr)); + break; + default: + return nsPrintfCString("Unknown(%08X)", + static_cast<int32_t>(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<int32_t>(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<int32_t>(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; + } + 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<InputScope>& 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<InputScope>& 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<ITfInputScope*>(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<InputScope> mInputScopes; +}; + +/******************************************************************/ +/* TSFStaticSink */ +/******************************************************************/ + +class TSFStaticSink final : public ITfInputProcessorProfileActivationSink { + public: + static TSFStaticSink* GetInstance() { + if (!sInstance) { + RefPtr<ITfThreadMgr> 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<ITfInputProcessorProfiles> 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<TSFStaticSink> 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<ITfInputProcessorProfileActivationSink*>(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 GetActiveTIPNameForTelemetry(nsAString& aName) { + if (!sInstance || !sInstance->EnsureInitActiveTIPKeyboard()) { + return false; + } + if (sInstance->mActiveTIPGUID == GUID_NULL) { + aName.Truncate(); + aName.AppendPrintf("0x%04X", sInstance->mLangID); + return true; + } + // 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 + aName.Truncate(); + aName.AppendPrintf("0x%04X|", sInstance->mLangID); + nsAutoString description; + description.Assign(sInstance->mActiveTIPKeyboardDescription); + static const uint32_t kMaxDescriptionLength = 72 - aName.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)); + } + aName.Append(description); + return true; + } + + 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<TSFStaticSink> 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<ITfThreadMgr> mThreadMgr; + RefPtr<ITfInputProcessorProfiles> 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<TSFStaticSink> sInstance; +}; + +StaticRefPtr<TSFStaticSink> 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<ITfSource> 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<ITfInputProcessorProfileActivationSink*>(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<ITfSource> 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; + TSFStaticSink::GetActiveTIPNameForTelemetry(key); + 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<ITfInputProcessorProfileMgr> 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<IEnumTfLanguageProfiles> 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<ITfThreadMgr> TSFTextStore::sThreadMgr; +StaticRefPtr<ITfMessagePump> TSFTextStore::sMessagePump; +StaticRefPtr<ITfKeystrokeMgr> TSFTextStore::sKeystrokeMgr; +StaticRefPtr<ITfDisplayAttributeMgr> TSFTextStore::sDisplayAttrMgr; +StaticRefPtr<ITfCategoryMgr> TSFTextStore::sCategoryMgr; +StaticRefPtr<ITfCompartment> TSFTextStore::sCompartmentForOpenClose; +StaticRefPtr<ITfDocumentMgr> TSFTextStore::sDisabledDocumentMgr; +StaticRefPtr<ITfContext> TSFTextStore::sDisabledContext; +StaticRefPtr<ITfInputProcessorProfiles> TSFTextStore::sInputProcessorProfiles; +StaticRefPtr<TSFTextStore> 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<ITfThreadMgr> threadMgr = sThreadMgr; + RefPtr<ITfDocumentMgr> 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<ITfContext> context; + hr = documentMgr->CreateContext(sClientId, 0, + static_cast<ITextStoreACP*>(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<bool> 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<ITextStoreACPSink> 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<ITfDocumentMgr> 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<ITextStoreACP*>(this); + } else if (IID_ITfContextOwnerCompositionSink == riid) { + *ppv = static_cast<ITfContextOwnerCompositionSink*>(this); + } else if (IID_ITfMouseTrackerACP == riid) { + *ppv = static_cast<ITfMouseTrackerACP*>(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<IUnknown> 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<IUnknown> 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<TSFTextStore> kungFuDeathGrip(this); + RefPtr<ITextStoreACPSink> 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<nsWindow> 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<uint32_t>(action.mSelectionStart); + selectionSet.mLength = static_cast<uint32_t>(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<uint32_t>(action.mSelectionStart); + selectionSet.mLength = static_cast<uint32_t>(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<TSFTextStore> 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 (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<ULONG>(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<Selection>& 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::Content>& 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<Selection>& 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()) { + MOZ_DIAGNOSTIC_ASSERT( + !mIsInitializingContentForTSF, + "TSFTextStore::ContentForTSF() shouldn't be called recursively"); + + AutoNotifyingTSFBatch deferNotifyingTSF(*this); + AutoRestore<bool> saveInitializingContetTSF(mIsInitializingContentForTSF); + mIsInitializingContentForTSF = true; + + 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::Selection>& 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"); + } + + MOZ_DIAGNOSTIC_ASSERT( + !mIsInitializingSelectionForTSF, + "TSFTextStore::SelectionForTSF() shouldn't be called recursively"); + + AutoNotifyingTSFBatch deferNotifyingTSF(*this); + AutoRestore<bool> saveInitializingSelectionForTSF( + mIsInitializingSelectionForTSF); + mIsInitializingSelectionForTSF = true; + + 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<ITfRangeACP> 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<ITfCategoryMgr> 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<ITfDisplayAttributeMgr> displayAttrMgr = GetDisplayAttributeMgr(); + if (NS_WARN_IF(!displayAttrMgr)) { + return E_FAIL; + } + RefPtr<ITfDisplayAttributeInfo> 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<ITfCompositionView> pComposition(mComposition->GetView()); + RefPtr<ITfRange> 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<Selection>& 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<Content>& 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<uint32_t>( + 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<ITfProperty> 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<ITfRange> 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<IEnumTfRanges> 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<Selection>& 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<ITfRange> 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<uint32_t>(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<LONG>(range.mStartOffset) == + start - mComposition->StartOffset() && + static_cast<LONG>(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<uint32_t>( + 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<Selection>& 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<uint32_t>(selectionInContent.acpStart); + uint32_t endOffset = static_cast<uint32_t>(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<Content>& 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<uint32_t>(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<uint32_t>(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<uint32_t>(acpStart) + : static_cast<uint32_t>(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: + case TextInputProcessorID::eMicrosoftIMEForKorean: + return true; + 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<char16ptr_t>(_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<IUnknown> 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<Selection>& 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<Selection>& 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<Content>& 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<LONG>(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 <panel>. + + 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<TSFTextStore> 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<Content>& 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<LONG>(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<uint32_t>(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<int32_t>(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<nsWindow*>(!!queryTextRectEvent.mReply->mFocusedWidget + ? queryTextRectEvent.mReply->mFocusedWidget + : static_cast<nsIWidget*>(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<Selection>& 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<int32_t>(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<int32_t>(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 (!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 (!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<int32_t>(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<nsWindow*>(!!queryEditorRectEvent.mReply->mFocusedWidget + ? queryEditorRectEvent.mReply->mFocusedWidget + : static_cast<nsIWidget*>(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<void*>(*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<Selection>& 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<LONG>(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<Content>& 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; + } + + const auto numberOfCRLFs = [&]() -> uint32_t { + const auto* str = aInsertStr.BeginReading(); + uint32_t num = 0; + for (uint32_t i = 0; i + 1 < aInsertStr.Length(); i++) { + if (str[i] == '\r' && str[i + 1] == '\n') { + num++; + i++; + } + } + return num; + }(); + if (numberOfCRLFs) { + nsAutoString key; + if (TSFStaticSink::GetActiveTIPNameForTelemetry(key)) { + Telemetry::ScalarSet( + Telemetry::ScalarID::WIDGET_IME_NAME_ON_WINDOWS_INSERTED_CRLF, key, + true); + } + } + + 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<Content>& 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<Selection>& 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<Content>& 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<ITfRange> 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<Selection>& 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<size_t>(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<ITfDocumentMgr> prevFocusedDocumentMgr; + bool hasFocus = ThinksHavingFocus(); + RefPtr<TSFTextStore> oldTextStore = sEnabledTextStore.forget(); + + // If currently oldTextStore still has focus, notifies TSF of losing focus. + if (hasFocus) { + RefPtr<ITfThreadMgr> threadMgr = sThreadMgr; + DebugOnly<HRESULT> 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<ITfThreadMgr> threadMgr = sThreadMgr; + RefPtr<ITfDocumentMgr> 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<TSFTextStore>& 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<TSFTextStore> 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<ITfDocumentMgr> 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<ITfContext> topContext; + newDocMgr->GetTop(getter_AddRefs(topContext)); + if (topContext && topContext != textStore->mContext) { + MarkContextAsKeyboardDisabled(topContext); + } + } + + HRESULT hr; + RefPtr<ITfThreadMgr> 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<ITfDocumentMgr> 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<ITextStoreACPSink> 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<LONG>(mPendingTextChangeData.mStartOffset); + textChange.acpOldEnd = + static_cast<LONG>(mPendingTextChangeData.mRemovedEndOffset); + textChange.acpNewEnd = + static_cast<LONG>(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<ITextStoreACPSink> 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()) { + MOZ_DIAGNOSTIC_ASSERT(!mIsInitializingSelectionForTSF, + "While mSelectionForTSF is being initialized, this " + "should not be called"); + 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<ITextStoreACPSink> 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<ITextStoreACPSink> 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<ITfContextOwnerServices> 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<WPARAM>(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<uint32_t>(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<Selection>& 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<nsWindow*>(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<ITextStoreACPSink> 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<ITfContext> context = mContext; + do { + if (context) { + RefPtr<ITfContextOwnerCompositionServices> 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<ITfCompartmentMgr> 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<ITfCompartment> 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<ITfCompartment> 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<TSFTextStore> 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()) { + if (!IMEHandler::GetFocusedWindow()) { + MOZ_LOG(gIMELog, LogLevel::Error, + (" TSFTextStore::SetInputContent() gets called to enable IME, " + "but IMEHandler has not received focus notification")); + } else { + 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<ITfCompartment> 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<ITfCompartment> 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<ITfThreadMgr> 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<ITfDocumentMgr> 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<ITfContext> 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<ITfThreadMgr> TSFTextStore::GetThreadMgr() { + RefPtr<ITfThreadMgr> threadMgr = sThreadMgr; + return threadMgr.forget(); +} + +// static +already_AddRefed<ITfMessagePump> TSFTextStore::GetMessagePump() { + static bool sInitialized = false; + if (!sThreadMgr) { + return nullptr; + } + if (sMessagePump) { + RefPtr<ITfMessagePump> 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<ITfMessagePump> 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<ITfDisplayAttributeMgr> +TSFTextStore::GetDisplayAttributeMgr() { + RefPtr<ITfDisplayAttributeMgr> 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<ITfCategoryMgr> TSFTextStore::GetCategoryMgr() { + RefPtr<ITfCategoryMgr> 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<ITfCompartment> TSFTextStore::GetCompartmentForOpenClose() { + if (sCompartmentForOpenClose) { + RefPtr<ITfCompartment> compartment = sCompartmentForOpenClose; + return compartment.forget(); + } + + if (!sThreadMgr) { + return nullptr; + } + + RefPtr<ITfCompartmentMgr> 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<ITfCompartment> 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<ITfInputProcessorProfiles> +TSFTextStore::GetInputProcessorProfiles() { + RefPtr<ITfInputProcessorProfiles> 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<ITfKeystrokeMgr> 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<TSFTextStore> 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<const MSG*> savePreviousKeyMsg(sHandlingKeyMsg); + AutoRestore<bool> saveKeyEventDispatched(sIsKeyboardEventDispatched); + sHandlingKeyMsg = &aMsg; + sIsKeyboardEventDispatched = false; + BOOL eaten; + RefPtr<ITfKeystrokeMgr> 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<TSFTextStore> 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<const MSG*> savePreviousKeyMsg(sHandlingKeyMsg); + AutoRestore<bool> saveKeyEventDispatched(sIsKeyboardEventDispatched); + sHandlingKeyMsg = &aMsg; + sIsKeyboardEventDispatched = false; + BOOL eaten; + RefPtr<ITfKeystrokeMgr> 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<TSFTextStore*>(aWParam); + if (maybeTextStore == sEnabledTextStore) { + RefPtr<TSFTextStore> 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<uint32_t>(mSelection->StartOffset()), + static_cast<uint32_t>(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<uint32_t>(aStart), static_cast<uint32_t>(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<uint32_t>(aStart - mComposition->StartOffset()), + static_cast<uint32_t>(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<uint32_t>(mComposition->StartOffset()) && + mMinModifiedOffset.value() < firstDifferentOffset) { + mMinModifiedOffset = Some(firstDifferentOffset); + } + } else if (mMinModifiedOffset.isSome() && + mMinModifiedOffset.value() < static_cast<uint32_t>(LONG_MAX) && + mComposition->IsOffsetInRange( + static_cast<long>(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<uint32_t>(aStart) + + FirstDifferentCharOffset(aReplaceString, replacedString); + } + mMinModifiedOffset = + mMinModifiedOffset.isNothing() + ? Some(firstDifferentOffset) + : Some(std::min(mMinModifiedOffset.value(), firstDifferentOffset)); + mText.Replace(static_cast<uint32_t>(aStart), static_cast<uint32_t>(aLength), + aReplaceString); + } + // Selection should be collapsed at the end of the inserted string. + mSelection = Some(TSFTextStore::Selection(static_cast<uint32_t>(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<uint32_t>(aCompStart.mSelectionStart), + static_cast<uint32_t>(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<uint32_t>(aCanceledCompositionEnd.mSelectionStart), + static_cast<uint32_t>(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<DWORD>(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<uint32_t>(start) || + textContent.Length() < static_cast<uint32_t>(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<ITfMouseSink> 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<ITfInputProcessorProfiles> 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<ITfInputProcessorProfileMgr> 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..17358a488d --- /dev/null +++ b/widget/windows/TSFTextStore.h @@ -0,0 +1,1161 @@ +/* -*- 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 <msctf.h> +#include <textstor.h> + +// 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 <initguid.h> +#endif +#ifdef TEXTATTRS_INIT_GUID +# include <tsattrs.h> +#endif +#include <inputscope.h> + +// TSF InputScope, for earlier SDK 8 +#define IS_SEARCH static_cast<InputScope>(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<TSFTextStore> 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<TSFTextStore> 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<TSFTextStore> 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<TSFTextStore> 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<TSFTextStore> 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<TSFTextStore> 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<void*>(&sThreadMgr); + case NS_NATIVE_TSF_CATEGORY_MGR: + return static_cast<void*>(&sCategoryMgr); + case NS_NATIVE_TSF_DISPLAY_ATTR_MGR: + return static_cast<void*>(&sDisplayAttrMgr); + default: + return nullptr; + } + } + + static void* GetThreadManager() { return static_cast<void*>(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<TSFTextStore>& 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<SelectionChangeData> 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<nsWindow> mWidget; + // mDispatcher is a helper class to dispatch composition events. + RefPtr<TextEventDispatcher> mDispatcher; + // Document manager for the currently focused editor + RefPtr<ITfDocumentMgr> mDocumentMgr; + // Edit cookie associated with the current editing context + DWORD mEditCookie; + // Editing context at the bottom of mDocumentMgr's context stack + RefPtr<ITfContext> mContext; + // Currently installed notification sink + RefPtr<ITextStoreACPSink> 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<LONG> { + public: + explicit Composition(ITfCompositionView* aCompositionView, + LONG aCompositionStartOffset, + const nsAString& aCompositionString) + : OffsetAndData<LONG>(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<LONG>=" + << static_cast<const OffsetAndData<LONG>&>(aComposition) << " }"; + return aStream; + } + + private: + RefPtr<ITfCompositionView> 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<Composition> 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<LONG>(aStart) || + mACP->acpEnd != static_cast<LONG>(aStart + aLength); + mACP = Some( + TS_SELECTION_ACP{.acpStart = static_cast<LONG>(aStart), + .acpEnd = static_cast<LONG>(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<LONG>(aOffset), + .acpEnd = static_cast<LONG>(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<uint32_t>(Length()) && + aChangedSelection.mOffset == static_cast<uint32_t>(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<TS_SELECTION_ACP> 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<Selection> 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<Selection>& 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<TextRangeArray> 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<PendingAction> 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<ULONG>(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<TSFTextStore> 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<OffsetAndData<LONG>>& LastComposition() const { + return mLastComposition; + } + const Maybe<uint32_t>& MinModifiedOffset() const { + return mMinModifiedOffset; + } + const Maybe<StartAndEndOffsets<LONG>>& 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<TSFTextStore::Composition>& Composition() { return mComposition; } + Maybe<TSFTextStore::Selection>& 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<OffsetAndData<LONG>> mLastComposition; + + Maybe<TSFTextStore::Composition>& mComposition; + Maybe<TSFTextStore::Selection>& mSelection; + + // The latest composition's start and end offset. + Maybe<StartAndEndOffsets<LONG>> mLatestCompositionRange; + + // The minimum offset of modified part of the text. + Maybe<uint32_t> 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<Content> mContentForTSF; + + Maybe<Content>& 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<DWORD>(-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<StartAndEndOffsets<LONG>> Range() const { return mRange; } + + private: + RefPtr<ITfMouseSink> mSink; + Maybe<StartAndEndOffsets<LONG>> mRange; + DWORD mCookie; + }; + // mMouseTrackers is an array to store each information of installed + // ITfMouseSink instance. + nsTArray<MouseTracker> mMouseTrackers; + + // The input scopes for this context, defaults to IS_DEFAULT. + nsTArray<InputScope> 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; + // Debug flag to check whether we're initializing mContentForTSF and + // mSelectionForTSF. + bool mIsInitializingContentForTSF = false; + bool mIsInitializingSelectionForTSF = false; + + // TSF thread manager object for the current application + static StaticRefPtr<ITfThreadMgr> sThreadMgr; + static already_AddRefed<ITfThreadMgr> GetThreadMgr(); + // sMessagePump is QI'ed from sThreadMgr + static StaticRefPtr<ITfMessagePump> sMessagePump; + + public: + // Expose GetMessagePump() for WinUtils. + static already_AddRefed<ITfMessagePump> GetMessagePump(); + + private: + // sKeystrokeMgr is QI'ed from sThreadMgr + static StaticRefPtr<ITfKeystrokeMgr> sKeystrokeMgr; + // TSF display attribute manager + static StaticRefPtr<ITfDisplayAttributeMgr> sDisplayAttrMgr; + static already_AddRefed<ITfDisplayAttributeMgr> GetDisplayAttributeMgr(); + // TSF category manager + static StaticRefPtr<ITfCategoryMgr> sCategoryMgr; + static already_AddRefed<ITfCategoryMgr> GetCategoryMgr(); + // Compartment for (Get|Set)IMEOpenState() + static StaticRefPtr<ITfCompartment> sCompartmentForOpenClose; + static already_AddRefed<ITfCompartment> 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<TSFTextStore> sEnabledTextStore; + + // For IME (keyboard) disabled state: + static StaticRefPtr<ITfDocumentMgr> sDisabledDocumentMgr; + static StaticRefPtr<ITfContext> sDisabledContext; + + static StaticRefPtr<ITfInputProcessorProfiles> sInputProcessorProfiles; + static already_AddRefed<ITfInputProcessorProfiles> + 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..2b958f84c1 --- /dev/null +++ b/widget/windows/TaskbarPreview.cpp @@ -0,0 +1,413 @@ +/* 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 <nsITaskbarPreviewController.h> +#include <windows.h> + +#include <nsError.h> +#include <nsCOMPtr.h> +#include <nsIWidget.h> +#include <nsServiceManagerUtils.h> + +#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; + + 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<nsITaskbarPreviewCallback> 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<nsIContent> content(do_QueryInterface(aCanvas)); + auto canvas = dom::HTMLCanvasElement::FromNodeOrNull(content); + if (!canvas) { + return NS_ERROR_FAILURE; + } + + RefPtr<gfx::SourceSurface> source = canvas->GetSurfaceSnapshot(); + if (!source) { + return NS_ERROR_FAILURE; + } + RefPtr<gfxWindowsSurface> target = new gfxWindowsSurface( + source->GetSize(), gfx::SurfaceFormat::A8R8G8B8_UINT32); + if (target->CairoStatus() != CAIRO_STATUS_SUCCESS) { + return NS_ERROR_FAILURE; + } + + using DataSrcSurf = gfx::DataSourceSurface; + RefPtr<DataSrcSurf> srcSurface = source->GetDataSurface(); + RefPtr<gfxImageSurface> 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<TaskbarPreview*>(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 <windows.h> +#include <shobjidl.h> +#undef LogSeverity // SetupAPI.h #defines this as DWORD + +#include "mozilla/RefPtr.h" +#include <nsITaskbarPreview.h> +#include <nsITaskbarPreviewController.h> +#include <nsString.h> +#include <nsIWeakReferenceUtils.h> +#include <nsIDocShell.h> +#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<ITaskbarList4> mTaskbar; + // Controller for this preview + nsCOMPtr<nsITaskbarPreviewController> 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 <window> 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<TaskbarPreview> 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 <windows.h> +#include <strsafe.h> + +#include "TaskbarWindowPreview.h" +#include "TaskbarPreviewButton.h" +#include "nsWindowGfx.h" +#include <imgIContainer.h> + +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 <windows.h> +#include <shobjidl.h> +#undef LogSeverity // SetupAPI.h #defines this as DWORD + +#include "mozilla/RefPtr.h" +#include <nsITaskbarPreviewButton.h> +#include <nsString.h> +#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<TaskbarWindowPreview> mPreview; + uint32_t mIndex; + nsString mTooltip; + nsCOMPtr<imgIContainer> 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..3421e68ffb --- /dev/null +++ b/widget/windows/TaskbarTabPreview.cpp @@ -0,0 +1,344 @@ +/* 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 <nsITaskbarPreviewController.h> + +#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<TaskbarTabPreview> 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 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<CREATESTRUCT*>(lParam); + preview = reinterpret_cast<TaskbarTabPreview*>(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<TaskbarTabPreview*>( + ::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<TaskbarTabPreview*>(aContext); + WINDOWPOS* pos = reinterpret_cast<WINDOWPOS*>(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<imgIContainer> mIconImage; + // Cached Windows icon of mIconImage + HICON mIcon; + // Preview that follows this preview in the taskbar (left-to-right order) + nsCOMPtr<nsITaskbarTabPreview> 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 <nsITaskbarPreviewController.h> +#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<TaskbarWindowPreview*>(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<nsITaskbarPreviewButton> 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<TaskbarWindowPreview> kungFuDeathGrip(this); + switch (nMsg) { + case WM_COMMAND: { + uint32_t id = LOWORD(wParam); + uint32_t index = id; + nsCOMPtr<nsITaskbarPreviewButton> 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<TaskbarWindowPreview*>(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..afdffc19ac --- /dev/null +++ b/widget/windows/ToastNotification.cpp @@ -0,0 +1,915 @@ +/* -*- 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 <windows.h> +#include <appmodel.h> +#include <ktmw32.h> +#include <windows.foundation.h> +#include <wrl/client.h> + +#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/widget/WinRegistry.h" +#include "mozilla/Logging.h" +#include "mozilla/Services.h" +#include "mozilla/WidgetUtils.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" + +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<WinToastNotification*>; +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 (!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<nsIObserverService> 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<nsAutoString>& aAumid) { + UINT32 len = 0; + // ERROR_INSUFFICIENT_BUFFER signals that we're in an MSIX package, and + // therefore should use the package's AUMID. + if (GetCurrentApplicationUserModelId(&len, nullptr) != + ERROR_INSUFFICIENT_BUFFER) { + MOZ_LOG(sWASLog, LogLevel::Debug, ("Not an MSIX package")); + return false; + } + mozilla::Buffer<wchar_t> buffer(len); + LONG success = GetCurrentApplicationUserModelId(&len, buffer.Elements()); + NS_ENSURE_TRUE(success == ERROR_SUCCESS, false); + + aAumid.emplace(buffer.Elements()); + return true; +} + +bool ToastNotification::AssignIfNsisAumid(nsAutoString& aInstallHash, + Maybe<nsAutoString>& aAumid) { + nsAutoString nsisAumidName = + u""_ns MOZ_TOAST_APP_NAME u"Toast-"_ns + aInstallHash; + nsAutoString nsisAumidPath = u"AppUserModelId\\"_ns + nsisAumidName; + if (!WinRegistry::HasKey(HKEY_CLASSES_ROOT, nsisAumidPath)) { + 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<nsAutoString>& 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<nsIFile> appdir; + nsresult rv = gDirServiceProvider->GetGREDir()->Clone(getter_AddRefs(appdir)); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr<nsIFile> 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<nsIFile> 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<const BYTE*>(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<wchar_t> 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<ToastNotificationHandler> 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<nsIAlertNotification> alert = + do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); + if (NS_WARN_IF(!alert)) { + return NS_ERROR_FAILURE; + } + // vibrate is unused for now + nsTArray<uint32_t> 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)); + if (!EnsureUTF16Validity(title)) { + MOZ_LOG(sWASLog, LogLevel::Warning, + ("Notification title was invalid UTF16, unpaired surrogates have " + "been replaced.")); + } + + nsAutoString text; + MOZ_TRY(aAlert->GetText(text)); + if (!EnsureUTF16Validity(text)) { + MOZ_LOG(sWASLog, LogLevel::Warning, + ("Notification text was invalid UTF16, unpaired surrogates have " + "been replaced.")); + } + + bool textClickable; + MOZ_TRY(aAlert->GetTextClickable(&textClickable)); + + bool isSilent; + MOZ_TRY(aAlert->GetSilent(&isSilent)); + + nsAutoString hostPort; + MOZ_TRY(aAlert->GetSource(hostPort)); + + nsAutoString opaqueRelaunchData; + MOZ_TRY(aAlert->GetOpaqueRelaunchData(opaqueRelaunchData)); + + bool requireInteraction; + MOZ_TRY(aAlert->GetRequireInteraction(&requireInteraction)); + + bool inPrivateBrowsing; + MOZ_TRY(aAlert->GetInPrivateBrowsing(&inPrivateBrowsing)); + + nsTArray<RefPtr<nsIAlertAction>> actions; + MOZ_TRY(aAlert->GetActions(actions)); + + nsCOMPtr<nsIPrincipal> principal; + MOZ_TRY(aAlert->GetPrincipal(getter_AddRefs(principal))); + bool isSystemPrincipal = principal && principal->IsSystemPrincipal(); + + bool handleActions = false; + auto imagePlacement = ImagePlacement::eInline; + if (isSystemPrincipal) { + nsCOMPtr<nsIWindowsAlertNotification> winAlert(do_QueryInterface(aAlert)); + if (winAlert) { + MOZ_TRY(winAlert->GetHandleActions(&handleActions)); + + nsIWindowsAlertNotification::ImagePlacement placement; + MOZ_TRY(winAlert->GetImagePlacement(&placement)); + switch (placement) { + case nsIWindowsAlertNotification::eHero: + imagePlacement = ImagePlacement::eHero; + break; + case nsIWindowsAlertNotification::eIcon: + imagePlacement = ImagePlacement::eIcon; + break; + case nsIWindowsAlertNotification::eInline: + imagePlacement = ImagePlacement::eInline; + break; + default: + MOZ_LOG(sWASLog, LogLevel::Error, + ("Invalid image placement enum value: %hhu", placement)); + return NS_ERROR_UNEXPECTED; + } + } + } + + RefPtr<ToastNotificationHandler> oldHandler = mActiveHandlers.Get(name); + + NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED); + RefPtr<ToastNotificationHandler> handler = new ToastNotificationHandler( + this, mAumid.ref(), aAlertListener, name, cookie, title, text, hostPort, + textClickable, requireInteraction, actions, isSystemPrincipal, + opaqueRelaunchData, inPrivateBrowsing, isSilent, handleActions, + imagePlacement); + 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 opaqueRelaunchData; + MOZ_TRY(aAlert->GetOpaqueRelaunchData(opaqueRelaunchData)); + + bool requireInteraction; + MOZ_TRY(aAlert->GetRequireInteraction(&requireInteraction)); + + bool inPrivateBrowsing; + MOZ_TRY(aAlert->GetInPrivateBrowsing(&inPrivateBrowsing)); + + nsTArray<RefPtr<nsIAlertAction>> actions; + MOZ_TRY(aAlert->GetActions(actions)); + + nsCOMPtr<nsIPrincipal> principal; + MOZ_TRY(aAlert->GetPrincipal(getter_AddRefs(principal))); + bool isSystemPrincipal = principal && principal->IsSystemPrincipal(); + + NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED); + RefPtr<ToastNotificationHandler> handler = new ToastNotificationHandler( + this, mAumid.ref(), nullptr /* aAlertListener */, name, cookie, title, + text, hostPort, textClickable, requireInteraction, actions, + isSystemPrincipal, opaqueRelaunchData, 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<ToastHandledPromise> 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<ToastNotificationHandler> 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())); + return ToastHandledPromise::CreateAndResolve(true, __func__); + } + } else { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Failed to get windowsTag for handler [%p]", handler.get())); + } + } + + // Fallback handling is required. + + MOZ_LOG(sWASLog, LogLevel::Debug, + ("External windowsTag '%s' is not handled", + NS_ConvertUTF16toUTF8(aWindowsTag).get())); + + RefPtr<ToastHandledPromise::Private> 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]() { fallbackPromise->Resolve(false, __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<nsIWindowMediator> winMediator( + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID)); + if (winMediator) { + nsCOMPtr<mozIDOMWindowProxy> navWin; + winMediator->GetMostRecentWindow(u"navigator:browser", + getter_AddRefs(navWin)); + if (navWin) { + nsCOMPtr<nsIWidget> 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<dom::Promise> promise = + dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv); + ENSURE_SUCCESS(rv, rv.StealNSResult()); + + this->VerifyTagPresentOrFallback(aWindowsTag) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [aWindowsTag = nsString(aWindowsTag), + promise](const bool aTagWasHandled) { + // 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 properties if + // fallback handling is necessary. + + JSContext* cx = js.cx(); + JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx)); + + JS::Rooted<JS::Value> attVal(cx, JS::BooleanValue(aTagWasHandled)); + Unused << NS_WARN_IF( + !JS_SetProperty(cx, obj, "tagWasHandled", attVal)); + + 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<ToastNotificationHandler> 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<ToastNotificationHandler> 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<ToastNotificationHandler> kungFuDeathGrip(aHandler); + mActiveHandlers.Remove(aAlertName); + aHandler->UnregisterHandler(); + } +} + +NS_IMETHODIMP +ToastNotification::RemoveAllNotificationsForInstall() { + HRESULT hr = S_OK; + + ComPtr<IToastNotificationManagerStatics> 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<IToastNotificationManagerStatics2> manager2; + hr = manager.As(&manager2); + NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); + + ComPtr<IToastNotificationHistory> 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<IToastNotifier> notifier; + hr = manager->CreateToastNotifierWithId(aumid.Get(), ¬ifier); + NS_ENSURE_TRUE_VOID(SUCCEEDED(hr)); + + ComPtr<IVectorView_ScheduledToastNotification> 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<IScheduledToastNotification> 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; +} + +NS_IMPL_ISUPPORTS_INHERITED(WindowsAlertNotification, AlertNotification, + nsIWindowsAlertNotification) + +NS_IMETHODIMP +WindowsAlertNotification::GetHandleActions(bool* aHandleActions) { + *aHandleActions = mHandleActions; + return NS_OK; +} + +NS_IMETHODIMP +WindowsAlertNotification::SetHandleActions(bool aHandleActions) { + mHandleActions = aHandleActions; + return NS_OK; +} + +NS_IMETHODIMP WindowsAlertNotification::GetImagePlacement( + nsIWindowsAlertNotification::ImagePlacement* aImagePlacement) { + *aImagePlacement = mImagePlacement; + return NS_OK; +} + +NS_IMETHODIMP WindowsAlertNotification::SetImagePlacement( + nsIWindowsAlertNotification::ImagePlacement aImagePlacement) { + switch (aImagePlacement) { + case eHero: + case eIcon: + case eInline: + mImagePlacement = aImagePlacement; + break; + default: + MOZ_LOG(sWASLog, LogLevel::Error, + ("Invalid image placement enum value: %hhu", aImagePlacement)); + return NS_ERROR_INVALID_ARG; + } + + 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..9beb08cc27 --- /dev/null +++ b/widget/windows/ToastNotification.h @@ -0,0 +1,83 @@ +/* -*- 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" +#include "mozilla/AlertNotification.h" + +namespace mozilla { +namespace widget { + +using ToastHandledPromise = MozPromise<bool, bool, true>; + +class ToastNotificationHandler; + +class WindowsAlertNotification final : public AlertNotification, + public nsIWindowsAlertNotification { + public: + NS_DECL_NSIWINDOWSALERTNOTIFICATION + NS_FORWARD_NSIALERTNOTIFICATION(AlertNotification::) + NS_DECL_ISUPPORTS_INHERITED + + WindowsAlertNotification() = default; + + protected: + virtual ~WindowsAlertNotification() = default; + bool mHandleActions = false; + nsIWindowsAlertNotification::ImagePlacement mImagePlacement = eInline; +}; + +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<nsAutoString>& aAumid); + static bool AssignIfNsisAumid(nsAutoString& aInstallHash, + Maybe<nsAutoString>& aAumid); + static bool RegisterRuntimeAumid(nsAutoString& aInstallHash, + Maybe<nsAutoString>& aAumid); + + RefPtr<ToastHandledPromise> VerifyTagPresentOrFallback( + const nsAString& aWindowsTag); + static void SignalComNotificationHandled(const nsAString& aWindowsTag); + + nsRefPtrHashtable<nsStringHashKey, ToastNotificationHandler> mActiveHandlers; + Maybe<nsAutoString> 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..a493342719 --- /dev/null +++ b/widget/windows/ToastNotificationHandler.cpp @@ -0,0 +1,1167 @@ +/* -*- 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 <windows.foundation.h> + +#include "gfxUtils.h" +#include "gfxPlatform.h" +#include "imgIContainer.h" +#include "imgIRequest.h" +#include "json/json.h" +#include "mozilla/gfx/2D.h" +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +#endif +#include "mozilla/HashFunctions.h" +#include "mozilla/JSONStringWriteFuncs.h" +#include "mozilla/Result.h" +#include "mozilla/Logging.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/Unused.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/intl/Localization.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<WinToastNotification*, IInspectable*>; +using ToastDismissedHandler = + ITypedEventHandler<WinToastNotification*, ToastDismissedEventArgs*>; +using ToastFailedHandler = + ITypedEventHandler<WinToastNotification*, ToastFailedEventArgs*>; +using IVectorView_ToastNotification = + Collections::IVectorView<WinToastNotification*>; + +NS_IMPL_ISUPPORTS(ToastNotificationHandler, nsIAlertNotificationImageListener) + +static bool SetNodeValueString(const nsString& aString, IXmlNode* node, + IXmlDocument* xml) { + ComPtr<IXmlText> inputText; + HRESULT hr; + hr = xml->CreateTextNode(HStringReference(aString.get()).Get(), &inputText); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr<IXmlNode> inputTextNode; + hr = inputText.As(&inputTextNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr<IXmlNode> appendedChild; + hr = node->AppendChild(inputTextNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +static bool SetAttribute(ComPtr<IXmlElement>& 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<IXmlDocument>& toastXml, + ComPtr<IXmlNode>& actionsNode, + const nsAString& actionTitle, + const nsAString& launchArg, + const nsAString& actionArgs, + const nsAString& actionPlacement = u""_ns, + const nsAString& activationType = u""_ns) { + ComPtr<IXmlElement> 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"`, `arguments` + // should be a Windows-specific keyword, namely "dismiss" or "snooze", which + // are supposed to make a system handled dismiss/snooze buttons. + // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml#snoozedismiss + // + // Note that while using it prevents calling our notification COM server, + // it somehow still calls OnActivate instead of OnDismiss. Thus, we still + // need to handle such callbacks manually by checking `arguments`. + success = SetAttribute(action, HStringReference(L"arguments"), actionArgs); + NS_ENSURE_TRUE(success, false); + } + + // Add <action> to <actions> + ComPtr<IXmlNode> actionNode; + hr = action.As(&actionNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr<IXmlNode> 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<nsString, nsresult> 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<nsIFile> 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<nsIToolkitProfileService> profileSvc = + do_GetService(NS_PROFILESERVICE_CONTRACTID); + if (profileSvc) { + nsCOMPtr<nsIToolkitProfile> 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; + } + + // `windowsTag` argument. + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgTag) + u"\n"_ns + mWindowsTag; + + // `logging` argument. + if (Preferences::GetBool( + "alerts.useSystemBackend.windows.notificationserver.verbose", + false)) { + // Signal notification to log verbose messages. + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgLogging) + u"\nverbose"_ns; + } + + return launchArg; +} + +static ComPtr<IToastNotificationManagerStatics> +GetToastNotificationManagerStatics() { + ComPtr<IToastNotificationManagerStatics> 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<nsresult> 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()); + +#ifdef MOZ_BACKGROUNDTASKS + nsAutoString imageUrl; + if (BackgroundTasks::IsBackgroundTaskMode() && + NS_SUCCEEDED(aAlert->GetImageURL(imageUrl)) && !imageUrl.IsEmpty()) { + // Bug 1870750: Image decoding relies on gfx and runs on a thread pool, + // which expects to have been initialized early and on the main thread. + // Since background tasks run headless this never occurs. In this case we + // force gfx initialization. + Unused << NS_WARN_IF(!gfxPlatform::GetPlatform()); + } +#endif + + 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<nsIFile> 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; +} + +nsString ToastNotificationHandler::ActionArgsJSONString( + const nsString& aAction, const nsString& aOpaqueRelaunchData = u""_ns) { + nsAutoCString actionArgsData; + + JSONStringRefWriteFunc js(actionArgsData); + JSONWriter w(js, JSONWriter::SingleLineStyle); + w.Start(); + + w.StringProperty("action", NS_ConvertUTF16toUTF8(aAction)); + + if (mIsSystemPrincipal) { + // Privileged/chrome alerts (not activated by Windows) can have custom + // relaunch data. + if (!aOpaqueRelaunchData.IsEmpty()) { + w.StringProperty("opaqueRelaunchData", + NS_ConvertUTF16toUTF8(aOpaqueRelaunchData)); + } + + // Privileged alerts include any provided name for metrics. + if (!mName.IsEmpty()) { + w.StringProperty("privilegedName", NS_ConvertUTF16toUTF8(mName)); + } + } else { + if (!mHostPort.IsEmpty()) { + w.StringProperty("launchUrl", NS_ConvertUTF16toUTF8(mHostPort)); + } + } + + w.End(); + + return NS_ConvertUTF8toUTF16(actionArgsData); +} + +ComPtr<IXmlDocument> ToastNotificationHandler::CreateToastXmlDocument() { + ComPtr<IToastNotificationManagerStatics> 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<IXmlDocument> toastXml; + toastNotificationManagerStatics->GetTemplateContent(toastTemplate, &toastXml); + + if (!toastXml) { + return nullptr; + } + + nsresult ns; + HRESULT hr; + bool success; + + if (mHasImage) { + ComPtr<IXmlNodeList> toastImageElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"image").Get(), + &toastImageElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> imageNode; + hr = toastImageElements->Item(0, &imageNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlElement> image; + hr = imageNode.As(&image); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetAttribute(image, HStringReference(L"src"), mImageUri); + NS_ENSURE_TRUE(success, nullptr); + + switch (mImagePlacement) { + case ImagePlacement::eHero: + success = + SetAttribute(image, HStringReference(L"placement"), u"hero"_ns); + NS_ENSURE_TRUE(success, nullptr); + break; + case ImagePlacement::eIcon: + success = SetAttribute(image, HStringReference(L"placement"), + u"appLogoOverride"_ns); + NS_ENSURE_TRUE(success, nullptr); + break; + case ImagePlacement::eInline: + // No attribute placement attribute for inline images. + break; + } + } + + ComPtr<IXmlNodeList> toastTextElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"text").Get(), + &toastTextElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> titleTextNodeRoot; + hr = toastTextElements->Item(0, &titleTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> 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<IXmlNodeList> toastElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"toast").Get(), + &toastElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> toastNodeRoot; + hr = toastElements->Item(0, &toastNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlElement> 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(); + + nsString launchArgWithoutAction = launchArg; + + if (!mIsSystemPrincipal) { + // Unprivileged/content alerts can't have custom relaunch data. + NS_WARNING_ASSERTION(mOpaqueRelaunchData.IsEmpty(), + "unprivileged/content alert " + "should have trivial `mOpaqueRelaunchData`"); + } + + launchArg += u"\n"_ns + nsDependentString(kLaunchArgAction) + u"\n"_ns + + ActionArgsJSONString(u""_ns, mOpaqueRelaunchData); + + success = SetAttribute(toastElement, HStringReference(L"launch"), launchArg); + NS_ENSURE_TRUE(success, nullptr); + + MOZ_LOG(sWASLog, LogLevel::Debug, + ("launchArg: '%s'", NS_ConvertUTF16toUTF8(launchArg).get())); + + // Use newer toast layout for system (chrome-privileged) toasts. This gains us + // UI elements such as new image placement options (default image placement is + // larger and inline) and buttons. + if (mIsSystemPrincipal) { + ComPtr<IXmlNodeList> bindingElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"binding").Get(), + &bindingElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> bindingNodeRoot; + hr = bindingElements->Item(0, &bindingNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlElement> 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<IXmlElement> actions; + hr = toastXml->CreateElement(HStringReference(L"actions").Get(), &actions); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> actionsNode; + hr = actions.As(&actionsNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + nsCOMPtr<nsIStringBundleService> sbs = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + NS_ENSURE_TRUE(sbs, nullptr); + + nsCOMPtr<nsIStringBundle> bundle; + sbs->CreateBundle("chrome://alerts/locale/alert.properties", + getter_AddRefs(bundle)); + NS_ENSURE_TRUE(bundle, nullptr); + + if (!mHostPort.IsEmpty()) { + AutoTArray<nsString, 1> formatStrings = {mHostPort}; + + ComPtr<IXmlNode> 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<IXmlElement> 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, + // TODO: launch into `about:preferences`? + launchArgWithoutAction, ActionArgsJSONString(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, launchArgWithoutAction, + // TODO: launch into `about:preferences`? + ActionArgsJSONString(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); + + nsString opaqueRelaunchData; + ns = action->GetOpaqueRelaunchData(opaqueRelaunchData); + NS_ENSURE_SUCCESS(ns, nullptr); + + MOZ_LOG(sWASLog, LogLevel::Debug, + ("launchArgWithoutAction for '%s': '%s'", + NS_ConvertUTF16toUTF8(actionString).get(), + NS_ConvertUTF16toUTF8(launchArgWithoutAction).get())); + + // 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); + + nsString actionArgs; + if (mIsSystemPrincipal && activationType) { + // Privileged/chrome alerts that are activated by Windows can't have + // custom relaunch data. + actionArgs = actionString; + + NS_WARNING_ASSERTION(opaqueRelaunchData.IsEmpty(), + "action with `windowsSystemActivationType=true` " + "should have trivial `opaqueRelaunchData`"); + } else { + actionArgs = ActionArgsJSONString(actionString, opaqueRelaunchData); + } + + success = AddActionNode(toastXml, actionsNode, title, + /* launchArg */ launchArgWithoutAction, + /* actionArgs */ actionArgs, + /* actionPlacement */ u""_ns, + /* activationType */ activationTypeString); + NS_ENSURE_TRUE(success, nullptr); + } + + // Windows ignores scenario=reminder added by mRequiredInteraction if + // there's no non-contextmenu action. + if (mRequireInteraction && !mActions.Length()) { + // `activationType="system" arguments="dismiss" content=""` provides + // localized text from Windows, but we support more locales than Windows + // does, so let's have our own. + nsTArray<nsCString> resIds = { + "toolkit/global/alert.ftl"_ns, + }; + RefPtr<intl::Localization> l10n = intl::Localization::Create(resIds, true); + IgnoredErrorResult rv; + nsAutoCString closeTitle; + l10n->FormatValueSync("notification-default-dismiss"_ns, {}, closeTitle, + rv); + NS_ENSURE_TRUE(!rv.Failed(), nullptr); + + NS_ENSURE_TRUE( + AddActionNode(toastXml, actionsNode, NS_ConvertUTF8toUTF16(closeTitle), + u""_ns, u"dismiss"_ns, u""_ns, u"system"_ns), + nullptr); + } + + ComPtr<IXmlNode> appendedChild; + hr = toastNodeRoot->AppendChild(actionsNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + if (mIsSilent) { + ComPtr<IXmlNode> audioNode; + // Create <audio silent="true"/> for silent notifications. + ComPtr<IXmlElement> audio; + hr = toastXml->CreateElement(HStringReference(L"audio").Get(), &audio); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + SetAttribute(audio, HStringReference(L"silent"), u"true"_ns); + + hr = audio.As(&audioNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + hr = toastNodeRoot->AppendChild(audioNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + } + + return toastXml; +} + +nsresult ToastNotificationHandler::CreateToastXmlString( + const nsAString& aImageURL, nsAString& aString) { + HRESULT hr; + + if (!aImageURL.IsEmpty()) { + // For testing: don't fetch and write image to disk, just include the URL. + mHasImage = true; + mImageUri.Assign(aImageURL); + } + + ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument(); + if (!toastXml) { + return NS_ERROR_FAILURE; + } + + ComPtr<IXmlNodeSerializer> ser; + hr = toastXml.As(&ser); + NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE); + + HString data; + hr = ser->GetXml(data.GetAddressOf()); + NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE); + + uint32_t len = 0; + const wchar_t* rawData = data.GetRawBuffer(&len); + NS_ENSURE_TRUE(rawData, NS_ERROR_FAILURE); + aString.Assign(rawData, len); + + return NS_OK; +} + +bool ToastNotificationHandler::ShowAlert() { + if (!mBackend->IsActiveHandler(mName, this)) { + return false; + } + + ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument(); + + if (!toastXml) { + return false; + } + + return CreateWindowsNotificationFromXml(toastXml); +} + +bool ToastNotificationHandler::IsPrivate() { return mInPrivateBrowsing; } + +void ToastNotificationHandler::HideAlert() { + if (mNotifier && mNotification) { + mNotifier->Hide(mNotification.Get()); + } +} + +bool ToastNotificationHandler::CreateWindowsNotificationFromXml( + ComPtr<IXmlDocument>& aXml) { + ComPtr<IToastNotificationFactory> factory; + HRESULT hr; + + hr = GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification) + .Get(), + &factory); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = factory->CreateToastNotification(aXml.Get(), &mNotification); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + RefPtr<ToastNotificationHandler> self = this; + + hr = mNotification->add_Activated( + Callback<ToastActivationHandler>([self](IToastNotification* aNotification, + IInspectable* aInspectable) { + return self->OnActivate(ComPtr<IToastNotification>(aNotification), + ComPtr<IInspectable>(aInspectable)); + }).Get(), + &mActivatedToken); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = mNotification->add_Dismissed( + Callback<ToastDismissedHandler>([self](IToastNotification* aNotification, + IToastDismissedEventArgs* aArgs) { + return self->OnDismiss(ComPtr<IToastNotification>(aNotification), + ComPtr<IToastDismissedEventArgs>(aArgs)); + }).Get(), + &mDismissedToken); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = mNotification->add_Failed( + Callback<ToastFailedHandler>([self](IToastNotification* aNotification, + IToastFailedEventArgs* aArgs) { + return self->OnFail(ComPtr<IToastNotification>(aNotification), + ComPtr<IToastFailedEventArgs>(aArgs)); + }).Get(), + &mFailedToken); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr<IToastNotification2> notification2; + hr = mNotification.As(¬ification2); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + HString hTag; + hr = hTag.Set(mWindowsTag.get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = notification2->put_Tag(hTag.Get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics = + GetToastNotificationManagerStatics(); + NS_ENSURE_TRUE(toastNotificationManagerStatics, false); + + HString aumid; + hr = aumid.Set(mAumid.get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + hr = toastNotificationManagerStatics->CreateToastNotifierWithId(aumid.Get(), + &mNotifier); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = mNotifier->Show(mNotification.Get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + if (mAlertListener) { + mAlertListener->Observe(nullptr, "alertshow", mCookie.get()); + } + + return true; +} + +void ToastNotificationHandler::SendFinished() { + if (!mSentFinished && mAlertListener) { + mAlertListener->Observe(nullptr, "alertfinished", mCookie.get()); + } + + mSentFinished = true; +} + +HRESULT +ToastNotificationHandler::OnActivate( + const ComPtr<IToastNotification>& notification, + const ComPtr<IInspectable>& inspectable) { + MOZ_LOG(sWASLog, LogLevel::Info, ("OnActivate")); + + if (mAlertListener) { + // Extract the `action` value from the argument string. + nsAutoString argumentsString; + nsAutoString actionString; + if (inspectable) { + ComPtr<IToastActivatedEventArgs> eventArgs; + HRESULT hr = inspectable.As(&eventArgs); + if (SUCCEEDED(hr)) { + HString arguments; + hr = eventArgs->get_Arguments(arguments.GetAddressOf()); + if (SUCCEEDED(hr)) { + uint32_t len = 0; + const char16_t* buffer = (char16_t*)arguments.GetRawBuffer(&len); + if (buffer) { + MOZ_LOG(sWASLog, LogLevel::Info, + ("OnActivate: arguments: %s", + NS_ConvertUTF16toUTF8(buffer).get())); + argumentsString.Assign(buffer); + + // Toast arguments are a newline separated key/value combination of + // launch arguments and an optional action argument provided as an + // argument to the toast's constructor. After the `action` key is + // found, the remainder of toast argument (including newlines) is + // the `action` value. + Tokenizer16 parse(buffer); + nsDependentSubstring token; + + while (parse.ReadUntil(Tokenizer16::Token::NewLine(), token)) { + if (token == nsDependentString(kLaunchArgAction)) { + Unused << parse.ReadUntil(Tokenizer16::Token::EndOfFile(), + actionString); + } else { + // Next line is a value in a key/value pair, skip. + parse.SkipUntil(Tokenizer16::Token::NewLine()); + } + // Skip newline. + Tokenizer16::Token unused; + Unused << parse.Next(unused); + } + } + } + } + } + + if (argumentsString.EqualsLiteral("dismiss")) { + // XXX: Somehow Windows still fires OnActivate instead of OnDismiss for + // supposedly system managed dismiss button (with activationType=system + // and arguments=dismiss). We have to manually treat such callback as a + // dismiss action. For this case `arguments` only includes a keyword so we + // don't need to compare with a parsed result. + SendFinished(); + } else if (actionString.EqualsLiteral("settings")) { + mAlertListener->Observe(nullptr, "alertsettingscallback", mCookie.get()); + } else if (actionString.EqualsLiteral("snooze")) { + mAlertListener->Observe(nullptr, "alertdisablecallback", mCookie.get()); + } else if (mClickable) { + // When clicking toast, focus moves to another process, but we want to set + // focus on Firefox process. + nsCOMPtr<nsIWindowMediator> winMediator( + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID)); + if (winMediator) { + nsCOMPtr<mozIDOMWindowProxy> navWin; + winMediator->GetMostRecentWindow(u"navigator:browser", + getter_AddRefs(navWin)); + if (navWin) { + nsCOMPtr<nsIWidget> widget = + WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin)); + if (widget) { + SetForegroundWindow( + static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW))); + } + } + } + + if (mHandleActions) { + Json::Value jsonData; + Json::Reader jsonReader; + + if (jsonReader.parse(NS_ConvertUTF16toUTF8(actionString).get(), + jsonData, false)) { + char actionKey[] = "action"; + if (jsonData.isMember(actionKey) && jsonData[actionKey].isString()) { + mAlertListener->Observe( + nullptr, "alertactioncallback", + NS_ConvertUTF8toUTF16(jsonData[actionKey].asCString()).get()); + } + } + } + + mAlertListener->Observe(nullptr, "alertclickcallback", mCookie.get()); + } + } + mBackend->RemoveHandler(mName, this); + return S_OK; +} + +// Returns `nullptr` if no such toast exists. +/* static */ ComPtr<IToastNotification> +ToastNotificationHandler::FindNotificationByTag(const nsAString& aWindowsTag, + const nsAString& aAumid) { + HRESULT hr = S_OK; + + HString current_id; + current_id.Set(PromiseFlatString(aWindowsTag).get()); + + ComPtr<IToastNotificationManagerStatics> manager = + GetToastNotificationManagerStatics(); + NS_ENSURE_TRUE(manager, nullptr); + + ComPtr<IToastNotificationManagerStatics2> manager2; + hr = manager.As(&manager2); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IToastNotificationHistory> history; + hr = manager2->get_History(&history); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + ComPtr<IToastNotificationHistory2> history2; + hr = history.As(&history2); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IVectorView_ToastNotification> toasts; + hr = history2->GetHistoryWithId( + HStringReference(PromiseFlatString(aAumid).get()).Get(), &toasts); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + unsigned int hist_size; + hr = toasts->get_Size(&hist_size); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + for (unsigned int i = 0; i < hist_size; i++) { + ComPtr<IToastNotification> hist_toast; + hr = toasts->GetAt(i, &hist_toast); + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + + ComPtr<IToastNotification2> hist_toast2; + hr = hist_toast.As(&hist_toast2); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + HString history_id; + hr = hist_toast2->get_Tag(history_id.GetAddressOf()); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + // We can not directly compare IToastNotification objects; their IUnknown + // pointers should be equivalent but under inspection were not. Therefore we + // use the notification's tag instead. + if (current_id == history_id) { + return hist_toast; + } + } + + return nullptr; +} + +// A single toast message can receive multiple dismiss events, at most one for +// the popup and at most one for the action center. We can't simply count +// dismiss events as the user may have disabled either popups or action center +// notifications, therefore we have to check if the toast remains in the history +// (action center) to determine if the toast is fully dismissed. +HRESULT +ToastNotificationHandler::OnDismiss( + const ComPtr<IToastNotification>& notification, + const ComPtr<IToastDismissedEventArgs>& aArgs) { + ComPtr<IToastNotification2> notification2; + HRESULT hr = notification.As(¬ification2); + NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL); + + HString tagHString; + hr = notification2->get_Tag(tagHString.GetAddressOf()); + NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL); + + unsigned int len; + const wchar_t* tagPtr = tagHString.GetRawBuffer(&len); + nsAutoString tag(tagPtr, len); + + if (FindNotificationByTag(tag, mAumid)) { + return S_OK; + } + + SendFinished(); + mBackend->RemoveHandler(mName, this); + return S_OK; +} + +HRESULT +ToastNotificationHandler::OnFail(const ComPtr<IToastNotification>& notification, + const ComPtr<IToastFailedEventArgs>& aArgs) { + HRESULT err; + aArgs->get_ErrorCode(&err); + MOZ_LOG(sWASLog, LogLevel::Error, + ("Error creating notification, error: %ld", err)); + + if (mHandleActions) { + mAlertListener->Observe(nullptr, "alerterror", mCookie.get()); + } + + SendFinished(); + mBackend->RemoveHandler(mName, this); + return S_OK; +} + +nsresult ToastNotificationHandler::TryShowAlert() { + if (NS_WARN_IF(!ShowAlert())) { + mBackend->RemoveHandler(mName, this); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +ToastNotificationHandler::OnImageMissing(nsISupports*) { + return TryShowAlert(); +} + +NS_IMETHODIMP +ToastNotificationHandler::OnImageReady(nsISupports*, imgIRequest* aRequest) { + nsresult rv = AsyncSaveImage(aRequest); + if (NS_FAILED(rv)) { + return TryShowAlert(); + } + return rv; +} + +nsresult ToastNotificationHandler::AsyncSaveImage(imgIRequest* aRequest) { + nsresult rv = + NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mImageFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mImageFile->Append(u"notificationimages"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mImageFile->Create(nsIFile::DIRECTORY_TYPE, 0500); + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) { + return rv; + } + + nsID uuid; + rv = nsID::GenerateUUIDInPlace(uuid); + NS_ENSURE_SUCCESS(rv, rv); + + NSID_TrimBracketsASCII uuidStr(uuid); + uuidStr.AppendLiteral(".png"); + mImageFile->AppendNative(uuidStr); + + nsCOMPtr<imgIContainer> imgContainer; + rv = aRequest->GetImage(getter_AddRefs(imgContainer)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMainThreadPtrHandle<ToastNotificationHandler> self( + new nsMainThreadPtrHolder<ToastNotificationHandler>( + "ToastNotificationHandler", this)); + + nsCOMPtr<nsIFile> imageFile(mImageFile); + RefPtr<mozilla::gfx::SourceSurface> surface = imgContainer->GetFrame( + imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "ToastNotificationHandler::AsyncWriteImage", + [self, imageFile, surface]() -> void { + nsresult rv = NS_ERROR_FAILURE; + if (surface) { + FILE* file = nullptr; + rv = imageFile->OpenANSIFileDesc("wb", &file); + if (NS_SUCCEEDED(rv)) { + rv = gfxUtils::EncodeSourceSurface(surface, ImageType::PNG, u""_ns, + gfxUtils::eBinaryEncode, file); + fclose(file); + } + } + + nsCOMPtr<nsIRunnable> cbRunnable = NS_NewRunnableFunction( + "ToastNotificationHandler::AsyncWriteImageCb", + [self, rv]() -> void { + auto handler = const_cast<ToastNotificationHandler*>(self.get()); + handler->OnWriteImageFinished(rv); + }); + + NS_DispatchToMainThread(cbRunnable); + }); + + return mBackend->BackgroundDispatch(r); +} + +void ToastNotificationHandler::OnWriteImageFinished(nsresult rv) { + if (NS_SUCCEEDED(rv)) { + OnWriteImageSuccess(); + } + TryShowAlert(); +} + +nsresult ToastNotificationHandler::OnWriteImageSuccess() { + nsresult rv; + + nsCOMPtr<nsIURI> fileURI; + rv = NS_NewFileURI(getter_AddRefs(fileURI), mImageFile); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString uriStr; + rv = fileURI->GetSpec(uriStr); + NS_ENSURE_SUCCESS(rv, rv); + + AppendUTF8toUTF16(uriStr, mImageUri); + + mHasImage = true; + + return NS_OK; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/ToastNotificationHandler.h b/widget/windows/ToastNotificationHandler.h new file mode 100644 index 0000000000..b3be34709c --- /dev/null +++ b/widget/windows/ToastNotificationHandler.h @@ -0,0 +1,162 @@ +/* -*- 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 ToastNotificationHandler_h__ +#define ToastNotificationHandler_h__ + +#include <windows.ui.notifications.h> +#include <windows.data.xml.dom.h> +#include <wrl.h> +#include "nsCOMPtr.h" +#include "nsICancelable.h" +#include "nsIFile.h" +#include "nsIWindowsAlertsService.h" +#include "nsString.h" +#include "mozilla/Result.h" + +namespace mozilla { +namespace widget { + +enum class ImagePlacement { + eInline, + eHero, + eIcon, +}; + +class ToastNotification; + +class ToastNotificationHandler final + : public nsIAlertNotificationImageListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIALERTNOTIFICATIONIMAGELISTENER + + ToastNotificationHandler( + ToastNotification* backend, const nsAString& aumid, + nsIObserver* aAlertListener, const nsAString& aName, + const nsAString& aCookie, const nsAString& aTitle, const nsAString& aMsg, + const nsAString& aHostPort, bool aClickable, bool aRequireInteraction, + const nsTArray<RefPtr<nsIAlertAction>>& aActions, bool aIsSystemPrincipal, + const nsAString& aOpaqueRelaunchData, bool aInPrivateBrowsing, + bool aIsSilent, bool aHandlesActions = false, + ImagePlacement aImagePlacement = ImagePlacement::eInline) + : mBackend(backend), + mAumid(aumid), + mHasImage(false), + mAlertListener(aAlertListener), + mName(aName), + mCookie(aCookie), + mTitle(aTitle), + mMsg(aMsg), + mHostPort(aHostPort), + mClickable(aClickable), + mRequireInteraction(aRequireInteraction), + mInPrivateBrowsing(aInPrivateBrowsing), + mActions(aActions.Clone()), + mIsSystemPrincipal(aIsSystemPrincipal), + mOpaqueRelaunchData(aOpaqueRelaunchData), + mIsSilent(aIsSilent), + mSentFinished(!aAlertListener), + mHandleActions(aHandlesActions), + mImagePlacement(aImagePlacement) {} + + nsresult InitAlertAsync(nsIAlertNotification* aAlert); + + void OnWriteImageFinished(nsresult rv); + + void HideAlert(); + bool IsPrivate(); + + void UnregisterHandler(); + + nsString ActionArgsJSONString( + const nsString& aAction, + const nsString& aOpaqueRelaunchData /* = u""_ns */); + nsresult CreateToastXmlString(const nsAString& aImageURL, nsAString& aString); + + nsresult GetWindowsTag(nsAString& aWindowsTag); + nsresult SetWindowsTag(const nsAString& aWindowsTag); + + // Exposed for consumption by `ToastNotification.cpp`. + static nsresult FindNotificationDataForWindowsTag( + const nsAString& aWindowsTag, const nsAString& aAumid, bool& aFoundTag, + nsAString& aNotificationData); + + protected: + virtual ~ToastNotificationHandler(); + + using IXmlDocument = ABI::Windows::Data::Xml::Dom::IXmlDocument; + using IToastNotifier = ABI::Windows::UI::Notifications::IToastNotifier; + using IToastNotification = + ABI::Windows::UI::Notifications::IToastNotification; + using IToastDismissedEventArgs = + ABI::Windows::UI::Notifications::IToastDismissedEventArgs; + using IToastFailedEventArgs = + ABI::Windows::UI::Notifications::IToastFailedEventArgs; + using ToastTemplateType = ABI::Windows::UI::Notifications::ToastTemplateType; + template <typename T> + using ComPtr = Microsoft::WRL::ComPtr<T>; + + Result<nsString, nsresult> GetLaunchArgument(); + + ComPtr<IToastNotification> mNotification; + ComPtr<IToastNotifier> mNotifier; + + RefPtr<ToastNotification> mBackend; + + nsString mAumid; + nsString mWindowsTag; + + nsCOMPtr<nsICancelable> mImageRequest; + nsCOMPtr<nsIFile> mImageFile; + nsString mImageUri; + bool mHasImage; + + EventRegistrationToken mActivatedToken; + EventRegistrationToken mDismissedToken; + EventRegistrationToken mFailedToken; + + nsCOMPtr<nsIObserver> mAlertListener; + nsString mName; + nsString mCookie; + nsString mTitle; + nsString mMsg; + nsString mHostPort; + bool mClickable; + bool mRequireInteraction; + bool mInPrivateBrowsing; + nsTArray<RefPtr<nsIAlertAction>> mActions; + bool mIsSystemPrincipal; + nsString mOpaqueRelaunchData; + bool mIsSilent; + bool mSentFinished; + bool mHandleActions; + ImagePlacement mImagePlacement; + + nsresult TryShowAlert(); + bool ShowAlert(); + nsresult AsyncSaveImage(imgIRequest* aRequest); + nsresult OnWriteImageSuccess(); + void SendFinished(); + + nsresult InitWindowsTag(); + bool CreateWindowsNotificationFromXml(ComPtr<IXmlDocument>& aToastXml); + ComPtr<IXmlDocument> CreateToastXmlDocument(); + + HRESULT OnActivate(const ComPtr<IToastNotification>& notification, + const ComPtr<IInspectable>& inspectable); + HRESULT OnDismiss(const ComPtr<IToastNotification>& notification, + const ComPtr<IToastDismissedEventArgs>& aArgs); + HRESULT OnFail(const ComPtr<IToastNotification>& notification, + const ComPtr<IToastFailedEventArgs>& aArgs); + + static ComPtr<IToastNotification> FindNotificationByTag( + const nsAString& aWindowsTag, const nsAString& aAumid); +}; + +} // namespace widget +} // namespace mozilla + +#endif diff --git a/widget/windows/ToastNotificationHeaderOnlyUtils.h b/widget/windows/ToastNotificationHeaderOnlyUtils.h new file mode 100644 index 0000000000..dd777c0c32 --- /dev/null +++ b/widget/windows/ToastNotificationHeaderOnlyUtils.h @@ -0,0 +1,155 @@ +/* -*- 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_ToastNotificationHeaderOnlyUtils_h +#define mozilla_ToastNotificationHeaderOnlyUtils_h + +/** + * This header is intended for self-contained, header-only, utility code to + * share between Windows toast notification code in firefox.exe and + * notificationserver.dll. + */ + +// Use XPCOM logging if we're in a XUL context, otherwise use Windows Event +// logging. +// NOTE: The `printf` `format` equivalent argument to `NOTIFY_LOG` is converted +// to a wide string when outside of a XUL context. String format specifiers need +// to specify they're a wide string with `%ls` or narrow string with `%hs`. +#include "mozilla/Logging.h" +#ifdef IMPL_LIBXUL +namespace mozilla::widget { +extern LazyLogModule sWASLog; +} // namespace mozilla::widget +# define NOTIFY_LOG(_level, _args) \ + MOZ_LOG(mozilla::widget::sWASLog, _level, _args) +#else +# include "mozilla/WindowsEventLog.h" + +bool gVerbose = false; + +# define NOTIFY_LOG(_level, _args) \ + if (gVerbose || _level == mozilla::LogLevel::Error) { \ + POST_EXPAND_NOTIFY_LOG(MOZ_LOG_EXPAND_ARGS _args); \ + } +# define POST_EXPAND_NOTIFY_LOG(...) \ + MOZ_WIN_EVENT_LOG_ERROR_MESSAGE( \ + L"" MOZ_APP_DISPLAYNAME " Notification Server", L"" __VA_ARGS__) +#endif + +#include <functional> +#include <string> + +#include "nsWindowsHelpers.h" + +namespace mozilla::widget::toastnotification { + +const wchar_t kLaunchArgProgram[] = L"program"; +const wchar_t kLaunchArgProfile[] = L"profile"; +const wchar_t kLaunchArgTag[] = L"windowsTag"; +const wchar_t kLaunchArgLogging[] = L"logging"; +const wchar_t kLaunchArgAction[] = L"action"; + +const DWORD kNotificationServerTimeoutMs = (10 * 1000); + +struct ToastNotificationPidMessage { + DWORD pid = 0; +}; + +struct ToastNotificationPermissionMessage { + DWORD setForegroundPermissionGranted = 0; +}; + +inline std::wstring GetNotificationPipeName(const wchar_t* aTag) { + // Prefix required by pipe API. + std::wstring pipeName(LR"(\\.\pipe\)"); + + pipeName += L"" MOZ_APP_NAME; + pipeName += aTag; + + return pipeName; +} + +inline bool WaitEventWithTimeout(const HANDLE& event) { + DWORD result = WaitForSingleObject(event, kNotificationServerTimeoutMs); + + switch (result) { + case WAIT_OBJECT_0: + NOTIFY_LOG(LogLevel::Info, ("Pipe wait signaled")); + return true; + case WAIT_TIMEOUT: + NOTIFY_LOG(LogLevel::Warning, ("Pipe wait timed out")); + return false; + case WAIT_FAILED: + NOTIFY_LOG(LogLevel::Error, + ("Pipe wait failed, error %lu", GetLastError())); + return false; + case WAIT_ABANDONED: + NOTIFY_LOG(LogLevel::Error, ("Pipe wait abandoned")); + return false; + default: + NOTIFY_LOG(LogLevel::Error, ("Pipe wait unknown error")); + return false; + } +} + +/* Handles running overlapped transactions for a Windows pipe. This function + * manages lifetimes of Event and OVERLAPPED objects to ensure they are not used + * while an overlapped operation is pending. */ +inline bool SyncDoOverlappedIOWithTimeout( + const nsAutoHandle& pipe, const size_t bytesExpected, + const std::function<BOOL(OVERLAPPED&)>& transactPipe) { + nsAutoHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (!event) { + NOTIFY_LOG( + LogLevel::Error, + ("Error creating pipe transaction event, error %lu", GetLastError())); + return false; + } + + OVERLAPPED overlapped{}; + overlapped.hEvent = event.get(); + BOOL result = transactPipe(overlapped); + + if (!result && GetLastError() != ERROR_IO_PENDING) { + NOTIFY_LOG(LogLevel::Error, + ("Error reading from pipe, error %lu", GetLastError())); + return false; + } + + if (!WaitEventWithTimeout(overlapped.hEvent)) { + NOTIFY_LOG(LogLevel::Warning, ("Pipe transaction timed out, canceling " + "(transaction may still succeed).")); + + CancelIo(pipe.get()); + + // Transaction may still succeed before cancellation is handled; fall + // through to normal handling. + } + + DWORD bytesTransferred = 0; + // Pipe transfer has either been signaled or cancelled by this point, so it + // should be safe to wait on. + BOOL overlappedResult = + GetOverlappedResult(pipe.get(), &overlapped, &bytesTransferred, TRUE); + + if (!overlappedResult) { + NOTIFY_LOG( + LogLevel::Error, + ("Error retrieving pipe overlapped result, error %lu", GetLastError())); + return false; + } else if (bytesTransferred != bytesExpected) { + NOTIFY_LOG(LogLevel::Error, + ("%lu bytes read from pipe, but %zu bytes expected", + bytesTransferred, bytesExpected)); + return false; + } + + return true; +} + +} // namespace mozilla::widget::toastnotification + +#endif // mozilla_ToastNotificationHeaderOnlyUtils_h diff --git a/widget/windows/UrlmonHeaderOnlyUtils.h b/widget/windows/UrlmonHeaderOnlyUtils.h new file mode 100644 index 0000000000..dd9209f78f --- /dev/null +++ b/widget/windows/UrlmonHeaderOnlyUtils.h @@ -0,0 +1,76 @@ +/* -*- 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_UrlmonHeaderOnlyUtils_h +#define mozilla_UrlmonHeaderOnlyUtils_h + +#include "mozilla/ShellHeaderOnlyUtils.h" + +namespace mozilla { + +/** + * We used to validate a uri with SHParseDisplayName to mitigate the Windows + * bug (Bug 394974). However, Bug 1573051 revealed an issue that a fragment, + * a string following a hash mark (#), is dropped when we extract a string + * from PIDL. This is the intended behavior of Windows. + * + * To deal with the fragment issue as well as keeping our mitigation, we + * decided to use CreateUri to validate a uri string, but we also keep using + * SHParseDisplayName as a pre-check. This is because there are several + * cases where CreateUri succeeds while SHParseDisplayName fails such as + * a non-existent file: uri. + * + * To minimize the impact of introducing CreateUri into the validation logic, + * we try to mimic the logic of windows_storage!IUriToPidl (ieframe!IUriToPidl + * in Win7) which is executed behind SHParseDisplayName. + * What IUriToPidl does is: + * 1) If a given uri has a fragment, removes a fragment. + * 2) Takes an absolute uri if it's available in the given uri, otherwise + * takes a raw uri. + * + * As we need to get a full uri including a fragment, this function does 2). + */ +inline LauncherResult<_bstr_t> UrlmonValidateUri(const wchar_t* aUri) { + LauncherResult<UniqueAbsolutePidl> pidlResult = ShellParseDisplayName(aUri); + if (pidlResult.isErr()) { + return pidlResult.propagateErr(); + } + + // The value of |flags| is the same value as used in ieframe!_EnsureIUri in + // Win7, which is called behind SHParseDisplayName. In Win10, on the other + // hand, an flag 0x03000000 is also passed to CreateUri, but we don't + // specify it because it's undocumented and unknown. + constexpr DWORD flags = + Uri_CREATE_NO_DECODE_EXTRA_INFO | Uri_CREATE_CANONICALIZE | + Uri_CREATE_CRACK_UNKNOWN_SCHEMES | Uri_CREATE_PRE_PROCESS_HTML_URI | + Uri_CREATE_IE_SETTINGS; + RefPtr<IUri> uri; + HRESULT hr; + SAFECALL_URLMON_FUNC(CreateUri, aUri, flags, 0, getter_AddRefs(uri)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + _bstr_t bstrUri; + + hr = uri->GetAbsoluteUri(bstrUri.GetAddress()); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + if (hr == S_FALSE) { + hr = uri->GetRawUri(bstrUri.GetAddress()); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + } + + return bstrUri; +} + +} // namespace mozilla + +#endif // mozilla_UrlmonHeaderOnlyUtils_h diff --git a/widget/windows/WidgetTraceEvent.cpp b/widget/windows/WidgetTraceEvent.cpp new file mode 100644 index 0000000000..15bb4d720c --- /dev/null +++ b/widget/windows/WidgetTraceEvent.cpp @@ -0,0 +1,121 @@ +/* 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/. */ + +/* + * Windows widget support for event loop instrumentation. + * See toolkit/xre/EventTracer.cpp for more details. + */ + +#include <stdio.h> +#include <windows.h> + +#include "mozilla/RefPtr.h" +#include "mozilla/WidgetTraceEvent.h" +#include "nsAppShellCID.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIAppShellService.h" +#include "nsIBaseWindow.h" +#include "nsIDocShell.h" +#include "nsISupportsImpl.h" +#include "nsIWidget.h" +#include "nsIAppWindow.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsWindowDefs.h" + +namespace { + +// Used for signaling the background thread from the main thread. +HANDLE sEventHandle = nullptr; + +// We need a runnable in order to find the hidden window on the main +// thread. +class HWNDGetter : public mozilla::Runnable { + public: + HWNDGetter() : Runnable("HWNDGetter"), hidden_window_hwnd(nullptr) {} + + HWND hidden_window_hwnd; + + NS_IMETHOD Run() override { + // Jump through some hoops to locate the hidden window. + nsCOMPtr<nsIAppShellService> appShell( + do_GetService(NS_APPSHELLSERVICE_CONTRACTID)); + nsCOMPtr<nsIAppWindow> hiddenWindow; + + nsresult rv = appShell->GetHiddenWindow(getter_AddRefs(hiddenWindow)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIDocShell> docShell; + rv = hiddenWindow->GetDocShell(getter_AddRefs(docShell)); + if (NS_FAILED(rv) || !docShell) { + return rv; + } + + nsCOMPtr<nsIBaseWindow> baseWindow(do_QueryInterface(docShell)); + + if (!baseWindow) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIWidget> widget; + baseWindow->GetMainWidget(getter_AddRefs(widget)); + + if (!widget) return NS_ERROR_FAILURE; + + hidden_window_hwnd = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW); + + return NS_OK; + } +}; + +HWND GetHiddenWindowHWND() { + // Need to dispatch this to the main thread because plenty of + // the things it wants to access are main-thread-only. + RefPtr<HWNDGetter> getter = new HWNDGetter(); + NS_DispatchAndSpinEventLoopUntilComplete( + "GetHiddenWindowHWND"_ns, mozilla::GetMainThreadSerialEventTarget(), + do_AddRef(getter)); + return getter->hidden_window_hwnd; +} + +} // namespace + +namespace mozilla { + +bool InitWidgetTracing() { + sEventHandle = CreateEventW(nullptr, FALSE, FALSE, nullptr); + return sEventHandle != nullptr; +} + +void CleanUpWidgetTracing() { + CloseHandle(sEventHandle); + sEventHandle = nullptr; +} + +// This function is called from the main (UI) thread. +void SignalTracerThread() { + if (sEventHandle != nullptr) SetEvent(sEventHandle); +} + +// This function is called from the background tracer thread. +bool FireAndWaitForTracerEvent() { + MOZ_ASSERT(sEventHandle, "Tracing not initialized!"); + + // First, try to find the hidden window. + static HWND hidden_window = nullptr; + if (hidden_window == nullptr) { + hidden_window = GetHiddenWindowHWND(); + } + + if (hidden_window == nullptr) return false; + + // Post the tracer message into the hidden window's message queue, + // and then block until it's processed. + PostMessage(hidden_window, MOZ_WM_TRACE, 0, 0); + WaitForSingleObject(sEventHandle, INFINITE); + return true; +} + +} // namespace mozilla diff --git a/widget/windows/WinCompositorWidget.cpp b/widget/windows/WinCompositorWidget.cpp new file mode 100644 index 0000000000..15957a1c3f --- /dev/null +++ b/widget/windows/WinCompositorWidget.cpp @@ -0,0 +1,105 @@ +/* -*- 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 "WinCompositorWidget.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 "nsWindow.h" +#include "VsyncDispatcher.h" +#include "WinCompositorWindowThread.h" +#include "VRShMem.h" + +#include <ddraw.h> + +namespace mozilla { +namespace widget { + +using namespace mozilla::gfx; +using namespace mozilla; + +WinCompositorWidget::WinCompositorWidget( + const WinCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions) + : CompositorWidget(aOptions), + mSetParentCompleted(false), + mWidgetKey(aInitData.widgetKey()), + mWnd(reinterpret_cast<HWND>(aInitData.hWnd())), + mCompositorWnds(nullptr, nullptr) { + MOZ_ASSERT(mWnd && ::IsWindow(mWnd)); +} + +WinCompositorWidget::~WinCompositorWidget() { DestroyCompositorWindow(); } + +uintptr_t WinCompositorWidget::GetWidgetKey() { return mWidgetKey; } + +void WinCompositorWidget::EnsureCompositorWindow() { + if (mCompositorWnds.mCompositorWnd || mCompositorWnds.mInitialParentWnd) { + return; + } + + mCompositorWnds = WinCompositorWindowThread::CreateCompositorWindow(); + UpdateCompositorWnd(mCompositorWnds.mCompositorWnd, mWnd); + + MOZ_ASSERT(mCompositorWnds.mCompositorWnd); + MOZ_ASSERT(mCompositorWnds.mInitialParentWnd); +} + +void WinCompositorWidget::DestroyCompositorWindow() { + if (!mCompositorWnds.mCompositorWnd && !mCompositorWnds.mInitialParentWnd) { + return; + } + WinCompositorWindowThread::DestroyCompositorWindow(mCompositorWnds); + mCompositorWnds = WinCompositorWnds(nullptr, nullptr); +} + +void WinCompositorWidget::UpdateCompositorWndSizeIfNecessary() { + if (!mCompositorWnds.mCompositorWnd) { + return; + } + + LayoutDeviceIntSize size = GetClientSize(); + if (mLastCompositorWndSize == size) { + return; + } + + // This code is racing with the compositor, which needs to reparent the + // compositor surface to the actual window (mWnd). To avoid racing mutations, + // we refuse to proceed until ::SetParent() is called in the parent process. + // After the ::SetParent() call, composition is scheduled in + // CompositorWidgetParent::UpdateCompositorWnd(). + if (!mSetParentCompleted) { + // ::SetParent() is not completed yet. + return; + } + + MOZ_ASSERT(mWnd == ::GetParent(mCompositorWnds.mCompositorWnd)); + + // Force a resize and redraw (but not a move, activate, etc.). + if (!::SetWindowPos( + mCompositorWnds.mCompositorWnd, nullptr, 0, 0, size.width, + size.height, + SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOOWNERZORDER | SWP_NOZORDER)) { + return; + } + + mLastCompositorWndSize = size; +} + +// Creates a new instance of FxROutputHandler so that this compositor widget +// can send its output to Firefox Reality for Desktop. +void WinCompositorWidget::RequestFxrOutput() { + MOZ_ASSERT(mFxrHandler == nullptr); + + mFxrHandler.reset(new FxROutputHandler()); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/WinCompositorWidget.h b/widget/windows/WinCompositorWidget.h new file mode 100644 index 0000000000..fef967380c --- /dev/null +++ b/widget/windows/WinCompositorWidget.h @@ -0,0 +1,103 @@ +/* -*- 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_WinCompositorWidget_h +#define widget_windows_WinCompositorWidget_h + +#include "CompositorWidget.h" +#include "mozilla/Atomics.h" +#include "mozilla/gfx/CriticalSection.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/layers/LayersTypes.h" +#include "mozilla/Mutex.h" +#include "mozilla/widget/WinCompositorWindowThread.h" +#include "FxROutputHandler.h" +#include "nsIWidget.h" + +class nsWindow; + +namespace mozilla { +namespace widget { + +class PlatformCompositorWidgetDelegate : public CompositorWidgetDelegate { + public: + // Callbacks for nsWindow. + virtual void EnterPresentLock() = 0; + virtual void LeavePresentLock() = 0; + virtual void OnDestroyWindow() = 0; + virtual bool OnWindowResize(const LayoutDeviceIntSize& aSize) = 0; + virtual void OnWindowModeChange(nsSizeMode aSizeMode) = 0; + + // Transparency handling. + virtual void UpdateTransparency(TransparencyMode aMode) = 0; + virtual void ClearTransparentWindow() = 0; + + // Deliver visibility info + virtual void NotifyVisibilityUpdated(nsSizeMode aSizeMode, + bool aIsFullyOccluded) = 0; + + // CompositorWidgetDelegate Overrides + + PlatformCompositorWidgetDelegate* AsPlatformSpecificDelegate() override { + return this; + } +}; + +class WinCompositorWidgetInitData; + +// 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 WinCompositorWidget : public CompositorWidget { + public: + WinCompositorWidget(const WinCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions); + ~WinCompositorWidget() override; + + // CompositorWidget Overrides + + uintptr_t GetWidgetKey() override; + WinCompositorWidget* AsWindows() override { return this; } + + HWND GetHwnd() const { + return mCompositorWnds.mCompositorWnd ? mCompositorWnds.mCompositorWnd + : mWnd; + } + + HWND GetCompositorHwnd() const { return mCompositorWnds.mCompositorWnd; } + + void EnsureCompositorWindow(); + void DestroyCompositorWindow(); + void UpdateCompositorWndSizeIfNecessary(); + + void RequestFxrOutput(); + bool HasFxrOutputHandler() const { return mFxrHandler != nullptr; } + FxROutputHandler* GetFxrOutputHandler() const { return mFxrHandler.get(); } + + virtual nsSizeMode GetWindowSizeMode() const = 0; + virtual bool GetWindowIsFullyOccluded() const = 0; + + virtual void UpdateCompositorWnd(const HWND aCompositorWnd, + const HWND aParentWnd) = 0; + virtual void SetRootLayerTreeID(const layers::LayersId& aRootLayerTreeId) = 0; + + protected: + bool mSetParentCompleted; + + private: + uintptr_t mWidgetKey; + HWND mWnd; + + WinCompositorWnds mCompositorWnds; + LayoutDeviceIntSize mLastCompositorWndSize; + + UniquePtr<FxROutputHandler> mFxrHandler; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_windows_WinCompositorWidget_h diff --git a/widget/windows/WinCompositorWindowThread.cpp b/widget/windows/WinCompositorWindowThread.cpp new file mode 100644 index 0000000000..3b06c098e6 --- /dev/null +++ b/widget/windows/WinCompositorWindowThread.cpp @@ -0,0 +1,294 @@ +/* -*- 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 "base/platform_thread.h" +#include "WinCompositorWindowThread.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/layers/SynchronousTask.h" +#include "mozilla/StaticPtr.h" +#include "transport/runnable_utils.h" +#include "mozilla/StaticPrefs_apz.h" + +namespace mozilla { +namespace widget { + +static StaticRefPtr<WinCompositorWindowThread> sWinCompositorWindowThread; + +/// A window procedure that logs when an input event is received to the gfx +/// error log +/// +/// This is done because this window is supposed to be WM_DISABLED, but +/// malfunctioning software may still end up targetting this window. If that +/// happens, it's almost-certainly a bug and should be brought to the attention +/// of the developers that are debugging the issue. +static LRESULT CALLBACK InputEventRejectingWindowProc(HWND window, UINT msg, + WPARAM wparam, + LPARAM lparam) { + switch (msg) { + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + case WM_MOUSEMOVE: + case WM_KEYDOWN: + case WM_KEYUP: + case WM_SYSKEYDOWN: + case WM_SYSKEYUP: + gfxCriticalNoteOnce + << "The compositor window received an input event even though it's " + "disabled. There is likely malfunctioning " + "software on the user's machine."; + + break; + default: + break; + } + return ::DefWindowProcW(window, msg, wparam, lparam); +} + +WinCompositorWindowThread::WinCompositorWindowThread(base::Thread* aThread) + : mThread(aThread), mMonitor("WinCompositorWindowThread") {} + +/* static */ +WinCompositorWindowThread* WinCompositorWindowThread::Get() { + if (!sWinCompositorWindowThread || + sWinCompositorWindowThread->mHasAttemptedShutdown) { + return nullptr; + } + return sWinCompositorWindowThread; +} + +/* static */ +void WinCompositorWindowThread::Start() { + MOZ_ASSERT(NS_IsMainThread()); + + base::Thread::Options options; + // HWND requests ui thread. + options.message_loop_type = MessageLoop::TYPE_UI; + + if (sWinCompositorWindowThread) { + // Try to reuse the thread, which involves stopping and restarting it. + sWinCompositorWindowThread->mThread->Stop(); + if (sWinCompositorWindowThread->mThread->StartWithOptions(options)) { + // Success! + sWinCompositorWindowThread->mHasAttemptedShutdown = false; + return; + } + // Restart failed, so null out our sWinCompositorWindowThread and + // try again with a new thread. This will cause the old singleton + // instance to be deallocated, which will destroy its mThread as well. + sWinCompositorWindowThread = nullptr; + } + + base::Thread* thread = new base::Thread("WinCompositor"); + if (!thread->StartWithOptions(options)) { + delete thread; + return; + } + + sWinCompositorWindowThread = new WinCompositorWindowThread(thread); +} + +/* static */ +void WinCompositorWindowThread::ShutDown() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(sWinCompositorWindowThread); + + sWinCompositorWindowThread->mHasAttemptedShutdown = true; + + // Our thread could hang while we're waiting for it to stop. + // Since we're shutting down, that's not a critical problem. + // We set a reasonable amount of time to wait for shutdown, + // and if it succeeds within that time, we correctly stop + // our thread by nulling out the refptr, which will cause it + // to be deallocated and join the thread. If it times out, + // we do nothing, which means that the thread will not be + // joined and sWinCompositorWindowThread memory will leak. + CVStatus status; + { + // It's important to hold the lock before posting the + // runnable. This ensures that the runnable can't begin + // until we've started our Wait, which prevents us from + // Waiting on a monitor that has already been notified. + MonitorAutoLock lock(sWinCompositorWindowThread->mMonitor); + + static const TimeDuration TIMEOUT = TimeDuration::FromSeconds(2.0); + RefPtr<Runnable> runnable = + NewRunnableMethod("WinCompositorWindowThread::ShutDownTask", + sWinCompositorWindowThread.get(), + &WinCompositorWindowThread::ShutDownTask); + Loop()->PostTask(runnable.forget()); + + // Monitor uses SleepConditionVariableSRW, which can have + // spurious wakeups which are reported as timeouts, so we + // check timestamps to ensure that we've waited as long we + // intended to. If we wake early, we don't bother calculating + // a precise amount for the next wait; we just wait the same + // amount of time. This means timeout might happen after as + // much as 2x the TIMEOUT time. + TimeStamp timeStart = TimeStamp::NowLoRes(); + do { + status = sWinCompositorWindowThread->mMonitor.Wait(TIMEOUT); + } while ((status == CVStatus::Timeout) && + ((TimeStamp::NowLoRes() - timeStart) < TIMEOUT)); + } + + if (status == CVStatus::NoTimeout) { + sWinCompositorWindowThread = nullptr; + } +} + +void WinCompositorWindowThread::ShutDownTask() { + MonitorAutoLock lock(mMonitor); + + MOZ_ASSERT(IsInCompositorWindowThread()); + mMonitor.NotifyAll(); +} + +/* static */ +MessageLoop* WinCompositorWindowThread::Loop() { + return sWinCompositorWindowThread + ? sWinCompositorWindowThread->mThread->message_loop() + : nullptr; +} + +/* static */ +bool WinCompositorWindowThread::IsInCompositorWindowThread() { + return sWinCompositorWindowThread && + sWinCompositorWindowThread->mThread->thread_id() == + PlatformThread::CurrentId(); +} + +const wchar_t kClassNameCompositorInitalParent[] = + L"MozillaCompositorInitialParentClass"; +const wchar_t kClassNameCompositor[] = L"MozillaCompositorWindowClass"; + +ATOM g_compositor_inital_parent_window_class; +ATOM g_compositor_window_class; + +// This runs on the window owner thread. +void InitializeInitialParentWindowClass() { + if (g_compositor_inital_parent_window_class) { + return; + } + + WNDCLASSW wc; + wc.style = 0; + wc.lpfnWndProc = ::DefWindowProcW; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hInstance = GetModuleHandle(nullptr); + wc.hIcon = nullptr; + wc.hCursor = nullptr; + wc.hbrBackground = nullptr; + wc.lpszMenuName = nullptr; + wc.lpszClassName = kClassNameCompositorInitalParent; + g_compositor_inital_parent_window_class = ::RegisterClassW(&wc); +} + +// This runs on the window owner thread. +void InitializeWindowClass() { + if (g_compositor_window_class) { + return; + } + + WNDCLASSW wc; + wc.style = CS_OWNDC; + wc.lpfnWndProc = InputEventRejectingWindowProc; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hInstance = GetModuleHandle(nullptr); + wc.hIcon = nullptr; + wc.hCursor = nullptr; + wc.hbrBackground = nullptr; + wc.lpszMenuName = nullptr; + wc.lpszClassName = kClassNameCompositor; + g_compositor_window_class = ::RegisterClassW(&wc); +} + +/* static */ +WinCompositorWnds WinCompositorWindowThread::CreateCompositorWindow() { + MOZ_ASSERT(Loop()); + + if (!Loop()) { + return WinCompositorWnds(nullptr, nullptr); + } + + layers::SynchronousTask task("Create compositor window"); + + HWND initialParentWnd = nullptr; + HWND compositorWnd = nullptr; + + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "WinCompositorWindowThread::CreateCompositorWindow::Runnable", [&]() { + layers::AutoCompleteTask complete(&task); + + InitializeInitialParentWindowClass(); + InitializeWindowClass(); + + // Create initial parent window. + // We could not directly create a compositor window with a main window + // as parent window, so instead create it with a temporary placeholder + // parent. Its parent is set as main window in UI process. + initialParentWnd = + ::CreateWindowEx(WS_EX_TOOLWINDOW, kClassNameCompositorInitalParent, + nullptr, WS_POPUP | WS_DISABLED, 0, 0, 1, 1, + nullptr, 0, GetModuleHandle(nullptr), 0); + if (!initialParentWnd) { + gfxCriticalNoteOnce << "Inital parent window failed " + << ::GetLastError(); + return; + } + + DWORD extendedStyle = WS_EX_NOPARENTNOTIFY | WS_EX_NOREDIRECTIONBITMAP; + + if (!StaticPrefs::apz_windows_force_disable_direct_manipulation()) { + extendedStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT; + } + + compositorWnd = ::CreateWindowEx( + extendedStyle, kClassNameCompositor, nullptr, + WS_CHILDWINDOW | WS_DISABLED | WS_VISIBLE, 0, 0, 1, 1, + initialParentWnd, 0, GetModuleHandle(nullptr), 0); + if (!compositorWnd) { + gfxCriticalNoteOnce << "Compositor window failed " + << ::GetLastError(); + } + }); + + Loop()->PostTask(runnable.forget()); + + task.Wait(); + + return WinCompositorWnds(compositorWnd, initialParentWnd); +} + +/* static */ +void WinCompositorWindowThread::DestroyCompositorWindow( + WinCompositorWnds aWnds) { + MOZ_ASSERT(aWnds.mCompositorWnd); + MOZ_ASSERT(aWnds.mInitialParentWnd); + MOZ_ASSERT(Loop()); + + if (!Loop()) { + return; + } + + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "WinCompositorWidget::CreateNativeWindow::Runnable", [aWnds]() { + ::DestroyWindow(aWnds.mCompositorWnd); + ::DestroyWindow(aWnds.mInitialParentWnd); + }); + + Loop()->PostTask(runnable.forget()); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/WinCompositorWindowThread.h b/widget/windows/WinCompositorWindowThread.h new file mode 100644 index 0000000000..372e3d91c2 --- /dev/null +++ b/widget/windows/WinCompositorWindowThread.h @@ -0,0 +1,67 @@ +/* -*- 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 widget_windows_WinCompositorWindowThread_h +#define widget_windows_WinCompositorWindowThread_h + +#include "base/thread.h" +#include "base/message_loop.h" +#include "mozilla/Monitor.h" + +namespace mozilla { +namespace widget { + +struct WinCompositorWnds { + HWND mCompositorWnd; + HWND mInitialParentWnd; + WinCompositorWnds(HWND aCompositorWnd, HWND aInitialParentWnd) + : mCompositorWnd(aCompositorWnd), mInitialParentWnd(aInitialParentWnd) {} +}; + +class WinCompositorWindowThread final { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD( + WinCompositorWindowThread) + + public: + /// Can be called from any thread. + static WinCompositorWindowThread* Get(); + + /// Can only be called from the main thread. + static void Start(); + + /// Can only be called from the main thread. + static void ShutDown(); + + /// Can be called from any thread. + static MessageLoop* Loop(); + + /// Can be called from any thread. + static bool IsInCompositorWindowThread(); + + /// Can be called from any thread. + static WinCompositorWnds CreateCompositorWindow(); + + /// Can be called from any thread. + static void DestroyCompositorWindow(WinCompositorWnds aWnds); + + private: + explicit WinCompositorWindowThread(base::Thread* aThread); + ~WinCompositorWindowThread() {} + + void ShutDownTask(); + + UniquePtr<base::Thread> const mThread; + Monitor mMonitor; + + // Has ShutDown been called on us? We might have survived if our thread join + // timed out. + bool mHasAttemptedShutdown = false; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_windows_WinCompositorWindowThread_h diff --git a/widget/windows/WinEventObserver.cpp b/widget/windows/WinEventObserver.cpp new file mode 100644 index 0000000000..7abac8a59a --- /dev/null +++ b/widget/windows/WinEventObserver.cpp @@ -0,0 +1,223 @@ +/* -*- 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 <windows.h> +#include <winuser.h> +#include <wtsapi32.h> + +#include "WinEventObserver.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPtr.h" +#include "nsHashtablesFwd.h" +#include "nsdefs.h" + +namespace mozilla::widget { + +LazyLogModule gWinEventObserverLog("WinEventObserver"); +#define LOG(...) MOZ_LOG(gWinEventObserverLog, LogLevel::Info, (__VA_ARGS__)) + +// static +StaticRefPtr<WinEventHub> WinEventHub::sInstance; + +// static +bool WinEventHub::Ensure() { + if (sInstance) { + return true; + } + + LOG("WinEventHub::Ensure()"); + + RefPtr<WinEventHub> instance = new WinEventHub(); + if (!instance->Initialize()) { + MOZ_ASSERT_UNREACHABLE("unexpected to be called"); + return false; + } + sInstance = instance; + ClearOnShutdown(&sInstance); + return true; +} + +WinEventHub::WinEventHub() { + MOZ_ASSERT(NS_IsMainThread()); + LOG("WinEventHub::WinEventHub()"); +} + +WinEventHub::~WinEventHub() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mObservers.IsEmpty()); + LOG("WinEventHub::~WinEventHub()"); + + if (mHWnd) { + ::DestroyWindow(mHWnd); + mHWnd = nullptr; + } +} + +bool WinEventHub::Initialize() { + WNDCLASSW wc; + HMODULE hSelf = ::GetModuleHandle(nullptr); + + if (!GetClassInfoW(hSelf, L"MozillaWinEventHubClass", &wc)) { + ZeroMemory(&wc, sizeof(WNDCLASSW)); + wc.hInstance = hSelf; + wc.lpfnWndProc = WinEventProc; + wc.lpszClassName = L"MozillaWinEventHubClass"; + RegisterClassW(&wc); + } + + mHWnd = ::CreateWindowW(L"MozillaWinEventHubClass", L"WinEventHub", 0, 0, 0, + 0, 0, nullptr, nullptr, hSelf, nullptr); + if (!mHWnd) { + return false; + } + + return true; +} + +// static +LRESULT CALLBACK WinEventHub::WinEventProc(HWND aHwnd, UINT aMsg, + WPARAM aWParam, LPARAM aLParam) { + if (sInstance) { + sInstance->ProcessWinEventProc(aHwnd, aMsg, aWParam, aLParam); + } + return ::DefWindowProc(aHwnd, aMsg, aWParam, aLParam); +} + +void WinEventHub::ProcessWinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam, + LPARAM aLParam) { + for (const auto& observer : mObservers) { + observer->OnWinEventProc(aHwnd, aMsg, aWParam, aLParam); + } +} + +void WinEventHub::AddObserver(WinEventObserver* aObserver) { + LOG("WinEventHub::AddObserver() aObserver %p", aObserver); + + mObservers.Insert(aObserver); +} + +void WinEventHub::RemoveObserver(WinEventObserver* aObserver) { + LOG("WinEventHub::RemoveObserver() aObserver %p", aObserver); + + mObservers.Remove(aObserver); +} + +WinEventObserver::~WinEventObserver() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mDestroyed); +} + +void WinEventObserver::Destroy() { + LOG("WinEventObserver::Destroy() this %p", this); + + WinEventHub::Get()->RemoveObserver(this); + mDestroyed = true; +} + +// static +already_AddRefed<DisplayStatusObserver> DisplayStatusObserver::Create( + DisplayStatusListener* aListener) { + if (!WinEventHub::Ensure()) { + return nullptr; + } + RefPtr<DisplayStatusObserver> observer = new DisplayStatusObserver(aListener); + WinEventHub::Get()->AddObserver(observer); + return observer.forget(); +} + +DisplayStatusObserver::DisplayStatusObserver(DisplayStatusListener* aListener) + : mListener(aListener) { + MOZ_ASSERT(NS_IsMainThread()); + LOG("DisplayStatusObserver::DisplayStatusObserver() this %p", this); + + mDisplayStatusHandle = ::RegisterPowerSettingNotification( + WinEventHub::Get()->GetWnd(), &GUID_SESSION_DISPLAY_STATUS, + DEVICE_NOTIFY_WINDOW_HANDLE); +} + +DisplayStatusObserver::~DisplayStatusObserver() { + MOZ_ASSERT(NS_IsMainThread()); + LOG("DisplayStatusObserver::~DisplayStatusObserver() this %p", this); + + if (mDisplayStatusHandle) { + ::UnregisterPowerSettingNotification(mDisplayStatusHandle); + mDisplayStatusHandle = nullptr; + } +} + +void DisplayStatusObserver::OnWinEventProc(HWND aHwnd, UINT aMsg, + WPARAM aWParam, LPARAM aLParam) { + if (aMsg == WM_POWERBROADCAST && aWParam == PBT_POWERSETTINGCHANGE) { + POWERBROADCAST_SETTING* setting = (POWERBROADCAST_SETTING*)aLParam; + if (setting && + ::IsEqualGUID(setting->PowerSetting, GUID_SESSION_DISPLAY_STATUS) && + setting->DataLength == sizeof(DWORD)) { + bool displayOn = PowerMonitorOff != + static_cast<MONITOR_DISPLAY_STATE>(setting->Data[0]); + + LOG("DisplayStatusObserver::OnWinEventProc() displayOn %d this %p", + displayOn, this); + mListener->OnDisplayStateChanged(displayOn); + } + } +} + +// static +already_AddRefed<SessionChangeObserver> SessionChangeObserver::Create( + SessionChangeListener* aListener) { + if (!WinEventHub::Ensure()) { + return nullptr; + } + RefPtr<SessionChangeObserver> observer = new SessionChangeObserver(aListener); + WinEventHub::Get()->AddObserver(observer); + return observer.forget(); +} + +SessionChangeObserver::SessionChangeObserver(SessionChangeListener* aListener) + : mListener(aListener) { + MOZ_ASSERT(NS_IsMainThread()); + LOG("SessionChangeObserver::SessionChangeObserver() this %p", this); + + auto hwnd = WinEventHub::Get()->GetWnd(); + DebugOnly<BOOL> wtsRegistered = + ::WTSRegisterSessionNotification(hwnd, NOTIFY_FOR_THIS_SESSION); + NS_ASSERTION(wtsRegistered, "WTSRegisterSessionNotification failed!\n"); +} +SessionChangeObserver::~SessionChangeObserver() { + MOZ_ASSERT(NS_IsMainThread()); + LOG("SessionChangeObserver::~SessionChangeObserver() this %p", this); + + auto hwnd = WinEventHub::Get()->GetWnd(); + // Unregister notifications from terminal services + ::WTSUnRegisterSessionNotification(hwnd); +} + +void SessionChangeObserver::OnWinEventProc(HWND aHwnd, UINT aMsg, + WPARAM aWParam, LPARAM aLParam) { + if (aMsg == WM_WTSSESSION_CHANGE && + (aWParam == WTS_SESSION_LOCK || aWParam == WTS_SESSION_UNLOCK)) { + Maybe<bool> isCurrentSession; + DWORD currentSessionId = 0; + if (!::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId)) { + isCurrentSession = Nothing(); + } else { + LOG("SessionChangeObserver::OnWinEventProc() aWParam %zu aLParam " + "%" PRIdLPTR + " " + "currentSessionId %lu this %p", + aWParam, aLParam, currentSessionId, this); + + isCurrentSession = Some(static_cast<DWORD>(aLParam) == currentSessionId); + } + mListener->OnSessionChange(aWParam, isCurrentSession); + } +} + +#undef LOG + +} // namespace mozilla::widget diff --git a/widget/windows/WinEventObserver.h b/widget/windows/WinEventObserver.h new file mode 100644 index 0000000000..6a267871dd --- /dev/null +++ b/widget/windows/WinEventObserver.h @@ -0,0 +1,115 @@ +/* -*- 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 widget_windows_WinEventObserver_h +#define widget_windows_WinEventObserver_h + +#include <windows.h> + +#include "mozilla/Maybe.h" +#include "nsISupportsImpl.h" +#include "nsTHashSet.h" + +namespace mozilla { + +namespace widget { + +class WinEventObserver { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinEventObserver) + public: + virtual void OnWinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam, + LPARAM aLParam) {} + virtual void Destroy(); + + protected: + virtual ~WinEventObserver(); + + bool mDestroyed = false; +}; + +// Uses singleton window to observe events like display status and session +// change. +class WinEventHub final { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinEventHub) + + public: + // Returns true if the singleton exists (or was created). Will return + // false if the singleton couldn't be created, in which case a call + // to Get() will return a nullptr. It is safe to call this function + // repeatedly. + static bool Ensure(); + static RefPtr<WinEventHub> Get() { return sInstance; } + + void AddObserver(WinEventObserver* aObserver); + void RemoveObserver(WinEventObserver* aObserver); + + HWND GetWnd() { return mHWnd; } + + private: + WinEventHub(); + ~WinEventHub(); + + static LRESULT CALLBACK WinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam, + LPARAM aLParam); + + bool Initialize(); + void ProcessWinEventProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); + + HWND mHWnd = nullptr; + nsTHashSet<nsRefPtrHashKey<WinEventObserver>> mObservers; + + static StaticRefPtr<WinEventHub> sInstance; +}; + +class DisplayStatusListener { + public: + virtual void OnDisplayStateChanged(bool aDisplayOn) = 0; +}; + +// Observe Display on/off event +class DisplayStatusObserver final : public WinEventObserver { + public: + static already_AddRefed<DisplayStatusObserver> Create( + DisplayStatusListener* aListener); + + private: + explicit DisplayStatusObserver(DisplayStatusListener* aListener); + virtual ~DisplayStatusObserver(); + void OnWinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam, + LPARAM aLParam) override; + + DisplayStatusListener* mListener; + + HPOWERNOTIFY mDisplayStatusHandle = nullptr; +}; + +class SessionChangeListener { + public: + virtual void OnSessionChange(WPARAM aStatusCode, + Maybe<bool> aIsCurrentSession) = 0; +}; + +// Observe session lock/unlock event +class SessionChangeObserver : public WinEventObserver { + public: + static already_AddRefed<SessionChangeObserver> Create( + SessionChangeListener* aListener); + + private: + explicit SessionChangeObserver(SessionChangeListener* aListener); + virtual ~SessionChangeObserver(); + + void Initialize(); + void OnWinEventProc(HWND aHwnd, UINT aMsg, WPARAM aWParam, + LPARAM aLParam) override; + + SessionChangeListener* mListener; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_windows_WinEventObserver_h diff --git a/widget/windows/WinHeaderOnlyUtils.h b/widget/windows/WinHeaderOnlyUtils.h new file mode 100644 index 0000000000..15861cc73b --- /dev/null +++ b/widget/windows/WinHeaderOnlyUtils.h @@ -0,0 +1,820 @@ +/* -*- 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_WinHeaderOnlyUtils_h +#define mozilla_WinHeaderOnlyUtils_h + +#include <windows.h> +#include <winerror.h> +#include <winnt.h> +#include <winternl.h> +#include <objbase.h> +#include <shlwapi.h> +#undef ParseURL +#include <stdlib.h> +#include <tuple> + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/DynamicallyLinkedFunctionPtr.h" +#include "mozilla/Maybe.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/UniquePtr.h" +#include "nsWindowsHelpers.h" + +#if defined(MOZILLA_INTERNAL_API) +# include "nsIFile.h" +# include "nsString.h" +#endif // defined(MOZILLA_INTERNAL_API) + +/** + * This header is intended for self-contained, header-only, utility code for + * Win32. It may be used outside of xul.dll, in places such as firefox.exe or + * mozglue.dll. If your code creates dependencies on Mozilla libraries, you + * should put it elsewhere. + */ + +#if !defined(STATUS_SUCCESS) +# define STATUS_SUCCESS ((NTSTATUS)0x00000000L) +#endif // !defined(STATUS_SUCCESS) + +// Our data indicates a few users of Win7 x86 hit failure to load urlmon.dll +// for unknown reasons. Since we don't always require urlmon.dll on Win7, +// we delay-load it, which causes a crash if loading urlmon.dll fails. This +// macro is to safely load and call urlmon's API graciously without crash. +#if defined(_X86_) +# define SAFECALL_URLMON_FUNC(FuncName, ...) \ + do { \ + static const mozilla::StaticDynamicallyLinkedFunctionPtr< \ + decltype(&::FuncName)> \ + func(L"urlmon.dll", #FuncName); \ + hr = \ + func ? func(__VA_ARGS__) : HRESULT_FROM_WIN32(ERROR_PROC_NOT_FOUND); \ + } while (0) +#else +# define SAFECALL_URLMON_FUNC(FuncName, ...) hr = ::FuncName(__VA_ARGS__) +#endif + +namespace mozilla { + +class WindowsError final { + private: + // HRESULT and NTSTATUS are both typedefs of LONG, so we cannot use + // overloading to properly differentiate between the two. Instead we'll use + // static functions to convert the various error types to HRESULTs before + // instantiating. + explicit constexpr WindowsError(HRESULT aHResult) : mHResult(aHResult) {} + + public: + using UniqueString = UniquePtr<WCHAR[], LocalFreeDeleter>; + + static constexpr WindowsError FromNtStatus(NTSTATUS aNtStatus) { + if (aNtStatus == STATUS_SUCCESS) { + // Special case: we don't want to set FACILITY_NT_BIT + // (HRESULT_FROM_NT does not handle this case, unlike HRESULT_FROM_WIN32) + return WindowsError(S_OK); + } + + return WindowsError(HRESULT_FROM_NT(aNtStatus)); + } + + static constexpr WindowsError FromHResult(HRESULT aHResult) { + return WindowsError(aHResult); + } + + static constexpr WindowsError FromWin32Error(DWORD aWin32Err) { + return WindowsError(HRESULT_FROM_WIN32(aWin32Err)); + } + + static WindowsError FromLastError() { + return FromWin32Error(::GetLastError()); + } + + static WindowsError CreateSuccess() { return WindowsError(S_OK); } + + static WindowsError CreateGeneric() { + return FromWin32Error(ERROR_UNIDENTIFIED_ERROR); + } + + bool IsSuccess() const { return SUCCEEDED(mHResult); } + + bool IsFailure() const { return FAILED(mHResult); } + + bool IsAvailableAsWin32Error() const { + return IsAvailableAsNtStatus() || + HRESULT_FACILITY(mHResult) == FACILITY_WIN32; + } + + bool IsAvailableAsNtStatus() const { + return mHResult == S_OK || (mHResult & FACILITY_NT_BIT); + } + + bool IsAvailableAsHResult() const { return true; } + + UniqueString AsString() const { + LPWSTR rawMsgBuf = nullptr; + constexpr DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS; + DWORD result = + ::FormatMessageW(flags, nullptr, mHResult, 0, + reinterpret_cast<LPWSTR>(&rawMsgBuf), 0, nullptr); + if (!result) { + return nullptr; + } + + return UniqueString(rawMsgBuf); + } + + HRESULT AsHResult() const { return mHResult; } + + // Not all HRESULTs are convertible to Win32 Errors, so we use Maybe + Maybe<DWORD> AsWin32Error() const { + if (mHResult == S_OK) { + return Some(static_cast<DWORD>(ERROR_SUCCESS)); + } + + if (HRESULT_FACILITY(mHResult) == FACILITY_WIN32) { + // This is the inverse of HRESULT_FROM_WIN32 + return Some(static_cast<DWORD>(HRESULT_CODE(mHResult))); + } + + // The NTSTATUS facility is a special case and thus does not utilize the + // HRESULT_FACILITY and HRESULT_CODE macros. + if (mHResult & FACILITY_NT_BIT) { + return Some(NtStatusToWin32Error( + static_cast<NTSTATUS>(mHResult & ~FACILITY_NT_BIT))); + } + + return Nothing(); + } + + // Not all HRESULTs are convertible to NTSTATUS, so we use Maybe + Maybe<NTSTATUS> AsNtStatus() const { + if (mHResult == S_OK) { + return Some(STATUS_SUCCESS); + } + + // The NTSTATUS facility is a special case and thus does not utilize the + // HRESULT_FACILITY and HRESULT_CODE macros. + if (mHResult & FACILITY_NT_BIT) { + return Some(static_cast<NTSTATUS>(mHResult & ~FACILITY_NT_BIT)); + } + + return Nothing(); + } + + constexpr bool operator==(const WindowsError& aOther) const { + return mHResult == aOther.mHResult; + } + + constexpr bool operator!=(const WindowsError& aOther) const { + return mHResult != aOther.mHResult; + } + + static DWORD NtStatusToWin32Error(NTSTATUS aNtStatus) { + static const StaticDynamicallyLinkedFunctionPtr< + decltype(&RtlNtStatusToDosError)> + pRtlNtStatusToDosError(L"ntdll.dll", "RtlNtStatusToDosError"); + + MOZ_ASSERT(!!pRtlNtStatusToDosError); + if (!pRtlNtStatusToDosError) { + return ERROR_UNIDENTIFIED_ERROR; + } + + return pRtlNtStatusToDosError(aNtStatus); + } + + private: + // We store the error code as an HRESULT because they can encode both Win32 + // error codes and NTSTATUS codes. + HRESULT mHResult; +}; + +namespace detail { +template <> +struct UnusedZero<WindowsError> { + using StorageType = WindowsError; + + static constexpr bool value = true; + static constexpr StorageType nullValue = WindowsError::FromHResult(S_OK); + + static constexpr void AssertValid(StorageType aValue) {} + static constexpr const WindowsError& Inspect(const StorageType& aValue) { + return aValue; + } + static constexpr WindowsError Unwrap(StorageType aValue) { return aValue; } + static constexpr StorageType Store(WindowsError aValue) { return aValue; } +}; +} // namespace detail + +enum DetourResultCode : uint32_t { + RESULT_OK = 0, + INTERCEPTOR_MOD_NULL, + INTERCEPTOR_MOD_INACCESSIBLE, + INTERCEPTOR_PROC_NULL, + INTERCEPTOR_PROC_INACCESSIBLE, + DETOUR_PATCHER_RESERVE_FOR_MODULE_PE_ERROR, + DETOUR_PATCHER_RESERVE_FOR_MODULE_TEXT_ERROR, + DETOUR_PATCHER_RESERVE_FOR_MODULE_RESERVE_ERROR, + DETOUR_PATCHER_DO_RESERVE_ERROR, + DETOUR_PATCHER_NEXT_TRAMPOLINE_ERROR, + DETOUR_PATCHER_INVALID_TRAMPOLINE, + DETOUR_PATCHER_WRITE_POINTER_ERROR, + DETOUR_PATCHER_CREATE_TRAMPOLINE_ERROR, + FUNCHOOKCROSSPROCESS_COPYSTUB_ERROR, + MMPOLICY_RESERVE_INVALIDARG, + MMPOLICY_RESERVE_ZERO_RESERVATIONSIZE, + MMPOLICY_RESERVE_CREATEFILEMAPPING, + MMPOLICY_RESERVE_MAPVIEWOFFILE, + MMPOLICY_RESERVE_NOBOUND_RESERVE_ERROR, + MMPOLICY_RESERVE_FINDREGION_INVALIDLEN, + MMPOLICY_RESERVE_FINDREGION_INVALIDRANGE, + MMPOLICY_RESERVE_FINDREGION_VIRTUALQUERY_ERROR, + MMPOLICY_RESERVE_FINDREGION_NO_FREE_REGION, + MMPOLICY_RESERVE_FINAL_RESERVE_ERROR, +}; + +#if defined(NIGHTLY_BUILD) +struct DetourError { + // We have a 16-bytes buffer, but only minimum bytes to detour per + // architecture are copied. See CreateTrampoline in PatcherDetour.h. + DetourResultCode mErrorCode; + uint8_t mOrigBytes[16]; + explicit DetourError(DetourResultCode aError) + : mErrorCode(aError), mOrigBytes{} {} + DetourError(DetourResultCode aError, DWORD aWin32Error) + : mErrorCode(aError), mOrigBytes{} { + static_assert(sizeof(mOrigBytes) >= sizeof(aWin32Error), + "Can't fit a DWORD in mOrigBytes"); + *reinterpret_cast<DWORD*>(mOrigBytes) = aWin32Error; + } + operator WindowsError() const { + return WindowsError::FromHResult(mErrorCode); + } +}; +#endif // defined(NIGHTLY_BUILD) + +template <typename T> +using WindowsErrorResult = Result<T, WindowsError>; + +struct LauncherError { + LauncherError(const char* aFile, int aLine, WindowsError aWin32Error) + : mFile(aFile), mLine(aLine), mError(aWin32Error) {} + +#if defined(NIGHTLY_BUILD) + LauncherError(const char* aFile, int aLine, + const Maybe<DetourError>& aDetourError) + : mFile(aFile), + mLine(aLine), + mError(aDetourError.isSome() ? aDetourError.value() + : WindowsError::CreateGeneric()), + mDetourError(aDetourError) {} +#endif // defined(NIGHTLY_BUILD) + + const char* mFile; + int mLine; + WindowsError mError; +#if defined(NIGHTLY_BUILD) + Maybe<DetourError> mDetourError; +#endif // defined(NIGHTLY_BUILD) + + bool operator==(const LauncherError& aOther) const { + return mError == aOther.mError; + } + + bool operator!=(const LauncherError& aOther) const { + return mError != aOther.mError; + } + + bool operator==(const WindowsError& aOther) const { return mError == aOther; } + + bool operator!=(const WindowsError& aOther) const { return mError != aOther; } +}; + +#if defined(MOZ_USE_LAUNCHER_ERROR) + +template <typename T> +using LauncherResult = Result<T, LauncherError>; + +template <typename T> +using LauncherResultWithLineInfo = LauncherResult<T>; + +using WindowsErrorType = LauncherError; + +#else + +template <typename T> +using LauncherResult = WindowsErrorResult<T>; + +template <typename T> +using LauncherResultWithLineInfo = Result<T, LauncherError>; + +using WindowsErrorType = WindowsError; + +#endif // defined(MOZ_USE_LAUNCHER_ERROR) + +using LauncherVoidResult = LauncherResult<Ok>; + +using LauncherVoidResultWithLineInfo = LauncherResultWithLineInfo<Ok>; + +#if defined(MOZ_USE_LAUNCHER_ERROR) + +# define LAUNCHER_ERROR_GENERIC() \ + ::mozilla::Err(::mozilla::LauncherError( \ + __FILE__, __LINE__, ::mozilla::WindowsError::CreateGeneric())) + +# if defined(NIGHTLY_BUILD) +# define LAUNCHER_ERROR_FROM_DETOUR_ERROR(err) \ + ::mozilla::Err(::mozilla::LauncherError(__FILE__, __LINE__, err)) +# else +# define LAUNCHER_ERROR_FROM_DETOUR_ERROR(err) LAUNCHER_ERROR_GENERIC() +# endif // defined(NIGHTLY_BUILD) + +# define LAUNCHER_ERROR_FROM_WIN32(err) \ + ::mozilla::Err(::mozilla::LauncherError( \ + __FILE__, __LINE__, ::mozilla::WindowsError::FromWin32Error(err))) + +# define LAUNCHER_ERROR_FROM_LAST() \ + ::mozilla::Err(::mozilla::LauncherError( \ + __FILE__, __LINE__, ::mozilla::WindowsError::FromLastError())) + +# define LAUNCHER_ERROR_FROM_NTSTATUS(ntstatus) \ + ::mozilla::Err(::mozilla::LauncherError( \ + __FILE__, __LINE__, ::mozilla::WindowsError::FromNtStatus(ntstatus))) + +# define LAUNCHER_ERROR_FROM_HRESULT(hresult) \ + ::mozilla::Err(::mozilla::LauncherError( \ + __FILE__, __LINE__, ::mozilla::WindowsError::FromHResult(hresult))) + +// This macro wraps the supplied WindowsError with a LauncherError +# define LAUNCHER_ERROR_FROM_MOZ_WINDOWS_ERROR(err) \ + ::mozilla::Err(::mozilla::LauncherError(__FILE__, __LINE__, err)) + +#else + +# define LAUNCHER_ERROR_GENERIC() \ + ::mozilla::Err(::mozilla::WindowsError::CreateGeneric()) + +# define LAUNCHER_ERROR_FROM_DETOUR_ERROR(err) LAUNCHER_ERROR_GENERIC() + +# define LAUNCHER_ERROR_FROM_WIN32(err) \ + ::mozilla::Err(::mozilla::WindowsError::FromWin32Error(err)) + +# define LAUNCHER_ERROR_FROM_LAST() \ + ::mozilla::Err(::mozilla::WindowsError::FromLastError()) + +# define LAUNCHER_ERROR_FROM_NTSTATUS(ntstatus) \ + ::mozilla::Err(::mozilla::WindowsError::FromNtStatus(ntstatus)) + +# define LAUNCHER_ERROR_FROM_HRESULT(hresult) \ + ::mozilla::Err(::mozilla::WindowsError::FromHResult(hresult)) + +# define LAUNCHER_ERROR_FROM_MOZ_WINDOWS_ERROR(err) ::mozilla::Err(err) + +#endif // defined(MOZ_USE_LAUNCHER_ERROR) + +// How long to wait for a created process to become available for input, +// to prevent that process's windows being forced to the background. +// This is used across update, restart, and the launcher. +const DWORD kWaitForInputIdleTimeoutMS = 10 * 1000; + +/** + * Wait for a child GUI process to become "idle." Idle means that the process + * has created its message queue and has begun waiting for user input. + * + * Note that this must only be used when the child process is going to display + * GUI! Otherwise you're going to be waiting for a very long time ;-) + * + * @return true if we successfully waited for input idle; + * false if we timed out or failed to wait. + */ +inline bool WaitForInputIdle(HANDLE aProcess, + DWORD aTimeoutMs = kWaitForInputIdleTimeoutMS) { + const DWORD kSleepTimeMs = 10; + const DWORD waitStart = aTimeoutMs == INFINITE ? 0 : ::GetTickCount(); + DWORD elapsed = 0; + + while (true) { + if (aTimeoutMs != INFINITE) { + elapsed = ::GetTickCount() - waitStart; + } + + if (elapsed >= aTimeoutMs) { + return false; + } + + // ::WaitForInputIdle() doesn't always set the last-error code on failure + ::SetLastError(ERROR_SUCCESS); + + DWORD waitResult = ::WaitForInputIdle(aProcess, aTimeoutMs - elapsed); + if (!waitResult) { + return true; + } + + if (waitResult == WAIT_FAILED && + ::GetLastError() == ERROR_NOT_GUI_PROCESS) { + ::Sleep(kSleepTimeMs); + continue; + } + + return false; + } +} + +enum class PathType { + eNtPath, + eDosPath, +}; + +class FileUniqueId final { + public: + explicit FileUniqueId(const wchar_t* aPath, PathType aPathType) + : mId(FILE_ID_INFO()) { + if (!aPath) { + mId = LAUNCHER_ERROR_FROM_HRESULT(E_INVALIDARG); + return; + } + + nsAutoHandle file; + + switch (aPathType) { + default: + mId = LAUNCHER_ERROR_FROM_HRESULT(E_INVALIDARG); + MOZ_ASSERT_UNREACHABLE("Unhandled PathType"); + return; + + case PathType::eNtPath: { + UNICODE_STRING unicodeString; + ::RtlInitUnicodeString(&unicodeString, aPath); + OBJECT_ATTRIBUTES objectAttributes; + InitializeObjectAttributes(&objectAttributes, &unicodeString, + OBJ_CASE_INSENSITIVE, nullptr, nullptr); + IO_STATUS_BLOCK ioStatus = {}; + HANDLE ntHandle; + NTSTATUS status = ::NtOpenFile( + &ntHandle, SYNCHRONIZE | FILE_READ_ATTRIBUTES, &objectAttributes, + &ioStatus, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_FOR_BACKUP_INTENT); + // We don't need to check |ntHandle| for INVALID_HANDLE_VALUE here, + // as that value is set by the Win32 layer. + if (!NT_SUCCESS(status)) { + mId = LAUNCHER_ERROR_FROM_NTSTATUS(status); + return; + } + + file.own(ntHandle); + break; + } + + case PathType::eDosPath: { + file.own(::CreateFileW( + aPath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr)); + if (file == INVALID_HANDLE_VALUE) { + mId = LAUNCHER_ERROR_FROM_LAST(); + return; + } + + break; + } + } + + GetId(file); + } + + explicit FileUniqueId(const nsAutoHandle& aFile) : mId(FILE_ID_INFO()) { + GetId(aFile); + } + + ~FileUniqueId() = default; + + bool IsError() const { return mId.isErr(); } + + const WindowsErrorType& GetError() const { return mId.inspectErr(); } + + FileUniqueId(FileUniqueId&& aOther) = default; + FileUniqueId& operator=(FileUniqueId&& aOther) = delete; + + bool operator==(const FileUniqueId& aOther) const { + return mId.isOk() && aOther.mId.isOk() && + !memcmp(&mId.inspect(), &aOther.mId.inspect(), sizeof(FILE_ID_INFO)); + } + + bool operator!=(const FileUniqueId& aOther) const { + return !((*this) == aOther); + } + + private: + void GetId(const nsAutoHandle& aFile) { + FILE_ID_INFO fileIdInfo = {}; + if (::GetFileInformationByHandleEx(aFile.get(), FileIdInfo, &fileIdInfo, + sizeof(fileIdInfo))) { + mId = fileIdInfo; + return; + } + // Only NTFS and ReFS support FileIdInfo. So we have to fallback if + // GetFileInformationByHandleEx failed. + + BY_HANDLE_FILE_INFORMATION info = {}; + if (!::GetFileInformationByHandle(aFile.get(), &info)) { + mId = LAUNCHER_ERROR_FROM_LAST(); + return; + } + + fileIdInfo.VolumeSerialNumber = info.dwVolumeSerialNumber; + memcpy(&fileIdInfo.FileId.Identifier[0], &info.nFileIndexLow, + sizeof(DWORD)); + memcpy(&fileIdInfo.FileId.Identifier[sizeof(DWORD)], &info.nFileIndexHigh, + sizeof(DWORD)); + mId = fileIdInfo; + } + + private: + LauncherResult<FILE_ID_INFO> mId; +}; + +class MOZ_RAII AutoVirtualProtect final { + public: + AutoVirtualProtect(void* aAddress, size_t aLength, DWORD aProtFlags, + HANDLE aTargetProcess = ::GetCurrentProcess()) + : mAddress(aAddress), + mLength(aLength), + mTargetProcess(aTargetProcess), + mPrevProt(0), + mError(WindowsError::CreateSuccess()) { + if (!::VirtualProtectEx(aTargetProcess, aAddress, aLength, aProtFlags, + &mPrevProt)) { + mError = WindowsError::FromLastError(); + } + } + + ~AutoVirtualProtect() { + if (mError.IsFailure()) { + return; + } + + ::VirtualProtectEx(mTargetProcess, mAddress, mLength, mPrevProt, + &mPrevProt); + } + + explicit operator bool() const { return mError.IsSuccess(); } + + WindowsError GetError() const { return mError; } + + DWORD PrevProt() const { return mPrevProt; } + + AutoVirtualProtect(const AutoVirtualProtect&) = delete; + AutoVirtualProtect(AutoVirtualProtect&&) = delete; + AutoVirtualProtect& operator=(const AutoVirtualProtect&) = delete; + AutoVirtualProtect& operator=(AutoVirtualProtect&&) = delete; + + private: + void* mAddress; + size_t mLength; + HANDLE mTargetProcess; + DWORD mPrevProt; + WindowsError mError; +}; + +inline UniquePtr<wchar_t[]> GetFullModulePath(HMODULE aModule) { + DWORD bufLen = MAX_PATH; + mozilla::UniquePtr<wchar_t[]> buf; + DWORD retLen; + + while (true) { + buf = mozilla::MakeUnique<wchar_t[]>(bufLen); + retLen = ::GetModuleFileNameW(aModule, buf.get(), bufLen); + if (!retLen) { + return nullptr; + } + + if (retLen == bufLen && ::GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + bufLen *= 2; + continue; + } + + break; + } + + // Upon success, retLen *excludes* the null character + ++retLen; + + // Since we're likely to have a bunch of unused space in buf, let's + // reallocate a string to the actual size of the file name. + auto result = mozilla::MakeUnique<wchar_t[]>(retLen); + if (wcscpy_s(result.get(), retLen, buf.get())) { + return nullptr; + } + + return result; +} + +inline UniquePtr<wchar_t[]> GetFullBinaryPath() { + return GetFullModulePath(nullptr); +} + +// Generates the install directory without a trailing path separator. +inline bool GetInstallDirectory(UniquePtr<wchar_t[]>& installPath) { + installPath = GetFullBinaryPath(); + // It's not safe to use PathRemoveFileSpecW with strings longer than MAX_PATH + // (including null terminator). + if (wcslen(installPath.get()) >= MAX_PATH) { + return false; + } + ::PathRemoveFileSpecW(installPath.get()); + return true; +} + +class ModuleVersion final { + public: + constexpr ModuleVersion() : mVersion(0ULL) {} + + explicit ModuleVersion(const VS_FIXEDFILEINFO& aFixedInfo) + : mVersion((static_cast<uint64_t>(aFixedInfo.dwFileVersionMS) << 32) | + static_cast<uint64_t>(aFixedInfo.dwFileVersionLS)) {} + + explicit ModuleVersion(const uint64_t aVersion) : mVersion(aVersion) {} + + ModuleVersion(const ModuleVersion& aOther) : mVersion(aOther.mVersion) {} + + uint64_t AsInteger() const { return mVersion; } + + operator uint64_t() const { return AsInteger(); } + + std::tuple<uint16_t, uint16_t, uint16_t, uint16_t> AsTuple() const { + uint16_t major = static_cast<uint16_t>((mVersion >> 48) & 0xFFFFU); + uint16_t minor = static_cast<uint16_t>((mVersion >> 32) & 0xFFFFU); + uint16_t patch = static_cast<uint16_t>((mVersion >> 16) & 0xFFFFU); + uint16_t build = static_cast<uint16_t>(mVersion & 0xFFFFU); + + return {major, minor, patch, build}; + } + + explicit operator bool() const { return !!mVersion; } + + bool operator<(const ModuleVersion& aOther) const { + return mVersion < aOther.mVersion; + } + + bool operator<(const uint64_t& aOther) const { return mVersion < aOther; } + + ModuleVersion& operator=(const uint64_t aIntVersion) { + mVersion = aIntVersion; + return *this; + } + + private: + uint64_t mVersion; +}; + +inline LauncherResult<ModuleVersion> GetModuleVersion( + const wchar_t* aModuleFullPath) { + DWORD verInfoLen = ::GetFileVersionInfoSizeW(aModuleFullPath, nullptr); + if (!verInfoLen) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + auto verInfoBuf = MakeUnique<BYTE[]>(verInfoLen); + if (!::GetFileVersionInfoW(aModuleFullPath, 0, verInfoLen, + verInfoBuf.get())) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + UINT fixedInfoLen; + VS_FIXEDFILEINFO* fixedInfo = nullptr; + if (!::VerQueryValueW(verInfoBuf.get(), L"\\", + reinterpret_cast<LPVOID*>(&fixedInfo), &fixedInfoLen)) { + // VerQueryValue may fail if the resource does not exist. This is not an + // error; we'll return 0 in this case. + return ModuleVersion(0ULL); + } + + return ModuleVersion(*fixedInfo); +} + +inline LauncherResult<ModuleVersion> GetModuleVersion(HMODULE aModule) { + UniquePtr<wchar_t[]> fullPath(GetFullModulePath(aModule)); + if (!fullPath) { + return LAUNCHER_ERROR_GENERIC(); + } + + return GetModuleVersion(fullPath.get()); +} + +#if defined(MOZILLA_INTERNAL_API) +inline LauncherResult<ModuleVersion> GetModuleVersion(nsIFile* aFile) { + if (!aFile) { + return LAUNCHER_ERROR_FROM_HRESULT(E_INVALIDARG); + } + + nsAutoString fullPath; + nsresult rv = aFile->GetPath(fullPath); + if (NS_FAILED(rv)) { + return LAUNCHER_ERROR_GENERIC(); + } + + return GetModuleVersion(fullPath.get()); +} +#endif // defined(MOZILLA_INTERNAL_API) + +struct CoTaskMemFreeDeleter { + void operator()(void* aPtr) { ::CoTaskMemFree(aPtr); } +}; + +inline LauncherResult<TOKEN_ELEVATION_TYPE> GetElevationType( + const nsAutoHandle& aToken) { + DWORD retLen; + TOKEN_ELEVATION_TYPE elevationType; + if (!::GetTokenInformation(aToken.get(), TokenElevationType, &elevationType, + sizeof(elevationType), &retLen)) { + return LAUNCHER_ERROR_FROM_LAST(); + } + + return elevationType; +} + +inline bool HasPackageIdentity() { + HMODULE kernel32Dll = ::GetModuleHandleW(L"kernel32"); + if (!kernel32Dll) { + return false; + } + + typedef LONG(WINAPI * GetCurrentPackageIdProc)(UINT32*, BYTE*); + GetCurrentPackageIdProc pGetCurrentPackageId = + (GetCurrentPackageIdProc)::GetProcAddress(kernel32Dll, + "GetCurrentPackageId"); + + // If there was any package identity to retrieve, we get + // ERROR_INSUFFICIENT_BUFFER. If there had been no package identity it + // would instead return APPMODEL_ERROR_NO_PACKAGE. + UINT32 packageNameSize = 0; + return pGetCurrentPackageId && + (pGetCurrentPackageId(&packageNameSize, nullptr) == + ERROR_INSUFFICIENT_BUFFER); +} + +inline UniquePtr<wchar_t[]> GetPackageFamilyName() { + HMODULE kernel32Dll = ::GetModuleHandleW(L"kernel32"); + if (!kernel32Dll) { + return nullptr; + } + + typedef LONG(WINAPI * GetCurrentPackageFamilyNameProc)(UINT32*, PWSTR); + GetCurrentPackageFamilyNameProc pGetCurrentPackageFamilyName = + (GetCurrentPackageFamilyNameProc)::GetProcAddress( + kernel32Dll, "GetCurrentPackageFamilyName"); + if (!pGetCurrentPackageFamilyName) { + return nullptr; + } + + UINT32 packageNameSize = 0; + if (pGetCurrentPackageFamilyName(&packageNameSize, nullptr) != + ERROR_INSUFFICIENT_BUFFER) { + return nullptr; + } + + UniquePtr<wchar_t[]> packageIdentity = MakeUnique<wchar_t[]>(packageNameSize); + if (pGetCurrentPackageFamilyName(&packageNameSize, packageIdentity.get()) != + ERROR_SUCCESS) { + return nullptr; + } + + return packageIdentity; +} + +// This implementation is equivalent to PathGetDriveNumber[AW]. +// We define our own version because using PathGetDriveNumber +// delay-loads shlwapi.dll, which may fail when the process is +// sandboxed. +template <typename T> +int MozPathGetDriveNumber(const T* aPath) { + const auto ToDriveNumber = [](const T* aPath) -> int { + if (*aPath == '\0' || *(aPath + 1) != ':') { + return -1; + } + + T c = *aPath; + return (c >= 'A' && c <= 'Z') ? c - 'A' + : (c >= 'a' && c <= 'z') ? c - 'a' + : -1; + }; + + if (!aPath) { + return -1; + } + + if (*aPath == '\\' && *(aPath + 1) == '\\' && *(aPath + 2) == '?' && + *(aPath + 3) == '\\') { + return ToDriveNumber(aPath + 4); + } + + return ToDriveNumber(aPath); +} + +} // namespace mozilla + +#endif // mozilla_WinHeaderOnlyUtils_h diff --git a/widget/windows/WinIMEHandler.cpp b/widget/windows/WinIMEHandler.cpp new file mode 100644 index 0000000000..961c61fae0 --- /dev/null +++ b/widget/windows/WinIMEHandler.cpp @@ -0,0 +1,1081 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WinIMEHandler.h" + +#include "IMMHandler.h" +#include "KeyboardLayout.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_intl.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/TextEvents.h" +#include "mozilla/Unused.h" +#include "mozilla/WindowsVersion.h" +#include "nsWindowDefs.h" +#include "WinTextEventDispatcherListener.h" + +#include "TSFTextStore.h" + +#include "OSKInputPaneManager.h" +#include "OSKTabTipManager.h" +#include "OSKVRManager.h" +#include "nsLookAndFeel.h" +#include "nsWindow.h" +#include "WinUtils.h" +#include "nsIWindowsRegKey.h" +#include "WindowsUIUtils.h" + +#ifdef ACCESSIBILITY +# include "nsAccessibilityService.h" +#endif // #ifdef ACCESSIBILITY + +#include "shellapi.h" +#include "shlobj.h" +#include "powrprof.h" +#include "setupapi.h" +#include "cfgmgr32.h" + +#include "FxRWindowManager.h" +#include "moz_external_vr.h" + +const char* kOskEnabled = "ui.osk.enabled"; +const char* kOskDetectPhysicalKeyboard = "ui.osk.detect_physical_keyboard"; +const char* kOskDebugReason = "ui.osk.debug.keyboardDisplayReason"; + +namespace mozilla { +namespace widget { + +/****************************************************************************** + * IMEHandler + ******************************************************************************/ + +nsWindow* IMEHandler::sFocusedWindow = nullptr; +InputContextAction::Cause IMEHandler::sLastContextActionCause = + InputContextAction::CAUSE_UNKNOWN; +bool IMEHandler::sMaybeEditable = false; +bool IMEHandler::sForceDisableCurrentIMM_IME = false; +bool IMEHandler::sNativeCaretIsCreated = false; +bool IMEHandler::sHasNativeCaretBeenRequested = false; + +bool IMEHandler::sIsInTSFMode = false; +bool IMEHandler::sIsIMMEnabled = true; +decltype(SetInputScopes)* IMEHandler::sSetInputScopes = nullptr; + +static POWER_PLATFORM_ROLE sPowerPlatformRole = PlatformRoleUnspecified; +static bool sDeterminedPowerPlatformRole = false; + +// static +void IMEHandler::Initialize() { + TSFTextStore::Initialize(); + sIsInTSFMode = TSFTextStore::IsInTSFMode(); + sIsIMMEnabled = + !sIsInTSFMode || StaticPrefs::intl_tsf_support_imm_AtStartup(); + if (!sIsInTSFMode) { + // When full TSFTextStore is not available, try to use SetInputScopes API + // to enable at least InputScope. Use GET_MODULE_HANDLE_EX_FLAG_PIN to + // ensure that msctf.dll will not be unloaded. + HMODULE module = nullptr; + if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN, L"msctf.dll", + &module)) { + sSetInputScopes = reinterpret_cast<decltype(SetInputScopes)*>( + GetProcAddress(module, "SetInputScopes")); + } + } + + IMMHandler::Initialize(); + + sForceDisableCurrentIMM_IME = IMMHandler::IsActiveIMEInBlockList(); +} + +// static +void IMEHandler::Terminate() { + if (sIsInTSFMode) { + TSFTextStore::Terminate(); + sIsInTSFMode = false; + } + + IMMHandler::Terminate(); + WinTextEventDispatcherListener::Shutdown(); +} + +// static +void* IMEHandler::GetNativeData(nsWindow* aWindow, uint32_t aDataType) { + if (aDataType == NS_RAW_NATIVE_IME_CONTEXT) { + if (IsTSFAvailable()) { + return TSFTextStore::GetThreadManager(); + } + IMEContext context(aWindow); + if (context.IsValid()) { + return context.get(); + } + // If IMC isn't associated with the window, IME is disabled on the window + // now. In such case, we should return default IMC instead. + const IMEContext& defaultIMC = aWindow->DefaultIMC(); + if (defaultIMC.IsValid()) { + return defaultIMC.get(); + } + // If there is no default IMC, we should return the pointer to the window + // since if we return nullptr, IMEStateManager cannot manage composition + // with TextComposition instance. This is possible if no IME is installed, + // but composition may occur with dead key sequence. + return aWindow; + } + + void* result = TSFTextStore::GetNativeData(aDataType); + if (!result || !(*(static_cast<void**>(result)))) { + return nullptr; + } + // XXX During the TSF module test, sIsInTSFMode must be true. After that, + // the value should be restored but currently, there is no way for that. + // When the TSF test is enabled again, we need to fix this. Perhaps, + // sending a message can fix this. + sIsInTSFMode = true; + return result; +} + +// static +bool IMEHandler::ProcessRawKeyMessage(const MSG& aMsg) { + if (StaticPrefs::ui_key_layout_load_when_first_needed()) { + // Getting instance creates the singleton instance and that will + // automatically load active keyboard layout data. We should do that + // before TSF or TranslateMessage handles a key message. + Unused << KeyboardLayout::GetInstance(); + } + if (IsTSFAvailable()) { + return TSFTextStore::ProcessRawKeyMessage(aMsg); + } + return false; // noting to do in IMM mode. +} + +// static +bool IMEHandler::ProcessMessage(nsWindow* aWindow, UINT aMessage, + WPARAM& aWParam, LPARAM& aLParam, + MSGResult& aResult) { + // If we're putting native caret over our caret, Windows dispatches + // EVENT_OBJECT_LOCATIONCHANGE event on other applications which hook + // the event with ::SetWinEventHook() and handles WM_GETOBJECT for + // OBJID_CARET (this is request of caret from such applications) instead + // of us. If a11y module is active, it observes every our caret change + // and put native caret over it automatically. However, if other + // applications require only caret information, activating a11y module is + // overwork and such applications may requires carets only in editors. + // Therefore, if it'd be possible, IMEHandler should put native caret over + // our caret, but there is a problem. Some versions of ATOK (Japanese TIP) + // refer native caret and if there is, the behavior is worse than the + // behavior without native caret. Therefore, we shouldn't put native caret + // as far as possible. + if (!sHasNativeCaretBeenRequested && aMessage == WM_GETOBJECT && + static_cast<LONG>(aLParam) == OBJID_CARET) { + // So, when we receive first WM_GETOBJECT for OBJID_CARET, let's start to + // create native caret for such applications. + sHasNativeCaretBeenRequested = true; + // If an editable element has focus, we can put native caret now. + // XXX Should we avoid doing this if there is composition? + MaybeCreateNativeCaret(aWindow); + } + + if (IsTSFAvailable()) { + TSFTextStore::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult); + if (aResult.mConsumed) { + return true; + } + // If we don't support IMM in TSF mode, we don't use IMMHandler. + if (!sIsIMMEnabled) { + return false; + } + // IME isn't implemented with IMM, IMMHandler shouldn't handle any + // messages. + if (!IsIMMActive()) { + return false; + } + } + + bool keepGoing = + IMMHandler::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult); + + // If user changes active IME to an IME which is listed in our block list, + // we should disassociate IMC from the window for preventing the IME to work + // and crash. + if (aMessage == WM_INPUTLANGCHANGE) { + bool disableIME = IMMHandler::IsActiveIMEInBlockList(); + if (disableIME != sForceDisableCurrentIMM_IME) { + bool enable = + !disableIME && WinUtils::IsIMEEnabled(aWindow->InputContextRef()); + AssociateIMEContext(aWindow, enable); + sForceDisableCurrentIMM_IME = disableIME; + } + } + + return keepGoing; +} + +// static +bool IMEHandler::IsA11yHandlingNativeCaret() { +#ifndef ACCESSIBILITY + return false; +#else // #ifndef ACCESSIBILITY + // Let's assume that when there is the service, it handles native caret. + return GetAccService() != nullptr; +#endif // #ifndef ACCESSIBILITY #else +} + +// static +bool IMEHandler::IsIMMActive() { return TSFTextStore::IsIMM_IMEActive(); } + +// static +bool IMEHandler::IsComposing() { + if (IsTSFAvailable()) { + return TSFTextStore::IsComposing() || IMMHandler::IsComposing(); + } + + return IMMHandler::IsComposing(); +} + +// static +bool IMEHandler::IsComposingOn(nsWindow* aWindow) { + if (IsTSFAvailable()) { + return TSFTextStore::IsComposingOn(aWindow) || + IMMHandler::IsComposingOn(aWindow); + } + + return IMMHandler::IsComposingOn(aWindow); +} + +// static +nsresult IMEHandler::NotifyIME(nsWindow* aWindow, + const IMENotification& aIMENotification) { + if (IsTSFAvailable()) { + switch (aIMENotification.mMessage) { + case NOTIFY_IME_OF_SELECTION_CHANGE: { + nsresult rv = TSFTextStore::OnSelectionChange(aIMENotification); + // If IMM IME is active, we need to notify IMMHandler of updating + // composition change. It will adjust candidate window position or + // composition window position. + bool isIMMActive = IsIMMActive(); + if (isIMMActive) { + IMMHandler::OnUpdateComposition(aWindow); + } + IMMHandler::OnSelectionChange(aWindow, aIMENotification, isIMMActive); + return rv; + } + case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: + // If IMM IME is active, we need to notify IMMHandler of updating + // composition change. It will adjust candidate window position or + // composition window position. + if (IsIMMActive()) { + IMMHandler::OnUpdateComposition(aWindow); + } else { + TSFTextStore::OnUpdateComposition(); + } + return NS_OK; + case NOTIFY_IME_OF_TEXT_CHANGE: + return TSFTextStore::OnTextChange(aIMENotification); + case NOTIFY_IME_OF_FOCUS: { + sFocusedWindow = aWindow; + IMMHandler::OnFocusChange(true, aWindow); + nsresult rv = TSFTextStore::OnFocusChange(true, aWindow, + aWindow->GetInputContext()); + MaybeCreateNativeCaret(aWindow); + IMEHandler::MaybeShowOnScreenKeyboard(aWindow, + aWindow->GetInputContext()); + return rv; + } + case NOTIFY_IME_OF_BLUR: + sFocusedWindow = nullptr; + IMEHandler::MaybeDismissOnScreenKeyboard(aWindow); + IMMHandler::OnFocusChange(false, aWindow); + return TSFTextStore::OnFocusChange(false, aWindow, + aWindow->GetInputContext()); + case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT: + // If IMM IME is active, we should send a mouse button event via IMM. + if (IsIMMActive()) { + return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification); + } + return TSFTextStore::OnMouseButtonEvent(aIMENotification); + case REQUEST_TO_COMMIT_COMPOSITION: + if (TSFTextStore::IsComposingOn(aWindow)) { + TSFTextStore::CommitComposition(false); + } else if (IsIMMActive()) { + IMMHandler::CommitComposition(aWindow); + } + return NS_OK; + case REQUEST_TO_CANCEL_COMPOSITION: + if (TSFTextStore::IsComposingOn(aWindow)) { + TSFTextStore::CommitComposition(true); + } else if (IsIMMActive()) { + IMMHandler::CancelComposition(aWindow); + } + return NS_OK; + case NOTIFY_IME_OF_POSITION_CHANGE: + return TSFTextStore::OnLayoutChange(); + default: + return NS_ERROR_NOT_IMPLEMENTED; + } + } + + switch (aIMENotification.mMessage) { + case REQUEST_TO_COMMIT_COMPOSITION: + IMMHandler::CommitComposition(aWindow); + return NS_OK; + case REQUEST_TO_CANCEL_COMPOSITION: + IMMHandler::CancelComposition(aWindow); + return NS_OK; + case NOTIFY_IME_OF_POSITION_CHANGE: + case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: + IMMHandler::OnUpdateComposition(aWindow); + return NS_OK; + case NOTIFY_IME_OF_SELECTION_CHANGE: + IMMHandler::OnSelectionChange(aWindow, aIMENotification, true); + // IMMHandler::OnSelectionChange() cannot work without its singleton + // instance. Therefore, IMEHandler needs to create native caret instead + // if it's necessary. + MaybeCreateNativeCaret(aWindow); + return NS_OK; + case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT: + return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification); + case NOTIFY_IME_OF_FOCUS: + sFocusedWindow = aWindow; + IMMHandler::OnFocusChange(true, aWindow); + IMEHandler::MaybeShowOnScreenKeyboard(aWindow, + aWindow->GetInputContext()); + MaybeCreateNativeCaret(aWindow); + return NS_OK; + case NOTIFY_IME_OF_BLUR: + sFocusedWindow = nullptr; + IMEHandler::MaybeDismissOnScreenKeyboard(aWindow); + IMMHandler::OnFocusChange(false, aWindow); + // If a plugin gets focus while TSF has focus, we need to notify TSF of + // the blur. + if (TSFTextStore::ThinksHavingFocus()) { + return TSFTextStore::OnFocusChange(false, aWindow, + aWindow->GetInputContext()); + } + return NS_OK; + default: + return NS_ERROR_NOT_IMPLEMENTED; + } +} + +// static +IMENotificationRequests IMEHandler::GetIMENotificationRequests() { + if (IsTSFAvailable()) { + if (!sIsIMMEnabled) { + return TSFTextStore::GetIMENotificationRequests(); + } + // Even if TSF is available, the active IME may be an IMM-IME. + // Unfortunately, changing the result of GetIMENotificationRequests() while + // an editor has focus isn't supported by IMEContentObserver nor + // ContentCacheInParent. Therefore, we need to request whole notifications + // which are necessary either IMMHandler or TSFTextStore. + return IMMHandler::GetIMENotificationRequests() | + TSFTextStore::GetIMENotificationRequests(); + } + + return IMMHandler::GetIMENotificationRequests(); +} + +// static +TextEventDispatcherListener* +IMEHandler::GetNativeTextEventDispatcherListener() { + return WinTextEventDispatcherListener::GetInstance(); +} + +// static +bool IMEHandler::GetOpenState(nsWindow* aWindow) { + if (IsTSFAvailable() && !IsIMMActive()) { + return TSFTextStore::GetIMEOpenState(); + } + + IMEContext context(aWindow); + return context.GetOpenState(); +} + +// static +void IMEHandler::OnDestroyWindow(nsWindow* aWindow) { + // When focus is in remote process, but the window is being destroyed, we + // need to clean up TSFTextStore here since NOTIFY_IME_OF_BLUR won't reach + // here because BrowserParent already lost the reference to the nsWindow when + // it receives from the remote process. + if (sFocusedWindow == aWindow) { + MOZ_ASSERT(aWindow->GetInputContext().IsOriginContentProcess(), + "input context of focused widget should've been set by a remote " + "process " + "if IME focus isn't cleared before destroying the widget"); + NotifyIME(aWindow, IMENotification(NOTIFY_IME_OF_BLUR)); + } + + // We need to do nothing here for TSF. Just restore the default context + // if it's been disassociated. + if (!sIsInTSFMode) { + // MSDN says we need to set IS_DEFAULT to avoid memory leak when we use + // SetInputScopes API. Use an empty string to do this. + SetInputScopeForIMM32(aWindow, u""_ns, u""_ns, false); + } + AssociateIMEContext(aWindow, true); +} + +// static +bool IMEHandler::NeedsToAssociateIMC() { return !sForceDisableCurrentIMM_IME; } + +// static +void IMEHandler::SetInputContext(nsWindow* aWindow, InputContext& aInputContext, + const InputContextAction& aAction) { + sLastContextActionCause = aAction.mCause; + // FYI: If there is no composition, this call will do nothing. + NotifyIME(aWindow, IMENotification(REQUEST_TO_COMMIT_COMPOSITION)); + + if (aInputContext.mHTMLInputMode.EqualsLiteral("none")) { + IMEHandler::MaybeDismissOnScreenKeyboard(aWindow, Sync::Yes); + } else if (aAction.UserMightRequestOpenVKB()) { + IMEHandler::MaybeShowOnScreenKeyboard(aWindow, aInputContext); + } + + bool enable = WinUtils::IsIMEEnabled(aInputContext); + bool adjustOpenState = (enable && aInputContext.mIMEState.mOpen != + IMEState::DONT_CHANGE_OPEN_STATE); + bool open = + (adjustOpenState && aInputContext.mIMEState.mOpen == IMEState::OPEN); + + // Note that even while a plugin has focus, we need to notify TSF of that. + if (sIsInTSFMode) { + TSFTextStore::SetInputContext(aWindow, aInputContext, aAction); + if (IsTSFAvailable()) { + if (sIsIMMEnabled) { + // Associate IMC with aWindow only when it's necessary. + AssociateIMEContext(aWindow, enable && NeedsToAssociateIMC()); + } + if (adjustOpenState) { + TSFTextStore::SetIMEOpenState(open); + } + return; + } + } else { + // Set at least InputScope even when TextStore is not available. + SetInputScopeForIMM32(aWindow, aInputContext.mHTMLInputType, + aInputContext.mHTMLInputMode, + aInputContext.mInPrivateBrowsing); + } + + AssociateIMEContext(aWindow, enable); + + IMEContext context(aWindow); + if (adjustOpenState) { + context.SetOpenState(open); + } +} + +// static +void IMEHandler::AssociateIMEContext(nsWindow* aWindowBase, bool aEnable) { + IMEContext context(aWindowBase); + if (aEnable) { + context.AssociateDefaultContext(); + return; + } + // Don't disassociate the context after the window is destroyed. + if (aWindowBase->Destroyed()) { + return; + } + context.Disassociate(); +} + +// static +void IMEHandler::InitInputContext(nsWindow* aWindow, + InputContext& aInputContext) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->GetWindowHandle(), + "IMEHandler::SetInputContext() requires non-nullptr HWND"); + + static bool sInitialized = false; + if (!sInitialized) { + sInitialized = true; + // Some TIPs like QQ Input (Simplified Chinese) may need normal window + // (i.e., windows except message window) when initializing themselves. + // Therefore, we need to initialize TSF/IMM modules after first normal + // window is created. InitInputContext() should be called immediately + // after creating each normal window, so, here is a good place to + // initialize these modules. + Initialize(); + } + + // For a11y, the default enabled state should be 'enabled'. + aInputContext.mIMEState.mEnabled = IMEEnabled::Enabled; + + if (sIsInTSFMode) { + TSFTextStore::SetInputContext( + aWindow, aInputContext, + InputContextAction(InputContextAction::CAUSE_UNKNOWN, + InputContextAction::WIDGET_CREATED)); + // IME context isn't necessary in pure TSF mode. + if (!sIsIMMEnabled) { + AssociateIMEContext(aWindow, false); + } + return; + } + +#ifdef DEBUG + // NOTE: IMC may be null if IMM module isn't installed. + IMEContext context(aWindow); + MOZ_ASSERT(context.IsValid() || !CurrentKeyboardLayoutHasIME()); +#endif // #ifdef DEBUG +} + +#ifdef DEBUG +// static +bool IMEHandler::CurrentKeyboardLayoutHasIME() { + if (sIsInTSFMode) { + return TSFTextStore::CurrentKeyboardLayoutHasIME(); + } + + return IMMHandler::IsIMEAvailable(); +} +#endif // #ifdef DEBUG + +// static +void IMEHandler::OnKeyboardLayoutChanged() { + // Be aware, this method won't be called until TSFStaticSink starts to + // observe active TIP change. If you need to be notified of this, you + // need to create TSFStaticSink::Observe() or something and call it + // TSFStaticSink::EnsureInitActiveTIPKeyboard() forcibly. + + if (!sIsIMMEnabled || !IsTSFAvailable()) { + return; + } +} + +// static +void IMEHandler::SetInputScopeForIMM32(nsWindow* aWindow, + const nsAString& aHTMLInputType, + const nsAString& aHTMLInputMode, + bool aInPrivateBrowsing) { + if (sIsInTSFMode || !sSetInputScopes || aWindow->Destroyed()) { + return; + } + AutoTArray<InputScope, 3> scopes; + + // IME may refer only first input scope, but we will append inputmode's + // input scopes since IME may refer it like Chrome. + AppendInputScopeFromType(aHTMLInputType, scopes); + AppendInputScopeFromInputMode(aHTMLInputMode, scopes); + + if (aInPrivateBrowsing) { + scopes.AppendElement(IS_PRIVATE); + } + + if (scopes.IsEmpty()) { + // At least, 1 item is necessary. + scopes.AppendElement(IS_DEFAULT); + } + + sSetInputScopes(aWindow->GetWindowHandle(), scopes.Elements(), + scopes.Length(), nullptr, 0, nullptr, nullptr); +} + +// static +void IMEHandler::AppendInputScopeFromInputMode(const nsAString& aHTMLInputMode, + nsTArray<InputScope>& aScopes) { + if (aHTMLInputMode.EqualsLiteral("mozAwesomebar")) { + // Even if Awesomebar has focus, user may not input URL directly. + // However, on-screen keyboard for URL should be shown because it has + // some useful additional keys like ".com" and they are not hindrances + // even when inputting non-URL text, e.g., words to search something in + // the web. On the other hand, a lot of Microsoft's IMEs and Google + // Japanese Input make their open state "closed" automatically if we + // notify them of URL as the input scope. However, this is very annoying + // for the users when they try to input some words to search the web or + // bookmark/history items. Therefore, if they are active, we need to + // notify them of the default input scope for avoiding this issue. + // FYI: We cannot check active TIP without TSF. Therefore, if it's + // not in TSF mode, this will check only if active IMM-IME is Google + // Japanese Input. Google Japanese Input is a TIP of TSF basically. + // However, if the OS is Win7 or it's installed on Win7 but has not + // been updated yet even after the OS is upgraded to Win8 or later, + // it's installed as IMM-IME. + if (TSFTextStore::ShouldSetInputScopeOfURLBarToDefault()) { + return; + } + // Don't append IS_SEARCH here for showing on-screen keyboard for URL. + if (!aScopes.Contains(IS_URL)) { + aScopes.AppendElement(IS_URL); + } + return; + } + + // https://html.spec.whatwg.org/dev/interaction.html#attr-inputmode + if (aHTMLInputMode.EqualsLiteral("url")) { + if (!aScopes.Contains(IS_SEARCH)) { + aScopes.AppendElement(IS_URL); + } + return; + } + if (aHTMLInputMode.EqualsLiteral("email")) { + if (!aScopes.Contains(IS_EMAIL_SMTPEMAILADDRESS)) { + aScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS); + } + return; + } + if (aHTMLInputMode.EqualsLiteral("tel")) { + if (!aScopes.Contains(IS_TELEPHONE_FULLTELEPHONENUMBER)) { + aScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER); + } + if (!aScopes.Contains(IS_TELEPHONE_LOCALNUMBER)) { + aScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER); + } + return; + } + if (aHTMLInputMode.EqualsLiteral("numeric")) { + if (!aScopes.Contains(IS_DIGITS)) { + aScopes.AppendElement(IS_DIGITS); + } + return; + } + if (aHTMLInputMode.EqualsLiteral("decimal")) { + if (!aScopes.Contains(IS_NUMBER)) { + aScopes.AppendElement(IS_NUMBER); + } + return; + } + if (aHTMLInputMode.EqualsLiteral("search")) { + if (NeedsSearchInputScope() && !aScopes.Contains(IS_SEARCH)) { + aScopes.AppendElement(IS_SEARCH); + } + return; + } +} + +// static +void IMEHandler::AppendInputScopeFromType(const nsAString& aHTMLInputType, + nsTArray<InputScope>& aScopes) { + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html + if (aHTMLInputType.EqualsLiteral("url")) { + aScopes.AppendElement(IS_URL); + return; + } + if (aHTMLInputType.EqualsLiteral("search")) { + if (NeedsSearchInputScope()) { + aScopes.AppendElement(IS_SEARCH); + } + return; + } + if (aHTMLInputType.EqualsLiteral("email")) { + aScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS); + return; + } + if (aHTMLInputType.EqualsLiteral("password")) { + aScopes.AppendElement(IS_PASSWORD); + return; + } + if (aHTMLInputType.EqualsLiteral("datetime") || + aHTMLInputType.EqualsLiteral("datetime-local")) { + aScopes.AppendElement(IS_DATE_FULLDATE); + aScopes.AppendElement(IS_TIME_FULLTIME); + return; + } + if (aHTMLInputType.EqualsLiteral("date") || + aHTMLInputType.EqualsLiteral("month") || + aHTMLInputType.EqualsLiteral("week")) { + aScopes.AppendElement(IS_DATE_FULLDATE); + return; + } + if (aHTMLInputType.EqualsLiteral("time")) { + aScopes.AppendElement(IS_TIME_FULLTIME); + return; + } + if (aHTMLInputType.EqualsLiteral("tel")) { + aScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER); + aScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER); + return; + } + if (aHTMLInputType.EqualsLiteral("number")) { + aScopes.AppendElement(IS_NUMBER); + return; + } +} + +// static +bool IMEHandler::NeedsSearchInputScope() { + return !StaticPrefs::intl_tsf_hack_atok_search_input_scope_disabled() || + !TSFTextStore::IsATOKActive(); +} + +// static +bool IMEHandler::IsOnScreenKeyboardSupported() { +#ifdef NIGHTLY_BUILD + if (FxRWindowManager::GetInstance()->IsFxRWindow(sFocusedWindow)) { + return true; + } +#endif // NIGHTLY_BUILD + if (!Preferences::GetBool(kOskEnabled, true) || + !IMEHandler::NeedOnScreenKeyboard()) { + return false; + } + + // On Windows 11, we ignore tablet mode (see bug 1722208) + if (!IsWin11OrLater()) { + // On Windows 10 we require tablet mode, unless the user has set the + // relevant setting to enable the on-screen keyboard in desktop mode. + if (!IsInTabletMode() && !AutoInvokeOnScreenKeyboardInDesktopMode()) { + return false; + } + } + + return true; +} + +// static +void IMEHandler::MaybeShowOnScreenKeyboard(nsWindow* aWindow, + const InputContext& aInputContext) { + if (aInputContext.mHTMLInputMode.EqualsLiteral("none")) { + return; + } + + if (!IsOnScreenKeyboardSupported()) { + return; + } + + IMEHandler::ShowOnScreenKeyboard(aWindow); +} + +// static +void IMEHandler::MaybeDismissOnScreenKeyboard(nsWindow* aWindow, Sync aSync) { +#ifdef NIGHTLY_BUILD + if (FxRWindowManager::GetInstance()->IsFxRWindow(aWindow)) { + OSKVRManager::DismissOnScreenKeyboard(); + } +#endif // NIGHTLY_BUILD + if (aSync == Sync::Yes) { + DismissOnScreenKeyboard(aWindow); + return; + } + + RefPtr<nsWindow> window(aWindow); + NS_DispatchToCurrentThreadQueue( + NS_NewRunnableFunction("IMEHandler::MaybeDismissOnScreenKeyboard", + [window]() { + if (window->Destroyed()) { + return; + } + if (!sFocusedWindow) { + DismissOnScreenKeyboard(window); + } + }), + EventQueuePriority::Idle); +} + +// static +bool IMEHandler::WStringStartsWithCaseInsensitive(const std::wstring& aHaystack, + const std::wstring& aNeedle) { + std::wstring lowerCaseHaystack(aHaystack); + std::wstring lowerCaseNeedle(aNeedle); + std::transform(lowerCaseHaystack.begin(), lowerCaseHaystack.end(), + lowerCaseHaystack.begin(), ::tolower); + std::transform(lowerCaseNeedle.begin(), lowerCaseNeedle.end(), + lowerCaseNeedle.begin(), ::tolower); + return wcsstr(lowerCaseHaystack.c_str(), lowerCaseNeedle.c_str()) == + lowerCaseHaystack.c_str(); +} + +// Returns false if a physical keyboard is detected on Windows 8 and up, +// or there is some other reason why an onscreen keyboard is not necessary. +// Returns true if no keyboard is found and this device looks like it needs +// an on-screen keyboard for text input. +// static +bool IMEHandler::NeedOnScreenKeyboard() { + if (!Preferences::GetBool(kOskDetectPhysicalKeyboard, true)) { + Preferences::SetString(kOskDebugReason, L"IKPOS: Detection disabled."); + return true; + } + + // If the last focus cause was not user-initiated (ie a result of code + // setting focus to an element) then don't auto-show a keyboard. This + // avoids cases where the keyboard would pop up "just" because e.g. a + // web page chooses to focus a search field on the page, even when that + // really isn't what the user is trying to do at that moment. + if (!InputContextAction::IsHandlingUserInput(sLastContextActionCause)) { + return false; + } + + // This function should be only invoked for machines with touch screens. + if ((::GetSystemMetrics(SM_DIGITIZER) & NID_INTEGRATED_TOUCH) != + NID_INTEGRATED_TOUCH) { + Preferences::SetString(kOskDebugReason, L"IKPOS: Touch screen not found."); + return false; + } + + // If the device is docked, the user is treating the device as a PC. + if (::GetSystemMetrics(SM_SYSTEMDOCKED) != 0) { + Preferences::SetString(kOskDebugReason, L"IKPOS: System docked."); + return false; + } + + // To determine whether a keyboard is present on the device, we do the + // following:- + // 1. If the platform role is that of a mobile or slate device, check the + // system metric SM_CONVERTIBLESLATEMODE to see if it is being used + // in slate mode. If it is, also check that the last input was a touch. + // If all of this is true, then we should show the on-screen keyboard. + + // 2. If step 1 didn't determine we should show the keyboard, we check if + // this device has keyboards attached to it. + + // Check if the device is being used as a laptop or a tablet. This can be + // checked by first checking the role of the device and then the + // corresponding system metric (SM_CONVERTIBLESLATEMODE). If it is being + // used as a tablet then we want the OSK to show up. + if (!sDeterminedPowerPlatformRole) { + sDeterminedPowerPlatformRole = true; + sPowerPlatformRole = WinUtils::GetPowerPlatformRole(); + } + + // If this a mobile or slate (tablet) device, check if it is in slate mode. + // If the last input was touch, ignore whether or not a keyboard is present. + if ((sPowerPlatformRole == PlatformRoleMobile || + sPowerPlatformRole == PlatformRoleSlate) && + ::GetSystemMetrics(SM_CONVERTIBLESLATEMODE) == 0 && + sLastContextActionCause == InputContextAction::CAUSE_TOUCH) { + Preferences::SetString( + kOskDebugReason, + L"IKPOS: Mobile/Slate Platform role, in slate mode with touch event."); + return true; + } + + return !IMEHandler::IsKeyboardPresentOnSlate(); +} + +// Uses the Setup APIs to enumerate the attached keyboards and returns true +// if the keyboard count is 1 or more. While this will work in most cases +// it won't work if there are devices which expose keyboard interfaces which +// are attached to the machine. +// Based on IsKeyboardPresentOnSlate() in Chromium's base/win/win_util.cc. +// static +bool IMEHandler::IsKeyboardPresentOnSlate() { + const GUID KEYBOARD_CLASS_GUID = { + 0x4D36E96B, + 0xE325, + 0x11CE, + {0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18}}; + + // Query for all the keyboard devices. + HDEVINFO device_info = ::SetupDiGetClassDevs(&KEYBOARD_CLASS_GUID, nullptr, + nullptr, DIGCF_PRESENT); + if (device_info == INVALID_HANDLE_VALUE) { + Preferences::SetString(kOskDebugReason, L"IKPOS: No keyboard info."); + return false; + } + + // Enumerate all keyboards and look for ACPI\PNP and HID\VID devices. If + // the count is more than 1 we assume that a keyboard is present. This is + // under the assumption that there will always be one keyboard device. + for (DWORD i = 0;; ++i) { + SP_DEVINFO_DATA device_info_data = {0}; + device_info_data.cbSize = sizeof(device_info_data); + if (!::SetupDiEnumDeviceInfo(device_info, i, &device_info_data)) { + break; + } + + // Get the device ID. + wchar_t device_id[MAX_DEVICE_ID_LEN]; + CONFIGRET status = ::CM_Get_Device_ID(device_info_data.DevInst, device_id, + MAX_DEVICE_ID_LEN, 0); + if (status == CR_SUCCESS) { + static const std::wstring BT_HID_DEVICE = L"HID\\{00001124"; + static const std::wstring BT_HOGP_DEVICE = L"HID\\{00001812"; + // To reduce the scope of the hack we only look for ACPI and HID\\VID + // prefixes in the keyboard device ids. + if (IMEHandler::WStringStartsWithCaseInsensitive(device_id, L"ACPI") || + IMEHandler::WStringStartsWithCaseInsensitive(device_id, + L"HID\\VID") || + IMEHandler::WStringStartsWithCaseInsensitive(device_id, + BT_HID_DEVICE) || + IMEHandler::WStringStartsWithCaseInsensitive(device_id, + BT_HOGP_DEVICE)) { + // The heuristic we are using is to check the count of keyboards and + // return true if the API's report one or more keyboards. Please note + // that this will break for non keyboard devices which expose a + // keyboard PDO. + Preferences::SetString(kOskDebugReason, + L"IKPOS: Keyboard presence confirmed."); + return true; + } + } + } + Preferences::SetString(kOskDebugReason, + L"IKPOS: Lack of keyboard confirmed."); + return false; +} + +// static +bool IMEHandler::IsInTabletMode() { + bool isInTabletMode = WindowsUIUtils::GetInTabletMode(); + if (isInTabletMode) { + Preferences::SetString(kOskDebugReason, L"IITM: GetInTabletMode=true."); + } else { + Preferences::SetString(kOskDebugReason, L"IITM: GetInTabletMode=false."); + } + return isInTabletMode; +} + +static bool ReadEnableDesktopModeAutoInvoke(uint32_t aRoot, + nsIWindowsRegKey* aRegKey, + uint32_t& aValue) { + nsresult rv; + rv = aRegKey->Open(aRoot, u"SOFTWARE\\Microsoft\\TabletTip\\1.7"_ns, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) { + Preferences::SetString(kOskDebugReason, + L"AIOSKIDM: failed opening regkey."); + return false; + } + // EnableDesktopModeAutoInvoke is an opt-in option from the Windows + // Settings to "Automatically show the touch keyboard in windowed apps + // when there's no keyboard attached to your device." If the user has + // opted-in to this behavior, the tablet-mode requirement is skipped. + rv = aRegKey->ReadIntValue(u"EnableDesktopModeAutoInvoke"_ns, &aValue); + if (NS_FAILED(rv)) { + Preferences::SetString(kOskDebugReason, + L"AIOSKIDM: failed reading value of regkey."); + return false; + } + return true; +} + +// static +bool IMEHandler::AutoInvokeOnScreenKeyboardInDesktopMode() { + nsresult rv; + nsCOMPtr<nsIWindowsRegKey> regKey( + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv)); + if (NS_WARN_IF(NS_FAILED(rv))) { + Preferences::SetString(kOskDebugReason, + L"AIOSKIDM: " + L"nsIWindowsRegKey not available"); + return false; + } + + uint32_t value; + if (!ReadEnableDesktopModeAutoInvoke(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + regKey, value) && + !ReadEnableDesktopModeAutoInvoke(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, + regKey, value)) { + return false; + } + if (!!value) { + Preferences::SetString(kOskDebugReason, L"AIOSKIDM: regkey value=true."); + } else { + Preferences::SetString(kOskDebugReason, L"AIOSKIDM: regkey value=false."); + } + return !!value; +} + +// Based on DisplayVirtualKeyboard() in Chromium's base/win/win_util.cc. +// static +void IMEHandler::ShowOnScreenKeyboard(nsWindow* aWindow) { +#ifdef NIGHTLY_BUILD + if (FxRWindowManager::GetInstance()->IsFxRWindow(sFocusedWindow)) { + OSKVRManager::ShowOnScreenKeyboard(); + return; + } +#endif // NIGHTLY_BUILD + + if (IsWin10AnniversaryUpdateOrLater()) { + OSKInputPaneManager::ShowOnScreenKeyboard(aWindow->GetWindowHandle()); + return; + } + + OSKTabTipManager::ShowOnScreenKeyboard(); +} + +// Based on DismissVirtualKeyboard() in Chromium's base/win/win_util.cc. +// static +void IMEHandler::DismissOnScreenKeyboard(nsWindow* aWindow) { + // Dismiss the virtual keyboard if it's open + if (IsWin10AnniversaryUpdateOrLater()) { + OSKInputPaneManager::DismissOnScreenKeyboard(aWindow->GetWindowHandle()); + return; + } + + OSKTabTipManager::DismissOnScreenKeyboard(); +} + +bool IMEHandler::MaybeCreateNativeCaret(nsWindow* aWindow) { + MOZ_ASSERT(aWindow); + + if (IsA11yHandlingNativeCaret()) { + return false; + } + + if (!sHasNativeCaretBeenRequested) { + // If we have not received WM_GETOBJECT for OBJID_CARET, there may be new + // application which requires our caret information. For kicking its + // window event proc, we should fire a window event here. + // (If there is such application, sHasNativeCaretBeenRequested will be set + // to true later.) + // FYI: If we create native caret and move its position, native caret + // causes EVENT_OBJECT_LOCATIONCHANGE event with OBJID_CARET and + // OBJID_CLIENT. + ::NotifyWinEvent(EVENT_OBJECT_LOCATIONCHANGE, aWindow->GetWindowHandle(), + OBJID_CARET, OBJID_CLIENT); + return false; + } + + MaybeDestroyNativeCaret(); + + // If focused content is not text editable, we don't support caret + // caret information without a11y module. + if (!aWindow->GetInputContext().mIMEState.IsEditable()) { + return false; + } + + WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, aWindow); + aWindow->InitEvent(queryCaretRectEvent); + + WidgetQueryContentEvent::Options options; + options.mRelativeToInsertionPoint = true; + queryCaretRectEvent.InitForQueryCaretRect(0, options); + + aWindow->DispatchWindowEvent(queryCaretRectEvent); + if (NS_WARN_IF(queryCaretRectEvent.Failed())) { + return false; + } + + return CreateNativeCaret(aWindow, queryCaretRectEvent.mReply->mRect); +} + +bool IMEHandler::CreateNativeCaret(nsWindow* aWindow, + const LayoutDeviceIntRect& aCaretRect) { + MOZ_ASSERT(aWindow); + + MOZ_ASSERT(!IsA11yHandlingNativeCaret()); + + sNativeCaretIsCreated = + ::CreateCaret(aWindow->GetWindowHandle(), nullptr, aCaretRect.Width(), + aCaretRect.Height()); + if (!sNativeCaretIsCreated) { + return false; + } + nsWindow* toplevelWindow = aWindow->GetTopLevelWindow(false); + if (NS_WARN_IF(!toplevelWindow)) { + MaybeDestroyNativeCaret(); + return false; + } + + LayoutDeviceIntPoint caretPosition(aCaretRect.TopLeft()); + if (toplevelWindow != aWindow) { + caretPosition += toplevelWindow->WidgetToScreenOffset(); + caretPosition -= aWindow->WidgetToScreenOffset(); + } + + ::SetCaretPos(caretPosition.x, caretPosition.y); + return true; +} + +void IMEHandler::MaybeDestroyNativeCaret() { + if (!sNativeCaretIsCreated) { + return; + } + ::DestroyCaret(); + sNativeCaretIsCreated = false; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/WinIMEHandler.h b/widget/windows/WinIMEHandler.h new file mode 100644 index 0000000000..410e1ebd22 --- /dev/null +++ b/widget/windows/WinIMEHandler.h @@ -0,0 +1,247 @@ +/* -*- 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 WinIMEHandler_h_ +#define WinIMEHandler_h_ + +#include "nscore.h" +#include "nsWindow.h" +#include "npapi.h" +#include <windows.h> +#include <inputscope.h> + +#define NS_WM_IMEFIRST WM_IME_SETCONTEXT +#define NS_WM_IMELAST WM_IME_KEYUP + +class nsWindow; + +namespace mozilla { +namespace widget { + +struct MSGResult; + +/** + * IMEHandler class is a mediator class. On Windows, there are two IME API + * sets: One is IMM which is legacy API set. The other is TSF which is modern + * API set. By using this class, non-IME handler classes don't need to worry + * that we're in which mode. + */ +class IMEHandler final { + private: + /** + * Initialize() initializes both TSF modules and IMM modules. Some TIPs + * may require a normal window (i.e., not message window) belonging to + * this process. Therefore, this is called immediately after first normal + * window is created. + */ + static void Initialize(); + + public: + static void Terminate(); + + /** + * Returns TSF related native data or native IME context. + */ + static void* GetNativeData(nsWindow* aWindow, uint32_t aDataType); + + /** + * ProcessRawKeyMessage() message is called before calling TranslateMessage() + * and DispatchMessage(). If this returns true, the message is consumed. + * Then, caller must not perform TranslateMessage() nor DispatchMessage(). + */ + static bool ProcessRawKeyMessage(const MSG& aMsg); + + /** + * When the message is not needed to handle anymore by the caller, this + * returns true. Otherwise, false. + */ + static bool ProcessMessage(nsWindow* aWindow, UINT aMessage, WPARAM& aWParam, + LPARAM& aLParam, MSGResult& aResult); + + /** + * IsA11yHandlingNativeCaret() returns true if a11y is handling + * native caret. In such case, IME modules shouldn't touch native caret. + **/ + static bool IsA11yHandlingNativeCaret(); + + /** + * NeedsToCreateNativeCaret() returns true if IME handler needs to create + * native caret for other applications which requests OBJID_CARET with + * WM_GETOBJECT and a11y module isn't active (if a11y module is active, + * it always creates native caret, i.e., even if no editor has focus). + */ + static bool NeedsToCreateNativeCaret() { + return sHasNativeCaretBeenRequested && !IsA11yHandlingNativeCaret(); + } + + /** + * CreateNativeCaret() create native caret if this has been created it. + * + * @param aWindow The window which owns the caret. + * @param aCaretRect The caret rect relative to aWindow. + */ + static bool CreateNativeCaret(nsWindow* aWindow, + const LayoutDeviceIntRect& aCaretRect); + + /** + * MaybeDestroyNativeCaret() destroies native caret if it has been created + * by IMEHandler. + */ + static void MaybeDestroyNativeCaret(); + + /** + * HasNativeCaret() returns true if there is native caret and it was created + * by IMEHandler. + */ + static bool HasNativeCaret() { return sNativeCaretIsCreated; } + + /** + * When there is a composition, returns true. Otherwise, false. + */ + static bool IsComposing(); + + /** + * When there is a composition and it's in the window, returns true. + * Otherwise, false. + */ + static bool IsComposingOn(nsWindow* aWindow); + + /** + * Notifies IME of the notification (a request or an event). + */ + static nsresult NotifyIME(nsWindow* aWindow, + const IMENotification& aIMENotification); + + /** + * Returns notification requests of IME. + */ + static IMENotificationRequests GetIMENotificationRequests(); + + /** + * Returns native text event dispatcher listener. + */ + static TextEventDispatcherListener* GetNativeTextEventDispatcherListener(); + + /** + * Returns IME open state on the window. + */ + static bool GetOpenState(nsWindow* aWindow); + + /** + * Called when the window is destroying. + */ + static void OnDestroyWindow(nsWindow* aWindow); + + /** + * Called when nsIWidget::SetInputContext() is called before the window's + * InputContext is modified actually. + */ + static void SetInputContext(nsWindow* aWindow, InputContext& aInputContext, + const InputContextAction& aAction); + + /** + * Associate or disassociate IME context to/from the aWindowBase. + */ + static void AssociateIMEContext(nsWindow* aWindowBase, bool aEnable); + + /** + * Called when the window is created. + */ + static void InitInputContext(nsWindow* aWindow, InputContext& aInputContext); + + /** + * This is called by TSFStaticSink when active IME is changed. + */ + static void OnKeyboardLayoutChanged(); + +#ifdef DEBUG + /** + * Returns true when current keyboard layout has IME. Otherwise, false. + */ + static bool CurrentKeyboardLayoutHasIME(); +#endif // #ifdef DEBUG + + /** + * Append InputScope values from inputmode string. + */ + static void AppendInputScopeFromInputMode(const nsAString& aHTMLInputMode, + nsTArray<InputScope>& aScopes); + + /** + * Append InputScope values from type attreibute string of input element + */ + static void AppendInputScopeFromType(const nsAString& aInputType, + nsTArray<InputScope>& aScopes); + + /** + * Return focused window if this receives focus notification and has not + * received blur notification yet. + */ + static nsWindow* GetFocusedWindow() { return sFocusedWindow; } + + private: + static nsWindow* sFocusedWindow; + static InputContextAction::Cause sLastContextActionCause; + + static bool sMaybeEditable; + static bool sForceDisableCurrentIMM_IME; + static bool sNativeCaretIsCreated; + static bool sHasNativeCaretBeenRequested; + + /** + * MaybeCreateNativeCaret() may create native caret over our caret if + * focused content is text editable and we need to create native caret + * for other applications. + * + * @param aWindow The window which owns the native caret. + */ + static bool MaybeCreateNativeCaret(nsWindow* aWindow); + + static decltype(SetInputScopes)* sSetInputScopes; + static void SetInputScopeForIMM32(nsWindow* aWindow, + const nsAString& aHTMLInputType, + const nsAString& aHTMLInputMode, + bool aInPrivateBrowsing); + static bool sIsInTSFMode; + // If sIMMEnabled is false, any IME messages are not handled in TSF mode. + // Additionally, IME context is always disassociated from focused window. + static bool sIsIMMEnabled; + + static bool IsTSFAvailable() { return sIsInTSFMode; } + static bool IsIMMActive(); + + static bool IsOnScreenKeyboardSupported(); + + static void MaybeShowOnScreenKeyboard(nsWindow* aWindow, + const InputContext& aInputContext); + enum class Sync { Yes, No }; + static void MaybeDismissOnScreenKeyboard(nsWindow* aWindow, + Sync aSync = Sync::No); + static bool WStringStartsWithCaseInsensitive(const std::wstring& aHaystack, + const std::wstring& aNeedle); + static bool NeedOnScreenKeyboard(); + static bool IsKeyboardPresentOnSlate(); + static bool IsInTabletMode(); + static bool AutoInvokeOnScreenKeyboardInDesktopMode(); + static bool NeedsToAssociateIMC(); + static bool NeedsSearchInputScope(); + + /** + * Show the Windows on-screen keyboard. Only allowed for + * chrome documents and Windows 8 and higher. + */ + static void ShowOnScreenKeyboard(nsWindow* aWindow); + + /** + * Dismiss the Windows on-screen keyboard. Only allowed for + * Windows 8 and higher. + */ + static void DismissOnScreenKeyboard(nsWindow* aWindow); +}; + +} // namespace widget +} // namespace mozilla + +#endif // #ifndef WinIMEHandler_h_ diff --git a/widget/windows/WinMessages.h b/widget/windows/WinMessages.h new file mode 100644 index 0000000000..8c90460e8e --- /dev/null +++ b/widget/windows/WinMessages.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 mozilla_widget_WinMessages_h_ +#define mozilla_widget_WinMessages_h_ + +/***************************************************************************** + * MOZ_WM_* messages + ****************************************************************************/ + +// A magic APP message that can be sent to quit, sort of like a +// QUERYENDSESSION/ENDSESSION, but without the query. +#define MOZ_WM_APP_QUIT (WM_APP + 0x0300) +// Used as a "tracer" event to probe event loop latency. +#define MOZ_WM_TRACE (WM_APP + 0x0301) +// accessibility priming +#define MOZ_WM_STARTA11Y (WM_APP + 0x0302) +// Our internal message for WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_VSCROLL and +// WM_HSCROLL +#define MOZ_WM_MOUSEVWHEEL (WM_APP + 0x0310) +#define MOZ_WM_MOUSEHWHEEL (WM_APP + 0x0311) +#define MOZ_WM_VSCROLL (WM_APP + 0x0312) +#define MOZ_WM_HSCROLL (WM_APP + 0x0313) +#define MOZ_WM_MOUSEWHEEL_FIRST MOZ_WM_MOUSEVWHEEL +#define MOZ_WM_MOUSEWHEEL_LAST MOZ_WM_HSCROLL +// If a popup window is being activated, we try to reactivate the previous +// window with this message. +#define MOZ_WM_REACTIVATE (WM_APP + 0x0314) +// If TSFTextStore needs to notify TSF/TIP of layout change later, this +// message is posted. +#define MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE (WM_APP + 0x0315) +// Internal message used in correcting backwards clock skew +#define MOZ_WM_SKEWFIX (WM_APP + 0x0316) +// Internal message used for rolling up popups for dmanip events +#define MOZ_WM_DMANIP (WM_APP + 0x0317) + +// Internal message used to work around race condition in explorer.exe's +// fullscreen window-state update handler in Windows 10+. (See bug 1835851.) +#define MOZ_WM_FULLSCREEN_STATE_UPDATE (WM_APP + 0x0318) + +// XXX Should rename them to MOZ_WM_* and use safer values! +// Messages for fullscreen transition window +#define WM_FULLSCREEN_TRANSITION_BEFORE (WM_USER + 0) +#define WM_FULLSCREEN_TRANSITION_AFTER (WM_USER + 1) + +#ifndef APPCOMMAND_BROWSER_BACKWARD +# define APPCOMMAND_BROWSER_BACKWARD 1 +# define APPCOMMAND_BROWSER_FORWARD 2 +# define APPCOMMAND_BROWSER_REFRESH 3 +# define APPCOMMAND_BROWSER_STOP 4 +# define APPCOMMAND_BROWSER_SEARCH 5 +# define APPCOMMAND_BROWSER_FAVORITES 6 +# define APPCOMMAND_BROWSER_HOME 7 + +# define APPCOMMAND_MEDIA_NEXTTRACK 11 +# define APPCOMMAND_MEDIA_PREVIOUSTRACK 12 +# define APPCOMMAND_MEDIA_STOP 13 +# define APPCOMMAND_MEDIA_PLAY_PAUSE 14 + +/* + * Additional commands currently not in use. + * + *#define APPCOMMAND_VOLUME_MUTE 8 + *#define APPCOMMAND_VOLUME_DOWN 9 + *#define APPCOMMAND_VOLUME_UP 10 + *#define APPCOMMAND_LAUNCH_MAIL 15 + *#define APPCOMMAND_LAUNCH_MEDIA_SELECT 16 + *#define APPCOMMAND_LAUNCH_APP1 17 + *#define APPCOMMAND_LAUNCH_APP2 18 + *#define APPCOMMAND_BASS_DOWN 19 + *#define APPCOMMAND_BASS_BOOST 20 + *#define APPCOMMAND_BASS_UP 21 + *#define APPCOMMAND_TREBLE_DOWN 22 + *#define APPCOMMAND_TREBLE_UP 23 + *#define FAPPCOMMAND_MOUSE 0x8000 + *#define FAPPCOMMAND_KEY 0 + *#define FAPPCOMMAND_OEM 0x1000 + */ + +# define GET_APPCOMMAND_LPARAM(lParam) \ + ((short)(HIWORD(lParam) & ~FAPPCOMMAND_MASK)) + +/* + *#define GET_DEVICE_LPARAM(lParam) ((WORD)(HIWORD(lParam) & + *FAPPCOMMAND_MASK)) #define GET_MOUSEORKEY_LPARAM GET_DEVICE_LPARAM + *#define GET_FLAGS_LPARAM(lParam) (LOWORD(lParam)) + *#define GET_KEYSTATE_LPARAM(lParam) GET_FLAGS_LPARAM(lParam) + */ +#endif // #ifndef APPCOMMAND_BROWSER_BACKWARD + +#endif // #ifndef mozilla_widget_WinMessages_h_ diff --git a/widget/windows/WinModifierKeyState.h b/widget/windows/WinModifierKeyState.h new file mode 100644 index 0000000000..0788c25939 --- /dev/null +++ b/widget/windows/WinModifierKeyState.h @@ -0,0 +1,59 @@ +/* -*- 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_WinModifierKeyState_h_ +#define mozilla_widget_WinModifierKeyState_h_ + +#include "mozilla/RefPtr.h" +#include "mozilla/EventForwards.h" +#include "nsStringFwd.h" +#include <windows.h> + +namespace mozilla { +namespace widget { + +class MOZ_STACK_CLASS ModifierKeyState final { + public: + ModifierKeyState(); + explicit ModifierKeyState(Modifiers aModifiers); + + void Update(); + + void Unset(Modifiers aRemovingModifiers); + void Set(Modifiers aAddingModifiers); + + void InitInputEvent(WidgetInputEvent& aInputEvent) const; + + // Do not create IsAltGr() because it's unclear whether: + // - AltGr key is actually pressed. + // - Both Ctrl and Alt keys are pressed when a keyboard layout which + // has AltGr key. + // - Both Ctrl and Alt keys are pressed when a keyboard layout which + // does not have AltGr key. + bool IsShift() const; + bool IsControl() const; + bool IsAlt() const; + bool IsWin() const; + + bool MaybeMatchShortcutKey() const; + + bool IsCapsLocked() const; + bool IsNumLocked() const; + bool IsScrollLocked() const; + + MOZ_ALWAYS_INLINE Modifiers GetModifiers() const { return mModifiers; } + + private: + Modifiers mModifiers; + + void InitMouseEvent(WidgetInputEvent& aMouseEvent) const; +}; + +const nsCString ToString(const ModifierKeyState& aModifierKeyState); + +} // namespace widget +} // namespace mozilla + +#endif // #ifndef mozilla_widget_WinModifierKeyState_h_ diff --git a/widget/windows/WinMouseScrollHandler.cpp b/widget/windows/WinMouseScrollHandler.cpp new file mode 100644 index 0000000000..e10f7b29dc --- /dev/null +++ b/widget/windows/WinMouseScrollHandler.cpp @@ -0,0 +1,1634 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "mozilla/DebugOnly.h" + +#include "mozilla/Logging.h" + +#include "WinMouseScrollHandler.h" +#include "nsWindow.h" +#include "nsWindowDefs.h" +#include "KeyboardLayout.h" +#include "WinUtils.h" +#include "nsGkAtoms.h" +#include "nsIDOMWindowUtils.h" + +#include "mozilla/MiscEvents.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/WheelEventBinding.h" +#include "mozilla/StaticPrefs_mousewheel.h" +#include "mozilla/widget/WinRegistry.h" + +#include <psapi.h> + +namespace mozilla { +namespace widget { + +LazyLogModule gMouseScrollLog("MouseScrollHandlerWidgets"); + +static const char* GetBoolName(bool aBool) { return aBool ? "TRUE" : "FALSE"; } + +MouseScrollHandler* MouseScrollHandler::sInstance = nullptr; + +bool MouseScrollHandler::Device::sFakeScrollableWindowNeeded = false; + +bool MouseScrollHandler::Device::SynTP::sInitialized = false; +int32_t MouseScrollHandler::Device::SynTP::sMajorVersion = 0; +int32_t MouseScrollHandler::Device::SynTP::sMinorVersion = -1; + +bool MouseScrollHandler::Device::Elantech::sUseSwipeHack = false; +bool MouseScrollHandler::Device::Elantech::sUsePinchHack = false; +DWORD MouseScrollHandler::Device::Elantech::sZoomUntil = 0; + +bool MouseScrollHandler::Device::Apoint::sInitialized = false; +int32_t MouseScrollHandler::Device::Apoint::sMajorVersion = 0; +int32_t MouseScrollHandler::Device::Apoint::sMinorVersion = -1; + +bool MouseScrollHandler::Device::SetPoint::sMightBeUsing = false; + +// The duration until timeout of events transaction. The value is 1.5 sec, +// it's just a magic number, it was suggested by Logitech's engineer, see +// bug 605648 comment 90. +#define DEFAULT_TIMEOUT_DURATION 1500 + +/****************************************************************************** + * + * MouseScrollHandler + * + ******************************************************************************/ + +/* static */ +POINTS +MouseScrollHandler::GetCurrentMessagePos() { + if (SynthesizingEvent::IsSynthesizing()) { + return sInstance->mSynthesizingEvent->GetCursorPoint(); + } + DWORD pos = ::GetMessagePos(); + return MAKEPOINTS(pos); +} + +// Get rid of the GetMessagePos() API. +#define GetMessagePos() + +/* static */ +void MouseScrollHandler::Initialize() { Device::Init(); } + +/* static */ +void MouseScrollHandler::Shutdown() { + delete sInstance; + sInstance = nullptr; +} + +/* static */ +MouseScrollHandler* MouseScrollHandler::GetInstance() { + if (!sInstance) { + sInstance = new MouseScrollHandler(); + } + return sInstance; +} + +MouseScrollHandler::MouseScrollHandler() + : mIsWaitingInternalMessage(false), mSynthesizingEvent(nullptr) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll: Creating an instance, this=%p, sInstance=%p", this, + sInstance)); +} + +MouseScrollHandler::~MouseScrollHandler() { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll: Destroying an instance, this=%p, sInstance=%p", this, + sInstance)); + + delete mSynthesizingEvent; +} + +/* static */ +void MouseScrollHandler::MaybeLogKeyState() { + if (!MOZ_LOG_TEST(gMouseScrollLog, LogLevel::Debug)) { + return; + } + BYTE keyboardState[256]; + if (::GetKeyboardState(keyboardState)) { + for (size_t i = 0; i < ArrayLength(keyboardState); i++) { + if (keyboardState[i]) { + MOZ_LOG(gMouseScrollLog, LogLevel::Debug, + (" Current key state: keyboardState[0x%02zX]=0x%02X (%s)", i, + keyboardState[i], + ((keyboardState[i] & 0x81) == 0x81) ? "Pressed and Toggled" + : (keyboardState[i] & 0x80) ? "Pressed" + : (keyboardState[i] & 0x01) ? "Toggled" + : "Unknown")); + } + } + } else { + MOZ_LOG( + gMouseScrollLog, LogLevel::Debug, + ("MouseScroll::MaybeLogKeyState(): Failed to print current keyboard " + "state")); + } +} + +/* static */ +bool MouseScrollHandler::NeedsMessage(UINT aMsg) { + switch (aMsg) { + case WM_SETTINGCHANGE: + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + case WM_HSCROLL: + case WM_VSCROLL: + case MOZ_WM_MOUSEVWHEEL: + case MOZ_WM_MOUSEHWHEEL: + case MOZ_WM_HSCROLL: + case MOZ_WM_VSCROLL: + case WM_KEYDOWN: + case WM_KEYUP: + return true; + } + return false; +} + +/* static */ +bool MouseScrollHandler::ProcessMessage(nsWindow* aWidget, UINT msg, + WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + Device::Elantech::UpdateZoomUntil(); + + switch (msg) { + case WM_SETTINGCHANGE: + if (!sInstance) { + return false; + } + if (wParam == SPI_SETWHEELSCROLLLINES || + wParam == SPI_SETWHEELSCROLLCHARS) { + sInstance->mSystemSettings.MarkDirty(); + } + return false; + + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + GetInstance()->ProcessNativeMouseWheelMessage(aWidget, msg, wParam, + lParam); + sInstance->mSynthesizingEvent->NotifyNativeMessageHandlingFinished(); + // We don't need to call next wndproc for WM_MOUSEWHEEL and + // WM_MOUSEHWHEEL. We should consume them always. If the messages + // would be handled by our window again, it caused making infinite + // message loop. + aResult.mConsumed = true; + aResult.mResult = (msg != WM_MOUSEHWHEEL); + return true; + + case WM_HSCROLL: + case WM_VSCROLL: + aResult.mConsumed = GetInstance()->ProcessNativeScrollMessage( + aWidget, msg, wParam, lParam); + sInstance->mSynthesizingEvent->NotifyNativeMessageHandlingFinished(); + aResult.mResult = 0; + return true; + + case MOZ_WM_MOUSEVWHEEL: + case MOZ_WM_MOUSEHWHEEL: + GetInstance()->HandleMouseWheelMessage(aWidget, msg, wParam, lParam); + sInstance->mSynthesizingEvent->NotifyInternalMessageHandlingFinished(); + // Doesn't need to call next wndproc for internal wheel message. + aResult.mConsumed = true; + return true; + + case MOZ_WM_HSCROLL: + case MOZ_WM_VSCROLL: + GetInstance()->HandleScrollMessageAsMouseWheelMessage(aWidget, msg, + wParam, lParam); + sInstance->mSynthesizingEvent->NotifyInternalMessageHandlingFinished(); + // Doesn't need to call next wndproc for internal scroll message. + aResult.mConsumed = true; + return true; + + case WM_KEYDOWN: + case WM_KEYUP: + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessMessage(): aWidget=%p, " + "msg=%s(0x%04X), wParam=0x%02zX, ::GetMessageTime()=%ld", + aWidget, + msg == WM_KEYDOWN ? "WM_KEYDOWN" + : msg == WM_KEYUP ? "WM_KEYUP" + : "Unknown", + msg, wParam, ::GetMessageTime())); + MaybeLogKeyState(); + if (Device::Elantech::HandleKeyMessage(aWidget, msg, wParam, lParam)) { + aResult.mResult = 0; + aResult.mConsumed = true; + return true; + } + return false; + + default: + return false; + } +} + +/* static */ +nsresult MouseScrollHandler::SynthesizeNativeMouseScrollEvent( + nsWindow* aWidget, const LayoutDeviceIntPoint& aPoint, + uint32_t aNativeMessage, int32_t aDelta, uint32_t aModifierFlags, + uint32_t aAdditionalFlags) { + bool useFocusedWindow = !( + aAdditionalFlags & nsIDOMWindowUtils::MOUSESCROLL_PREFER_WIDGET_AT_POINT); + + POINT pt; + pt.x = aPoint.x; + pt.y = aPoint.y; + + HWND target = useFocusedWindow ? ::WindowFromPoint(pt) : ::GetFocus(); + NS_ENSURE_TRUE(target, NS_ERROR_FAILURE); + + WPARAM wParam = 0; + LPARAM lParam = 0; + switch (aNativeMessage) { + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: { + lParam = MAKELPARAM(pt.x, pt.y); + WORD mod = 0; + if (aModifierFlags & (nsIWidget::CTRL_L | nsIWidget::CTRL_R)) { + mod |= MK_CONTROL; + } + if (aModifierFlags & (nsIWidget::SHIFT_L | nsIWidget::SHIFT_R)) { + mod |= MK_SHIFT; + } + wParam = MAKEWPARAM(mod, aDelta); + break; + } + case WM_VSCROLL: + case WM_HSCROLL: + lParam = (aAdditionalFlags & + nsIDOMWindowUtils::MOUSESCROLL_WIN_SCROLL_LPARAM_NOT_NULL) + ? reinterpret_cast<LPARAM>(target) + : 0; + wParam = aDelta; + break; + default: + return NS_ERROR_INVALID_ARG; + } + + // Ensure to make the instance. + GetInstance(); + + BYTE kbdState[256]; + memset(kbdState, 0, sizeof(kbdState)); + + AutoTArray<KeyPair, 10> keySequence; + WinUtils::SetupKeyModifiersSequence(&keySequence, aModifierFlags, + aNativeMessage); + + for (uint32_t i = 0; i < keySequence.Length(); ++i) { + uint8_t key = keySequence[i].mGeneral; + uint8_t keySpecific = keySequence[i].mSpecific; + kbdState[key] = 0x81; // key is down and toggled on if appropriate + if (keySpecific) { + kbdState[keySpecific] = 0x81; + } + } + + if (!sInstance->mSynthesizingEvent) { + sInstance->mSynthesizingEvent = new SynthesizingEvent(); + } + + POINTS pts; + pts.x = static_cast<SHORT>(pt.x); + pts.y = static_cast<SHORT>(pt.y); + return sInstance->mSynthesizingEvent->Synthesize(pts, target, aNativeMessage, + wParam, lParam, kbdState); +} + +/* static */ +void MouseScrollHandler::InitEvent(nsWindow* aWidget, WidgetGUIEvent& aEvent, + LPARAM* aPoint) { + NS_ENSURE_TRUE_VOID(aWidget); + + // If a point is provided, use it; otherwise, get current message point or + // synthetic point + POINTS pointOnScreen; + if (aPoint != nullptr) { + pointOnScreen = MAKEPOINTS(*aPoint); + } else { + pointOnScreen = GetCurrentMessagePos(); + } + + // InitEvent expects the point to be in window coordinates, so translate the + // point from screen coordinates. + POINT pointOnWindow; + POINTSTOPOINT(pointOnWindow, pointOnScreen); + ::ScreenToClient(aWidget->GetWindowHandle(), &pointOnWindow); + + LayoutDeviceIntPoint point; + point.x = pointOnWindow.x; + point.y = pointOnWindow.y; + + aWidget->InitEvent(aEvent, &point); +} + +/* static */ +ModifierKeyState MouseScrollHandler::GetModifierKeyState(UINT aMessage) { + ModifierKeyState result; + // Assume the Control key is down if the Elantech touchpad has sent the + // mis-ordered WM_KEYDOWN/WM_MOUSEWHEEL messages. (See the comment in + // MouseScrollHandler::Device::Elantech::HandleKeyMessage().) + if ((aMessage == MOZ_WM_MOUSEVWHEEL || aMessage == WM_MOUSEWHEEL) && + !result.IsControl() && Device::Elantech::IsZooming()) { + // XXX Do we need to unset MODIFIER_SHIFT, MODIFIER_ALT, MODIFIER_META too? + // If one of them are true, the default action becomes not zooming. + result.Unset(MODIFIER_ALTGRAPH); + result.Set(MODIFIER_CONTROL); + } + return result; +} + +POINT +MouseScrollHandler::ComputeMessagePos(UINT aMessage, WPARAM aWParam, + LPARAM aLParam) { + POINT point; + if (Device::SetPoint::IsGetMessagePosResponseValid(aMessage, aWParam, + aLParam)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ComputeMessagePos: Using ::GetCursorPos()")); + ::GetCursorPos(&point); + } else { + POINTS pts = GetCurrentMessagePos(); + point.x = pts.x; + point.y = pts.y; + } + return point; +} + +void MouseScrollHandler::ProcessNativeMouseWheelMessage(nsWindow* aWidget, + UINT aMessage, + WPARAM aWParam, + LPARAM aLParam) { + if (SynthesizingEvent::IsSynthesizing()) { + mSynthesizingEvent->NativeMessageReceived(aWidget, aMessage, aWParam, + aLParam); + } + + POINT point = ComputeMessagePos(aMessage, aWParam, aLParam); + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessNativeMouseWheelMessage: aWidget=%p, " + "aMessage=%s, wParam=0x%08zX, lParam=0x%08" PRIXLPTR + ", point: { x=%ld, y=%ld }", + aWidget, + aMessage == WM_MOUSEWHEEL ? "WM_MOUSEWHEEL" + : aMessage == WM_MOUSEHWHEEL ? "WM_MOUSEHWHEEL" + : aMessage == WM_VSCROLL ? "WM_VSCROLL" + : "WM_HSCROLL", + aWParam, aLParam, point.x, point.y)); + MaybeLogKeyState(); + + HWND underCursorWnd = ::WindowFromPoint(point); + if (!underCursorWnd) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessNativeMouseWheelMessage: " + "No window is not found under the cursor")); + return; + } + + if (Device::Elantech::IsPinchHackNeeded() && + Device::Elantech::IsHelperWindow(underCursorWnd)) { + // The Elantech driver places a window right underneath the cursor + // when sending a WM_MOUSEWHEEL event to us as part of a pinch-to-zoom + // gesture. We detect that here, and search for our window that would + // be beneath the cursor if that window wasn't there. + underCursorWnd = WinUtils::FindOurWindowAtPoint(point); + if (!underCursorWnd) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessNativeMouseWheelMessage: " + "Our window is not found under the Elantech helper window")); + return; + } + } + + // Handle most cases first. If the window under mouse cursor is our window + // except plugin window (MozillaWindowClass), we should handle the message + // on the window. + if (WinUtils::IsOurProcessWindow(underCursorWnd)) { + nsWindow* destWindow = WinUtils::GetNSWindowPtr(underCursorWnd); + if (!destWindow) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessNativeMouseWheelMessage: " + "Found window under the cursor isn't managed by nsWindow...")); + HWND wnd = ::GetParent(underCursorWnd); + for (; wnd; wnd = ::GetParent(wnd)) { + destWindow = WinUtils::GetNSWindowPtr(wnd); + if (destWindow) { + break; + } + } + if (!wnd) { + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessNativeMouseWheelMessage: Our window which is " + "managed by nsWindow is not found under the cursor")); + return; + } + } + + MOZ_ASSERT(destWindow, "destWindow must not be NULL"); + + // Some odd touchpad utils sets focus to window under the mouse cursor. + // this emulates the odd behavior for debug. + if (mUserPrefs.ShouldEmulateToMakeWindowUnderCursorForeground() && + (aMessage == WM_MOUSEWHEEL || aMessage == WM_MOUSEHWHEEL) && + ::GetForegroundWindow() != destWindow->GetWindowHandle()) { + ::SetForegroundWindow(destWindow->GetWindowHandle()); + } + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessNativeMouseWheelMessage: Succeeded, " + "Posting internal message to an nsWindow (%p)...", + destWindow)); + mIsWaitingInternalMessage = true; + UINT internalMessage = WinUtils::GetInternalMessage(aMessage); + ::PostMessage(destWindow->GetWindowHandle(), internalMessage, aWParam, + aLParam); + return; + } + + // If the window under cursor is not in our process, it means: + // 1. The window may be a plugin window (GeckoPluginWindow or its descendant). + // 2. The window may be another application's window. + HWND pluginWnd = WinUtils::FindOurProcessWindow(underCursorWnd); + if (!pluginWnd) { + // If there is no plugin window in ancestors of the window under cursor, + // the window is for another applications (case 2). + // We don't need to handle this message. + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessNativeMouseWheelMessage: " + "Our window is not found under the cursor")); + return; + } + + // If the window is a part of plugin, we should post the message to it. + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessNativeMouseWheelMessage: Succeeded, " + "Redirecting the message to a window which is a plugin child window")); + ::PostMessage(underCursorWnd, aMessage, aWParam, aLParam); +} + +bool MouseScrollHandler::ProcessNativeScrollMessage(nsWindow* aWidget, + UINT aMessage, + WPARAM aWParam, + LPARAM aLParam) { + if (aLParam || mUserPrefs.IsScrollMessageHandledAsWheelMessage()) { + // Scroll message generated by Thinkpad Trackpoint Driver or similar + // Treat as a mousewheel message and scroll appropriately + ProcessNativeMouseWheelMessage(aWidget, aMessage, aWParam, aLParam); + // Always consume the scroll message if we try to emulate mouse wheel + // action. + return true; + } + + if (SynthesizingEvent::IsSynthesizing()) { + mSynthesizingEvent->NativeMessageReceived(aWidget, aMessage, aWParam, + aLParam); + } + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::ProcessNativeScrollMessage: aWidget=%p, " + "aMessage=%s, wParam=0x%08zX, lParam=0x%08" PRIXLPTR, + aWidget, aMessage == WM_VSCROLL ? "WM_VSCROLL" : "WM_HSCROLL", + aWParam, aLParam)); + + // Scroll message generated by external application + WidgetContentCommandEvent commandEvent(true, eContentCommandScroll, aWidget); + commandEvent.mScroll.mIsHorizontal = (aMessage == WM_HSCROLL); + + switch (LOWORD(aWParam)) { + case SB_LINEUP: // SB_LINELEFT + commandEvent.mScroll.mUnit = + WidgetContentCommandEvent::eCmdScrollUnit_Line; + commandEvent.mScroll.mAmount = -1; + break; + case SB_LINEDOWN: // SB_LINERIGHT + commandEvent.mScroll.mUnit = + WidgetContentCommandEvent::eCmdScrollUnit_Line; + commandEvent.mScroll.mAmount = 1; + break; + case SB_PAGEUP: // SB_PAGELEFT + commandEvent.mScroll.mUnit = + WidgetContentCommandEvent::eCmdScrollUnit_Page; + commandEvent.mScroll.mAmount = -1; + break; + case SB_PAGEDOWN: // SB_PAGERIGHT + commandEvent.mScroll.mUnit = + WidgetContentCommandEvent::eCmdScrollUnit_Page; + commandEvent.mScroll.mAmount = 1; + break; + case SB_TOP: // SB_LEFT + commandEvent.mScroll.mUnit = + WidgetContentCommandEvent::eCmdScrollUnit_Whole; + commandEvent.mScroll.mAmount = -1; + break; + case SB_BOTTOM: // SB_RIGHT + commandEvent.mScroll.mUnit = + WidgetContentCommandEvent::eCmdScrollUnit_Whole; + commandEvent.mScroll.mAmount = 1; + break; + default: + return false; + } + // XXX If this is a plugin window, we should dispatch the event from + // parent window. + aWidget->DispatchContentCommandEvent(&commandEvent); + return true; +} + +void MouseScrollHandler::HandleMouseWheelMessage(nsWindow* aWidget, + UINT aMessage, WPARAM aWParam, + LPARAM aLParam) { + MOZ_ASSERT((aMessage == MOZ_WM_MOUSEVWHEEL || aMessage == MOZ_WM_MOUSEHWHEEL), + "HandleMouseWheelMessage must be called with " + "MOZ_WM_MOUSEVWHEEL or MOZ_WM_MOUSEHWHEEL"); + + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScroll::HandleMouseWheelMessage: aWidget=%p, " + "aMessage=MOZ_WM_MOUSE%sWHEEL, aWParam=0x%08zX, aLParam=0x%08" PRIXLPTR, + aWidget, aMessage == MOZ_WM_MOUSEVWHEEL ? "V" : "H", aWParam, aLParam)); + + mIsWaitingInternalMessage = false; + + // If it's not allowed to cache system settings, we need to reset the cache + // before handling the mouse wheel message. + mSystemSettings.TrustedScrollSettingsDriver(); + + EventInfo eventInfo(aWidget, WinUtils::GetNativeMessage(aMessage), aWParam, + aLParam); + if (!eventInfo.CanDispatchWheelEvent()) { + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScroll::HandleMouseWheelMessage: Cannot dispatch the events")); + mLastEventInfo.ResetTransaction(); + return; + } + + // Discard the remaining delta if current wheel message and last one are + // received by different window or to scroll different direction or + // different unit scroll. Furthermore, if the last event was too old. + if (!mLastEventInfo.CanContinueTransaction(eventInfo)) { + mLastEventInfo.ResetTransaction(); + } + + mLastEventInfo.RecordEvent(eventInfo); + + ModifierKeyState modKeyState = GetModifierKeyState(aMessage); + + // Grab the widget, it might be destroyed by a DOM event handler. + RefPtr<nsWindow> kungFuDethGrip(aWidget); + + WidgetWheelEvent wheelEvent(true, eWheel, aWidget); + if (mLastEventInfo.InitWheelEvent(aWidget, wheelEvent, modKeyState, + aLParam)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::HandleMouseWheelMessage: dispatching " + "eWheel event")); + aWidget->DispatchWheelEvent(&wheelEvent); + if (aWidget->Destroyed()) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::HandleMouseWheelMessage: The window was destroyed " + "by eWheel event")); + mLastEventInfo.ResetTransaction(); + return; + } + } else { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::HandleMouseWheelMessage: eWheel event is not " + "dispatched")); + } +} + +void MouseScrollHandler::HandleScrollMessageAsMouseWheelMessage( + nsWindow* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) { + MOZ_ASSERT((aMessage == MOZ_WM_VSCROLL || aMessage == MOZ_WM_HSCROLL), + "HandleScrollMessageAsMouseWheelMessage must be called with " + "MOZ_WM_VSCROLL or MOZ_WM_HSCROLL"); + + mIsWaitingInternalMessage = false; + + ModifierKeyState modKeyState = GetModifierKeyState(aMessage); + + WidgetWheelEvent wheelEvent(true, eWheel, aWidget); + double& delta = + (aMessage == MOZ_WM_VSCROLL) ? wheelEvent.mDeltaY : wheelEvent.mDeltaX; + int32_t& lineOrPageDelta = (aMessage == MOZ_WM_VSCROLL) + ? wheelEvent.mLineOrPageDeltaY + : wheelEvent.mLineOrPageDeltaX; + + delta = 1.0; + lineOrPageDelta = 1; + + switch (LOWORD(aWParam)) { + case SB_PAGEUP: + delta = -1.0; + lineOrPageDelta = -1; + [[fallthrough]]; + case SB_PAGEDOWN: + wheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_PAGE; + break; + + case SB_LINEUP: + delta = -1.0; + lineOrPageDelta = -1; + [[fallthrough]]; + case SB_LINEDOWN: + wheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_LINE; + break; + + default: + return; + } + modKeyState.InitInputEvent(wheelEvent); + + // Current mouse position may not be same as when the original message + // is received. However, this data is not available with the original + // message, which is why nullptr is passed in. We need to know the actual + // mouse cursor position when the original message was received. + InitEvent(aWidget, wheelEvent, nullptr); + + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScroll::HandleScrollMessageAsMouseWheelMessage: aWidget=%p, " + "aMessage=MOZ_WM_%sSCROLL, aWParam=0x%08zX, aLParam=0x%08" PRIXLPTR ", " + "wheelEvent { mRefPoint: { x: %d, y: %d }, mDeltaX: %f, mDeltaY: %f, " + "mLineOrPageDeltaX: %d, mLineOrPageDeltaY: %d, " + "isShift: %s, isControl: %s, isAlt: %s, isMeta: %s }", + aWidget, (aMessage == MOZ_WM_VSCROLL) ? "V" : "H", aWParam, aLParam, + wheelEvent.mRefPoint.x.value, wheelEvent.mRefPoint.y.value, + wheelEvent.mDeltaX, wheelEvent.mDeltaY, wheelEvent.mLineOrPageDeltaX, + wheelEvent.mLineOrPageDeltaY, GetBoolName(wheelEvent.IsShift()), + GetBoolName(wheelEvent.IsControl()), GetBoolName(wheelEvent.IsAlt()), + GetBoolName(wheelEvent.IsMeta()))); + + aWidget->DispatchWheelEvent(&wheelEvent); +} + +/****************************************************************************** + * + * EventInfo + * + ******************************************************************************/ + +MouseScrollHandler::EventInfo::EventInfo(nsWindow* aWidget, UINT aMessage, + WPARAM aWParam, LPARAM aLParam) { + MOZ_ASSERT( + aMessage == WM_MOUSEWHEEL || aMessage == WM_MOUSEHWHEEL, + "EventInfo must be initialized with WM_MOUSEWHEEL or WM_MOUSEHWHEEL"); + + MouseScrollHandler::GetInstance()->mSystemSettings.Init(); + + mIsVertical = (aMessage == WM_MOUSEWHEEL); + mIsPage = + MouseScrollHandler::sInstance->mSystemSettings.IsPageScroll(mIsVertical); + mDelta = (short)HIWORD(aWParam); + mWnd = aWidget->GetWindowHandle(); + mTimeStamp = TimeStamp::Now(); +} + +bool MouseScrollHandler::EventInfo::CanDispatchWheelEvent() const { + if (!GetScrollAmount()) { + // XXX I think that we should dispatch mouse wheel events even if the + // operation will not scroll because the wheel operation really happened + // and web application may want to handle the event for non-scroll action. + return false; + } + + return (mDelta != 0); +} + +int32_t MouseScrollHandler::EventInfo::GetScrollAmount() const { + if (mIsPage) { + return 1; + } + return MouseScrollHandler::sInstance->mSystemSettings.GetScrollAmount( + mIsVertical); +} + +/****************************************************************************** + * + * LastEventInfo + * + ******************************************************************************/ + +bool MouseScrollHandler::LastEventInfo::CanContinueTransaction( + const EventInfo& aNewEvent) { + int32_t timeout = MouseScrollHandler::sInstance->mUserPrefs + .GetMouseScrollTransactionTimeout(); + return !mWnd || + (mWnd == aNewEvent.GetWindowHandle() && + IsPositive() == aNewEvent.IsPositive() && + mIsVertical == aNewEvent.IsVertical() && + mIsPage == aNewEvent.IsPage() && + (timeout < 0 || TimeStamp::Now() - mTimeStamp <= + TimeDuration::FromMilliseconds(timeout))); +} + +void MouseScrollHandler::LastEventInfo::ResetTransaction() { + if (!mWnd) { + return; + } + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::LastEventInfo::ResetTransaction()")); + + mWnd = nullptr; + mAccumulatedDelta = 0; +} + +void MouseScrollHandler::LastEventInfo::RecordEvent(const EventInfo& aEvent) { + mWnd = aEvent.GetWindowHandle(); + mDelta = aEvent.GetNativeDelta(); + mIsVertical = aEvent.IsVertical(); + mIsPage = aEvent.IsPage(); + mTimeStamp = TimeStamp::Now(); +} + +/* static */ +int32_t MouseScrollHandler::LastEventInfo::RoundDelta(double aDelta) { + return (aDelta >= 0) ? (int32_t)floor(aDelta) : (int32_t)ceil(aDelta); +} + +bool MouseScrollHandler::LastEventInfo::InitWheelEvent( + nsWindow* aWidget, WidgetWheelEvent& aWheelEvent, + const ModifierKeyState& aModKeyState, LPARAM aLParam) { + MOZ_ASSERT(aWheelEvent.mMessage == eWheel); + + if (StaticPrefs::mousewheel_ignore_cursor_position_in_lparam()) { + InitEvent(aWidget, aWheelEvent, nullptr); + } else { + InitEvent(aWidget, aWheelEvent, &aLParam); + } + + aModKeyState.InitInputEvent(aWheelEvent); + + // Our positive delta value means to bottom or right. + // But positive native delta value means to top or right. + // Use orienter for computing our delta value with native delta value. + int32_t orienter = mIsVertical ? -1 : 1; + + aWheelEvent.mDeltaMode = mIsPage ? dom::WheelEvent_Binding::DOM_DELTA_PAGE + : dom::WheelEvent_Binding::DOM_DELTA_LINE; + + double ticks = double(mDelta) * orienter / double(WHEEL_DELTA); + if (mIsVertical) { + aWheelEvent.mWheelTicksY = ticks; + } else { + aWheelEvent.mWheelTicksX = ticks; + } + + double& delta = mIsVertical ? aWheelEvent.mDeltaY : aWheelEvent.mDeltaX; + int32_t& lineOrPageDelta = mIsVertical ? aWheelEvent.mLineOrPageDeltaY + : aWheelEvent.mLineOrPageDeltaX; + + double nativeDeltaPerUnit = + mIsPage ? double(WHEEL_DELTA) : double(WHEEL_DELTA) / GetScrollAmount(); + + delta = double(mDelta) * orienter / nativeDeltaPerUnit; + mAccumulatedDelta += mDelta; + lineOrPageDelta = + mAccumulatedDelta * orienter / RoundDelta(nativeDeltaPerUnit); + mAccumulatedDelta -= + lineOrPageDelta * orienter * RoundDelta(nativeDeltaPerUnit); + + if (aWheelEvent.mDeltaMode != dom::WheelEvent_Binding::DOM_DELTA_LINE) { + // If the scroll delta mode isn't per line scroll, we shouldn't allow to + // override the system scroll speed setting. + aWheelEvent.mAllowToOverrideSystemScrollSpeed = false; + } + + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScroll::LastEventInfo::InitWheelEvent: aWidget=%p, " + "aWheelEvent { mRefPoint: { x: %d, y: %d }, mDeltaX: %f, mDeltaY: %f, " + "mLineOrPageDeltaX: %d, mLineOrPageDeltaY: %d, " + "isShift: %s, isControl: %s, isAlt: %s, isMeta: %s, " + "mAllowToOverrideSystemScrollSpeed: %s }, " + "mAccumulatedDelta: %d", + aWidget, aWheelEvent.mRefPoint.x.value, aWheelEvent.mRefPoint.y.value, + aWheelEvent.mDeltaX, aWheelEvent.mDeltaY, aWheelEvent.mLineOrPageDeltaX, + aWheelEvent.mLineOrPageDeltaY, GetBoolName(aWheelEvent.IsShift()), + GetBoolName(aWheelEvent.IsControl()), GetBoolName(aWheelEvent.IsAlt()), + GetBoolName(aWheelEvent.IsMeta()), + GetBoolName(aWheelEvent.mAllowToOverrideSystemScrollSpeed), + mAccumulatedDelta)); + + return (delta != 0); +} + +/****************************************************************************** + * + * SystemSettings + * + ******************************************************************************/ + +void MouseScrollHandler::SystemSettings::Init() { + if (mInitialized) { + return; + } + + InitScrollLines(); + InitScrollChars(); + + mInitialized = true; + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::SystemSettings::Init(): initialized, " + "mScrollLines=%d, mScrollChars=%d", + mScrollLines, mScrollChars)); +} + +bool MouseScrollHandler::SystemSettings::InitScrollLines() { + int32_t oldValue = mInitialized ? mScrollLines : 0; + mIsReliableScrollLines = false; + mScrollLines = MouseScrollHandler::sInstance->mUserPrefs + .GetOverriddenVerticalScrollAmout(); + if (mScrollLines >= 0) { + // overridden by the pref. + mIsReliableScrollLines = true; + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::SystemSettings::InitScrollLines(): mScrollLines is " + "overridden by the pref: %d", + mScrollLines)); + } else if (!::SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &mScrollLines, + 0)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::SystemSettings::InitScrollLines(): " + "::SystemParametersInfo(" + "SPI_GETWHEELSCROLLLINES) failed")); + mScrollLines = DefaultScrollLines(); + } + + if (mScrollLines > WHEEL_DELTA) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::SystemSettings::InitScrollLines(): the result of " + "::SystemParametersInfo(SPI_GETWHEELSCROLLLINES) is too large: %d", + mScrollLines)); + // sScrollLines usually equals 3 or 0 (for no scrolling) + // However, if sScrollLines > WHEEL_DELTA, we assume that + // the mouse driver wants a page scroll. The docs state that + // sScrollLines should explicitly equal WHEEL_PAGESCROLL, but + // since some mouse drivers use an arbitrary large number instead, + // we have to handle that as well. + mScrollLines = WHEEL_PAGESCROLL; + } + + return oldValue != mScrollLines; +} + +bool MouseScrollHandler::SystemSettings::InitScrollChars() { + int32_t oldValue = mInitialized ? mScrollChars : 0; + mIsReliableScrollChars = false; + mScrollChars = MouseScrollHandler::sInstance->mUserPrefs + .GetOverriddenHorizontalScrollAmout(); + if (mScrollChars >= 0) { + // overridden by the pref. + mIsReliableScrollChars = true; + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::SystemSettings::InitScrollChars(): mScrollChars is " + "overridden by the pref: %d", + mScrollChars)); + } else if (!::SystemParametersInfo(SPI_GETWHEELSCROLLCHARS, 0, &mScrollChars, + 0)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::SystemSettings::InitScrollChars(): " + "::SystemParametersInfo(" + "SPI_GETWHEELSCROLLCHARS) failed, this is unexpected on Vista or " + "later")); + // XXX Should we use DefaultScrollChars()? + mScrollChars = 1; + } + + if (mScrollChars > WHEEL_DELTA) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::SystemSettings::InitScrollChars(): the result of " + "::SystemParametersInfo(SPI_GETWHEELSCROLLCHARS) is too large: %d", + mScrollChars)); + // See the comments for the case mScrollLines > WHEEL_DELTA. + mScrollChars = WHEEL_PAGESCROLL; + } + + return oldValue != mScrollChars; +} + +void MouseScrollHandler::SystemSettings::MarkDirty() { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScrollHandler::SystemSettings::MarkDirty(): " + "Marking SystemSettings dirty")); + mInitialized = false; + // When system settings are changed, we should reset current transaction. + MOZ_ASSERT(sInstance, + "Must not be called at initializing MouseScrollHandler"); + MouseScrollHandler::sInstance->mLastEventInfo.ResetTransaction(); +} + +void MouseScrollHandler::SystemSettings::RefreshCache() { + bool isChanged = InitScrollLines(); + isChanged = InitScrollChars() || isChanged; + if (!isChanged) { + return; + } + // If the scroll amount is changed, we should reset current transaction. + MOZ_ASSERT(sInstance, + "Must not be called at initializing MouseScrollHandler"); + MouseScrollHandler::sInstance->mLastEventInfo.ResetTransaction(); +} + +void MouseScrollHandler::SystemSettings::TrustedScrollSettingsDriver() { + if (!mInitialized) { + return; + } + + // if the cache is initialized with prefs, we don't need to refresh it. + if (mIsReliableScrollLines && mIsReliableScrollChars) { + return; + } + + MouseScrollHandler::UserPrefs& userPrefs = + MouseScrollHandler::sInstance->mUserPrefs; + + // If system settings cache is disabled, we should always refresh them. + if (!userPrefs.IsSystemSettingCacheEnabled()) { + RefreshCache(); + return; + } + + // If pref is set to as "always trust the cache", we shouldn't refresh them + // in any environments. + if (userPrefs.IsSystemSettingCacheForciblyEnabled()) { + return; + } + + // If SynTP of Synaptics or Apoint of Alps is installed, it may hook + // ::SystemParametersInfo() and returns different value from system settings. + if (Device::SynTP::IsDriverInstalled() || + Device::Apoint::IsDriverInstalled()) { + RefreshCache(); + return; + } + + // XXX We're not sure about other touchpad drivers... +} + +/****************************************************************************** + * + * UserPrefs + * + ******************************************************************************/ + +MouseScrollHandler::UserPrefs::UserPrefs() : mInitialized(false) { + // We need to reset mouse wheel transaction when all of mousewheel related + // prefs are changed. + DebugOnly<nsresult> rv = + Preferences::RegisterPrefixCallback(OnChange, "mousewheel.", this); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to register callback for mousewheel."); +} + +MouseScrollHandler::UserPrefs::~UserPrefs() { + DebugOnly<nsresult> rv = + Preferences::UnregisterPrefixCallback(OnChange, "mousewheel.", this); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to unregister callback for mousewheel."); +} + +void MouseScrollHandler::UserPrefs::Init() { + if (mInitialized) { + return; + } + + mInitialized = true; + + mScrollMessageHandledAsWheelMessage = + Preferences::GetBool("mousewheel.emulate_at_wm_scroll", false); + mEnableSystemSettingCache = + Preferences::GetBool("mousewheel.system_settings_cache.enabled", true); + mForceEnableSystemSettingCache = Preferences::GetBool( + "mousewheel.system_settings_cache.force_enabled", false); + mEmulateToMakeWindowUnderCursorForeground = Preferences::GetBool( + "mousewheel.debug.make_window_under_cursor_foreground", false); + mOverriddenVerticalScrollAmount = + Preferences::GetInt("mousewheel.windows.vertical_amount_override", -1); + mOverriddenHorizontalScrollAmount = + Preferences::GetInt("mousewheel.windows.horizontal_amount_override", -1); + mMouseScrollTransactionTimeout = Preferences::GetInt( + "mousewheel.windows.transaction.timeout", DEFAULT_TIMEOUT_DURATION); + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::UserPrefs::Init(): initialized, " + "mScrollMessageHandledAsWheelMessage=%s, " + "mEnableSystemSettingCache=%s, " + "mForceEnableSystemSettingCache=%s, " + "mEmulateToMakeWindowUnderCursorForeground=%s, " + "mOverriddenVerticalScrollAmount=%d, " + "mOverriddenHorizontalScrollAmount=%d, " + "mMouseScrollTransactionTimeout=%d", + GetBoolName(mScrollMessageHandledAsWheelMessage), + GetBoolName(mEnableSystemSettingCache), + GetBoolName(mForceEnableSystemSettingCache), + GetBoolName(mEmulateToMakeWindowUnderCursorForeground), + mOverriddenVerticalScrollAmount, mOverriddenHorizontalScrollAmount, + mMouseScrollTransactionTimeout)); +} + +void MouseScrollHandler::UserPrefs::MarkDirty() { + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScrollHandler::UserPrefs::MarkDirty(): Marking UserPrefs dirty")); + mInitialized = false; + // Some prefs might override system settings, so, we should mark them dirty. + MouseScrollHandler::sInstance->mSystemSettings.MarkDirty(); + // When user prefs for mousewheel are changed, we should reset current + // transaction. + MOZ_ASSERT(sInstance, + "Must not be called at initializing MouseScrollHandler"); + MouseScrollHandler::sInstance->mLastEventInfo.ResetTransaction(); +} + +/****************************************************************************** + * + * Device + * + ******************************************************************************/ + +/* static */ +bool MouseScrollHandler::Device::GetWorkaroundPref(const char* aPrefName, + bool aValueIfAutomatic) { + if (!aPrefName) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::GetWorkaroundPref(): Failed, aPrefName is " + "NULL")); + return aValueIfAutomatic; + } + + int32_t lHackValue = 0; + if (NS_FAILED(Preferences::GetInt(aPrefName, &lHackValue))) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::GetWorkaroundPref(): Preferences::GetInt() " + "failed," + " aPrefName=\"%s\", aValueIfAutomatic=%s", + aPrefName, GetBoolName(aValueIfAutomatic))); + return aValueIfAutomatic; + } + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::GetWorkaroundPref(): Succeeded, " + "aPrefName=\"%s\", aValueIfAutomatic=%s, lHackValue=%d", + aPrefName, GetBoolName(aValueIfAutomatic), lHackValue)); + + switch (lHackValue) { + case 0: // disabled + return false; + case 1: // enabled + return true; + default: // -1: autodetect + return aValueIfAutomatic; + } +} + +/* static */ +void MouseScrollHandler::Device::Init() { + // FYI: Thinkpad's TrackPoint is Apoint of Alps and UltraNav is SynTP of + // Synaptics. So, those drivers' information should be initialized + // before calling methods of TrackPoint and UltraNav. + SynTP::Init(); + Elantech::Init(); + Apoint::Init(); + + sFakeScrollableWindowNeeded = GetWorkaroundPref( + "ui.trackpoint_hack.enabled", (TrackPoint::IsDriverInstalled() || + UltraNav::IsObsoleteDriverInstalled())); + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::Init(): sFakeScrollableWindowNeeded=%s", + GetBoolName(sFakeScrollableWindowNeeded))); +} + +/****************************************************************************** + * + * Device::SynTP + * + ******************************************************************************/ + +/* static */ +void MouseScrollHandler::Device::SynTP::Init() { + if (sInitialized) { + return; + } + + sInitialized = true; + sMajorVersion = 0; + sMinorVersion = -1; + + wchar_t buf[40]; + if (!WinRegistry::GetString( + HKEY_LOCAL_MACHINE, u"Software\\Synaptics\\SynTP\\Install"_ns, + u"DriverVersion"_ns, buf, WinRegistry::kLegacyWinUtilsStringFlags)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::SynTP::Init(): " + "SynTP driver is not found")); + return; + } + + sMajorVersion = wcstol(buf, nullptr, 10); + sMinorVersion = 0; + wchar_t* p = wcschr(buf, L'.'); + if (p) { + sMinorVersion = wcstol(p + 1, nullptr, 10); + } + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::SynTP::Init(): " + "found driver version = %d.%d", + sMajorVersion, sMinorVersion)); +} + +/****************************************************************************** + * + * Device::Elantech + * + ******************************************************************************/ + +/* static */ +void MouseScrollHandler::Device::Elantech::Init() { + int32_t version = GetDriverMajorVersion(); + bool needsHack = Device::GetWorkaroundPref( + "ui.elantech_gesture_hacks.enabled", version != 0); + sUseSwipeHack = needsHack && version <= 7; + sUsePinchHack = needsHack && version <= 8; + + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::Elantech::Init(): version=%d, sUseSwipeHack=%s, " + "sUsePinchHack=%s", + version, GetBoolName(sUseSwipeHack), GetBoolName(sUsePinchHack))); +} + +/* static */ +int32_t MouseScrollHandler::Device::Elantech::GetDriverMajorVersion() { + wchar_t buf[40]; + // The driver version is found in one of these two registry keys. + if (!WinRegistry::GetString( + HKEY_CURRENT_USER, u"Software\\Elantech\\MainOption"_ns, + u"DriverVersion"_ns, buf, WinRegistry::kLegacyWinUtilsStringFlags) && + !WinRegistry::GetString(HKEY_CURRENT_USER, u"Software\\Elantech"_ns, + u"DriverVersion"_ns, buf, + WinRegistry::kLegacyWinUtilsStringFlags)) { + return 0; + } + + // Assume that the major version number can be found just after a space + // or at the start of the string. + for (wchar_t* p = buf; *p; p++) { + if (*p >= L'0' && *p <= L'9' && (p == buf || *(p - 1) == L' ')) { + return wcstol(p, nullptr, 10); + } + } + + return 0; +} + +/* static */ +bool MouseScrollHandler::Device::Elantech::IsHelperWindow(HWND aWnd) { + // The helper window cannot be distinguished based on its window class, so we + // need to check if it is owned by the helper process, ETDCtrl.exe. + + const wchar_t* filenameSuffix = L"\\etdctrl.exe"; + const int filenameSuffixLength = 12; + + DWORD pid; + ::GetWindowThreadProcessId(aWnd, &pid); + + HANDLE hProcess = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); + if (!hProcess) { + return false; + } + + bool result = false; + wchar_t path[256] = {L'\0'}; + if (::GetProcessImageFileNameW(hProcess, path, ArrayLength(path))) { + int pathLength = lstrlenW(path); + if (pathLength >= filenameSuffixLength) { + if (lstrcmpiW(path + pathLength - filenameSuffixLength, filenameSuffix) == + 0) { + result = true; + } + } + } + ::CloseHandle(hProcess); + + return result; +} + +/* static */ +bool MouseScrollHandler::Device::Elantech::HandleKeyMessage(nsWindow* aWidget, + UINT aMsg, + WPARAM aWParam, + LPARAM aLParam) { + // The Elantech touchpad driver understands three-finger swipe left and + // right gestures, and translates them into Page Up and Page Down key + // events for most applications. For Firefox 3.6, it instead sends + // Alt+Left and Alt+Right to trigger browser back/forward actions. As + // with the Thinkpad Driver hack in nsWindow::Create, the change in + // HWND structure makes Firefox not trigger the driver's heuristics + // any longer. + // + // The Elantech driver actually sends these messages for a three-finger + // swipe right: + // + // WM_KEYDOWN virtual_key = 0xCC or 0xFF ScanCode = 00 + // WM_KEYDOWN virtual_key = VK_NEXT ScanCode = 00 + // WM_KEYUP virtual_key = VK_NEXT ScanCode = 00 + // WM_KEYUP virtual_key = 0xCC or 0xFF ScanCode = 00 + // + // Whether 0xCC or 0xFF is sent is suspected to depend on the driver + // version. 7.0.4.12_14Jul09_WHQL, 7.0.5.10, and 7.0.6.0 generate 0xCC. + // 7.0.4.3 from Asus on EeePC generates 0xFF. + // + // On some hardware, IS_VK_DOWN(0xFF) returns true even when Elantech + // messages are not involved, meaning that alone is not enough to + // distinguish the gesture from a regular Page Up or Page Down key press. + // The ScanCode is therefore also tested to detect the gesture. + // We then pretend that we should dispatch "Go Forward" command. Similarly + // for VK_PRIOR and "Go Back" command. + if (sUseSwipeHack && (aWParam == VK_NEXT || aWParam == VK_PRIOR) && + WinUtils::GetScanCode(aLParam) == 0 && + (IS_VK_DOWN(0xFF) || IS_VK_DOWN(0xCC))) { + if (aMsg == WM_KEYDOWN) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::Elantech::HandleKeyMessage(): Dispatching " + "%s command event", + aWParam == VK_NEXT ? "Forward" : "Back")); + + WidgetCommandEvent appCommandEvent( + true, (aWParam == VK_NEXT) ? nsGkAtoms::Forward : nsGkAtoms::Back, + aWidget); + + // In this scenario, the coordinate of the event isn't supplied, so pass + // nullptr as an argument to indicate using the coordinate from the last + // available window message. + InitEvent(aWidget, appCommandEvent, nullptr); + aWidget->DispatchWindowEvent(appCommandEvent); + } else { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::Elantech::HandleKeyMessage(): Consumed")); + } + return true; // consume the message (doesn't need to dispatch key events) + } + + // Version 8 of the Elantech touchpad driver sends these messages for + // zoom gestures: + // + // WM_KEYDOWN virtual_key = 0xCC time = 10 + // WM_KEYDOWN virtual_key = VK_CONTROL time = 10 + // WM_MOUSEWHEEL time = ::GetTickCount() + // WM_KEYUP virtual_key = VK_CONTROL time = 10 + // WM_KEYUP virtual_key = 0xCC time = 10 + // + // The result of this is that we process all of the WM_KEYDOWN/WM_KEYUP + // messages first because their timestamps make them appear to have + // been sent before the WM_MOUSEWHEEL message. To work around this, + // we store the current time when we process the WM_KEYUP message and + // assume that any WM_MOUSEWHEEL message with a timestamp before that + // time is one that should be processed as if the Control key was down. + if (sUsePinchHack && aMsg == WM_KEYUP && aWParam == VK_CONTROL && + ::GetMessageTime() == 10) { + // We look only at the bottom 31 bits of the system tick count since + // GetMessageTime returns a LONG, which is signed, so we want values + // that are more easily comparable. + sZoomUntil = ::GetTickCount() & 0x7FFFFFFF; + + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::Elantech::HandleKeyMessage(): sZoomUntil=%lu", + sZoomUntil)); + } + + return false; +} + +/* static */ +void MouseScrollHandler::Device::Elantech::UpdateZoomUntil() { + if (!sZoomUntil) { + return; + } + + // For the Elantech Touchpad Zoom Gesture Hack, we should check that the + // system time (32-bit milliseconds) hasn't wrapped around. Otherwise we + // might get into the situation where wheel events for the next 50 days of + // system uptime are assumed to be Ctrl+Wheel events. (It is unlikely that + // we would get into that state, because the system would already need to be + // up for 50 days and the Control key message would need to be processed just + // before the system time overflow and the wheel message just after.) + // + // We also take the chance to reset sZoomUntil if we simply have passed that + // time. + LONG msgTime = ::GetMessageTime(); + if ((sZoomUntil >= 0x3fffffffu && DWORD(msgTime) < 0x40000000u) || + (sZoomUntil < DWORD(msgTime))) { + sZoomUntil = 0; + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::Elantech::UpdateZoomUntil(): " + "sZoomUntil was reset")); + } +} + +/* static */ +bool MouseScrollHandler::Device::Elantech::IsZooming() { + // Assume the Control key is down if the Elantech touchpad has sent the + // mis-ordered WM_KEYDOWN/WM_MOUSEWHEEL messages. (See the comment in + // OnKeyUp.) + return (sZoomUntil && static_cast<DWORD>(::GetMessageTime()) < sZoomUntil); +} + +/****************************************************************************** + * + * Device::Apoint + * + ******************************************************************************/ + +/* static */ +void MouseScrollHandler::Device::Apoint::Init() { + if (sInitialized) { + return; + } + + sInitialized = true; + sMajorVersion = 0; + sMinorVersion = -1; + + wchar_t buf[40]; + if (!WinRegistry::GetString(HKEY_LOCAL_MACHINE, u"Software\\Alps\\Apoint"_ns, + u"ProductVer"_ns, buf, + WinRegistry::kLegacyWinUtilsStringFlags)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::Apoint::Init(): " + "Apoint driver is not found")); + return; + } + + sMajorVersion = wcstol(buf, nullptr, 10); + sMinorVersion = 0; + wchar_t* p = wcschr(buf, L'.'); + if (p) { + sMinorVersion = wcstol(p + 1, nullptr, 10); + } + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::Apoint::Init(): " + "found driver version = %d.%d", + sMajorVersion, sMinorVersion)); +} + +/****************************************************************************** + * + * Device::TrackPoint + * + ******************************************************************************/ + +/* static */ +bool MouseScrollHandler::Device::TrackPoint::IsDriverInstalled() { + if (WinRegistry::HasKey(HKEY_CURRENT_USER, + u"Software\\Lenovo\\TrackPoint"_ns)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::TrackPoint::IsDriverInstalled(): " + "Lenovo's TrackPoint driver is found")); + return true; + } + + if (WinRegistry::HasKey(HKEY_CURRENT_USER, + u"Software\\Alps\\Apoint\\TrackPoint"_ns)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::TrackPoint::IsDriverInstalled(): " + "Alps's TrackPoint driver is found")); + return true; + } + + return false; +} + +/****************************************************************************** + * + * Device::UltraNav + * + ******************************************************************************/ + +/* static */ +bool MouseScrollHandler::Device::UltraNav::IsObsoleteDriverInstalled() { + if (WinRegistry::HasKey(HKEY_CURRENT_USER, + u"Software\\Lenovo\\UltraNav"_ns)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::UltraNav::IsObsoleteDriverInstalled(): " + "Lenovo's UltraNav driver is found")); + return true; + } + + bool installed = false; + if (WinRegistry::HasKey(HKEY_CURRENT_USER, + u"Software\\Synaptics\\SynTPEnh\\UltraNavUSB"_ns)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::UltraNav::IsObsoleteDriverInstalled(): " + "Synaptics's UltraNav (USB) driver is found")); + installed = true; + } else if (WinRegistry::HasKey( + HKEY_CURRENT_USER, + u"Software\\Synaptics\\SynTPEnh\\UltraNavPS2"_ns)) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::UltraNav::IsObsoleteDriverInstalled(): " + "Synaptics's UltraNav (PS/2) driver is found")); + installed = true; + } + + if (!installed) { + return false; + } + + int32_t majorVersion = Device::SynTP::GetDriverMajorVersion(); + if (!majorVersion) { + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::UltraNav::IsObsoleteDriverInstalled(): " + "Failed to get UltraNav driver version")); + return false; + } + int32_t minorVersion = Device::SynTP::GetDriverMinorVersion(); + return majorVersion < 15 || (majorVersion == 15 && minorVersion == 0); +} + +/****************************************************************************** + * + * Device::SetPoint + * + ******************************************************************************/ + +/* static */ +bool MouseScrollHandler::Device::SetPoint::IsGetMessagePosResponseValid( + UINT aMessage, WPARAM aWParam, LPARAM aLParam) { + if (aMessage != WM_MOUSEHWHEEL) { + return false; + } + + POINTS pts = MouseScrollHandler::GetCurrentMessagePos(); + LPARAM messagePos = MAKELPARAM(pts.x, pts.y); + + // XXX We should check whether SetPoint is installed or not by registry. + + // SetPoint, Logitech (Logicool) mouse driver, (confirmed with 4.82.11 and + // MX-1100) always sets 0 to the lParam of WM_MOUSEHWHEEL. The driver SENDs + // one message at first time, this time, ::GetMessagePos() works fine. + // Then, we will return 0 (0 means we process it) to the message. Then, the + // driver will POST the same messages continuously during the wheel tilted. + // But ::GetMessagePos() API always returns (0, 0) for them, even if the + // actual mouse cursor isn't 0,0. Therefore, we cannot trust the result of + // ::GetMessagePos API if the sender is SetPoint. + if (!sMightBeUsing && !aLParam && aLParam != messagePos && + ::InSendMessage()) { + sMightBeUsing = true; + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::SetPoint::IsGetMessagePosResponseValid(): " + "Might using SetPoint")); + } else if (sMightBeUsing && aLParam != 0 && ::InSendMessage()) { + // The user has changed the mouse from Logitech's to another one (e.g., + // the user has changed to the touchpad of the notebook. + sMightBeUsing = false; + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScroll::Device::SetPoint::IsGetMessagePosResponseValid(): " + "Might stop using SetPoint")); + } + return (sMightBeUsing && !aLParam && !messagePos); +} + +/****************************************************************************** + * + * SynthesizingEvent + * + ******************************************************************************/ + +/* static */ +bool MouseScrollHandler::SynthesizingEvent::IsSynthesizing() { + return MouseScrollHandler::sInstance && + MouseScrollHandler::sInstance->mSynthesizingEvent && + MouseScrollHandler::sInstance->mSynthesizingEvent->mStatus != + NOT_SYNTHESIZING; +} + +nsresult MouseScrollHandler::SynthesizingEvent::Synthesize( + const POINTS& aCursorPoint, HWND aWnd, UINT aMessage, WPARAM aWParam, + LPARAM aLParam, const BYTE (&aKeyStates)[256]) { + MOZ_LOG( + gMouseScrollLog, LogLevel::Info, + ("MouseScrollHandler::SynthesizingEvent::Synthesize(): aCursorPoint: { " + "x: %d, y: %d }, aWnd=0x%p, aMessage=0x%04X, aWParam=0x%08zX, " + "aLParam=0x%08" PRIXLPTR ", IsSynthesized()=%s, mStatus=%s", + aCursorPoint.x, aCursorPoint.y, aWnd, aMessage, aWParam, aLParam, + GetBoolName(IsSynthesizing()), GetStatusName())); + + if (IsSynthesizing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + ::GetKeyboardState(mOriginalKeyState); + + // Note that we cannot use ::SetCursorPos() because it works asynchronously. + // We should SEND the message for reducing the possibility of receiving + // unexpected message which were not sent from here. + mCursorPoint = aCursorPoint; + + mWnd = aWnd; + mMessage = aMessage; + mWParam = aWParam; + mLParam = aLParam; + + memcpy(mKeyState, aKeyStates, sizeof(mKeyState)); + ::SetKeyboardState(mKeyState); + + mStatus = SENDING_MESSAGE; + + // Don't assume that aWnd is always managed by nsWindow. It might be + // a plugin window. + ::SendMessage(aWnd, aMessage, aWParam, aLParam); + + return NS_OK; +} + +void MouseScrollHandler::SynthesizingEvent::NativeMessageReceived( + nsWindow* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) { + if (mStatus == SENDING_MESSAGE && mMessage == aMessage && + mWParam == aWParam && mLParam == aLParam) { + mStatus = NATIVE_MESSAGE_RECEIVED; + if (aWidget && aWidget->GetWindowHandle() == mWnd) { + return; + } + // Otherwise, the message may not be sent by us. + } + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScrollHandler::SynthesizingEvent::NativeMessageReceived(): " + "aWidget=%p, aWidget->GetWindowHandle()=0x%p, mWnd=0x%p, " + "aMessage=0x%04X, aWParam=0x%08zX, aLParam=0x%08" PRIXLPTR + ", mStatus=%s", + aWidget, aWidget ? aWidget->GetWindowHandle() : nullptr, mWnd, + aMessage, aWParam, aLParam, GetStatusName())); + + // We failed to receive our sent message, we failed to do the job. + Finish(); + + return; +} + +void MouseScrollHandler::SynthesizingEvent:: + NotifyNativeMessageHandlingFinished() { + if (!IsSynthesizing()) { + return; + } + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScrollHandler::SynthesizingEvent::" + "NotifyNativeMessageHandlingFinished(): IsWaitingInternalMessage=%s", + GetBoolName(MouseScrollHandler::IsWaitingInternalMessage()))); + + if (MouseScrollHandler::IsWaitingInternalMessage()) { + mStatus = INTERNAL_MESSAGE_POSTED; + return; + } + + // If the native message handler didn't post our internal message, + // we our job is finished. + // TODO: When we post the message to plugin window, there is remaning job. + Finish(); +} + +void MouseScrollHandler::SynthesizingEvent:: + NotifyInternalMessageHandlingFinished() { + if (!IsSynthesizing()) { + return; + } + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScrollHandler::SynthesizingEvent::" + "NotifyInternalMessageHandlingFinished()")); + + Finish(); +} + +void MouseScrollHandler::SynthesizingEvent::Finish() { + if (!IsSynthesizing()) { + return; + } + + MOZ_LOG(gMouseScrollLog, LogLevel::Info, + ("MouseScrollHandler::SynthesizingEvent::Finish()")); + + // Restore the original key state. + ::SetKeyboardState(mOriginalKeyState); + + mStatus = NOT_SYNTHESIZING; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/WinMouseScrollHandler.h b/widget/windows/WinMouseScrollHandler.h new file mode 100644 index 0000000000..ecf6c3df44 --- /dev/null +++ b/widget/windows/WinMouseScrollHandler.h @@ -0,0 +1,567 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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_WinMouseScrollHandler_h__ +#define mozilla_widget_WinMouseScrollHandler_h__ + +#include "nscore.h" +#include "nsDebug.h" +#include "mozilla/Assertions.h" +#include "mozilla/EventForwards.h" +#include "mozilla/TimeStamp.h" +#include "Units.h" +#include <windows.h> +#include "nsPoint.h" + +class nsWindow; + +namespace mozilla { +namespace widget { + +class ModifierKeyState; + +struct MSGResult; + +class MouseScrollHandler { + public: + static MouseScrollHandler* GetInstance(); + + static void Initialize(); + static void Shutdown(); + + static bool NeedsMessage(UINT aMsg); + static bool ProcessMessage(nsWindow* aWidget, UINT msg, WPARAM wParam, + LPARAM lParam, MSGResult& aResult); + + /** + * See nsIWidget::SynthesizeNativeMouseScrollEvent() for the detail about + * this method. + */ + static nsresult SynthesizeNativeMouseScrollEvent( + nsWindow* aWidget, const LayoutDeviceIntPoint& aPoint, + uint32_t aNativeMessage, int32_t aDelta, uint32_t aModifierFlags, + uint32_t aAdditionalFlags); + + /** + * IsWaitingInternalMessage() returns true if MouseScrollHandler posted + * an internal message for a native mouse wheel message and has not + * received it. Otherwise, false. + */ + static bool IsWaitingInternalMessage() { + return sInstance && sInstance->mIsWaitingInternalMessage; + } + + private: + MouseScrollHandler(); + ~MouseScrollHandler(); + + bool mIsWaitingInternalMessage; + + static void MaybeLogKeyState(); + + static MouseScrollHandler* sInstance; + + /** + * InitEvent() initializes the aEvent. If aPoint is null, the result of + * GetCurrentMessagePos() will be used. + */ + static void InitEvent(nsWindow* aWidget, WidgetGUIEvent& aEvent, + LPARAM* aPoint); + + /** + * GetModifierKeyState() returns current modifier key state. + * Note that some devices need some hack for the modifier key state. + * This method does it automatically. + * + * @param aMessage Handling message. + */ + static ModifierKeyState GetModifierKeyState(UINT aMessage); + + /** + * MozGetMessagePos() returns the mouse cursor position when GetMessage() + * was called last time. However, if we're sending a native message, + * this returns the specified cursor position by + * SynthesizeNativeMouseScrollEvent(). + */ + static POINTS GetCurrentMessagePos(); + + /** + * ProcessNativeMouseWheelMessage() processes WM_MOUSEWHEEL and + * WM_MOUSEHWHEEL. Additionally, processes WM_VSCROLL and WM_HSCROLL if they + * should be processed as mouse wheel message. + * This method posts MOZ_WM_MOUSEVWHEEL, MOZ_WM_MOUSEHWHEEL, + * MOZ_WM_VSCROLL or MOZ_WM_HSCROLL if we need to dispatch mouse scroll + * events. That avoids deadlock with plugin process. + * + * @param aWidget A window which receives the message. + * @param aMessage WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_VSCROLL or + * WM_HSCROLL. + * @param aWParam The wParam value of the message. + * @param aLParam The lParam value of the message. + */ + void ProcessNativeMouseWheelMessage(nsWindow* aWidget, UINT aMessage, + WPARAM aWParam, LPARAM aLParam); + + /** + * ProcessNativeScrollMessage() processes WM_VSCROLL and WM_HSCROLL. + * This method just call ProcessMouseWheelMessage() if the message should be + * processed as mouse wheel message. Otherwise, dispatches a content + * command event. + * + * @param aWidget A window which receives the message. + * @param aMessage WM_VSCROLL or WM_HSCROLL. + * @param aWParam The wParam value of the message. + * @param aLParam The lParam value of the message. + * @return TRUE if the message is processed. Otherwise, FALSE. + */ + bool ProcessNativeScrollMessage(nsWindow* aWidget, UINT aMessage, + WPARAM aWParam, LPARAM aLParam); + + /** + * HandleMouseWheelMessage() processes MOZ_WM_MOUSEVWHEEL and + * MOZ_WM_MOUSEHWHEEL which are posted when one of our windows received + * WM_MOUSEWHEEL or WM_MOUSEHWHEEL for avoiding deadlock with OOPP. + * + * @param aWidget A window which receives the wheel message. + * @param aMessage MOZ_WM_MOUSEWHEEL or MOZ_WM_MOUSEHWHEEL. + * @param aWParam The wParam value of the original message. + * @param aLParam The lParam value of the original message. + */ + void HandleMouseWheelMessage(nsWindow* aWidget, UINT aMessage, WPARAM aWParam, + LPARAM aLParam); + + /** + * HandleScrollMessageAsMouseWheelMessage() processes the MOZ_WM_VSCROLL and + * MOZ_WM_HSCROLL which are posted when one of mouse windows received + * WM_VSCROLL or WM_HSCROLL and user wants them to emulate mouse wheel + * message's behavior. + * + * @param aWidget A window which receives the scroll message. + * @param aMessage MOZ_WM_VSCROLL or MOZ_WM_HSCROLL. + * @param aWParam The wParam value of the original message. + * @param aLParam The lParam value of the original message. + */ + void HandleScrollMessageAsMouseWheelMessage(nsWindow* aWidget, UINT aMessage, + WPARAM aWParam, LPARAM aLParam); + + /** + * ComputeMessagePos() computes the cursor position when the message was + * added to the queue. + * + * @param aMessage Handling message. + * @param aWParam Handling message's wParam. + * @param aLParam Handling message's lParam. + * @return Mouse cursor position when the message is added to + * the queue or current cursor position if the result of + * ::GetMessagePos() is broken. + */ + POINT ComputeMessagePos(UINT aMessage, WPARAM aWParam, LPARAM aLParam); + + class EventInfo { + public: + /** + * @param aWidget An nsWindow which is handling the event. + * @param aMessage Must be WM_MOUSEWHEEL or WM_MOUSEHWHEEL. + */ + EventInfo(nsWindow* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam); + + bool CanDispatchWheelEvent() const; + + int32_t GetNativeDelta() const { return mDelta; } + HWND GetWindowHandle() const { return mWnd; } + const TimeStamp& GetTimeStamp() const { return mTimeStamp; } + bool IsVertical() const { return mIsVertical; } + bool IsPositive() const { return (mDelta > 0); } + bool IsPage() const { return mIsPage; } + + /** + * @return Number of lines or pages scrolled per WHEEL_DELTA. + */ + int32_t GetScrollAmount() const; + + protected: + EventInfo() + : mIsVertical(false), mIsPage(false), mDelta(0), mWnd(nullptr) {} + + // TRUE if event is for vertical scroll. Otherwise, FALSE. + bool mIsVertical; + // TRUE if event scrolls per page, otherwise, FALSE. + bool mIsPage; + // The native delta value. + int32_t mDelta; + // The window handle which is handling the event. + HWND mWnd; + // Timestamp of the event. + TimeStamp mTimeStamp; + }; + + class LastEventInfo : public EventInfo { + public: + LastEventInfo() : EventInfo(), mAccumulatedDelta(0) {} + + /** + * CanContinueTransaction() checks whether the new event can continue the + * last transaction or not. Note that if there is no transaction, this + * returns true. + */ + bool CanContinueTransaction(const EventInfo& aNewEvent); + + /** + * ResetTransaction() resets the transaction, i.e., the instance forgets + * the last event information. + */ + void ResetTransaction(); + + /** + * RecordEvent() saves the information of new event. + */ + void RecordEvent(const EventInfo& aEvent); + + /** + * InitWheelEvent() initializes NS_WHEEL_WHEEL event and + * recomputes the remaning detla for the event. + * This must be called only once during handling a message and after + * RecordEvent() is called. + * + * @param aWidget A window which will dispatch the event. + * @param aWheelEvent An NS_WHEEL_WHEEL event, this will be + * initialized. + * @param aModKeyState Current modifier key state. + * @return TRUE if the event is ready to dispatch. + * Otherwise, FALSE. + */ + bool InitWheelEvent(nsWindow* aWidget, WidgetWheelEvent& aWheelEvent, + const ModifierKeyState& aModKeyState, LPARAM aLParam); + + private: + static int32_t RoundDelta(double aDelta); + + int32_t mAccumulatedDelta; + }; + + LastEventInfo mLastEventInfo; + + class SystemSettings { + public: + SystemSettings() : mInitialized(false) {} + + void Init(); + void MarkDirty(); + void NotifyUserPrefsMayOverrideSystemSettings(); + + // On some environments, SystemParametersInfo() may be hooked by touchpad + // utility or something. In such case, when user changes active pointing + // device to another one, the result of SystemParametersInfo() may be + // changed without WM_SETTINGCHANGE message. For avoiding this trouble, + // we need to modify cache of system settings at every wheel message + // handling if we meet known device whose utility may hook the API. + void TrustedScrollSettingsDriver(); + + int32_t GetScrollAmount(bool aForVertical) const { + MOZ_ASSERT(mInitialized, "SystemSettings must be initialized"); + return aForVertical ? mScrollLines : mScrollChars; + } + + bool IsPageScroll(bool aForVertical) const { + MOZ_ASSERT(mInitialized, "SystemSettings must be initialized"); + return aForVertical ? (uint32_t(mScrollLines) == WHEEL_PAGESCROLL) + : (uint32_t(mScrollChars) == WHEEL_PAGESCROLL); + } + + // The default vertical and horizontal scrolling speed is 3, this is defined + // on the document of SystemParametersInfo in MSDN. + static int32_t DefaultScrollLines() { return 3; } + + private: + bool mInitialized; + // The result of SystemParametersInfo() may not be reliable since it may + // be hooked. So, if the values are initialized with prefs, we can trust + // the value. Following mIsReliableScroll* are set true when mScroll* are + // initialized with prefs. + bool mIsReliableScrollLines; + bool mIsReliableScrollChars; + + int32_t mScrollLines; + int32_t mScrollChars; + + // Returns true if cached value is changed. + bool InitScrollLines(); + bool InitScrollChars(); + + void RefreshCache(); + }; + + SystemSettings mSystemSettings; + + class UserPrefs { + public: + UserPrefs(); + ~UserPrefs(); + + void MarkDirty(); + + bool IsScrollMessageHandledAsWheelMessage() { + Init(); + return mScrollMessageHandledAsWheelMessage; + } + + bool IsSystemSettingCacheEnabled() { + Init(); + return mEnableSystemSettingCache; + } + + bool IsSystemSettingCacheForciblyEnabled() { + Init(); + return mForceEnableSystemSettingCache; + } + + bool ShouldEmulateToMakeWindowUnderCursorForeground() { + Init(); + return mEmulateToMakeWindowUnderCursorForeground; + } + + int32_t GetOverriddenVerticalScrollAmout() { + Init(); + return mOverriddenVerticalScrollAmount; + } + + int32_t GetOverriddenHorizontalScrollAmout() { + Init(); + return mOverriddenHorizontalScrollAmount; + } + + int32_t GetMouseScrollTransactionTimeout() { + Init(); + return mMouseScrollTransactionTimeout; + } + + private: + void Init(); + + static void OnChange(const char* aPrefName, void* aSelf) { + static_cast<UserPrefs*>(aSelf)->MarkDirty(); + } + + bool mInitialized; + bool mScrollMessageHandledAsWheelMessage; + bool mEnableSystemSettingCache; + bool mForceEnableSystemSettingCache; + bool mEmulateToMakeWindowUnderCursorForeground; + int32_t mOverriddenVerticalScrollAmount; + int32_t mOverriddenHorizontalScrollAmount; + int32_t mMouseScrollTransactionTimeout; + }; + + UserPrefs mUserPrefs; + + class SynthesizingEvent { + public: + SynthesizingEvent() + : mWnd(nullptr), + mMessage(0), + mWParam(0), + mLParam(0), + mStatus(NOT_SYNTHESIZING) {} + + ~SynthesizingEvent() {} + + static bool IsSynthesizing(); + + nsresult Synthesize(const POINTS& aCursorPoint, HWND aWnd, UINT aMessage, + WPARAM aWParam, LPARAM aLParam, + const BYTE (&aKeyStates)[256]); + + void NativeMessageReceived(nsWindow* aWidget, UINT aMessage, WPARAM aWParam, + LPARAM aLParam); + + void NotifyNativeMessageHandlingFinished(); + void NotifyInternalMessageHandlingFinished(); + + const POINTS& GetCursorPoint() const { return mCursorPoint; } + + private: + POINTS mCursorPoint; + HWND mWnd; + UINT mMessage; + WPARAM mWParam; + LPARAM mLParam; + BYTE mKeyState[256]; + BYTE mOriginalKeyState[256]; + + enum Status { + NOT_SYNTHESIZING, + SENDING_MESSAGE, + NATIVE_MESSAGE_RECEIVED, + INTERNAL_MESSAGE_POSTED, + }; + Status mStatus; + + const char* GetStatusName() { + switch (mStatus) { + case NOT_SYNTHESIZING: + return "NOT_SYNTHESIZING"; + case SENDING_MESSAGE: + return "SENDING_MESSAGE"; + case NATIVE_MESSAGE_RECEIVED: + return "NATIVE_MESSAGE_RECEIVED"; + case INTERNAL_MESSAGE_POSTED: + return "INTERNAL_MESSAGE_POSTED"; + default: + return "Unknown"; + } + } + + void Finish(); + }; // SynthesizingEvent + + SynthesizingEvent* mSynthesizingEvent; + + public: + class Device { + public: + // SynTP is a touchpad driver of Synaptics. + class SynTP { + public: + static bool IsDriverInstalled() { return sMajorVersion != 0; } + /** + * GetDriverMajorVersion() returns the installed driver's major version. + * If SynTP driver isn't installed, this returns 0. + */ + static int32_t GetDriverMajorVersion() { return sMajorVersion; } + /** + * GetDriverMinorVersion() returns the installed driver's minor version. + * If SynTP driver isn't installed, this returns -1. + */ + static int32_t GetDriverMinorVersion() { return sMinorVersion; } + + static void Init(); + + private: + static bool sInitialized; + static int32_t sMajorVersion; + static int32_t sMinorVersion; + }; + + class Elantech { + public: + /** + * GetDriverMajorVersion() returns the installed driver's major version. + * If Elantech's driver was installed, returns 0. + */ + static int32_t GetDriverMajorVersion(); + + /** + * IsHelperWindow() checks whether aWnd is a helper window of Elantech's + * touchpad. Returns TRUE if so. Otherwise, FALSE. + */ + static bool IsHelperWindow(HWND aWnd); + + /** + * Key message handler for Elantech's hack. Returns TRUE if the message + * is consumed by this handler. Otherwise, FALSE. + */ + static bool HandleKeyMessage(nsWindow* aWidget, UINT aMsg, WPARAM aWParam, + LPARAM aLParam); + + static void UpdateZoomUntil(); + static bool IsZooming(); + + static void Init(); + + static bool IsPinchHackNeeded() { return sUsePinchHack; } + + private: + // Whether to enable the Elantech swipe gesture hack. + static bool sUseSwipeHack; + // Whether to enable the Elantech pinch-to-zoom gesture hack. + static bool sUsePinchHack; + static DWORD sZoomUntil; + }; // class Elantech + + // Apoint is a touchpad driver of Alps. + class Apoint { + public: + static bool IsDriverInstalled() { return sMajorVersion != 0; } + /** + * GetDriverMajorVersion() returns the installed driver's major version. + * If Apoint driver isn't installed, this returns 0. + */ + static int32_t GetDriverMajorVersion() { return sMajorVersion; } + /** + * GetDriverMinorVersion() returns the installed driver's minor version. + * If Apoint driver isn't installed, this returns -1. + */ + static int32_t GetDriverMinorVersion() { return sMinorVersion; } + + static void Init(); + + private: + static bool sInitialized; + static int32_t sMajorVersion; + static int32_t sMinorVersion; + }; + + class TrackPoint { + public: + /** + * IsDriverInstalled() returns TRUE if TrackPoint's driver is installed. + * Otherwise, returns FALSE. + */ + static bool IsDriverInstalled(); + }; // class TrackPoint + + class UltraNav { + public: + /** + * IsObsoleteDriverInstalled() checks whether obsoleted UltraNav + * is installed on the environment. + * Returns TRUE if it was installed. Otherwise, FALSE. + */ + static bool IsObsoleteDriverInstalled(); + }; // class UltraNav + + class SetPoint { + public: + /** + * SetPoint, Logitech's mouse driver, may report wrong cursor position + * for WM_MOUSEHWHEEL message. See comment in the implementation for + * the detail. + */ + static bool IsGetMessagePosResponseValid(UINT aMessage, WPARAM aWParam, + LPARAM aLParam); + + private: + static bool sMightBeUsing; + }; + + static void Init(); + + static bool IsFakeScrollableWindowNeeded() { + return sFakeScrollableWindowNeeded; + } + + private: + /** + * Gets the bool value of aPrefName used to enable or disable an input + * workaround (like the Trackpoint hack). The pref can take values 0 (for + * disabled), 1 (for enabled) or -1 (to automatically detect whether to + * enable the workaround). + * + * @param aPrefName The name of the pref. + * @param aValueIfAutomatic Whether the given input workaround should be + * enabled by default. + */ + static bool GetWorkaroundPref(const char* aPrefName, + bool aValueIfAutomatic); + + static bool sFakeScrollableWindowNeeded; + }; // class Device +}; + +} // namespace widget +} // namespace mozilla + +#endif // mozilla_widget_WinMouseScrollHandler_h__ diff --git a/widget/windows/WinPointerEvents.cpp b/widget/windows/WinPointerEvents.cpp new file mode 100644 index 0000000000..57e19a0c4b --- /dev/null +++ b/widget/windows/WinPointerEvents.cpp @@ -0,0 +1,181 @@ +/* -*- 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/. */ + +/* + * WinPointerEvents - Helper functions to retrieve PointerEvent's attributes + */ + +#include "nscore.h" +#include "nsWindowDefs.h" +#include "WinPointerEvents.h" +#include "WinUtils.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/MouseEventBinding.h" + +using namespace mozilla; +using namespace mozilla::widget; + +const wchar_t WinPointerEvents::kPointerLibraryName[] = L"user32.dll"; +HMODULE WinPointerEvents::sLibraryHandle = nullptr; +WinPointerEvents::GetPointerTypePtr WinPointerEvents::getPointerType = nullptr; +WinPointerEvents::GetPointerInfoPtr WinPointerEvents::getPointerInfo = nullptr; +WinPointerEvents::GetPointerPenInfoPtr WinPointerEvents::getPointerPenInfo = + nullptr; + +WinPointerEvents::WinPointerEvents() { InitLibrary(); } + +/* Load and shutdown */ +void WinPointerEvents::InitLibrary() { + MOZ_ASSERT(XRE_IsParentProcess()); + if (getPointerType) { + // Return if we already initialized the PointerEvent related interfaces + return; + } + sLibraryHandle = ::LoadLibraryW(kPointerLibraryName); + MOZ_ASSERT(sLibraryHandle, "cannot load pointer library"); + if (sLibraryHandle) { + getPointerType = + (GetPointerTypePtr)GetProcAddress(sLibraryHandle, "GetPointerType"); + getPointerInfo = + (GetPointerInfoPtr)GetProcAddress(sLibraryHandle, "GetPointerInfo"); + getPointerPenInfo = (GetPointerPenInfoPtr)GetProcAddress( + sLibraryHandle, "GetPointerPenInfo"); + } + + if (!getPointerType || !getPointerInfo || !getPointerPenInfo) { + MOZ_ASSERT(false, "get PointerEvent interfaces failed"); + getPointerType = nullptr; + getPointerInfo = nullptr; + getPointerPenInfo = nullptr; + return; + } +} + +bool WinPointerEvents::ShouldHandleWinPointerMessages(UINT aMsg, + WPARAM aWParam) { + MOZ_ASSERT(aMsg == WM_POINTERDOWN || aMsg == WM_POINTERUP || + aMsg == WM_POINTERUPDATE || aMsg == WM_POINTERLEAVE); + if (!sLibraryHandle) { + return false; + } + + // We only handle WM_POINTER* when the input source is pen. This is because + // we need some information (e.g. tiltX, tiltY) which can't be retrieved by + // WM_*BUTTONDOWN. + uint32_t pointerId = GetPointerId(aWParam); + POINTER_INPUT_TYPE pointerType = PT_POINTER; + if (!GetPointerType(pointerId, &pointerType)) { + MOZ_ASSERT(false, "cannot find PointerType"); + return false; + } + return (pointerType == PT_PEN); +} + +bool WinPointerEvents::GetPointerType(uint32_t aPointerId, + POINTER_INPUT_TYPE* aPointerType) { + if (!getPointerType) { + return false; + } + return getPointerType(aPointerId, aPointerType); +} + +POINTER_INPUT_TYPE +WinPointerEvents::GetPointerType(uint32_t aPointerId) { + POINTER_INPUT_TYPE pointerType = PT_POINTER; + Unused << GetPointerType(aPointerId, &pointerType); + return pointerType; +} + +bool WinPointerEvents::GetPointerInfo(uint32_t aPointerId, + POINTER_INFO* aPointerInfo) { + if (!getPointerInfo) { + return false; + } + return getPointerInfo(aPointerId, aPointerInfo); +} + +bool WinPointerEvents::GetPointerPenInfo(uint32_t aPointerId, + POINTER_PEN_INFO* aPenInfo) { + if (!getPointerPenInfo) { + return false; + } + return getPointerPenInfo(aPointerId, aPenInfo); +} + +bool WinPointerEvents::ShouldRollupOnPointerEvent(UINT aMsg, WPARAM aWParam) { + MOZ_ASSERT(aMsg == WM_POINTERDOWN); + // Only roll up popups when we handling WM_POINTER* to fire Gecko + // WidgetMouseEvent and suppress Windows WM_*BUTTONDOWN. + return ShouldHandleWinPointerMessages(aMsg, aWParam) && + ShouldFirePointerEventByWinPointerMessages(); +} + +bool WinPointerEvents::ShouldFirePointerEventByWinPointerMessages() { + MOZ_ASSERT(sLibraryHandle); + return StaticPrefs::dom_w3c_pointer_events_dispatch_by_pointer_messages(); +} + +WinPointerInfo* WinPointerEvents::GetCachedPointerInfo(UINT aMsg, + WPARAM aWParam) { + if (!sLibraryHandle || + MOUSE_INPUT_SOURCE() != dom::MouseEvent_Binding::MOZ_SOURCE_PEN || + ShouldFirePointerEventByWinPointerMessages()) { + return nullptr; + } + switch (aMsg) { + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + return &mPenPointerDownInfo; + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + return &mPenPointerDownInfo; + case WM_MOUSEMOVE: + return &mPenPointerUpdateInfo; + default: + MOZ_ASSERT(false); + } + return nullptr; +} + +void WinPointerEvents::ConvertAndCachePointerInfo(UINT aMsg, WPARAM aWParam) { + MOZ_ASSERT( + !StaticPrefs::dom_w3c_pointer_events_dispatch_by_pointer_messages()); + // Windows doesn't support chorded buttons for pen, so we can simply keep the + // latest information from pen generated pointer messages and use them when + // handling mouse messages. Used different pointer info for pointerdown, + // pointerupdate, and pointerup because Windows doesn't always interleave + // pointer messages and mouse messages. + switch (aMsg) { + case WM_POINTERDOWN: + ConvertAndCachePointerInfo(aWParam, &mPenPointerDownInfo); + break; + case WM_POINTERUP: + ConvertAndCachePointerInfo(aWParam, &mPenPointerUpInfo); + break; + case WM_POINTERUPDATE: + ConvertAndCachePointerInfo(aWParam, &mPenPointerUpdateInfo); + break; + default: + break; + } +} + +void WinPointerEvents::ConvertAndCachePointerInfo(WPARAM aWParam, + WinPointerInfo* aInfo) { + MOZ_ASSERT( + !StaticPrefs::dom_w3c_pointer_events_dispatch_by_pointer_messages()); + aInfo->pointerId = GetPointerId(aWParam); + MOZ_ASSERT(GetPointerType(aInfo->pointerId) == PT_PEN); + POINTER_PEN_INFO penInfo; + GetPointerPenInfo(aInfo->pointerId, &penInfo); + aInfo->tiltX = penInfo.tiltX; + aInfo->tiltY = penInfo.tiltY; + // Windows defines the pen pressure is normalized to a range between 0 and + // 1024. Convert it to float. + aInfo->mPressure = penInfo.pressure ? (float)penInfo.pressure / 1024 : 0; +} diff --git a/widget/windows/WinPointerEvents.h b/widget/windows/WinPointerEvents.h new file mode 100644 index 0000000000..94179e7b4e --- /dev/null +++ b/widget/windows/WinPointerEvents.h @@ -0,0 +1,75 @@ +/* -*- 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 WinPointerEvents_h__ +#define WinPointerEvents_h__ + +#include "mozilla/MouseEvents.h" +#include "touchinjection_sdk80.h" +#include <windef.h> + +/****************************************************************************** + * WinPointerInfo + * + * This is a helper class to handle WM_POINTER*. It only supports Win8 or later. + * + ******************************************************************************/ +class WinPointerInfo final : public mozilla::WidgetPointerHelper { + public: + WinPointerInfo() : WidgetPointerHelper(), mPressure(0), mButtons(0) {} + + WinPointerInfo(uint32_t aPointerId, uint32_t aTiltX, uint32_t aTiltY, + float aPressure, int16_t aButtons) + : WidgetPointerHelper(aPointerId, aTiltX, aTiltY), + mPressure(aPressure), + mButtons(aButtons) {} + + float mPressure; + int16_t mButtons; +}; + +class WinPointerEvents final { + public: + explicit WinPointerEvents(); + + public: + bool ShouldHandleWinPointerMessages(UINT aMsg, WPARAM aWParam); + + uint32_t GetPointerId(WPARAM aWParam) { + return GET_POINTERID_WPARAM(aWParam); + } + bool GetPointerType(uint32_t aPointerId, POINTER_INPUT_TYPE* aPointerType); + POINTER_INPUT_TYPE GetPointerType(uint32_t aPointerId); + bool GetPointerInfo(uint32_t aPointerId, POINTER_INFO* aPointerInfo); + bool GetPointerPenInfo(uint32_t aPointerId, POINTER_PEN_INFO* aPenInfo); + bool ShouldRollupOnPointerEvent(UINT aMsg, WPARAM aWParam); + bool ShouldFirePointerEventByWinPointerMessages(); + WinPointerInfo* GetCachedPointerInfo(UINT aMsg, WPARAM aWParam); + void ConvertAndCachePointerInfo(UINT aMsg, WPARAM aWParam); + void ConvertAndCachePointerInfo(WPARAM aWParam, WinPointerInfo* aInfo); + + private: + // Function prototypes + typedef BOOL(WINAPI* GetPointerTypePtr)(uint32_t aPointerId, + POINTER_INPUT_TYPE* aPointerType); + typedef BOOL(WINAPI* GetPointerInfoPtr)(uint32_t aPointerId, + POINTER_INFO* aPointerInfo); + typedef BOOL(WINAPI* GetPointerPenInfoPtr)(uint32_t aPointerId, + POINTER_PEN_INFO* aPenInfo); + + void InitLibrary(); + + static HMODULE sLibraryHandle; + static const wchar_t kPointerLibraryName[]; + // Static function pointers + static GetPointerTypePtr getPointerType; + static GetPointerInfoPtr getPointerInfo; + static GetPointerPenInfoPtr getPointerPenInfo; + WinPointerInfo mPenPointerDownInfo; + WinPointerInfo mPenPointerUpInfo; + WinPointerInfo mPenPointerUpdateInfo; +}; + +#endif // #ifndef WinPointerEvents_h__ diff --git a/widget/windows/WinRegistry.cpp b/widget/windows/WinRegistry.cpp new file mode 100644 index 0000000000..b04ae1df45 --- /dev/null +++ b/widget/windows/WinRegistry.cpp @@ -0,0 +1,325 @@ +/* -*- 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 "WinRegistry.h" +#include "nsThreadUtils.h" + +namespace mozilla::widget::WinRegistry { + +Key::Key(HKEY aParent, const nsString& aPath, KeyMode aMode, CreateFlag) { + MOZ_ASSERT(aParent); + DWORD disposition; + ::RegCreateKeyExW(aParent, aPath.get(), 0, nullptr, REG_OPTION_NON_VOLATILE, + (REGSAM)aMode, nullptr, &mKey, &disposition); +} + +Key::Key(HKEY aParent, const nsString& aPath, KeyMode aMode) { + MOZ_ASSERT(aParent); + ::RegOpenKeyExW(aParent, aPath.get(), 0, (REGSAM)aMode, &mKey); +} + +uint32_t Key::GetChildCount() const { + MOZ_ASSERT(mKey); + DWORD result = 0; + ::RegQueryInfoKeyW(mKey, nullptr, nullptr, nullptr, &result, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr); + return result; +} + +bool Key::GetChildName(uint32_t aIndex, nsAString& aResult) const { + MOZ_ASSERT(mKey); + FILETIME lastWritten; + + wchar_t nameBuf[kMaxKeyNameLen + 1]; + DWORD nameLen = std::size(nameBuf); + + LONG rv = RegEnumKeyExW(mKey, aIndex, nameBuf, &nameLen, nullptr, nullptr, + nullptr, &lastWritten); + if (rv != ERROR_SUCCESS) { + return false; + } + aResult.Assign(nameBuf, nameLen); + return true; +} + +bool Key::RemoveChildKey(const nsString& aName) const { + MOZ_ASSERT(mKey); + return SUCCEEDED(RegDeleteKeyW(mKey, aName.get())); +} + +uint32_t Key::GetValueCount() const { + MOZ_ASSERT(mKey); + DWORD result = 0; + ::RegQueryInfoKeyW(mKey, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + &result, nullptr, nullptr, nullptr, nullptr); + return result; +} + +bool Key::GetValueName(uint32_t aIndex, nsAString& aResult) const { + MOZ_ASSERT(mKey); + wchar_t nameBuf[kMaxValueNameLen + 1]; + DWORD nameLen = std::size(nameBuf); + + LONG rv = RegEnumValueW(mKey, aIndex, nameBuf, &nameLen, nullptr, nullptr, + nullptr, nullptr); + if (rv != ERROR_SUCCESS) { + return false; + } + aResult.Assign(nameBuf, nameLen); + return true; +} + +ValueType Key::GetValueType(const nsString& aName) const { + MOZ_ASSERT(mKey); + DWORD result; + LONG rv = + RegQueryValueExW(mKey, aName.get(), nullptr, &result, nullptr, nullptr); + return SUCCEEDED(rv) ? ValueType(result) : ValueType::None; +} + +bool Key::RemoveValue(const nsString& aName) const { + MOZ_ASSERT(mKey); + return SUCCEEDED(RegDeleteValueW(mKey, aName.get())); +} + +Maybe<uint32_t> Key::GetValueAsDword(const nsString& aName) const { + MOZ_ASSERT(mKey); + DWORD type; + DWORD value = 0; + DWORD size = sizeof(DWORD); + HRESULT rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type, + (LPBYTE)&value, &size); + if (FAILED(rv) || type != REG_DWORD) { + return Nothing(); + } + return Some(value); +} + +bool Key::WriteValueAsDword(const nsString& aName, uint32_t aValue) { + MOZ_ASSERT(mKey); + return SUCCEEDED(RegSetValueExW(mKey, aName.get(), 0, REG_DWORD, + (const BYTE*)&aValue, sizeof(aValue))); +} + +Maybe<uint64_t> Key::GetValueAsQword(const nsString& aName) const { + MOZ_ASSERT(mKey); + DWORD type; + uint64_t value = 0; + DWORD size = sizeof(uint64_t); + HRESULT rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type, + (LPBYTE)&value, &size); + if (FAILED(rv) || type != REG_QWORD) { + return Nothing(); + } + return Some(value); +} + +bool Key::WriteValueAsQword(const nsString& aName, uint64_t aValue) { + MOZ_ASSERT(mKey); + return SUCCEEDED(RegSetValueExW(mKey, aName.get(), 0, REG_QWORD, + (const BYTE*)&aValue, sizeof(aValue))); +} + +bool Key::GetValueAsBinary(const nsString& aName, + nsTArray<uint8_t>& aResult) const { + MOZ_ASSERT(mKey); + DWORD type; + DWORD size; + LONG rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type, nullptr, &size); + if (FAILED(rv) || type != REG_BINARY) { + return false; + } + if (!aResult.SetLength(size, fallible)) { + return false; + } + rv = RegQueryValueExW(mKey, aName.get(), nullptr, nullptr, aResult.Elements(), + &size); + return SUCCEEDED(rv); +} + +Maybe<nsTArray<uint8_t>> Key::GetValueAsBinary(const nsString& aName) const { + nsTArray<uint8_t> value; + Maybe<nsTArray<uint8_t>> result; + if (GetValueAsBinary(aName, value)) { + result.emplace(std::move(value)); + } + return result; +} + +bool Key::WriteValueAsBinary(const nsString& aName, + Span<const uint8_t> aValue) { + MOZ_ASSERT(mKey); + return SUCCEEDED(RegSetValueExW(mKey, aName.get(), 0, REG_BINARY, + (const BYTE*)aValue.data(), aValue.size())); +} + +static bool IsStringType(DWORD aType, StringFlags aFlags) { + switch (aType) { + case REG_SZ: + return bool(aFlags & StringFlags::Sz); + case REG_EXPAND_SZ: + return bool(aFlags & StringFlags::ExpandSz); + case REG_MULTI_SZ: + return bool(aFlags & StringFlags::LegacyMultiSz); + default: + return false; + } +} + +bool Key::GetValueAsString(const nsString& aName, nsString& aResult, + StringFlags aFlags) const { + MOZ_ASSERT(mKey); + DWORD type; + DWORD size; + LONG rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type, nullptr, &size); + if (FAILED(rv) || !IsStringType(type, aFlags)) { + return false; + } + if (!size) { + aResult.Truncate(); + return true; + } + // The buffer size must be a multiple of 2. + if (NS_WARN_IF(size % 2 != 0)) { + return false; + } + size_t resultLen = size / 2; + { + auto handleOrError = + aResult.BulkWrite(resultLen, 0, /* aAllowShrinking = */ false); + if (NS_WARN_IF(handleOrError.isErr())) { + return false; + } + auto handle = handleOrError.unwrap(); + auto len = GetValueAsString(aName, {handle.Elements(), handle.Length() + 1}, + aFlags & ~StringFlags::ExpandEnvironment); + if (NS_WARN_IF(!len)) { + return false; + } + handle.Finish(*len, /* aAllowShrinking = */ false); + if (*len && !aResult.CharAt(*len - 1)) { + // The string passed to us had a null terminator in the final + // position. + aResult.Truncate(*len - 1); + } + } + if (type == REG_EXPAND_SZ && (aFlags & StringFlags::ExpandEnvironment)) { + resultLen = ExpandEnvironmentStringsW(aResult.get(), nullptr, 0); + if (resultLen > 1) { + nsString expandedResult; + // |resultLen| includes the terminating null character + resultLen--; + if (!expandedResult.SetLength(resultLen, fallible)) { + return false; + } + resultLen = ExpandEnvironmentStringsW(aResult.get(), expandedResult.get(), + resultLen + 1); + if (resultLen <= 0) { + return false; + } + aResult = std::move(expandedResult); + } else if (resultLen == 1) { + // It apparently expands to nothing (just a null terminator). + resultLen = 0; + aResult.Truncate(); + } + } + return true; +} + +Maybe<nsString> Key::GetValueAsString(const nsString& aName, + StringFlags aFlags) const { + nsString value; + Maybe<nsString> result; + if (GetValueAsString(aName, value, aFlags)) { + result.emplace(std::move(value)); + } + return result; +} + +Maybe<uint32_t> Key::GetValueAsString(const nsString& aName, + Span<char16_t> aBuffer, + StringFlags aFlags) const { + MOZ_ASSERT(mKey); + MOZ_ASSERT(aBuffer.Length(), "Empty buffer?"); + MOZ_ASSERT(!(aFlags & StringFlags::ExpandEnvironment), + "Environment expansion not performed on a single buffer"); + + DWORD size = aBuffer.LengthBytes(); + DWORD type; + HRESULT rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type, + (LPBYTE)aBuffer.data(), &size); + if (FAILED(rv)) { + return Nothing(); + } + if (!IsStringType(type, aFlags)) { + return Nothing(); + } + uint32_t len = size ? size / sizeof(char16_t) - 1 : 0; + aBuffer[len] = 0; + return Some(len); +} + +KeyWatcher::KeyWatcher(Key&& aKey, + nsISerialEventTarget* aTargetSerialEventTarget, + Callback&& aCallback) + : mKey(std::move(aKey)), + mEventTarget(aTargetSerialEventTarget), + mCallback(std::move(aCallback)) { + MOZ_ASSERT(mKey); + MOZ_ASSERT(mEventTarget); + MOZ_ASSERT(mCallback); + mEvent = CreateEvent(nullptr, /* bManualReset = */ FALSE, + /* bInitialState = */ FALSE, nullptr); + if (NS_WARN_IF(!mEvent)) { + return; + } + + if (NS_WARN_IF(!Register())) { + return; + } + + // The callback only dispatches to the relevant event target, so we can use + // WT_EXECUTEINWAITTHREAD. + RegisterWaitForSingleObject(&mWaitObject, mEvent, WatchCallback, this, + INFINITE, WT_EXECUTEINWAITTHREAD); +} + +void KeyWatcher::WatchCallback(void* aContext, BOOLEAN) { + auto* watcher = static_cast<KeyWatcher*>(aContext); + watcher->Register(); + watcher->mEventTarget->Dispatch( + NS_NewRunnableFunction("KeyWatcher callback", watcher->mCallback)); +} + +// As per the documentation in: +// https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regnotifychangekeyvalue +// +// This function detects a single change. After the caller receives a +// notification event, it should call the function again to receive the next +// notification. +bool KeyWatcher::Register() { + MOZ_ASSERT(mEvent); + DWORD flags = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_ATTRIBUTES | + REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_CHANGE_SECURITY | + REG_NOTIFY_THREAD_AGNOSTIC; + HRESULT rv = + RegNotifyChangeKeyValue(mKey.RawKey(), /* bWatchSubtree = */ TRUE, flags, + mEvent, /* fAsynchronous = */ TRUE); + return !NS_WARN_IF(FAILED(rv)); +} + +KeyWatcher::~KeyWatcher() { + if (mWaitObject) { + UnregisterWait(mWaitObject); + CloseHandle(mWaitObject); + } + if (mEvent) { + CloseHandle(mEvent); + } +} + +} // namespace mozilla::widget::WinRegistry diff --git a/widget/windows/WinRegistry.h b/widget/windows/WinRegistry.h new file mode 100644 index 0000000000..8ab221928e --- /dev/null +++ b/widget/windows/WinRegistry.h @@ -0,0 +1,235 @@ +/* -*- 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_WinRegistry_h__ +#define mozilla_widget_WinRegistry_h__ + +#include <windows.h> +#include <functional> +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" + +class nsISerialEventTarget; + +namespace mozilla::widget::WinRegistry { + +// According to MSDN, the following limits apply (in characters excluding room +// for terminating null character): +static constexpr size_t kMaxKeyNameLen = 255; +static constexpr size_t kMaxValueNameLen = 16383; + +/// https://learn.microsoft.com/en-us/windows/win32/shell/regsam +enum class KeyMode : uint32_t { + AllAccess = KEY_ALL_ACCESS, + QueryValue = KEY_QUERY_VALUE, + CreateLink = KEY_CREATE_LINK, + CreateSubKey = KEY_CREATE_SUB_KEY, + EnumerateSubkeys = KEY_ENUMERATE_SUB_KEYS, + Execute = KEY_EXECUTE, + Notify = KEY_NOTIFY, + Read = KEY_READ, + SetValue = KEY_SET_VALUE, + Write = KEY_WRITE, +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(KeyMode); + +enum class StringFlags : uint32_t { + // Whether to allow REG_SZ strings. + Sz = 1 << 0, + // Whether to read EXPAND_SZ strings. + ExpandSz = 1 << 1, + // Whether to treat MULTI_SZ values as strings. This is a historical + // idiosyncrasy of the nsIWindowsRegKey, but most likely not what you want. + LegacyMultiSz = 1 << 2, + // Whether to expand environment variables in EXPAND_SZ values. + // Only makes sense along with the ExpandSz variable. + ExpandEnvironment = 1 << 3, + // By default, only allow regular Sz, and don't perform environment expansion. + Default = Sz, +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(StringFlags); + +// Convenience alias for legacy WinUtils callers, to preserve behavior. +// Chances are users of these flags could just look at Sz strings, tho. +static constexpr auto kLegacyWinUtilsStringFlags = + StringFlags::Sz | StringFlags::ExpandSz; + +// https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types +enum class ValueType : uint32_t { + Binary = REG_BINARY, + Dword = REG_DWORD, + ExpandSz = REG_EXPAND_SZ, + Link = REG_LINK, + MultiSz = REG_MULTI_SZ, + None = REG_NONE, + Qword = REG_QWORD, + Sz = REG_SZ, +}; + +class Key { + public: + enum CreateFlag { + Create, + }; + + Key() = default; + Key(const Key&) = delete; + Key(Key&& aOther) { std::swap(mKey, aOther.mKey); } + + Key& operator=(const Key&) = delete; + Key& operator=(Key&& aOther) { + std::swap(mKey, aOther.mKey); + return *this; + } + + Key(HKEY aParent, const nsString& aPath, KeyMode aMode, CreateFlag); + Key(HKEY aParent, const nsString& aPath, KeyMode aMode); + + Key(const Key& aParent, const nsString& aPath, KeyMode aMode, CreateFlag) + : Key(aParent.mKey, aPath, aMode, Create) {} + Key(const Key& aParent, const nsString& aPath, KeyMode aMode) + : Key(aParent.mKey, aPath, aMode) {} + + ~Key() { + if (mKey) { + ::RegCloseKey(mKey); + } + } + + explicit operator bool() const { return !!mKey; } + + uint32_t GetChildCount() const; + [[nodiscard]] bool GetChildName(uint32_t aIndex, nsAString& aResult) const; + [[nodiscard]] bool RemoveChildKey(const nsString& aName) const; + + uint32_t GetValueCount() const; + [[nodiscard]] bool GetValueName(uint32_t aIndex, nsAString& aResult) const; + ValueType GetValueType(const nsString& aName) const; + bool RemoveValue(const nsString& aName) const; + + Maybe<uint32_t> GetValueAsDword(const nsString& aName) const; + bool WriteValueAsDword(const nsString& aName, uint32_t aValue); + + Maybe<uint64_t> GetValueAsQword(const nsString& aName) const; + [[nodiscard]] bool WriteValueAsQword(const nsString& aName, uint64_t aValue); + + [[nodiscard]] bool GetValueAsBinary(const nsString& aName, + nsTArray<uint8_t>&) const; + Maybe<nsTArray<uint8_t>> GetValueAsBinary(const nsString& aName) const; + [[nodiscard]] bool WriteValueAsBinary(const nsString& aName, + Span<const uint8_t> aValue); + + [[nodiscard]] bool GetValueAsString(const nsString& aName, nsString& aResult, + StringFlags = StringFlags::Default) const; + Maybe<nsString> GetValueAsString(const nsString& aName, + StringFlags = StringFlags::Default) const; + // Reads a string value into a buffer. Returns Some(length) if the string is + // read fully, in which case the passed memory region contains a + // null-terminated string. + // Doesn't perform environment expansion (and asserts if you pass the + // ExpandEnvironment flag). + [[nodiscard]] Maybe<uint32_t> GetValueAsString( + const nsString& aName, Span<char16_t>, + StringFlags = StringFlags::Default) const; + [[nodiscard]] Maybe<uint32_t> GetValueAsString( + const nsString& aName, Span<wchar_t> aBuffer, + StringFlags aFlags = StringFlags::Default) const { + return GetValueAsString( + aName, Span<char16_t>((char16_t*)aBuffer.data(), aBuffer.Length()), + aFlags); + } + + [[nodiscard]] bool WriteValueAsString(const nsString& aName, + const nsString& aValue) { + MOZ_ASSERT(mKey); + return SUCCEEDED(RegSetValueExW(mKey, aName.get(), 0, REG_SZ, + (const BYTE*)aValue.get(), + (aValue.Length() + 1) * sizeof(char16_t))); + } + + HKEY RawKey() const { return mKey; } + + private: + HKEY mKey = nullptr; +}; + +inline bool HasKey(HKEY aRootKey, const nsString& aKeyName) { + return !!Key(aRootKey, aKeyName, KeyMode::Read); +} + +// Returns a single string value from the registry into a buffer. +[[nodiscard]] inline bool GetString(HKEY aRootKey, const nsString& aKeyName, + const nsString& aValueName, + Span<char16_t> aBuffer, + StringFlags aFlags = StringFlags::Default) { + Key k(aRootKey, aKeyName, KeyMode::QueryValue); + return k && k.GetValueAsString(aValueName, aBuffer, aFlags); +} +[[nodiscard]] inline bool GetString(HKEY aRootKey, const nsString& aKeyName, + const nsString& aValueName, + Span<wchar_t> aBuffer, + StringFlags aFlags = StringFlags::Default) { + return GetString(aRootKey, aKeyName, aValueName, + Span<char16_t>((char16_t*)aBuffer.data(), aBuffer.Length()), + aFlags); +} +[[nodiscard]] inline bool GetString(HKEY aRootKey, const nsString& aKeyName, + const nsString& aValueName, + nsString& aBuffer, + StringFlags aFlags = StringFlags::Default) { + Key k(aRootKey, aKeyName, KeyMode::QueryValue); + return k && k.GetValueAsString(aValueName, aBuffer, aFlags); +} +inline Maybe<nsString> GetString(HKEY aRootKey, const nsString& aKeyName, + const nsString& aValueName, + StringFlags aFlags = StringFlags::Default) { + Key k(aRootKey, aKeyName, KeyMode::QueryValue); + if (!k) { + return Nothing(); + } + return k.GetValueAsString(aValueName, aFlags); +} + +class KeyWatcher final { + public: + using Callback = std::function<void()>; + + KeyWatcher(const KeyWatcher&) = delete; + + const Key& GetKey() const { return mKey; } + + // Start watching a key. The watching is recursive (the whole key subtree is + // watched), and the callback is executed every time the key or any of its + // descendants change until the watcher is destroyed. + // + // @param aKey the key to watch. Must have been opened with the + // KeyMode::Notify flag. + // @param aTargetSerialEventTarget the target event target to dispatch the + // callback to. + // @param aCallback the closure to run every time that registry key changes. + KeyWatcher(Key&& aKey, nsISerialEventTarget* aTargetSerialEventTarget, + Callback&& aCallback); + + ~KeyWatcher(); + + private: + static void CALLBACK WatchCallback(void* aContext, BOOLEAN); + bool Register(); + + Key mKey; + nsCOMPtr<nsISerialEventTarget> mEventTarget; + Callback mCallback; + HANDLE mEvent = nullptr; + HANDLE mWaitObject = nullptr; +}; + +} // namespace mozilla::widget::WinRegistry + +#endif diff --git a/widget/windows/WinTaskbar.cpp b/widget/windows/WinTaskbar.cpp new file mode 100644 index 0000000000..56608503da --- /dev/null +++ b/widget/windows/WinTaskbar.cpp @@ -0,0 +1,493 @@ +/* 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 "nsIWinTaskbar.h" +#include "WinTaskbar.h" +#include "TaskbarPreview.h" +#include "nsITaskbarPreviewController.h" + +#include "mozilla/RefPtr.h" +#include "mozilla/widget/JumpListBuilder.h" +#include <nsError.h> +#include <nsCOMPtr.h> +#include <nsIWidget.h> +#include <nsIBaseWindow.h> +#include <nsServiceManagerUtils.h> +#include "nsIXULAppInfo.h" +#include "nsILegacyJumpListBuilder.h" +#include "nsUXThemeData.h" +#include "nsWindow.h" +#include "WinUtils.h" +#include "TaskbarTabPreview.h" +#include "TaskbarWindowPreview.h" +#include "LegacyJumpListBuilder.h" +#include "nsWidgetsCID.h" +#include "nsPIDOMWindow.h" +#include "nsAppDirectoryServiceDefs.h" +#include "mozilla/Preferences.h" +#include "nsAppRunner.h" +#include "nsXREDirProvider.h" +#include "mozilla/widget/WinRegistry.h" +#include <io.h> +#include <propvarutil.h> +#include <propkey.h> +#include <shellapi.h> + +static NS_DEFINE_CID(kLegacyJumpListBuilderCID, + NS_WIN_LEGACYJUMPLISTBUILDER_CID); + +namespace { + +HWND GetHWNDFromDocShell(nsIDocShell* aShell) { + nsCOMPtr<nsIBaseWindow> baseWindow( + do_QueryInterface(reinterpret_cast<nsISupports*>(aShell))); + + if (!baseWindow) return nullptr; + + nsCOMPtr<nsIWidget> widget; + baseWindow->GetMainWidget(getter_AddRefs(widget)); + + return widget ? (HWND)widget->GetNativeData(NS_NATIVE_WINDOW) : nullptr; +} + +HWND GetHWNDFromDOMWindow(mozIDOMWindow* dw) { + nsCOMPtr<nsIWidget> widget; + + if (!dw) return nullptr; + + nsCOMPtr<nsPIDOMWindowInner> window = nsPIDOMWindowInner::From(dw); + return GetHWNDFromDocShell(window->GetDocShell()); +} + +nsresult SetWindowAppUserModelProp(mozIDOMWindow* aParent, + const nsString& aIdentifier) { + NS_ENSURE_ARG_POINTER(aParent); + + if (aIdentifier.IsEmpty()) return NS_ERROR_INVALID_ARG; + + HWND toplevelHWND = ::GetAncestor(GetHWNDFromDOMWindow(aParent), GA_ROOT); + + if (!toplevelHWND) return NS_ERROR_INVALID_ARG; + + RefPtr<IPropertyStore> pPropStore; + if (FAILED(SHGetPropertyStoreForWindow(toplevelHWND, IID_IPropertyStore, + getter_AddRefs(pPropStore)))) { + return NS_ERROR_INVALID_ARG; + } + + PROPVARIANT pv; + if (FAILED(InitPropVariantFromString(aIdentifier.get(), &pv))) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = NS_OK; + if (FAILED(pPropStore->SetValue(PKEY_AppUserModel_ID, pv)) || + FAILED(pPropStore->Commit())) { + rv = NS_ERROR_FAILURE; + } + + PropVariantClear(&pv); + + return rv; +} + +/////////////////////////////////////////////////////////////////////////////// +// default nsITaskbarPreviewController + +class DefaultController final : public nsITaskbarPreviewController { + ~DefaultController() {} + HWND mWnd; + + public: + explicit DefaultController(HWND hWnd) : mWnd(hWnd) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSITASKBARPREVIEWCONTROLLER +}; + +NS_IMETHODIMP +DefaultController::GetWidth(uint32_t* aWidth) { + RECT r; + ::GetClientRect(mWnd, &r); + *aWidth = r.right; + return NS_OK; +} + +NS_IMETHODIMP +DefaultController::GetHeight(uint32_t* aHeight) { + RECT r; + ::GetClientRect(mWnd, &r); + *aHeight = r.bottom; + return NS_OK; +} + +NS_IMETHODIMP +DefaultController::GetThumbnailAspectRatio(float* aThumbnailAspectRatio) { + uint32_t width, height; + GetWidth(&width); + GetHeight(&height); + if (!height) height = 1; + + *aThumbnailAspectRatio = width / float(height); + return NS_OK; +} + +NS_IMETHODIMP +DefaultController::RequestThumbnail(nsITaskbarPreviewCallback* aCallback, + uint32_t width, uint32_t height) { + return NS_OK; +} + +NS_IMETHODIMP +DefaultController::RequestPreview(nsITaskbarPreviewCallback* aCallback) { + return NS_OK; +} + +NS_IMETHODIMP +DefaultController::OnClose(void) { + MOZ_ASSERT_UNREACHABLE( + "OnClose should not be called for " + "TaskbarWindowPreviews"); + return NS_OK; +} + +NS_IMETHODIMP +DefaultController::OnActivate(bool* rAcceptActivation) { + *rAcceptActivation = true; + MOZ_ASSERT_UNREACHABLE( + "OnActivate should not be called for " + "TaskbarWindowPreviews"); + return NS_OK; +} + +NS_IMETHODIMP +DefaultController::OnClick(nsITaskbarPreviewButton* button) { return NS_OK; } + +NS_IMPL_ISUPPORTS(DefaultController, nsITaskbarPreviewController) +} // namespace + +namespace mozilla { +namespace widget { + +/////////////////////////////////////////////////////////////////////////////// +// nsIWinTaskbar + +NS_IMPL_ISUPPORTS(WinTaskbar, nsIWinTaskbar) + +bool WinTaskbar::Initialize() { + if (mTaskbar) return true; + + ::CoInitialize(nullptr); + HRESULT hr = + ::CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, + IID_ITaskbarList4, (void**)&mTaskbar); + if (FAILED(hr)) return false; + + hr = mTaskbar->HrInit(); + if (FAILED(hr)) { + // This may fail with shell extensions like blackbox installed. + NS_WARNING("Unable to initialize taskbar"); + NS_RELEASE(mTaskbar); + return false; + } + return true; +} + +WinTaskbar::WinTaskbar() : mTaskbar(nullptr) {} + +WinTaskbar::~WinTaskbar() { + if (mTaskbar) { // match successful Initialize() call + NS_RELEASE(mTaskbar); + ::CoUninitialize(); + } +} + +// static +bool WinTaskbar::GenerateAppUserModelID(nsAString& aAppUserModelId, + bool aPrivateBrowsing) { + // If marked as such in prefs, use a hash of the profile path for the id + // instead of the install path hash setup by the installer. + if (Preferences::GetBool("taskbar.grouping.useprofile", false)) { + nsCOMPtr<nsIFile> profileDir; + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profileDir)); + bool exists = false; + if (profileDir && NS_SUCCEEDED(profileDir->Exists(&exists)) && exists) { + nsAutoCString path; + if (NS_SUCCEEDED(profileDir->GetPersistentDescriptor(path))) { + nsAutoString id; + id.AppendInt(HashString(path)); + if (!id.IsEmpty()) { + aAppUserModelId.Assign(id); + return true; + } + } + } + } + + // The default value is set by the installer and is stored in the registry + // under (HKLM||HKCU)/Software/Mozilla/Firefox/TaskBarIDs. If for any reason + // hash generation operation fails, the installer will not store a value in + // the registry or set ids on shortcuts. A lack of an id can also occur for + // zipped builds. + nsCOMPtr<nsIXULAppInfo> appInfo = + do_GetService("@mozilla.org/xre/app-info;1"); + nsCString appName; + if (appInfo && NS_SUCCEEDED(appInfo->GetName(appName))) { + nsAutoString regKey; + regKey.AssignLiteral("Software\\Mozilla\\"); + AppendASCIItoUTF16(appName, regKey); + regKey.AppendLiteral("\\TaskBarIDs"); + + WCHAR path[MAX_PATH]; + if (GetModuleFileNameW(nullptr, path, MAX_PATH)) { + wchar_t* slash = wcsrchr(path, '\\'); + if (!slash) return false; + *slash = '\0'; // no trailing slash + + nsDependentString pathStr(path); + for (auto* rootKey : {HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER}) { + if (auto aumid = WinRegistry::GetString(rootKey, regKey, pathStr)) { + aAppUserModelId = std::move(*aumid); + break; + } + } + } + } + + // If we haven't found an ID yet then use the install hash. In xpcshell tests + // the directory provider may not have been initialized so bypass in this + // case. + if (aAppUserModelId.IsEmpty() && gDirServiceProvider) { + gDirServiceProvider->GetInstallHash(aAppUserModelId); + } + + if (aPrivateBrowsing) { + aAppUserModelId.AppendLiteral(";PrivateBrowsingAUMID"); + } + + return !aAppUserModelId.IsEmpty(); +} + +// static +bool WinTaskbar::GetAppUserModelID(nsAString& aAppUserModelId, + bool aPrivateBrowsing) { + // If an ID has already been set then use that. + PWSTR id; + if (SUCCEEDED(GetCurrentProcessExplicitAppUserModelID(&id))) { + aAppUserModelId.Assign(id); + CoTaskMemFree(id); + } + + return GenerateAppUserModelID(aAppUserModelId, aPrivateBrowsing); +} + +NS_IMETHODIMP +WinTaskbar::GetDefaultGroupId(nsAString& aDefaultGroupId) { + if (!GetAppUserModelID(aDefaultGroupId)) return NS_ERROR_UNEXPECTED; + + return NS_OK; +} + +NS_IMETHODIMP +WinTaskbar::GetDefaultPrivateGroupId(nsAString& aDefaultPrivateGroupId) { + if (!GetAppUserModelID(aDefaultPrivateGroupId, true)) + return NS_ERROR_UNEXPECTED; + + return NS_OK; +} + +// (static) Called from AppShell +bool WinTaskbar::RegisterAppUserModelID() { + nsAutoString uid; + if (!GetAppUserModelID(uid)) return false; + + return SUCCEEDED(SetCurrentProcessExplicitAppUserModelID(uid.get())); +} + +NS_IMETHODIMP +WinTaskbar::GetAvailable(bool* aAvailable) { + // ITaskbarList4::HrInit() may fail with shell extensions like blackbox + // installed. Initialize early to return available=false in those cases. + *aAvailable = Initialize(); + + return NS_OK; +} + +NS_IMETHODIMP +WinTaskbar::CreateTaskbarTabPreview(nsIDocShell* shell, + nsITaskbarPreviewController* controller, + nsITaskbarTabPreview** _retval) { + if (!Initialize()) return NS_ERROR_NOT_AVAILABLE; + + NS_ENSURE_ARG_POINTER(shell); + NS_ENSURE_ARG_POINTER(controller); + + HWND toplevelHWND = ::GetAncestor(GetHWNDFromDocShell(shell), GA_ROOT); + + if (!toplevelHWND) return NS_ERROR_INVALID_ARG; + + RefPtr<TaskbarTabPreview> preview( + new TaskbarTabPreview(mTaskbar, controller, toplevelHWND, shell)); + if (!preview) return NS_ERROR_OUT_OF_MEMORY; + + nsresult rv = preview->Init(); + if (NS_FAILED(rv)) { + return rv; + } + + preview.forget(_retval); + + return NS_OK; +} + +NS_IMETHODIMP +WinTaskbar::GetTaskbarWindowPreview(nsIDocShell* shell, + nsITaskbarWindowPreview** _retval) { + if (!Initialize()) return NS_ERROR_NOT_AVAILABLE; + + NS_ENSURE_ARG_POINTER(shell); + + HWND toplevelHWND = ::GetAncestor(GetHWNDFromDocShell(shell), GA_ROOT); + + if (!toplevelHWND) return NS_ERROR_INVALID_ARG; + + nsWindow* window = WinUtils::GetNSWindowPtr(toplevelHWND); + + if (!window) return NS_ERROR_FAILURE; + + nsCOMPtr<nsITaskbarWindowPreview> preview = window->GetTaskbarPreview(); + if (!preview) { + RefPtr<DefaultController> defaultController = + new DefaultController(toplevelHWND); + + TaskbarWindowPreview* previewRaw = new TaskbarWindowPreview( + mTaskbar, defaultController, toplevelHWND, shell); + if (!previewRaw) { + return NS_ERROR_OUT_OF_MEMORY; + } + + preview = previewRaw; + + nsresult rv = previewRaw->Init(); + if (NS_FAILED(rv)) { + return rv; + } + window->SetTaskbarPreview(preview); + } + + preview.forget(_retval); + + return NS_OK; +} + +NS_IMETHODIMP +WinTaskbar::GetTaskbarProgress(nsIDocShell* shell, + nsITaskbarProgress** _retval) { + nsCOMPtr<nsITaskbarWindowPreview> preview; + nsresult rv = GetTaskbarWindowPreview(shell, getter_AddRefs(preview)); + NS_ENSURE_SUCCESS(rv, rv); + + return CallQueryInterface(preview, _retval); +} + +NS_IMETHODIMP +WinTaskbar::GetOverlayIconController( + nsIDocShell* shell, nsITaskbarOverlayIconController** _retval) { + nsCOMPtr<nsITaskbarWindowPreview> preview; + nsresult rv = GetTaskbarWindowPreview(shell, getter_AddRefs(preview)); + NS_ENSURE_SUCCESS(rv, rv); + + return CallQueryInterface(preview, _retval); +} + +NS_IMETHODIMP +WinTaskbar::CreateLegacyJumpListBuilder( + bool aPrivateBrowsing, nsILegacyJumpListBuilder** aJumpListBuilder) { + nsresult rv; + + if (LegacyJumpListBuilder::sBuildingList) return NS_ERROR_ALREADY_INITIALIZED; + + nsCOMPtr<nsILegacyJumpListBuilder> builder = + do_CreateInstance(kLegacyJumpListBuilderCID, &rv); + if (NS_FAILED(rv)) return NS_ERROR_UNEXPECTED; + + NS_IF_ADDREF(*aJumpListBuilder = builder); + + nsAutoString aumid; + GenerateAppUserModelID(aumid, aPrivateBrowsing); + builder->SetAppUserModelID(aumid); + + return NS_OK; +} + +NS_IMETHODIMP +WinTaskbar::CreateJumpListBuilder(bool aPrivateBrowsing, + nsIJumpListBuilder** aJumpListBuilder) { + nsAutoString aumid; + GenerateAppUserModelID(aumid, aPrivateBrowsing); + + nsCOMPtr<nsIJumpListBuilder> builder = new JumpListBuilder(aumid); + if (!builder) { + return NS_ERROR_UNEXPECTED; + } + + NS_IF_ADDREF(*aJumpListBuilder = builder); + return NS_OK; +} + +NS_IMETHODIMP +WinTaskbar::GetGroupIdForWindow(mozIDOMWindow* aParent, + nsAString& aIdentifier) { + NS_ENSURE_ARG_POINTER(aParent); + HWND toplevelHWND = ::GetAncestor(GetHWNDFromDOMWindow(aParent), GA_ROOT); + if (!toplevelHWND) return NS_ERROR_INVALID_ARG; + RefPtr<IPropertyStore> pPropStore; + if (FAILED(SHGetPropertyStoreForWindow(toplevelHWND, IID_IPropertyStore, + getter_AddRefs(pPropStore)))) { + return NS_ERROR_INVALID_ARG; + } + PROPVARIANT pv; + PropVariantInit(&pv); + auto cleanupPropVariant = MakeScopeExit([&] { PropVariantClear(&pv); }); + if (FAILED(pPropStore->GetValue(PKEY_AppUserModel_ID, &pv))) { + return NS_ERROR_FAILURE; + } + if (pv.vt != VT_LPWSTR) { + // This can happen when there is no window specific group ID set + // It's not an error case so we have to check for empty strings + // returned from the function. + return NS_OK; + } + aIdentifier.Assign(char16ptr_t(pv.pwszVal)); + return NS_OK; +} + +NS_IMETHODIMP +WinTaskbar::SetGroupIdForWindow(mozIDOMWindow* aParent, + const nsAString& aIdentifier) { + return SetWindowAppUserModelProp(aParent, nsString(aIdentifier)); +} + +NS_IMETHODIMP +WinTaskbar::PrepareFullScreen(void* aHWND, bool aFullScreen) { + if (!Initialize()) return NS_ERROR_NOT_AVAILABLE; + + NS_ENSURE_ARG_POINTER(aHWND); + + if (!::IsWindow((HWND)aHWND)) return NS_ERROR_INVALID_ARG; + + HRESULT hr = mTaskbar->MarkFullscreenWindow((HWND)aHWND, aFullScreen); + if (FAILED(hr)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/WinTaskbar.h b/widget/windows/WinTaskbar.h new file mode 100644 index 0000000000..a6c483a561 --- /dev/null +++ b/widget/windows/WinTaskbar.h @@ -0,0 +1,46 @@ +/* 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 __WinTaskbar_h__ +#define __WinTaskbar_h__ + +#include <windows.h> +#include <shobjidl.h> +#undef LogSeverity // SetupAPI.h #defines this as DWORD +#include "nsIWinTaskbar.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace widget { + +class WinTaskbar final : public nsIWinTaskbar { + ~WinTaskbar(); + + public: + WinTaskbar(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWINTASKBAR + + static bool GenerateAppUserModelID(nsAString& aAppUserModelId, + bool aPrivateBrowsing = false); + // Registers the global app user model id for the instance. + // See comments in WinTaskbar.cpp for more information. + static bool RegisterAppUserModelID(); + static bool GetAppUserModelID(nsAString& aDefaultGroupId, + bool aPrivateBrowsing = false); + + private: + bool Initialize(); + + ITaskbarList4* mTaskbar; +}; + +} // namespace widget +} // namespace mozilla + +#endif /* __WinTaskbar_h__ */ diff --git a/widget/windows/WinTextEventDispatcherListener.cpp b/widget/windows/WinTextEventDispatcherListener.cpp new file mode 100644 index 0000000000..aeed8be01b --- /dev/null +++ b/widget/windows/WinTextEventDispatcherListener.cpp @@ -0,0 +1,68 @@ +/* -*- 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 "KeyboardLayout.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/widget/IMEData.h" +#include "nsWindow.h" +#include "WinIMEHandler.h" +#include "WinTextEventDispatcherListener.h" + +namespace mozilla { +namespace widget { + +StaticRefPtr<WinTextEventDispatcherListener> + WinTextEventDispatcherListener::sInstance; + +// static +WinTextEventDispatcherListener* WinTextEventDispatcherListener::GetInstance() { + if (!sInstance) { + sInstance = new WinTextEventDispatcherListener(); + } + return sInstance.get(); +} + +void WinTextEventDispatcherListener::Shutdown() { sInstance = nullptr; } + +NS_IMPL_ISUPPORTS(WinTextEventDispatcherListener, TextEventDispatcherListener, + nsISupportsWeakReference) + +WinTextEventDispatcherListener::WinTextEventDispatcherListener() {} + +WinTextEventDispatcherListener::~WinTextEventDispatcherListener() {} + +NS_IMETHODIMP +WinTextEventDispatcherListener::NotifyIME( + TextEventDispatcher* aTextEventDispatcher, + const IMENotification& aNotification) { + nsWindow* window = static_cast<nsWindow*>(aTextEventDispatcher->GetWidget()); + if (NS_WARN_IF(!window)) { + return NS_ERROR_FAILURE; + } + return IMEHandler::NotifyIME(window, aNotification); +} + +NS_IMETHODIMP_(IMENotificationRequests) +WinTextEventDispatcherListener::GetIMENotificationRequests() { + return IMEHandler::GetIMENotificationRequests(); +} + +NS_IMETHODIMP_(void) +WinTextEventDispatcherListener::OnRemovedFrom( + TextEventDispatcher* aTextEventDispatcher) { + // XXX When input transaction is being stolen by add-on, what should we do? +} + +NS_IMETHODIMP_(void) +WinTextEventDispatcherListener::WillDispatchKeyboardEvent( + TextEventDispatcher* aTextEventDispatcher, + WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress, + void* aData) { + static_cast<NativeKey*>(aData)->WillDispatchKeyboardEvent(aKeyboardEvent, + aIndexOfKeypress); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/WinTextEventDispatcherListener.h b/widget/windows/WinTextEventDispatcherListener.h new file mode 100644 index 0000000000..d799d0b280 --- /dev/null +++ b/widget/windows/WinTextEventDispatcherListener.h @@ -0,0 +1,50 @@ +/* -*- 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 WinTextEventDispatcherListener_h_ +#define WinTextEventDispatcherListener_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextEventDispatcherListener.h" + +namespace mozilla { +namespace widget { + +/** + * On Windows, it's enough TextEventDispatcherListener to be a singleton + * because we have only one input context per process (IMM can create + * multiple IM context but we don't support such behavior). + */ + +class WinTextEventDispatcherListener final + : public TextEventDispatcherListener { + public: + static WinTextEventDispatcherListener* GetInstance(); + static void Shutdown(); + + NS_DECL_ISUPPORTS + + NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher, + const IMENotification& aNotification) override; + NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override; + NS_IMETHOD_(void) + OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override; + NS_IMETHOD_(void) + WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher, + WidgetKeyboardEvent& aKeyboardEvent, + uint32_t aIndexOfKeypress, void* aData) override; + + private: + WinTextEventDispatcherListener(); + virtual ~WinTextEventDispatcherListener(); + + static StaticRefPtr<WinTextEventDispatcherListener> sInstance; +}; + +} // namespace widget +} // namespace mozilla + +#endif // #ifndef WinTextEventDispatcherListener_h_ diff --git a/widget/windows/WinUtils.cpp b/widget/windows/WinUtils.cpp new file mode 100644 index 0000000000..1d8fad16f7 --- /dev/null +++ b/widget/windows/WinUtils.cpp @@ -0,0 +1,2107 @@ +/* -*- 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 "WinUtils.h" + +#include <knownfolders.h> +#include <winioctl.h> + +#include "gfxPlatform.h" +#include "gfxUtils.h" +#include "nsWindow.h" +#include "nsWindowDefs.h" +#include "InputDeviceUtils.h" +#include "KeyboardLayout.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" +#include "mozilla/gfx/DisplayConfigWindows.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerThreadSleep.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/Unused.h" +#include "nsIContentPolicy.h" +#include "WindowsUIUtils.h" +#include "nsContentUtils.h" + +#include "mozilla/Logging.h" + +#include "nsString.h" +#include "nsDirectoryServiceUtils.h" +#include "imgIContainer.h" +#include "imgITools.h" +#include "nsNetUtil.h" +#include "nsIOutputStream.h" +#include "nsNetCID.h" +#include "prtime.h" +#ifdef MOZ_PLACES +# include "nsIFaviconService.h" +#endif +#include "nsIDownloader.h" +#include "nsIChannel.h" +#include "nsIThread.h" +#include "MainThreadUtils.h" +#include "nsLookAndFeel.h" +#include "nsUnicharUtils.h" +#include "nsWindowsHelpers.h" +#include "WinWindowOcclusionTracker.h" + +#include <textstor.h> +#include "TSFTextStore.h" + +#include <shellscalingapi.h> +#include <shlobj.h> +#include <shlwapi.h> + +mozilla::LazyLogModule gWindowsLog("Widget"); + +#define LOG_E(...) MOZ_LOG(gWindowsLog, LogLevel::Error, (__VA_ARGS__)) +#define LOG_D(...) MOZ_LOG(gWindowsLog, LogLevel::Debug, (__VA_ARGS__)) + +using namespace mozilla::gfx; + +namespace mozilla { +namespace widget { + +#ifdef MOZ_PLACES +NS_IMPL_ISUPPORTS(myDownloadObserver, nsIDownloadObserver) +NS_IMPL_ISUPPORTS(AsyncFaviconDataReady, nsIFaviconDataCallback) +#endif +NS_IMPL_ISUPPORTS(AsyncEncodeAndWriteIcon, nsIRunnable) +NS_IMPL_ISUPPORTS(AsyncDeleteAllFaviconsFromDisk, nsIRunnable) + +const char FaviconHelper::kJumpListCacheDir[] = "jumpListCache"; +const char FaviconHelper::kShortcutCacheDir[] = "shortcutCache"; + +struct CoTaskMemFreePolicy { + void operator()(void* aPtr) { ::CoTaskMemFree(aPtr); } +}; + +SetThreadDpiAwarenessContextProc WinUtils::sSetThreadDpiAwarenessContext = NULL; +EnableNonClientDpiScalingProc WinUtils::sEnableNonClientDpiScaling = NULL; +GetSystemMetricsForDpiProc WinUtils::sGetSystemMetricsForDpi = NULL; +bool WinUtils::sHasPackageIdentity = false; + +using GetDpiForWindowProc = UINT(WINAPI*)(HWND); +static GetDpiForWindowProc sGetDpiForWindow = NULL; + +/* static */ +void WinUtils::Initialize() { + // Dpi-Awareness is not supported with Win32k Lockdown enabled, so we don't + // initialize DPI-related members and assert later that nothing accidently + // uses these static members + if (!IsWin32kLockedDown()) { + HMODULE user32Dll = ::GetModuleHandleW(L"user32"); + if (user32Dll) { + auto getThreadDpiAwarenessContext = + (decltype(GetThreadDpiAwarenessContext)*)::GetProcAddress( + user32Dll, "GetThreadDpiAwarenessContext"); + auto areDpiAwarenessContextsEqual = + (decltype(AreDpiAwarenessContextsEqual)*)::GetProcAddress( + user32Dll, "AreDpiAwarenessContextsEqual"); + if (getThreadDpiAwarenessContext && areDpiAwarenessContextsEqual && + areDpiAwarenessContextsEqual( + getThreadDpiAwarenessContext(), + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) { + // Only per-monitor v1 requires these workarounds. + sEnableNonClientDpiScaling = + (EnableNonClientDpiScalingProc)::GetProcAddress( + user32Dll, "EnableNonClientDpiScaling"); + sSetThreadDpiAwarenessContext = + (SetThreadDpiAwarenessContextProc)::GetProcAddress( + user32Dll, "SetThreadDpiAwarenessContext"); + } + + sGetSystemMetricsForDpi = (GetSystemMetricsForDpiProc)::GetProcAddress( + user32Dll, "GetSystemMetricsForDpi"); + sGetDpiForWindow = + (GetDpiForWindowProc)::GetProcAddress(user32Dll, "GetDpiForWindow"); + } + } + + sHasPackageIdentity = mozilla::HasPackageIdentity(); +} + +// static +LRESULT WINAPI WinUtils::NonClientDpiScalingDefWindowProcW(HWND hWnd, UINT msg, + WPARAM wParam, + LPARAM lParam) { + MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown()); + + // NOTE: this function was copied out into the body of the pre-XUL skeleton + // UI window proc (PreXULSkeletonUI.cpp). If this function changes at any + // point, we should probably factor this out and use it from both locations. + if (msg == WM_NCCREATE && sEnableNonClientDpiScaling) { + sEnableNonClientDpiScaling(hWnd); + } + return ::DefWindowProcW(hWnd, msg, wParam, lParam); +} + +// static +void WinUtils::LogW(const wchar_t* fmt, ...) { + va_list args = nullptr; + if (!lstrlenW(fmt)) { + return; + } + va_start(args, fmt); + int buflen = _vscwprintf(fmt, args); + wchar_t* buffer = new wchar_t[buflen + 1]; + if (!buffer) { + va_end(args); + return; + } + vswprintf(buffer, buflen, fmt, args); + va_end(args); + + // MSVC, including remote debug sessions + OutputDebugStringW(buffer); + OutputDebugStringW(L"\n"); + + int len = + WideCharToMultiByte(CP_ACP, 0, buffer, -1, nullptr, 0, nullptr, nullptr); + if (len) { + char* utf8 = new char[len]; + if (WideCharToMultiByte(CP_ACP, 0, buffer, -1, utf8, len, nullptr, + nullptr) > 0) { + // desktop console + printf("%s\n", utf8); + NS_ASSERTION(gWindowsLog, + "Called WinUtils Log() but Widget " + "log module doesn't exist!"); + MOZ_LOG(gWindowsLog, LogLevel::Error, ("%s", utf8)); + } + delete[] utf8; + } + delete[] buffer; +} + +// static +void WinUtils::Log(const char* fmt, ...) { + va_list args = nullptr; + if (!strlen(fmt)) { + return; + } + va_start(args, fmt); + int buflen = _vscprintf(fmt, args); + char* buffer = new char[buflen + 1]; + if (!buffer) { + va_end(args); + return; + } + vsprintf(buffer, fmt, args); + va_end(args); + + // MSVC, including remote debug sessions + OutputDebugStringA(buffer); + OutputDebugStringW(L"\n"); + + // desktop console + printf("%s\n", buffer); + + NS_ASSERTION(gWindowsLog, + "Called WinUtils Log() but Widget " + "log module doesn't exist!"); + MOZ_LOG(gWindowsLog, LogLevel::Error, ("%s", buffer)); + delete[] buffer; +} + +// static +float WinUtils::SystemDPI() { + // The result of GetDeviceCaps won't change dynamically, as it predates + // per-monitor DPI and support for on-the-fly resolution changes. + // Therefore, we only need to look it up once. + static float dpi = 0; + if (dpi <= 0) { + HDC screenDC = GetDC(nullptr); + dpi = GetDeviceCaps(screenDC, LOGPIXELSY); + ReleaseDC(nullptr, screenDC); + } + + // Bug 1012487 - dpi can be 0 when the Screen DC is used off the + // main thread on windows. For now just assume a 100% DPI for this + // drawing call. + // XXX - fixme! + return dpi > 0 ? dpi : 96; +} + +// static +double WinUtils::SystemScaleFactor() { return SystemDPI() / 96.0; } + +typedef HRESULT(WINAPI* GETDPIFORMONITORPROC)(HMONITOR, MONITOR_DPI_TYPE, UINT*, + UINT*); + +typedef HRESULT(WINAPI* GETPROCESSDPIAWARENESSPROC)(HANDLE, + PROCESS_DPI_AWARENESS*); + +GETDPIFORMONITORPROC sGetDpiForMonitor; +GETPROCESSDPIAWARENESSPROC sGetProcessDpiAwareness; + +static bool SlowIsPerMonitorDPIAware() { + // Intentionally leak the handle. + HMODULE shcore = LoadLibraryEx(L"shcore", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (shcore) { + sGetDpiForMonitor = + (GETDPIFORMONITORPROC)GetProcAddress(shcore, "GetDpiForMonitor"); + sGetProcessDpiAwareness = (GETPROCESSDPIAWARENESSPROC)GetProcAddress( + shcore, "GetProcessDpiAwareness"); + } + PROCESS_DPI_AWARENESS dpiAwareness; + return sGetDpiForMonitor && sGetProcessDpiAwareness && + SUCCEEDED( + sGetProcessDpiAwareness(GetCurrentProcess(), &dpiAwareness)) && + dpiAwareness == PROCESS_PER_MONITOR_DPI_AWARE; +} + +/* static */ +bool WinUtils::IsPerMonitorDPIAware() { + static bool perMonitorDPIAware = SlowIsPerMonitorDPIAware(); + return perMonitorDPIAware; +} + +/* static */ +float WinUtils::MonitorDPI(HMONITOR aMonitor) { + if (IsPerMonitorDPIAware()) { + UINT dpiX, dpiY = 96; + sGetDpiForMonitor(aMonitor ? aMonitor : GetPrimaryMonitor(), + MDT_EFFECTIVE_DPI, &dpiX, &dpiY); + return dpiY; + } + + // We're not per-monitor aware, use system DPI instead. + return SystemDPI(); +} + +/* static */ +double WinUtils::LogToPhysFactor(HMONITOR aMonitor) { + return MonitorDPI(aMonitor) / 96.0; +} + +/* static */ +int32_t WinUtils::LogToPhys(HMONITOR aMonitor, double aValue) { + return int32_t(NS_round(aValue * LogToPhysFactor(aMonitor))); +} + +/* static */ +double WinUtils::LogToPhysFactor(HWND aWnd) { + // if there's an ancestor window, we want to share its DPI setting + HWND ancestor = ::GetAncestor(aWnd, GA_ROOTOWNER); + + // The GetDpiForWindow api is not available everywhere where we run as + // per-monitor, but if it is available rely on it to tell us the scale + // factor of the window. See bug 1722085. + if (sGetDpiForWindow) { + UINT dpi = sGetDpiForWindow(ancestor ? ancestor : aWnd); + if (dpi > 0) { + return static_cast<double>(dpi) / 96.0; + } + } + return LogToPhysFactor(::MonitorFromWindow(ancestor ? ancestor : aWnd, + MONITOR_DEFAULTTOPRIMARY)); +} + +/* static */ +HMONITOR +WinUtils::GetPrimaryMonitor() { + const POINT pt = {0, 0}; + return ::MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY); +} + +/* static */ +HMONITOR +WinUtils::MonitorFromRect(const gfx::Rect& rect) { + // convert coordinates from desktop to device pixels for MonitorFromRect + double dpiScale = + IsPerMonitorDPIAware() ? 1.0 : LogToPhysFactor(GetPrimaryMonitor()); + + RECT globalWindowBounds = {NSToIntRound(dpiScale * rect.X()), + NSToIntRound(dpiScale * rect.Y()), + NSToIntRound(dpiScale * (rect.XMost())), + NSToIntRound(dpiScale * (rect.YMost()))}; + + return ::MonitorFromRect(&globalWindowBounds, MONITOR_DEFAULTTONEAREST); +} + +/* static */ +bool WinUtils::HasSystemMetricsForDpi() { + MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown()); + return (sGetSystemMetricsForDpi != NULL); +} + +/* static */ +int WinUtils::GetSystemMetricsForDpi(int nIndex, UINT dpi) { + MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown()); + if (HasSystemMetricsForDpi()) { + return sGetSystemMetricsForDpi(nIndex, dpi); + } else { + double scale = IsPerMonitorDPIAware() ? dpi / SystemDPI() : 1.0; + return NSToIntRound(::GetSystemMetrics(nIndex) * scale); + } +} + +/* static */ +gfx::MarginDouble WinUtils::GetUnwriteableMarginsForDeviceInInches(HDC aHdc) { + if (!aHdc) { + return gfx::MarginDouble(); + } + + int pixelsPerInchY = ::GetDeviceCaps(aHdc, LOGPIXELSY); + int marginTop = ::GetDeviceCaps(aHdc, PHYSICALOFFSETY); + int printableAreaHeight = ::GetDeviceCaps(aHdc, VERTRES); + int physicalHeight = ::GetDeviceCaps(aHdc, PHYSICALHEIGHT); + + double marginTopInch = double(marginTop) / pixelsPerInchY; + + double printableAreaHeightInch = double(printableAreaHeight) / pixelsPerInchY; + double physicalHeightInch = double(physicalHeight) / pixelsPerInchY; + double marginBottomInch = + physicalHeightInch - printableAreaHeightInch - marginTopInch; + + int pixelsPerInchX = ::GetDeviceCaps(aHdc, LOGPIXELSX); + int marginLeft = ::GetDeviceCaps(aHdc, PHYSICALOFFSETX); + int printableAreaWidth = ::GetDeviceCaps(aHdc, HORZRES); + int physicalWidth = ::GetDeviceCaps(aHdc, PHYSICALWIDTH); + + double marginLeftInch = double(marginLeft) / pixelsPerInchX; + + double printableAreaWidthInch = double(printableAreaWidth) / pixelsPerInchX; + double physicalWidthInch = double(physicalWidth) / pixelsPerInchX; + double marginRightInch = + physicalWidthInch - printableAreaWidthInch - marginLeftInch; + + return gfx::MarginDouble(marginTopInch, marginRightInch, marginBottomInch, + marginLeftInch); +} + +#ifdef ACCESSIBILITY +/* static */ +a11y::LocalAccessible* WinUtils::GetRootAccessibleForHWND(HWND aHwnd) { + nsWindow* window = GetNSWindowPtr(aHwnd); + if (!window) { + return nullptr; + } + + return window->GetAccessible(); +} +#endif // ACCESSIBILITY + +/* static */ +bool WinUtils::PeekMessage(LPMSG aMsg, HWND aWnd, UINT aFirstMessage, + UINT aLastMessage, UINT aOption) { + RefPtr<ITfMessagePump> msgPump = TSFTextStore::GetMessagePump(); + if (msgPump) { + BOOL ret = FALSE; + HRESULT hr = msgPump->PeekMessageW(aMsg, aWnd, aFirstMessage, aLastMessage, + aOption, &ret); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + return ret; + } + return ::PeekMessageW(aMsg, aWnd, aFirstMessage, aLastMessage, aOption); +} + +/* static */ +bool WinUtils::GetMessage(LPMSG aMsg, HWND aWnd, UINT aFirstMessage, + UINT aLastMessage) { + RefPtr<ITfMessagePump> msgPump = TSFTextStore::GetMessagePump(); + if (msgPump) { + BOOL ret = FALSE; + HRESULT hr = + msgPump->GetMessageW(aMsg, aWnd, aFirstMessage, aLastMessage, &ret); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + return ret; + } + return ::GetMessageW(aMsg, aWnd, aFirstMessage, aLastMessage); +} + +#if defined(ACCESSIBILITY) +static DWORD GetWaitFlags() { + DWORD result = MWMO_INPUTAVAILABLE; + if (XRE_IsContentProcess()) { + result |= MWMO_ALERTABLE; + } + return result; +} +#endif + +/* static */ +void WinUtils::WaitForMessage(DWORD aTimeoutMs) { +#if defined(ACCESSIBILITY) + static const DWORD waitFlags = GetWaitFlags(); +#else + const DWORD waitFlags = MWMO_INPUTAVAILABLE; +#endif + + const DWORD waitStart = ::GetTickCount(); + DWORD elapsed = 0; + while (true) { + if (aTimeoutMs != INFINITE) { + elapsed = ::GetTickCount() - waitStart; + } + if (elapsed >= aTimeoutMs) { + break; + } + DWORD result; + { + AUTO_PROFILER_THREAD_SLEEP; + result = ::MsgWaitForMultipleObjectsEx(0, NULL, aTimeoutMs - elapsed, + MOZ_QS_ALLEVENT, waitFlags); + } + NS_WARNING_ASSERTION(result != WAIT_FAILED, "Wait failed"); + if (result == WAIT_TIMEOUT) { + break; + } +#if defined(ACCESSIBILITY) + if (result == WAIT_IO_COMPLETION) { + if (NS_IsMainThread()) { + // We executed an APC that would have woken up the hang monitor. Since + // there are no more APCs pending and we are now going to sleep again, + // we should notify the hang monitor. + mozilla::BackgroundHangMonitor().NotifyWait(); + } + continue; + } +#endif // defined(ACCESSIBILITY) + + // Sent messages (via SendMessage and friends) are processed differently + // than queued messages (via PostMessage); the destination window procedure + // of the sent message is called during (Get|Peek)Message. Since PeekMessage + // does not tell us whether it processed any sent messages, we need to query + // this ahead of time. + bool haveSentMessagesPending = + (HIWORD(::GetQueueStatus(QS_SENDMESSAGE)) & QS_SENDMESSAGE) != 0; + + MSG msg = {0}; + if (haveSentMessagesPending || + ::PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE)) { + break; + } + // The message is intended for another thread that has been synchronized + // with our input queue; yield to give other threads an opportunity to + // process the message. This should prevent busy waiting if resumed due + // to another thread's message. + ::SwitchToThread(); + } +} + +/* static */ +HWND WinUtils::GetTopLevelHWND(HWND aWnd, bool aStopIfNotChild, + bool aStopIfNotPopup) { + HWND curWnd = aWnd; + HWND topWnd = nullptr; + + while (curWnd) { + topWnd = curWnd; + + if (aStopIfNotChild) { + DWORD_PTR style = ::GetWindowLongPtrW(curWnd, GWL_STYLE); + + VERIFY_WINDOW_STYLE(style); + + if (!(style & WS_CHILD)) // first top-level window + break; + } + + HWND upWnd = ::GetParent(curWnd); // Parent or owner (if has no parent) + + // GetParent will only return the owner if the passed in window + // has the WS_POPUP style. + if (!upWnd && !aStopIfNotPopup) { + upWnd = ::GetWindow(curWnd, GW_OWNER); + } + curWnd = upWnd; + } + + return topWnd; +} + +// Map from native window handles to nsWindow structures. Does not AddRef. +// Inherently unsafe to access outside the main thread. +static nsTHashMap<HWND, nsWindow*> sExtantNSWindows; + +/* static */ +void WinUtils::SetNSWindowPtr(HWND aWnd, nsWindow* aWindow) { + MOZ_ASSERT(NS_IsMainThread()); + if (!aWindow) { + sExtantNSWindows.Remove(aWnd); + } else { + sExtantNSWindows.InsertOrUpdate(aWnd, aWindow); + } +} + +/* static */ +nsWindow* WinUtils::GetNSWindowPtr(HWND aWnd) { + MOZ_ASSERT(NS_IsMainThread()); + return sExtantNSWindows.Get(aWnd); // or nullptr +} + +/* static */ +bool WinUtils::IsOurProcessWindow(HWND aWnd) { + if (!aWnd) { + return false; + } + DWORD processId = 0; + ::GetWindowThreadProcessId(aWnd, &processId); + return (processId == ::GetCurrentProcessId()); +} + +/* static */ +HWND WinUtils::FindOurProcessWindow(HWND aWnd) { + for (HWND wnd = ::GetParent(aWnd); wnd; wnd = ::GetParent(wnd)) { + if (IsOurProcessWindow(wnd)) { + return wnd; + } + } + return nullptr; +} + +static bool IsPointInWindow(HWND aWnd, const POINT& aPointInScreen) { + RECT bounds; + if (!::GetWindowRect(aWnd, &bounds)) { + return false; + } + + return (aPointInScreen.x >= bounds.left && aPointInScreen.x < bounds.right && + aPointInScreen.y >= bounds.top && aPointInScreen.y < bounds.bottom); +} + +/** + * FindTopmostWindowAtPoint() returns the topmost child window (topmost means + * forground in this context) of aWnd. + */ + +static HWND FindTopmostWindowAtPoint(HWND aWnd, const POINT& aPointInScreen) { + if (!::IsWindowVisible(aWnd) || !IsPointInWindow(aWnd, aPointInScreen)) { + return nullptr; + } + + HWND childWnd = ::GetTopWindow(aWnd); + while (childWnd) { + HWND topmostWnd = FindTopmostWindowAtPoint(childWnd, aPointInScreen); + if (topmostWnd) { + return topmostWnd; + } + childWnd = ::GetNextWindow(childWnd, GW_HWNDNEXT); + } + + return aWnd; +} + +struct FindOurWindowAtPointInfo { + POINT mInPointInScreen; + HWND mOutWnd; +}; + +static BOOL CALLBACK FindOurWindowAtPointCallback(HWND aWnd, LPARAM aLPARAM) { + if (!WinUtils::IsOurProcessWindow(aWnd)) { + // This isn't one of our top-level windows; continue enumerating. + return TRUE; + } + + // Get the top-most child window under the point. If there's no child + // window, and the point is within the top-level window, then the top-level + // window will be returned. (This is the usual case. A child window + // would be returned for plugins.) + FindOurWindowAtPointInfo* info = + reinterpret_cast<FindOurWindowAtPointInfo*>(aLPARAM); + HWND childWnd = FindTopmostWindowAtPoint(aWnd, info->mInPointInScreen); + if (!childWnd) { + // This window doesn't contain the point; continue enumerating. + return TRUE; + } + + // Return the HWND and stop enumerating. + info->mOutWnd = childWnd; + return FALSE; +} + +/* static */ +HWND WinUtils::FindOurWindowAtPoint(const POINT& aPointInScreen) { + FindOurWindowAtPointInfo info; + info.mInPointInScreen = aPointInScreen; + info.mOutWnd = nullptr; + + // This will enumerate all top-level windows in order from top to bottom. + EnumWindows(FindOurWindowAtPointCallback, reinterpret_cast<LPARAM>(&info)); + return info.mOutWnd; +} + +/* static */ +UINT WinUtils::GetInternalMessage(UINT aNativeMessage) { + switch (aNativeMessage) { + case WM_MOUSEWHEEL: + return MOZ_WM_MOUSEVWHEEL; + case WM_MOUSEHWHEEL: + return MOZ_WM_MOUSEHWHEEL; + case WM_VSCROLL: + return MOZ_WM_VSCROLL; + case WM_HSCROLL: + return MOZ_WM_HSCROLL; + default: + return aNativeMessage; + } +} + +/* static */ +UINT WinUtils::GetNativeMessage(UINT aInternalMessage) { + switch (aInternalMessage) { + case MOZ_WM_MOUSEVWHEEL: + return WM_MOUSEWHEEL; + case MOZ_WM_MOUSEHWHEEL: + return WM_MOUSEHWHEEL; + case MOZ_WM_VSCROLL: + return WM_VSCROLL; + case MOZ_WM_HSCROLL: + return WM_HSCROLL; + default: + return aInternalMessage; + } +} + +/* static */ +uint16_t WinUtils::GetMouseInputSource() { + int32_t inputSource = dom::MouseEvent_Binding::MOZ_SOURCE_MOUSE; + LPARAM lParamExtraInfo = ::GetMessageExtraInfo(); + if ((lParamExtraInfo & TABLET_INK_SIGNATURE) == TABLET_INK_CHECK) { + inputSource = (lParamExtraInfo & TABLET_INK_TOUCH) + ? dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH + : dom::MouseEvent_Binding::MOZ_SOURCE_PEN; + } + return static_cast<uint16_t>(inputSource); +} + +/* static */ +uint16_t WinUtils::GetMousePointerID() { + LPARAM lParamExtraInfo = ::GetMessageExtraInfo(); + return lParamExtraInfo & TABLET_INK_ID_MASK; +} + +/* static */ +bool WinUtils::GetIsMouseFromTouch(EventMessage aEventMessage) { + const uint32_t MOZ_T_I_SIGNATURE = TABLET_INK_TOUCH | TABLET_INK_SIGNATURE; + const uint32_t MOZ_T_I_CHECK_TCH = TABLET_INK_TOUCH | TABLET_INK_CHECK; + return ((aEventMessage == eMouseMove || aEventMessage == eMouseDown || + aEventMessage == eMouseUp || aEventMessage == eMouseAuxClick || + aEventMessage == eMouseDoubleClick) && + (GetMessageExtraInfo() & MOZ_T_I_SIGNATURE) == MOZ_T_I_CHECK_TCH); +} + +/* static */ +MSG WinUtils::InitMSG(UINT aMessage, WPARAM wParam, LPARAM lParam, HWND aWnd) { + MSG msg; + msg.message = aMessage; + msg.wParam = wParam; + msg.lParam = lParam; + msg.hwnd = aWnd; + return msg; +} + +#ifdef MOZ_PLACES +/************************************************************************ + * Constructs as AsyncFaviconDataReady Object + * @param aIOThread : the thread which performs the action + * @param aURLShortcut : Differentiates between (false)Jumplistcache and + * (true)Shortcutcache + * @param aRunnable : Executed in the aIOThread when the favicon cache is + * avaiable + ************************************************************************/ + +AsyncFaviconDataReady::AsyncFaviconDataReady( + nsIURI* aNewURI, RefPtr<LazyIdleThread>& aIOThread, const bool aURLShortcut, + already_AddRefed<nsIRunnable> aRunnable) + : mNewURI(aNewURI), + mIOThread(aIOThread), + mRunnable(aRunnable), + mURLShortcut(aURLShortcut) {} + +NS_IMETHODIMP +myDownloadObserver::OnDownloadComplete(nsIDownloader* downloader, + nsIRequest* request, nsresult status, + nsIFile* result) { + return NS_OK; +} + +nsresult AsyncFaviconDataReady::OnFaviconDataNotAvailable(void) { + if (!mURLShortcut) { + return NS_OK; + } + + nsCOMPtr<nsIFile> icoFile; + nsresult rv = + FaviconHelper::GetOutputIconPath(mNewURI, icoFile, mURLShortcut); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> mozIconURI; + rv = NS_NewURI(getter_AddRefs(mozIconURI), "moz-icon://.html?size=32"); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), mozIconURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_INTERNAL_IMAGE); + + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDownloadObserver> downloadObserver = new myDownloadObserver; + nsCOMPtr<nsIStreamListener> listener; + rv = NS_NewDownloader(getter_AddRefs(listener), downloadObserver, icoFile); + NS_ENSURE_SUCCESS(rv, rv); + + return channel->AsyncOpen(listener); +} + +NS_IMETHODIMP +AsyncFaviconDataReady::OnComplete(nsIURI* aFaviconURI, uint32_t aDataLen, + const uint8_t* aData, + const nsACString& aMimeType, + uint16_t aWidth) { + if (!aDataLen || !aData) { + if (mURLShortcut) { + OnFaviconDataNotAvailable(); + } + + return NS_OK; + } + + nsCOMPtr<nsIFile> icoFile; + nsresult rv = + FaviconHelper::GetOutputIconPath(mNewURI, icoFile, mURLShortcut); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString path; + rv = icoFile->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + + // Decode the image from the format it was returned to us in (probably PNG) + nsCOMPtr<imgIContainer> container; + nsCOMPtr<imgITools> imgtool = do_CreateInstance("@mozilla.org/image/tools;1"); + rv = imgtool->DecodeImageFromBuffer(reinterpret_cast<const char*>(aData), + aDataLen, aMimeType, + getter_AddRefs(container)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<SourceSurface> surface = container->GetFrame( + imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE); + + RefPtr<DataSourceSurface> dataSurface; + IntSize size; + + if (mURLShortcut && + (surface->GetSize().width < 48 || surface->GetSize().height < 48)) { + // Create a 48x48 surface and paint the icon into the central rect. + size.width = std::max(surface->GetSize().width, 48); + size.height = std::max(surface->GetSize().height, 48); + dataSurface = + Factory::CreateDataSourceSurface(size, SurfaceFormat::B8G8R8A8); + NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE); + + DataSourceSurface::MappedSurface map; + if (!dataSurface->Map(DataSourceSurface::MapType::WRITE, &map)) { + return NS_ERROR_FAILURE; + } + + RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData( + BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride, + dataSurface->GetFormat()); + if (!dt) { + gfxWarning() << "AsyncFaviconDataReady::OnComplete failed in " + "CreateDrawTargetForData"; + return NS_ERROR_OUT_OF_MEMORY; + } + dt->FillRect(Rect(0, 0, size.width, size.height), + ColorPattern(ToDeviceColor(sRGBColor::OpaqueWhite()))); + IntPoint point; + point.x = (size.width - surface->GetSize().width) / 2; + point.y = (size.height - surface->GetSize().height) / 2; + dt->DrawSurface(surface, + Rect(point.x, point.y, surface->GetSize().width, + surface->GetSize().height), + Rect(Point(0, 0), Size(surface->GetSize().width, + surface->GetSize().height))); + + dataSurface->Unmap(); + } else { + // By using the input image surface's size, we may end up encoding + // to a different size than a 16x16 (or bigger for higher DPI) ICO, but + // Windows will resize appropriately for us. If we want to encode ourselves + // one day because we like our resizing better, we'd have to manually + // resize the image here and use GetSystemMetrics w/ SM_CXSMICON and + // SM_CYSMICON. We don't support resizing images asynchronously at the + // moment anyway so getting the DPI aware icon size won't help. + size.width = surface->GetSize().width; + size.height = surface->GetSize().height; + dataSurface = surface->GetDataSurface(); + NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE); + } + + // Allocate a new buffer that we own and can use out of line in + // another thread. + UniquePtr<uint8_t[]> data = SurfaceToPackedBGRA(dataSurface); + if (!data) { + return NS_ERROR_OUT_OF_MEMORY; + } + int32_t stride = 4 * size.width; + + // AsyncEncodeAndWriteIcon takes ownership of the heap allocated buffer + nsCOMPtr<nsIRunnable> event = + new AsyncEncodeAndWriteIcon(path, std::move(data), stride, size.width, + size.height, mRunnable.forget()); + mIOThread->Dispatch(event, NS_DISPATCH_NORMAL); + + return NS_OK; +} +#endif + +// Warning: AsyncEncodeAndWriteIcon assumes ownership of the aData buffer passed +// in +AsyncEncodeAndWriteIcon::AsyncEncodeAndWriteIcon( + const nsAString& aIconPath, UniquePtr<uint8_t[]> aBuffer, uint32_t aStride, + uint32_t aWidth, uint32_t aHeight, already_AddRefed<nsIRunnable> aRunnable) + : mIconPath(aIconPath), + mBuffer(std::move(aBuffer)), + mRunnable(aRunnable), + mStride(aStride), + mWidth(aWidth), + mHeight(aHeight) {} + +NS_IMETHODIMP AsyncEncodeAndWriteIcon::Run() { + MOZ_ASSERT(!NS_IsMainThread(), "Should not be called on the main thread."); + + // Note that since we're off the main thread we can't use + // gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget() + RefPtr<DataSourceSurface> surface = Factory::CreateWrappingDataSourceSurface( + mBuffer.get(), mStride, IntSize(mWidth, mHeight), + SurfaceFormat::B8G8R8A8); + + FILE* file = _wfopen(mIconPath.get(), L"wb"); + if (!file) { + // Maybe the directory doesn't exist; try creating it, then fopen again. + nsresult rv = NS_ERROR_FAILURE; + nsCOMPtr<nsIFile> comFile = do_CreateInstance("@mozilla.org/file/local;1"); + if (comFile) { + rv = comFile->InitWithPath(mIconPath); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIFile> dirPath; + comFile->GetParent(getter_AddRefs(dirPath)); + if (dirPath) { + rv = dirPath->Create(nsIFile::DIRECTORY_TYPE, 0777); + if (NS_SUCCEEDED(rv) || rv == NS_ERROR_FILE_ALREADY_EXISTS) { + file = _wfopen(mIconPath.get(), L"wb"); + if (!file) { + rv = NS_ERROR_FAILURE; + } + } + } + } + } + if (!file) { + return rv; + } + } + nsresult rv = gfxUtils::EncodeSourceSurface(surface, ImageType::ICO, u""_ns, + gfxUtils::eBinaryEncode, file); + fclose(file); + NS_ENSURE_SUCCESS(rv, rv); + + if (mRunnable) { + mRunnable->Run(); + } + return rv; +} + +AsyncEncodeAndWriteIcon::~AsyncEncodeAndWriteIcon() {} + +AsyncDeleteAllFaviconsFromDisk::AsyncDeleteAllFaviconsFromDisk( + bool aIgnoreRecent) + : mIgnoreRecent(aIgnoreRecent) { + // We can't call FaviconHelper::GetICOCacheSecondsTimeout() on non-main + // threads, as it reads a pref, so cache its value here. + mIcoNoDeleteSeconds = FaviconHelper::GetICOCacheSecondsTimeout() + 600; + + // Prepare the profile directory cache on the main thread, to ensure we wont + // do this on non-main threads. + Unused << NS_GetSpecialDirectory("ProfLDS", + getter_AddRefs(mJumpListCacheDir)); +} + +NS_IMETHODIMP AsyncDeleteAllFaviconsFromDisk::Run() { + if (!mJumpListCacheDir) { + return NS_ERROR_FAILURE; + } + // Construct the path of our jump list cache + nsresult rv = mJumpListCacheDir->AppendNative( + nsDependentCString(FaviconHelper::kJumpListCacheDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDirectoryEnumerator> entries; + rv = mJumpListCacheDir->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ENSURE_SUCCESS(rv, rv); + + // Loop through each directory entry and remove all ICO files found + do { + nsCOMPtr<nsIFile> 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; + + if (mIgnoreRecent) { + // Check to make sure the icon wasn't just recently created. + // If it was created recently, don't delete it yet. + int64_t fileModTime = 0; + rv = currFile->GetLastModifiedTime(&fileModTime); + fileModTime /= PR_MSEC_PER_SEC; + // If the icon is older than the regeneration time (+ 10 min to be + // safe), then it's old and we can get rid of it. + // This code is only hit directly after a regeneration. + int64_t nowTime = PR_Now() / int64_t(PR_USEC_PER_SEC); + if (NS_FAILED(rv) || (nowTime - fileModTime) < mIcoNoDeleteSeconds) { + continue; + } + } + + // We found an ICO file that exists, so we should remove it + currFile->Remove(false); + } + } while (true); + + return NS_OK; +} + +AsyncDeleteAllFaviconsFromDisk::~AsyncDeleteAllFaviconsFromDisk() {} + +/* + * (static) If the data is available, will return the path on disk where + * the favicon for page aFaviconPageURI is stored. If the favicon does not + * exist, or its cache is expired, this method will kick off an async request + * for the icon so that next time the method is called it will be available. + * @param aFaviconPageURI The URI of the page to obtain + * @param aICOFilePath The path of the icon file + * @param aIOThread The thread to perform the Fetch on + * @param aURLShortcut to distinguish between jumplistcache(false) and + * shortcutcache(true) + * @param aRunnable Executed in the aIOThread when the favicon cache is + * avaiable + */ +nsresult FaviconHelper::ObtainCachedIconFile( + nsCOMPtr<nsIURI> aFaviconPageURI, nsString& aICOFilePath, + RefPtr<LazyIdleThread>& aIOThread, bool aURLShortcut, + already_AddRefed<nsIRunnable> aRunnable) { + nsCOMPtr<nsIRunnable> runnable = aRunnable; + // Obtain the ICO file path + nsCOMPtr<nsIFile> icoFile; + nsresult rv = GetOutputIconPath(aFaviconPageURI, icoFile, aURLShortcut); + NS_ENSURE_SUCCESS(rv, rv); + + // Check if the cached ICO file already exists + bool exists; + rv = icoFile->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + // Obtain the file's last modification date in seconds + int64_t fileModTime = 0; + rv = icoFile->GetLastModifiedTime(&fileModTime); + fileModTime /= PR_MSEC_PER_SEC; + int32_t icoReCacheSecondsTimeout = GetICOCacheSecondsTimeout(); + int64_t nowTime = PR_Now() / int64_t(PR_USEC_PER_SEC); + + // If the last mod call failed or the icon is old then re-cache it + // This check is in case the favicon of a page changes + // the next time we try to build the jump list, the data will be available. + if (NS_FAILED(rv) || (nowTime - fileModTime) > icoReCacheSecondsTimeout) { + CacheIconFileFromFaviconURIAsync(aFaviconPageURI, icoFile, aIOThread, + aURLShortcut, runnable.forget()); + return NS_ERROR_NOT_AVAILABLE; + } + } else { + // The file does not exist yet, obtain it async from the favicon service so + // that the next time we try to build the jump list it'll be available. + CacheIconFileFromFaviconURIAsync(aFaviconPageURI, icoFile, aIOThread, + aURLShortcut, runnable.forget()); + return NS_ERROR_NOT_AVAILABLE; + } + + // The icoFile is filled with a path that exists, get its path + rv = icoFile->GetPath(aICOFilePath); + return rv; +} + +// Hash a URI using a cryptographic hash function (currently SHA-256) +// Output will be a base64-encoded string of the hash. +static nsresult HashURI(nsIURI* aUri, nsACString& aUriHash) { + nsAutoCString spec; + nsresult rv = aUri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsICryptoHash> cryptoHash = + do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cryptoHash->Init(nsICryptoHash::SHA256); + NS_ENSURE_SUCCESS(rv, rv); + + // Add some context to the hash to even further reduce the chances of + // collision. Note that we are hashing this string with its null-terminator. + const char kHashUriContext[] = "firefox-uri"; + rv = cryptoHash->Update(reinterpret_cast<const uint8_t*>(kHashUriContext), + sizeof(kHashUriContext)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cryptoHash->Update(reinterpret_cast<const uint8_t*>(spec.BeginReading()), + spec.Length()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cryptoHash->Finish(true, aUriHash); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// (static) Obtains the ICO file for the favicon at page aFaviconPageURI +// If successful, the file path on disk is in the format: +// <ProfLDS>\jumpListCache\<hash(aFaviconPageURI)>.ico +// +// We generate the name with a cryptographically secure hash function in order +// to ensure that malicious websites can't intentionally craft URLs to collide +// with legitimate websites. +nsresult FaviconHelper::GetOutputIconPath(nsCOMPtr<nsIURI> aFaviconPageURI, + nsCOMPtr<nsIFile>& aICOFile, + bool aURLShortcut) { + nsAutoCString inputURIHash; + nsresult rv = HashURI(aFaviconPageURI, inputURIHash); + NS_ENSURE_SUCCESS(rv, rv); + char* cur = inputURIHash.BeginWriting(); + char* end = inputURIHash.EndWriting(); + for (; cur < end; ++cur) { + if ('/' == *cur) { + *cur = '_'; + } + } + + // Obtain the local profile directory and construct the output icon file path + rv = NS_GetSpecialDirectory("ProfLDS", getter_AddRefs(aICOFile)); + NS_ENSURE_SUCCESS(rv, rv); + if (!aURLShortcut) + rv = aICOFile->AppendNative(nsDependentCString(kJumpListCacheDir)); + else + rv = aICOFile->AppendNative(nsDependentCString(kShortcutCacheDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // Append the icon extension + inputURIHash.AppendLiteral(".ico"); + rv = aICOFile->AppendNative(inputURIHash); + + return rv; +} + +// (static) Asynchronously creates a cached ICO file on disk for the favicon of +// page aFaviconPageURI and stores it to disk at the path of aICOFile. +nsresult FaviconHelper::CacheIconFileFromFaviconURIAsync( + nsCOMPtr<nsIURI> aFaviconPageURI, nsCOMPtr<nsIFile> aICOFile, + RefPtr<LazyIdleThread>& aIOThread, bool aURLShortcut, + already_AddRefed<nsIRunnable> aRunnable) { + nsCOMPtr<nsIRunnable> runnable = aRunnable; +#ifdef MOZ_PLACES + // Obtain the favicon service and get the favicon for the specified page + nsCOMPtr<nsIFaviconService> favIconSvc( + do_GetService("@mozilla.org/browser/favicon-service;1")); + NS_ENSURE_TRUE(favIconSvc, NS_ERROR_FAILURE); + + nsCOMPtr<nsIFaviconDataCallback> callback = + new mozilla::widget::AsyncFaviconDataReady( + aFaviconPageURI, aIOThread, aURLShortcut, runnable.forget()); + + favIconSvc->GetFaviconDataForPage(aFaviconPageURI, callback, 0); +#endif + return NS_OK; +} + +// Obtains the jump list 'ICO cache timeout in seconds' pref +int32_t FaviconHelper::GetICOCacheSecondsTimeout() { + // Only obtain the setting at most once from the pref service. + // In the rare case that 2 threads call this at the same + // time it is no harm and we will simply obtain the pref twice. + // None of the taskbar list prefs are currently updated via a + // pref observer so I think this should suffice. + const int32_t kSecondsPerDay = 86400; + static bool alreadyObtained = false; + static int32_t icoReCacheSecondsTimeout = kSecondsPerDay; + if (alreadyObtained) { + return icoReCacheSecondsTimeout; + } + + // Obtain the pref + const char PREF_ICOTIMEOUT[] = "browser.taskbar.lists.icoTimeoutInSeconds"; + icoReCacheSecondsTimeout = + Preferences::GetInt(PREF_ICOTIMEOUT, kSecondsPerDay); + alreadyObtained = true; + return icoReCacheSecondsTimeout; +} + +/* static */ +LayoutDeviceIntRegion WinUtils::ConvertHRGNToRegion(HRGN aRgn) { + NS_ASSERTION(aRgn, "Don't pass NULL region here"); + + LayoutDeviceIntRegion rgn; + + DWORD size = ::GetRegionData(aRgn, 0, nullptr); + AutoTArray<uint8_t, 100> buffer; + buffer.SetLength(size); + + RGNDATA* data = reinterpret_cast<RGNDATA*>(buffer.Elements()); + if (!::GetRegionData(aRgn, size, data)) return rgn; + + if (data->rdh.nCount > MAX_RECTS_IN_REGION) { + rgn = ToIntRect(data->rdh.rcBound); + return rgn; + } + + RECT* rects = reinterpret_cast<RECT*>(data->Buffer); + for (uint32_t i = 0; i < data->rdh.nCount; ++i) { + RECT* r = rects + i; + rgn.Or(rgn, ToIntRect(*r)); + } + + return rgn; +} + +LayoutDeviceIntRect WinUtils::ToIntRect(const RECT& aRect) { + return LayoutDeviceIntRect(aRect.left, aRect.top, aRect.right - aRect.left, + aRect.bottom - aRect.top); +} + +/* static */ +bool WinUtils::IsIMEEnabled(const InputContext& aInputContext) { + return IsIMEEnabled(aInputContext.mIMEState.mEnabled); +} + +/* static */ +bool WinUtils::IsIMEEnabled(IMEEnabled aIMEState) { + return aIMEState == IMEEnabled::Enabled; +} + +/* static */ +void WinUtils::SetupKeyModifiersSequence(nsTArray<KeyPair>* aArray, + uint32_t aModifiers, UINT aMessage) { + MOZ_ASSERT(!(aModifiers & nsIWidget::ALTGRAPH) || + !(aModifiers & (nsIWidget::CTRL_L | nsIWidget::ALT_R))); + if (aMessage == WM_KEYUP) { + // If AltGr is released, ControlLeft key is released first, then, + // AltRight key is released. + if (aModifiers & nsIWidget::ALTGRAPH) { + aArray->AppendElement( + KeyPair(VK_CONTROL, VK_LCONTROL, ScanCode::eControlLeft)); + aArray->AppendElement(KeyPair(VK_MENU, VK_RMENU, ScanCode::eAltRight)); + } + for (uint32_t i = ArrayLength(sModifierKeyMap); i; --i) { + const uint32_t* map = sModifierKeyMap[i - 1]; + if (aModifiers & map[0]) { + aArray->AppendElement(KeyPair(map[1], map[2], map[3])); + } + } + } else { + for (uint32_t i = 0; i < ArrayLength(sModifierKeyMap); ++i) { + const uint32_t* map = sModifierKeyMap[i]; + if (aModifiers & map[0]) { + aArray->AppendElement(KeyPair(map[1], map[2], map[3])); + } + } + // If AltGr is pressed, ControlLeft key is pressed first, then, + // AltRight key is pressed. + if (aModifiers & nsIWidget::ALTGRAPH) { + aArray->AppendElement( + KeyPair(VK_CONTROL, VK_LCONTROL, ScanCode::eControlLeft)); + aArray->AppendElement(KeyPair(VK_MENU, VK_RMENU, ScanCode::eAltRight)); + } + } +} + +/* static */ +nsresult WinUtils::WriteBitmap(nsIFile* aFile, imgIContainer* aImage) { + RefPtr<SourceSurface> surface = aImage->GetFrame( + imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE); + + return WriteBitmap(aFile, surface); +} + +/* static */ +nsresult WinUtils::WriteBitmap(nsIFile* aFile, SourceSurface* surface) { + nsresult rv; + + // For either of the following formats we want to set the biBitCount member + // of the BITMAPINFOHEADER struct to 32, below. For that value the bitmap + // format defines that the A8/X8 WORDs in the bitmap byte stream be ignored + // for the BI_RGB value we use for the biCompression member. + MOZ_ASSERT(surface->GetFormat() == SurfaceFormat::B8G8R8A8 || + surface->GetFormat() == SurfaceFormat::B8G8R8X8); + + RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface(); + NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE); + + int32_t width = dataSurface->GetSize().width; + int32_t height = dataSurface->GetSize().height; + int32_t bytesPerPixel = 4 * sizeof(uint8_t); + uint32_t bytesPerRow = bytesPerPixel * width; + bool hasAlpha = surface->GetFormat() == SurfaceFormat::B8G8R8A8; + + // initialize these bitmap structs which we will later + // serialize directly to the head of the bitmap file + BITMAPV4HEADER bmi; + memset(&bmi, 0, sizeof(BITMAPV4HEADER)); + bmi.bV4Size = sizeof(BITMAPV4HEADER); + bmi.bV4Width = width; + bmi.bV4Height = height; + bmi.bV4Planes = 1; + bmi.bV4BitCount = (WORD)bytesPerPixel * 8; + bmi.bV4V4Compression = hasAlpha ? BI_BITFIELDS : BI_RGB; + bmi.bV4SizeImage = bytesPerRow * height; + bmi.bV4CSType = LCS_sRGB; + if (hasAlpha) { + bmi.bV4RedMask = 0x00FF0000; + bmi.bV4GreenMask = 0x0000FF00; + bmi.bV4BlueMask = 0x000000FF; + bmi.bV4AlphaMask = 0xFF000000; + } + + BITMAPFILEHEADER bf; + DWORD colormask[3]; + bf.bfType = 0x4D42; // 'BM' + bf.bfReserved1 = 0; + bf.bfReserved2 = 0; + bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPV4HEADER) + + (hasAlpha ? sizeof(colormask) : 0); + bf.bfSize = bf.bfOffBits + bmi.bV4SizeImage; + + // get a file output stream + nsCOMPtr<nsIOutputStream> stream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), aFile); + NS_ENSURE_SUCCESS(rv, rv); + + DataSourceSurface::MappedSurface map; + if (!dataSurface->Map(DataSourceSurface::MapType::READ, &map)) { + return NS_ERROR_FAILURE; + } + + // write the bitmap headers and rgb pixel data to the file + rv = NS_ERROR_FAILURE; + if (stream) { + uint32_t written; + stream->Write((const char*)&bf, sizeof(BITMAPFILEHEADER), &written); + if (written == sizeof(BITMAPFILEHEADER)) { + stream->Write((const char*)&bmi, sizeof(BITMAPV4HEADER), &written); + if (written == sizeof(BITMAPV4HEADER)) { + if (hasAlpha) { + // color mask + colormask[0] = 0x00FF0000; + colormask[1] = 0x0000FF00; + colormask[2] = 0x000000FF; + + stream->Write((const char*)colormask, sizeof(colormask), &written); + } + if (!hasAlpha || written == sizeof(colormask)) { + // write out the image data backwards because the desktop won't + // show bitmaps with negative heights for top-to-bottom + uint32_t i = map.mStride * height; + do { + i -= map.mStride; + stream->Write(((const char*)map.mData) + i, bytesPerRow, &written); + if (written == bytesPerRow) { + rv = NS_OK; + } else { + rv = NS_ERROR_FAILURE; + break; + } + } while (i != 0); + } + } + } + + stream->Close(); + } + + dataSurface->Unmap(); + + return rv; +} + +// This is in use here and in dom/events/TouchEvent.cpp +/* static */ +uint32_t WinUtils::IsTouchDeviceSupportPresent() { + int32_t touchCapabilities = ::GetSystemMetrics(SM_DIGITIZER); + int32_t touchFlags = NID_EXTERNAL_TOUCH | NID_INTEGRATED_TOUCH; + if (StaticPrefs::dom_w3c_pointer_events_scroll_by_pen_enabled()) { + touchFlags |= NID_EXTERNAL_PEN | NID_INTEGRATED_PEN; + } + return (touchCapabilities & NID_READY) && (touchCapabilities & touchFlags); +} + +/* static */ +uint32_t WinUtils::GetMaxTouchPoints() { + if (IsTouchDeviceSupportPresent()) { + return GetSystemMetrics(SM_MAXIMUMTOUCHES); + } + return 0; +} + +/* static */ +POWER_PLATFORM_ROLE +WinUtils::GetPowerPlatformRole() { + typedef POWER_PLATFORM_ROLE(WINAPI * + PowerDeterminePlatformRoleEx)(ULONG Version); + static PowerDeterminePlatformRoleEx power_determine_platform_role = + reinterpret_cast<PowerDeterminePlatformRoleEx>(::GetProcAddress( + ::LoadLibraryW(L"PowrProf.dll"), "PowerDeterminePlatformRoleEx")); + + POWER_PLATFORM_ROLE powerPlatformRole = PlatformRoleUnspecified; + if (!power_determine_platform_role) { + return powerPlatformRole; + } + + return power_determine_platform_role(POWER_PLATFORM_ROLE_V2); +} + +// static +bool WinUtils::GetAutoRotationState(AR_STATE* aRotationState) { + typedef BOOL(WINAPI * GetAutoRotationStateFunc)(PAR_STATE pState); + static GetAutoRotationStateFunc get_auto_rotation_state_func = + reinterpret_cast<GetAutoRotationStateFunc>(::GetProcAddress( + GetModuleHandleW(L"user32.dll"), "GetAutoRotationState")); + if (get_auto_rotation_state_func) { + ZeroMemory(aRotationState, sizeof(AR_STATE)); + return get_auto_rotation_state_func(aRotationState); + } + return false; +} + +// static +void WinUtils::GetClipboardFormatAsString(UINT aFormat, nsAString& aOutput) { + wchar_t buf[256] = {}; + // Get registered format name and ensure the existence of a terminating '\0' + // if the registered name is more than 256 characters. + if (::GetClipboardFormatNameW(aFormat, buf, ARRAYSIZE(buf) - 1)) { + aOutput.Append(buf); + return; + } + // Standard clipboard formats + // https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats + switch (aFormat) { + case CF_TEXT: // 1 + aOutput.Append(u"CF_TEXT"_ns); + break; + case CF_BITMAP: // 2 + aOutput.Append(u"CF_BITMAP"_ns); + break; + case CF_DIB: // 8 + aOutput.Append(u"CF_DIB"_ns); + break; + case CF_UNICODETEXT: // 13 + aOutput.Append(u"CF_UNICODETEXT"_ns); + break; + case CF_HDROP: // 15 + aOutput.Append(u"CF_HDROP"_ns); + break; + case CF_DIBV5: // 17 + aOutput.Append(u"CF_DIBV5"_ns); + break; + default: + aOutput.AppendPrintf("%u", aFormat); + break; + } +} + +static bool IsTabletDevice() { + // Guarantees that: + // - The device has a touch screen. + // - It is used as a tablet which means that it has no keyboard connected. + // On Windows 10 it means that it is verifying with ConvertibleSlateMode. + + if (WindowsUIUtils::GetInTabletMode()) { + return true; + } + + if (!GetSystemMetrics(SM_MAXIMUMTOUCHES)) { + return false; + } + + // If the device is docked, the user is treating the device as a PC. + if (GetSystemMetrics(SM_SYSTEMDOCKED)) { + return false; + } + + // If the device is not supporting rotation, it's unlikely to be a tablet, + // a convertible or a detachable. See: + // https://msdn.microsoft.com/en-us/library/windows/desktop/dn629263(v=vs.85).aspx + AR_STATE rotation_state; + if (WinUtils::GetAutoRotationState(&rotation_state) && + (rotation_state & (AR_NOT_SUPPORTED | AR_LAPTOP | AR_NOSENSOR))) { + return false; + } + + // PlatformRoleSlate was added in Windows 8+. + POWER_PLATFORM_ROLE role = WinUtils::GetPowerPlatformRole(); + if (role == PlatformRoleMobile || role == PlatformRoleSlate) { + return !GetSystemMetrics(SM_CONVERTIBLESLATEMODE); + } + return false; +} + +static bool SystemHasMouse() { + // As per MSDN, this value is rarely false because of virtual mice, and + // some machines report the existance of a mouse port as a mouse. + // + // We probably could try to distinguish if we wanted, but a virtual mouse + // might be there for a reason, and maybe we shouldn't assume we know + // better. + return !!::GetSystemMetrics(SM_MOUSEPRESENT); +} + +/* static */ +PointerCapabilities WinUtils::GetPrimaryPointerCapabilities() { + if (IsTabletDevice()) { + return PointerCapabilities::Coarse; + } + + if (SystemHasMouse()) { + return PointerCapabilities::Fine | PointerCapabilities::Hover; + } + + if (IsTouchDeviceSupportPresent()) { + return PointerCapabilities::Coarse; + } + + return PointerCapabilities::None; +} + +static bool SystemHasTouchscreen() { + int digitizerMetrics = ::GetSystemMetrics(SM_DIGITIZER); + return (digitizerMetrics & NID_INTEGRATED_TOUCH) || + (digitizerMetrics & NID_EXTERNAL_TOUCH); +} + +static bool SystemHasPenDigitizer() { + int digitizerMetrics = ::GetSystemMetrics(SM_DIGITIZER); + return (digitizerMetrics & NID_INTEGRATED_PEN) || + (digitizerMetrics & NID_EXTERNAL_PEN); +} + +/* static */ +PointerCapabilities WinUtils::GetAllPointerCapabilities() { + PointerCapabilities pointerCapabilities = PointerCapabilities::None; + + if (SystemHasTouchscreen()) { + pointerCapabilities |= PointerCapabilities::Coarse; + } + + if (SystemHasPenDigitizer() || SystemHasMouse()) { + pointerCapabilities |= + PointerCapabilities::Fine | PointerCapabilities::Hover; + } + + return pointerCapabilities; +} + +void WinUtils::GetPointerExplanation(nsAString* aExplanation) { + // To support localization, we will return a comma-separated list of + // Fluent IDs + *aExplanation = u"pointing-device-none"; + + bool first = true; + auto append = [&](const char16_t* str) { + if (first) { + aExplanation->Truncate(); + first = false; + } else { + aExplanation->Append(u","); + } + aExplanation->Append(str); + }; + + if (SystemHasTouchscreen()) { + append(u"pointing-device-touchscreen"); + } + + if (SystemHasPenDigitizer()) { + append(u"pointing-device-pen-digitizer"); + } + + if (SystemHasMouse()) { + append(u"pointing-device-mouse"); + } +} + +/* static */ +bool WinUtils::ResolveJunctionPointsAndSymLinks(std::wstring& aPath) { + LOG_D("ResolveJunctionPointsAndSymLinks: Resolving path: %S", aPath.c_str()); + + wchar_t path[MAX_PATH] = {0}; + + nsAutoHandle handle(::CreateFileW( + aPath.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr)); + + if (handle == INVALID_HANDLE_VALUE) { + LOG_E("Failed to open file handle to resolve path. GetLastError=%lu", + GetLastError()); + return false; + } + + DWORD pathLen = GetFinalPathNameByHandleW( + handle, path, MAX_PATH, FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + if (pathLen == 0 || pathLen >= MAX_PATH) { + LOG_E("GetFinalPathNameByHandleW failed. GetLastError=%lu", GetLastError()); + return false; + } + aPath = path; + + // GetFinalPathNameByHandle sticks a '\\?\' in front of the path, + // but that confuses some APIs so strip it off. It will also put + // '\\?\UNC\' in front of network paths, we convert that to '\\'. + if (aPath.compare(0, 7, L"\\\\?\\UNC") == 0) { + aPath.erase(2, 6); + } else if (aPath.compare(0, 4, L"\\\\?\\") == 0) { + aPath.erase(0, 4); + } + + LOG_D("ResolveJunctionPointsAndSymLinks: Resolved path to: %S", + aPath.c_str()); + return true; +} + +/* static */ +bool WinUtils::ResolveJunctionPointsAndSymLinks(nsIFile* aPath) { + MOZ_ASSERT(aPath); + + nsAutoString filePath; + nsresult rv = aPath->GetPath(filePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + std::wstring resolvedPath(filePath.get()); + if (!ResolveJunctionPointsAndSymLinks(resolvedPath)) { + return false; + } + + rv = aPath->InitWithPath(nsDependentString(resolvedPath.c_str())); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return true; +} + +/* static */ +bool WinUtils::RunningFromANetworkDrive() { + wchar_t exePath[MAX_PATH]; + if (!::GetModuleFileNameW(nullptr, exePath, MAX_PATH)) { + return false; + } + + std::wstring exeString(exePath); + if (!widget::WinUtils::ResolveJunctionPointsAndSymLinks(exeString)) { + return false; + } + + wchar_t volPath[MAX_PATH]; + if (!::GetVolumePathNameW(exeString.c_str(), volPath, MAX_PATH)) { + return false; + } + + return (::GetDriveTypeW(volPath) == DRIVE_REMOTE); +} + +/* static */ +bool WinUtils::CanonicalizePath(nsAString& aPath) { + wchar_t tempPath[MAX_PATH + 1]; + if (!PathCanonicalizeW(tempPath, + (char16ptr_t)PromiseFlatString(aPath).get())) { + return false; + } + aPath = tempPath; + MOZ_ASSERT(aPath.Length() <= MAX_PATH); + return true; +} + +/* static */ +bool WinUtils::MakeLongPath(nsAString& aPath) { + wchar_t tempPath[MAX_PATH + 1]; + DWORD longResult = + GetLongPathNameW((char16ptr_t)PromiseFlatString(aPath).get(), tempPath, + ArrayLength(tempPath)); + if (longResult > ArrayLength(tempPath)) { + // Our buffer is too short, and we're guaranteeing <= MAX_PATH results. + return false; + } else if (longResult) { + // Success. + aPath = tempPath; + MOZ_ASSERT(aPath.Length() <= MAX_PATH); + } + // GetLongPathNameW returns 0 if the path is not found or is not rooted, + // but we shouldn't consider that a failure condition. + return true; +} + +/* static */ +bool WinUtils::UnexpandEnvVars(nsAString& aPath) { + wchar_t tempPath[MAX_PATH + 1]; + // PathUnExpandEnvStringsW returns false if it doesn't make any + // substitutions. Silently continue using the unaltered path. + if (PathUnExpandEnvStringsW((char16ptr_t)PromiseFlatString(aPath).get(), + tempPath, ArrayLength(tempPath))) { + aPath = tempPath; + MOZ_ASSERT(aPath.Length() <= MAX_PATH); + } + return true; +} + +/* static */ +WinUtils::WhitelistVec WinUtils::BuildWhitelist() { + WhitelistVec result; + + Unused << result.emplaceBack( + std::make_pair(nsString(u"%ProgramFiles%"_ns), nsDependentString())); + + // When no substitution is required, set the void flag + result.back().second.SetIsVoid(true); + + Unused << result.emplaceBack( + std::make_pair(nsString(u"%SystemRoot%"_ns), nsDependentString())); + result.back().second.SetIsVoid(true); + + wchar_t tmpPath[MAX_PATH + 1] = {}; + if (GetTempPath(MAX_PATH, tmpPath)) { + // GetTempPath's result always ends with a backslash, which we don't want + uint32_t tmpPathLen = wcslen(tmpPath); + if (tmpPathLen) { + tmpPath[tmpPathLen - 1] = 0; + } + + nsAutoString cleanTmpPath(tmpPath); + if (UnexpandEnvVars(cleanTmpPath)) { + constexpr auto tempVar = u"%TEMP%"_ns; + Unused << result.emplaceBack(std::make_pair( + nsString(cleanTmpPath), nsDependentString(tempVar, 0))); + } + } + + // If we add more items to the whitelist, ensure we still don't invoke an + // unnecessary heap allocation. + MOZ_ASSERT(result.length() <= kMaxWhitelistedItems); + + return result; +} + +/** + * This function provides an array of (system path, substitution) pairs that are + * considered to be acceptable with respect to privacy, for the purposes of + * submitting within telemetry or crash reports. + * + * The substitution string's void flag may be set. If it is, no subsitution is + * necessary. Otherwise, the consumer should replace the system path with the + * substitution. + * + * @see PreparePathForTelemetry for an example of its usage. + */ +/* static */ +const WinUtils::WhitelistVec& WinUtils::GetWhitelistedPaths() { + static WhitelistVec sWhitelist([]() -> WhitelistVec { + auto setClearFn = [ptr = &sWhitelist]() -> void { + RunOnShutdown([ptr]() -> void { ptr->clear(); }, + ShutdownPhase::XPCOMShutdownFinal); + }; + + if (NS_IsMainThread()) { + setClearFn(); + } else { + SchedulerGroup::Dispatch(NS_NewRunnableFunction( + "WinUtils::GetWhitelistedPaths", std::move(setClearFn))); + } + + return BuildWhitelist(); + }()); + return sWhitelist; +} + +/** + * This function is located here (as opposed to nsSystemInfo or elsewhere) + * because we need to gather this information as early as possible during + * startup. + */ +/* static */ +bool WinUtils::GetAppInitDLLs(nsAString& aOutput) { + aOutput.Truncate(); + HKEY hkey = NULL; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows", + 0, KEY_QUERY_VALUE, &hkey)) { + return false; + } + nsAutoRegKey key(hkey); + LONG status; + const wchar_t kLoadAppInitDLLs[] = L"LoadAppInit_DLLs"; + DWORD loadAppInitDLLs = 0; + DWORD loadAppInitDLLsLen = sizeof(loadAppInitDLLs); + status = RegQueryValueExW(hkey, kLoadAppInitDLLs, nullptr, nullptr, + (LPBYTE)&loadAppInitDLLs, &loadAppInitDLLsLen); + if (status != ERROR_SUCCESS) { + return false; + } + if (!loadAppInitDLLs) { + // If loadAppInitDLLs is zero then AppInit_DLLs is disabled. + // In this case we'll return true along with an empty output string. + return true; + } + DWORD numBytes = 0; + const wchar_t kAppInitDLLs[] = L"AppInit_DLLs"; + // Query for required buffer size + status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, nullptr, nullptr, + &numBytes); + if (status != ERROR_SUCCESS) { + return false; + } + // Allocate the buffer and query for the actual data + mozilla::UniquePtr<wchar_t[]> data = + mozilla::MakeUnique<wchar_t[]>(numBytes / sizeof(wchar_t)); + status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, nullptr, + (LPBYTE)data.get(), &numBytes); + if (status != ERROR_SUCCESS) { + return false; + } + // For each token, split up the filename components and then check the + // name of the file. + const wchar_t kDelimiters[] = L", "; + wchar_t* tokenContext = nullptr; + wchar_t* token = wcstok_s(data.get(), kDelimiters, &tokenContext); + while (token) { + nsAutoString cleanPath(token); + // Since these paths are short paths originating from the registry, we need + // to canonicalize them, lengthen them, and sanitize them before we can + // check them against the whitelist + if (PreparePathForTelemetry(cleanPath)) { + if (!aOutput.IsEmpty()) { + aOutput += L";"; + } + aOutput += cleanPath; + } + token = wcstok_s(nullptr, kDelimiters, &tokenContext); + } + return true; +} + +/* static */ +bool WinUtils::PreparePathForTelemetry(nsAString& aPath, + PathTransformFlags aFlags) { + if (aFlags & PathTransformFlags::Canonicalize) { + if (!CanonicalizePath(aPath)) { + return false; + } + } + if (aFlags & PathTransformFlags::Lengthen) { + if (!MakeLongPath(aPath)) { + return false; + } + } + if (aFlags & PathTransformFlags::UnexpandEnvVars) { + if (!UnexpandEnvVars(aPath)) { + return false; + } + } + + const WhitelistVec& whitelistedPaths = GetWhitelistedPaths(); + + for (uint32_t i = 0; i < whitelistedPaths.length(); ++i) { + const nsString& testPath = whitelistedPaths[i].first; + const nsDependentString& substitution = whitelistedPaths[i].second; + if (StringBeginsWith(aPath, testPath, nsCaseInsensitiveStringComparator)) { + if (!substitution.IsVoid()) { + aPath.Replace(0, testPath.Length(), substitution); + } + return true; + } + } + + // For non-whitelisted paths, we strip the path component and just leave + // the filename. We can't use nsLocalFile to do this because these paths may + // begin with environment variables, and nsLocalFile doesn't like + // non-absolute paths. + const nsString& flatPath = PromiseFlatString(aPath); + LPCWSTR leafStart = ::PathFindFileNameW(flatPath.get()); + ptrdiff_t cutLen = leafStart - flatPath.get(); + if (cutLen) { + aPath.Cut(0, cutLen); + } else if (aFlags & PathTransformFlags::RequireFilePath) { + return false; + } + + return true; +} + +nsString WinUtils::GetPackageFamilyName() { + nsString rv; + + UniquePtr<wchar_t[]> packageIdentity = mozilla::GetPackageFamilyName(); + if (packageIdentity) { + rv = packageIdentity.get(); + } + + return rv; +} + +bool WinUtils::GetClassName(HWND aHwnd, nsAString& aClassName) { + const int bufferLength = 256; + aClassName.SetLength(bufferLength); + + int length = ::GetClassNameW(aHwnd, (char16ptr_t)aClassName.BeginWriting(), + bufferLength); + if (length == 0) { + return false; + } + MOZ_RELEASE_ASSERT(length <= (bufferLength - 1)); + aClassName.Truncate(length); + return true; +} + +static BOOL CALLBACK EnumUpdateWindowOcclusionProc(HWND aHwnd, LPARAM aLParam) { + const bool* const enable = reinterpret_cast<bool*>(aLParam); + nsWindow* window = WinUtils::GetNSWindowPtr(aHwnd); + if (window) { + window->MaybeEnableWindowOcclusion(*enable); + } + return TRUE; +} + +void WinUtils::EnableWindowOcclusion(const bool aEnable) { + if (aEnable) { + WinWindowOcclusionTracker::Ensure(); + } + ::EnumWindows(EnumUpdateWindowOcclusionProc, + reinterpret_cast<LPARAM>(&aEnable)); +} + +bool WinUtils::GetTimezoneName(wchar_t* aBuffer) { + DYNAMIC_TIME_ZONE_INFORMATION tzInfo; + DWORD tzid = GetDynamicTimeZoneInformation(&tzInfo); + + if (tzid == TIME_ZONE_ID_INVALID) { + return false; + } + + wcscpy_s(aBuffer, 128, tzInfo.TimeZoneKeyName); + + return true; +} + +// There are undocumented APIs to query/change the system DPI settings found by +// https://github.com/lihas/ . We use those APIs only for testing purpose, i.e. +// in mochitests or some such. To avoid exposing them in our official release +// builds unexpectedly we restrict them only in debug builds. +#ifdef DEBUG + +# define DISPLAYCONFIG_DEVICE_INFO_SET_SOURCE_DPI_SCALE (int)-4 +# define DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_DPI_SCALE (int)-3 + +// Following two struts are copied from +// https://github.com/lihas/windows-DPI-scaling-sample/blob/master/DPIHelper/DpiHelper.h + +/* + * struct DISPLAYCONFIG_SOURCE_DPI_SCALE_GET + * @brief used to fetch min, max, suggested, and currently applied DPI scaling + * values. All values are relative to the recommended DPI scaling value Note + * that DPI scaling is a property of the source, and not of target. + */ +struct DISPLAYCONFIG_SOURCE_DPI_SCALE_GET { + DISPLAYCONFIG_DEVICE_INFO_HEADER header; + /* + * @brief min value of DPI scaling is always 100, minScaleRel gives no. of + * steps down from recommended scaling eg. if minScaleRel is -3 => 100 is 3 + * steps down from recommended scaling => recommended scaling is 175% + */ + int32_t minScaleRel; + + /* + * @brief currently applied DPI scaling value wrt the recommended value. eg. + * if recommended value is 175%, + * => if curScaleRel == 0 the current scaling is 175%, if curScaleRel == -1, + * then current scale is 150% + */ + int32_t curScaleRel; + + /* + * @brief maximum supported DPI scaling wrt recommended value + */ + int32_t maxScaleRel; +}; + +/* + * struct DISPLAYCONFIG_SOURCE_DPI_SCALE_SET + * @brief set DPI scaling value of a source + * Note that DPI scaling is a property of the source, and not of target. + */ +struct DISPLAYCONFIG_SOURCE_DPI_SCALE_SET { + DISPLAYCONFIG_DEVICE_INFO_HEADER header; + /* + * @brief The value we want to set. The value should be relative to the + * recommended DPI scaling value of source. eg. if scaleRel == 1, and + * recommended value is 175% => we are trying to set 200% scaling for the + * source + */ + int32_t scaleRel; +}; + +static int32_t sCurRelativeScaleStep = std::numeric_limits<int32_t>::max(); + +static LONG SetRelativeScaleStep(LUID aAdapterId, int32_t aRelativeScaleStep) { + DISPLAYCONFIG_SOURCE_DPI_SCALE_SET setDPIScale = {}; + setDPIScale.header.adapterId = aAdapterId; + setDPIScale.header.type = (DISPLAYCONFIG_DEVICE_INFO_TYPE) + DISPLAYCONFIG_DEVICE_INFO_SET_SOURCE_DPI_SCALE; + setDPIScale.header.size = sizeof(setDPIScale); + setDPIScale.scaleRel = aRelativeScaleStep; + + return DisplayConfigSetDeviceInfo(&setDPIScale.header); +} + +nsresult WinUtils::SetHiDPIMode(bool aHiDPI) { + auto config = GetDisplayConfig(); + if (!config) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (config->mPaths.empty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + DISPLAYCONFIG_SOURCE_DPI_SCALE_GET dpiScale = {}; + dpiScale.header.adapterId = config->mPaths[0].targetInfo.adapterId; + dpiScale.header.type = (DISPLAYCONFIG_DEVICE_INFO_TYPE) + DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_DPI_SCALE; + dpiScale.header.size = sizeof(dpiScale); + LONG result = ::DisplayConfigGetDeviceInfo(&dpiScale.header); + if (result != ERROR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + if (dpiScale.minScaleRel == dpiScale.maxScaleRel) { + // We can't change the setting at all. + return NS_ERROR_NOT_AVAILABLE; + } + + if (aHiDPI && dpiScale.curScaleRel == dpiScale.maxScaleRel) { + // We've already at the maximum level. + if (sCurRelativeScaleStep == std::numeric_limits<int32_t>::max()) { + sCurRelativeScaleStep = dpiScale.curScaleRel; + } + return NS_OK; + } + + if (!aHiDPI && dpiScale.curScaleRel == dpiScale.minScaleRel) { + // We've already at the minimum level. + if (sCurRelativeScaleStep == std::numeric_limits<int32_t>::max()) { + sCurRelativeScaleStep = dpiScale.curScaleRel; + } + return NS_OK; + } + + result = SetRelativeScaleStep( + config->mPaths[0].targetInfo.adapterId, + aHiDPI ? dpiScale.maxScaleRel : dpiScale.minScaleRel); + if (result != ERROR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + if (sCurRelativeScaleStep == std::numeric_limits<int>::max()) { + sCurRelativeScaleStep = dpiScale.curScaleRel; + } + + return NS_OK; +} + +nsresult WinUtils::RestoreHiDPIMode() { + if (sCurRelativeScaleStep == std::numeric_limits<int>::max()) { + // The DPI setting hasn't been changed. + return NS_ERROR_UNEXPECTED; + } + + auto config = GetDisplayConfig(); + if (!config) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (config->mPaths.empty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + LONG result = SetRelativeScaleStep(config->mPaths[0].targetInfo.adapterId, + sCurRelativeScaleStep); + sCurRelativeScaleStep = std::numeric_limits<int32_t>::max(); + if (result != ERROR_SUCCESS) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} +#endif + +/* static */ +const char* WinUtils::WinEventToEventName(UINT msg) { + const auto eventMsgInfo = mozilla::widget::gAllEvents.find(msg); + return eventMsgInfo != mozilla::widget::gAllEvents.end() + ? eventMsgInfo->second.mStr + : nullptr; +} + +// Note to testers and/or test-authors: on Windows 10, and possibly on other +// versions as well, supplying the `WS_EX_LAYOUTRTL` flag here has no effect +// whatsoever on child common-dialogs **unless the system UI locale is also set +// to an RTL language**. +// +// If it is, the flag is still required; otherwise, the picker dialog will be +// presented in English (or possibly some other LTR language) as a fallback. +ScopedRtlShimWindow::ScopedRtlShimWindow(nsIWidget* aParent) : mWnd(nullptr) { + NS_ENSURE_TRUE_VOID(aParent); + + // Headless windows don't have HWNDs, but also probably shouldn't be launching + // print dialogs. + HWND const hwnd = (HWND)aParent->GetNativeData(NS_NATIVE_WINDOW); + NS_ENSURE_TRUE_VOID(hwnd); + + nsWindow* const win = WinUtils::GetNSWindowPtr(hwnd); + NS_ENSURE_TRUE_VOID(win); + + ATOM const wclass = ::GetClassWord(hwnd, GCW_ATOM); + mWnd = ::CreateWindowExW( + win->IsRTL() ? WS_EX_LAYOUTRTL : 0, (LPCWSTR)(uintptr_t)wclass, L"", + WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + hwnd, nullptr, nsToolkit::mDllInstance, nullptr); + + MOZ_ASSERT(mWnd); +} + +ScopedRtlShimWindow::~ScopedRtlShimWindow() { + if (mWnd) { + ::DestroyWindow(mWnd); + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/WinUtils.h b/widget/windows/WinUtils.h new file mode 100644 index 0000000000..daef5ff4bc --- /dev/null +++ b/widget/windows/WinUtils.h @@ -0,0 +1,684 @@ +/* -*- 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_WinUtils_h__ +#define mozilla_widget_WinUtils_h__ + +#include "nscore.h" +#include <windows.h> +#include <shobjidl.h> +#include <uxtheme.h> +#include <dwmapi.h> +#include <unordered_map> +#include <utility> + +// Undo the windows.h damage +#undef GetMessage +#undef CreateEvent +#undef GetClassName +#undef GetBinaryType +#undef RemoveDirectory + +#include "nsString.h" +#include "nsRegion.h" +#include "nsRect.h" + +#include "nsIRunnable.h" +#include "nsICryptoHash.h" +#ifdef MOZ_PLACES +# include "nsIFaviconService.h" +#endif +#include "nsIDownloader.h" +#include "nsIURI.h" +#include "nsIWidget.h" +#include "nsIThread.h" + +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/HalScreenConfiguration.h" +#include "mozilla/HashTable.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" +#include "mozilla/WindowsDpiAwareness.h" +#include "mozilla/WindowsProcessMitigations.h" +#include "mozilla/gfx/2D.h" + +/** + * NS_INLINE_DECL_IUNKNOWN_REFCOUNTING should be used for defining and + * implementing AddRef() and Release() of IUnknown interface. + * This depends on xpcom/base/nsISupportsImpl.h. + */ + +#define NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(_class) \ + public: \ + STDMETHODIMP_(ULONG) AddRef() { \ + MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(_class) \ + MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); \ + NS_ASSERT_OWNINGTHREAD(_class); \ + ++mRefCnt; \ + NS_LOG_ADDREF(this, mRefCnt, #_class, sizeof(*this)); \ + return static_cast<ULONG>(mRefCnt.get()); \ + } \ + STDMETHODIMP_(ULONG) Release() { \ + MOZ_ASSERT(int32_t(mRefCnt) > 0, \ + "Release called on object that has already been released!"); \ + NS_ASSERT_OWNINGTHREAD(_class); \ + --mRefCnt; \ + NS_LOG_RELEASE(this, mRefCnt, #_class); \ + if (mRefCnt == 0) { \ + NS_ASSERT_OWNINGTHREAD(_class); \ + mRefCnt = 1; /* stabilize */ \ + delete this; \ + return 0; \ + } \ + return static_cast<ULONG>(mRefCnt.get()); \ + } \ + \ + protected: \ + nsAutoRefCnt mRefCnt; \ + NS_DECL_OWNINGTHREAD \ + public: + +class nsWindow; +struct KeyPair; + +namespace mozilla { +enum class PointerCapabilities : uint8_t; +#if defined(ACCESSIBILITY) +namespace a11y { +class LocalAccessible; +} // namespace a11y +#endif // defined(ACCESSIBILITY) + +// Helper function: enumerate all the toplevel HWNDs attached to the current +// thread via ::EnumThreadWindows(). +// +// Note that this use of ::EnumThreadWindows() is, unfortunately, not an +// abstract implementation detail. +template <typename F> +void EnumerateThreadWindows(F&& f) +// requires requires(F f, HWND h) { f(h); } +{ + class Impl { + public: + F f; + explicit Impl(F&& f) : f(std::forward<F>(f)) {} + + void invoke() { + WNDENUMPROC proc = &Impl::Callback; + ::EnumThreadWindows(::GetCurrentThreadId(), proc, + reinterpret_cast<LPARAM>(&f)); + } + + private: + static BOOL CALLBACK Callback(HWND hwnd, LPARAM lp) { + (*reinterpret_cast<F*>(lp))(hwnd); + return TRUE; + } + }; + + Impl(std::forward<F>(f)).invoke(); +} + +namespace widget { + +// More complete QS definitions for MsgWaitForMultipleObjects() and +// GetQueueStatus() that include newer win8 specific defines. + +#ifndef QS_RAWINPUT +# define QS_RAWINPUT 0x0400 +#endif + +#ifndef QS_TOUCH +# define QS_TOUCH 0x0800 +# define QS_POINTER 0x1000 +#endif + +#define MOZ_QS_ALLEVENT \ + (QS_KEY | QS_MOUSEMOVE | QS_MOUSEBUTTON | QS_POSTMESSAGE | QS_TIMER | \ + QS_PAINT | QS_SENDMESSAGE | QS_HOTKEY | QS_ALLPOSTMESSAGE | QS_RAWINPUT | \ + QS_TOUCH | QS_POINTER) + +// Logging macros +#define LogFunction() mozilla::widget::WinUtils::Log(__FUNCTION__) +#define LogThread() \ + mozilla::widget::WinUtils::Log("%s: IsMainThread:%d ThreadId:%X", \ + __FUNCTION__, NS_IsMainThread(), \ + GetCurrentThreadId()) +#define LogThis() mozilla::widget::WinUtils::Log("[%X] %s", this, __FUNCTION__) +#define LogException(e) \ + mozilla::widget::WinUtils::Log("%s Exception:%s", __FUNCTION__, \ + e->ToString()->Data()) +#define LogHRESULT(hr) \ + mozilla::widget::WinUtils::Log("%s hr=%X", __FUNCTION__, hr) + +#ifdef MOZ_PLACES +class myDownloadObserver final : public nsIDownloadObserver { + ~myDownloadObserver() {} + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDOWNLOADOBSERVER +}; +#endif + +class WinUtils { + // Function pointers for APIs that may not be available depending on + // the Win10 update version -- will be set up in Initialize(). + static SetThreadDpiAwarenessContextProc sSetThreadDpiAwarenessContext; + static EnableNonClientDpiScalingProc sEnableNonClientDpiScaling; + static GetSystemMetricsForDpiProc sGetSystemMetricsForDpi; + + // Set on Initialize(). + static bool sHasPackageIdentity; + + public: + class AutoSystemDpiAware { + public: + AutoSystemDpiAware() { + MOZ_DIAGNOSTIC_ASSERT(!IsWin32kLockedDown()); + + if (sSetThreadDpiAwarenessContext) { + mPrevContext = + sSetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE); + } + } + + ~AutoSystemDpiAware() { + if (sSetThreadDpiAwarenessContext) { + sSetThreadDpiAwarenessContext(mPrevContext); + } + } + + private: + DPI_AWARENESS_CONTEXT mPrevContext; + }; + + // Wrapper for DefWindowProc that will enable non-client dpi scaling on the + // window during creation. + static LRESULT WINAPI NonClientDpiScalingDefWindowProcW(HWND hWnd, UINT msg, + WPARAM wParam, + LPARAM lParam); + + /** + * Get the system's default logical-to-physical DPI scaling factor, + * which is based on the primary display. Note however that unlike + * LogToPhysFactor(GetPrimaryMonitor()), this will not change during + * a session even if the displays are reconfigured. This scale factor + * is used by Windows theme metrics etc, which do not fully support + * dynamic resolution changes but are only updated on logout. + */ + static double SystemScaleFactor(); + + static bool IsPerMonitorDPIAware(); + /** + * Get the DPI of the given monitor if it's per-monitor DPI aware, otherwise + * return the system DPI. + */ + static float MonitorDPI(HMONITOR aMonitor); + static float SystemDPI(); + /** + * Functions to convert between logical pixels as used by most Windows APIs + * and physical (device) pixels. + */ + static double LogToPhysFactor(HMONITOR aMonitor); + static double LogToPhysFactor(HWND aWnd); + static double LogToPhysFactor(HDC aDC) { + return LogToPhysFactor(::WindowFromDC(aDC)); + } + static int32_t LogToPhys(HMONITOR aMonitor, double aValue); + static HMONITOR GetPrimaryMonitor(); + static HMONITOR MonitorFromRect(const gfx::Rect& rect); + + static bool HasSystemMetricsForDpi(); + static int GetSystemMetricsForDpi(int nIndex, UINT dpi); + + /** + * @param msg Windows event message + * @return User-friendly event name, or nullptr if no + * match is found. + */ + static const char* WinEventToEventName(UINT msg); + + /** + * @param aHdc HDC for printer + * @return unwritable margins for currently set page on aHdc or empty margins + * if aHdc is null + */ + static gfx::MarginDouble GetUnwriteableMarginsForDeviceInInches(HDC aHdc); + + static bool HasPackageIdentity() { return sHasPackageIdentity; } + + /* + * The "family name" of a Windows app package is the full name without any of + * the components that might change during the life cycle of the app (such as + * the version number, or the architecture). This leaves only those properties + * which together serve to uniquely identify the app within one Windows + * installation, namely the base name and the publisher name. Meaning, this + * string is safe to use anywhere that a string uniquely identifying an app + * installation is called for (because multiple copies of the same app on the + * same system is not a supported feature in the app framework). + */ + static nsString GetPackageFamilyName(); + + /** + * Logging helpers that dump output to prlog module 'Widget', console, and + * OutputDebugString. Note these output in both debug and release builds. + */ + static void Log(const char* fmt, ...); + static void LogW(const wchar_t* fmt, ...); + + /** + * PeekMessage() and GetMessage() are wrapper methods for PeekMessageW(), + * GetMessageW(), ITfMessageMgr::PeekMessageW() and + * ITfMessageMgr::GetMessageW(). + * Don't call the native APIs directly. You MUST use these methods instead. + */ + static bool PeekMessage(LPMSG aMsg, HWND aWnd, UINT aFirstMessage, + UINT aLastMessage, UINT aOption); + static bool GetMessage(LPMSG aMsg, HWND aWnd, UINT aFirstMessage, + UINT aLastMessage); + + /** + * Wait until a message is ready to be processed. + * Prefer using this method to directly calling ::WaitMessage since + * ::WaitMessage will wait if there is an unread message in the queue. + * That can cause freezes until another message enters the queue if the + * message is marked read by a call to PeekMessage which the caller is + * not aware of (e.g., from a different thread). + * Note that this method may cause sync dispatch of sent (as opposed to + * posted) messages. + * @param aTimeoutMs Timeout for waiting in ms, defaults to INFINITE + */ + static void WaitForMessage(DWORD aTimeoutMs = INFINITE); + + /** + * GetTopLevelHWND() returns a window handle of the top level window which + * aWnd belongs to. Note that the result may not be our window, i.e., it + * may not be managed by nsWindow. + * + * See follwing table for the detail of the result window type. + * + * +-------------------------+-----------------------------------------------+ + * | | aStopIfNotPopup | + * +-------------------------+-----------------------+-----------------------+ + * | | TRUE | FALSE | + + +-----------------+-------+-----------------------+-----------------------+ + * | | | * an independent top level window | + * | | TRUE | * a pupup window (WS_POPUP) | + * | | | * an owned top level window (like dialog) | + * | aStopIfNotChild +-------+-----------------------+-----------------------+ + * | | | * independent window | * only an independent | + * | | FALSE | * non-popup-owned- | top level window | + * | | | window like dialog | | + * +-----------------+-------+-----------------------+-----------------------+ + */ + static HWND GetTopLevelHWND(HWND aWnd, bool aStopIfNotChild = false, + bool aStopIfNotPopup = true); + + /** + * SetNSWindowPtr() associates aWindow with aWnd. If aWidget is nullptr, it + * instead dissociates any nsWindow from aWnd. + * + * No AddRef is performed. May not be used off of the main thread. + */ + static void SetNSWindowPtr(HWND aWnd, nsWindow* aWindow); + /** + * GetNSWindowPtr() returns a pointer to the associated nsWindow pointer, if + * one exists, or nullptr, if not. + * + * No AddRef is performed. May not be used off of the main thread. + */ + static nsWindow* GetNSWindowPtr(HWND aWnd); + + /** + * IsOurProcessWindow() returns TRUE if aWnd belongs our process. + * Otherwise, FALSE. + */ + static bool IsOurProcessWindow(HWND aWnd); + + /** + * FindOurProcessWindow() returns the nearest ancestor window which + * belongs to our process. If it fails to find our process's window by the + * top level window, returns nullptr. And note that this is using + * ::GetParent() for climbing the window hierarchy, therefore, it gives + * up at an owned top level window except popup window (e.g., dialog). + */ + static HWND FindOurProcessWindow(HWND aWnd); + + /** + * FindOurWindowAtPoint() returns the topmost child window which belongs to + * our process's top level window. + * + * NOTE: the topmost child window may NOT be our process's window like a + * plugin's window. + */ + static HWND FindOurWindowAtPoint(const POINT& aPointInScreen); + + /** + * InitMSG() returns an MSG struct which was initialized by the params. + * Don't trust the other members in the result. + */ + static MSG InitMSG(UINT aMessage, WPARAM wParam, LPARAM lParam, HWND aWnd); + + /** + * GetScanCode() returns a scan code for the LPARAM of WM_KEYDOWN, WM_KEYUP, + * WM_CHAR and WM_UNICHAR. + * + */ + static WORD GetScanCode(LPARAM aLParam) { return (aLParam >> 16) & 0xFF; } + + /** + * IsExtendedScanCode() returns TRUE if the LPARAM indicates the key message + * is an extended key event. + */ + static bool IsExtendedScanCode(LPARAM aLParam) { + return (aLParam & 0x1000000) != 0; + } + + /** + * GetInternalMessage() converts a native message to an internal message. + * If there is no internal message for the given native message, returns + * the native message itself. + */ + static UINT GetInternalMessage(UINT aNativeMessage); + + /** + * GetNativeMessage() converts an internal message to a native message. + * If aInternalMessage is a native message, returns the native message itself. + */ + static UINT GetNativeMessage(UINT aInternalMessage); + + /** + * GetMouseInputSource() returns a pointing device information. The value is + * one of MouseEvent_Binding::MOZ_SOURCE_*. This method MUST be called during + * mouse message handling. + */ + static uint16_t GetMouseInputSource(); + + /** + * Windows also fires mouse window messages for pens and touches, so we should + * retrieve their pointer ID on receiving mouse events as well. Please refer + * to + * https://msdn.microsoft.com/en-us/library/windows/desktop/ms703320(v=vs.85).aspx + */ + static uint16_t GetMousePointerID(); + + static bool GetIsMouseFromTouch(EventMessage aEventType); + + /** + * ConvertHRGNToRegion converts a Windows HRGN to an LayoutDeviceIntRegion. + * + * aRgn the HRGN to convert. + * returns the LayoutDeviceIntRegion. + */ + static LayoutDeviceIntRegion ConvertHRGNToRegion(HRGN aRgn); + + /** + * ToIntRect converts a Windows RECT to a LayoutDeviceIntRect. + * + * aRect the RECT to convert. + * returns the LayoutDeviceIntRect. + */ + static LayoutDeviceIntRect ToIntRect(const RECT& aRect); + + /** + * Returns true if the context or IME state is enabled. Otherwise, false. + */ + static bool IsIMEEnabled(const InputContext& aInputContext); + static bool IsIMEEnabled(IMEEnabled aIMEState); + + /** + * Returns modifier key array for aModifiers. This is for + * nsIWidget::SynthethizeNative*Event(). + */ + static void SetupKeyModifiersSequence(nsTArray<KeyPair>* aArray, + uint32_t aModifiers, UINT aMessage); + + /** + * Does device have touch support + */ + static uint32_t IsTouchDeviceSupportPresent(); + + /** + * The maximum number of simultaneous touch contacts supported by the device. + * In the case of devices with multiple digitizers (e.g. multiple touch + * screens), the value will be the maximum of the set of maximum supported + * contacts by each individual digitizer. + */ + static uint32_t GetMaxTouchPoints(); + + /** + * Returns the windows power platform role, which is useful for detecting + * tablets. + */ + static POWER_PLATFORM_ROLE GetPowerPlatformRole(); + + // For pointer and hover media queries features. + static PointerCapabilities GetPrimaryPointerCapabilities(); + // For any-pointer and any-hover media queries features. + static PointerCapabilities GetAllPointerCapabilities(); + // Returns a string containing a comma-separated list of Fluent IDs + // representing the currently active pointing devices + static void GetPointerExplanation(nsAString* aExplanation); + + /** + * Fully resolves a path to its final path name. So if path contains + * junction points or symlinks to other folders, we'll resolve the path + * fully to the actual path that the links target. + * + * @param aPath path to be resolved. + * @return true if successful, including if nothing needs to be changed. + * false if something failed or aPath does not exist, aPath will + * remain unchanged. + */ + static bool ResolveJunctionPointsAndSymLinks(std::wstring& aPath); + static bool ResolveJunctionPointsAndSymLinks(nsIFile* aPath); + + /** + * Returns true if executable's path is on a network drive. + */ + static bool RunningFromANetworkDrive(); + + static void Initialize(); + + static nsresult WriteBitmap(nsIFile* aFile, + mozilla::gfx::SourceSurface* surface); + // This function is a helper, but it cannot be called from the main thread. + // Use the one above! + static nsresult WriteBitmap(nsIFile* aFile, imgIContainer* aImage); + + /** + * Wrapper for PathCanonicalize(). + * Upon success, the resulting output string length is <= MAX_PATH. + * @param aPath [in,out] The path to transform. + * @return true on success, false on failure. + */ + static bool CanonicalizePath(nsAString& aPath); + + /** + * Converts short paths (e.g. "C:\\PROGRA~1\\XYZ") to full paths. + * Upon success, the resulting output string length is <= MAX_PATH. + * @param aPath [in,out] The path to transform. + * @return true on success, false on failure. + */ + static bool MakeLongPath(nsAString& aPath); + + /** + * Wrapper for PathUnExpandEnvStringsW(). + * Upon success, the resulting output string length is <= MAX_PATH. + * @param aPath [in,out] The path to transform. + * @return true on success, false on failure. + */ + static bool UnexpandEnvVars(nsAString& aPath); + + /** + * Retrieve a semicolon-delimited list of DLL files derived from AppInit_DLLs + */ + static bool GetAppInitDLLs(nsAString& aOutput); + + enum class PathTransformFlags : uint32_t { + Canonicalize = 1, + Lengthen = 2, + UnexpandEnvVars = 4, + RequireFilePath = 8, + + Default = 7, // Default omits RequireFilePath + }; + + /** + * Given a path, transforms it in preparation to be reported via telemetry. + * That can include canonicalization, converting short to long paths, + * unexpanding environment strings, and removing potentially sensitive data + * from the path. + * + * @param aPath [in,out] The path to transform. + * @param aFlags [in] Specifies which transformations to perform, allowing + * the caller to skip operations they know have already been + * performed. + * @return true on success, false on failure. + */ + static bool PreparePathForTelemetry( + nsAString& aPath, + PathTransformFlags aFlags = PathTransformFlags::Default); + + static const size_t kMaxWhitelistedItems = 3; + using WhitelistVec = + Vector<std::pair<nsString, nsDependentString>, kMaxWhitelistedItems>; + + static const WhitelistVec& GetWhitelistedPaths(); + + static bool GetClassName(HWND aHwnd, nsAString& aName); + + static void EnableWindowOcclusion(const bool aEnable); + + static bool GetTimezoneName(wchar_t* aBuffer); + +#ifdef DEBUG + static nsresult SetHiDPIMode(bool aHiDPI); + static nsresult RestoreHiDPIMode(); +#endif + + static bool GetAutoRotationState(AR_STATE* aRotationState); + + static void GetClipboardFormatAsString(UINT aFormat, nsAString& aOutput); + + private: + static WhitelistVec BuildWhitelist(); + + public: +#ifdef ACCESSIBILITY + static a11y::LocalAccessible* GetRootAccessibleForHWND(HWND aHwnd); +#endif +}; + +#ifdef MOZ_PLACES +class AsyncFaviconDataReady final : public nsIFaviconDataCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIFAVICONDATACALLBACK + + AsyncFaviconDataReady(nsIURI* aNewURI, RefPtr<LazyIdleThread>& aIOThread, + const bool aURLShortcut, + already_AddRefed<nsIRunnable> aRunnable); + nsresult OnFaviconDataNotAvailable(void); + + private: + ~AsyncFaviconDataReady() {} + + nsCOMPtr<nsIURI> mNewURI; + RefPtr<LazyIdleThread> mIOThread; + nsCOMPtr<nsIRunnable> mRunnable; + const bool mURLShortcut; +}; +#endif + +/** + * Asynchronously tries add the list to the build + */ +class AsyncEncodeAndWriteIcon : public nsIRunnable { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE + + // Warning: AsyncEncodeAndWriteIcon assumes ownership of the aData buffer + // passed in + AsyncEncodeAndWriteIcon(const nsAString& aIconPath, + UniquePtr<uint8_t[]> aData, uint32_t aStride, + uint32_t aWidth, uint32_t aHeight, + already_AddRefed<nsIRunnable> aRunnable); + + private: + virtual ~AsyncEncodeAndWriteIcon(); + + nsAutoString mIconPath; + UniquePtr<uint8_t[]> mBuffer; + nsCOMPtr<nsIRunnable> mRunnable; + uint32_t mStride; + uint32_t mWidth; + uint32_t mHeight; +}; + +class AsyncDeleteAllFaviconsFromDisk : public nsIRunnable { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE + + explicit AsyncDeleteAllFaviconsFromDisk(bool aIgnoreRecent = false); + + private: + virtual ~AsyncDeleteAllFaviconsFromDisk(); + + int32_t mIcoNoDeleteSeconds; + bool mIgnoreRecent; + nsCOMPtr<nsIFile> mJumpListCacheDir; +}; + +class FaviconHelper { + public: + static const char kJumpListCacheDir[]; + static const char kShortcutCacheDir[]; + static nsresult ObtainCachedIconFile( + nsCOMPtr<nsIURI> aFaviconPageURI, nsString& aICOFilePath, + RefPtr<LazyIdleThread>& aIOThread, bool aURLShortcut, + already_AddRefed<nsIRunnable> aRunnable = nullptr); + + static nsresult GetOutputIconPath(nsCOMPtr<nsIURI> aFaviconPageURI, + nsCOMPtr<nsIFile>& aICOFile, + bool aURLShortcut); + + static nsresult CacheIconFileFromFaviconURIAsync( + nsCOMPtr<nsIURI> aFaviconPageURI, nsCOMPtr<nsIFile> aICOFile, + RefPtr<LazyIdleThread>& aIOThread, bool aURLShortcut, + already_AddRefed<nsIRunnable> aRunnable); + + static int32_t GetICOCacheSecondsTimeout(); +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(WinUtils::PathTransformFlags); + +// RTL shim windows are temporary child windows of our nsWindows created to +// address RTL issues in picker dialogs. (See bug 588735.) +class ScopedRtlShimWindow { + public: + explicit ScopedRtlShimWindow(nsIWidget* aParent); + ~ScopedRtlShimWindow(); + + ScopedRtlShimWindow(const ScopedRtlShimWindow&) = delete; + ScopedRtlShimWindow(ScopedRtlShimWindow&& that) noexcept : mWnd(that.mWnd) { + that.mWnd = nullptr; + }; + + HWND get() const { return mWnd; } + + private: + HWND mWnd; +}; + +} // namespace widget +} // namespace mozilla + +#endif // mozilla_widget_WinUtils_h__ diff --git a/widget/windows/WinWindowOcclusionTracker.cpp b/widget/windows/WinWindowOcclusionTracker.cpp new file mode 100644 index 0000000000..a38f950585 --- /dev/null +++ b/widget/windows/WinWindowOcclusionTracker.cpp @@ -0,0 +1,1471 @@ +/* -*- 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 <queue> +#include <windows.h> +#include <winuser.h> +#include <wtsapi32.h> + +#include "WinWindowOcclusionTracker.h" + +#include "base/thread.h" +#include "base/message_loop.h" +#include "base/platform_thread.h" +#include "gfxConfig.h" +#include "nsThreadUtils.h" +#include "mozilla/DataMutex.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/StaticPtr.h" +#include "nsBaseWidget.h" +#include "nsWindow.h" +#include "transport/runnable_utils.h" +#include "WinUtils.h" + +namespace mozilla::widget { + +// Can be called on Main thread +LazyLogModule gWinOcclusionTrackerLog("WinOcclusionTracker"); +#define LOG(type, ...) MOZ_LOG(gWinOcclusionTrackerLog, type, (__VA_ARGS__)) + +// Can be called on OcclusionCalculator thread +LazyLogModule gWinOcclusionCalculatorLog("WinOcclusionCalculator"); +#define CALC_LOG(type, ...) \ + MOZ_LOG(gWinOcclusionCalculatorLog, type, (__VA_ARGS__)) + +// ~16 ms = time between frames when frame rate is 60 FPS. +const int kOcclusionUpdateRunnableDelayMs = 16; + +class OcclusionUpdateRunnable : public CancelableRunnable { + public: + explicit OcclusionUpdateRunnable( + WinWindowOcclusionTracker::WindowOcclusionCalculator* + aOcclusionCalculator) + : CancelableRunnable("OcclusionUpdateRunnable"), + mOcclusionCalculator(aOcclusionCalculator) { + mTimeStamp = TimeStamp::Now(); + } + + NS_IMETHOD Run() override { + if (mIsCanceled) { + return NS_OK; + } + MOZ_ASSERT(WinWindowOcclusionTracker::IsInWinWindowOcclusionThread()); + + uint32_t latencyMs = + round((TimeStamp::Now() - mTimeStamp).ToMilliseconds()); + CALC_LOG(LogLevel::Debug, + "ComputeNativeWindowOcclusionStatus() latencyMs %u", latencyMs); + + mOcclusionCalculator->ComputeNativeWindowOcclusionStatus(); + return NS_OK; + } + + nsresult Cancel() override { + mIsCanceled = true; + mOcclusionCalculator = nullptr; + return NS_OK; + } + + private: + bool mIsCanceled = false; + RefPtr<WinWindowOcclusionTracker::WindowOcclusionCalculator> + mOcclusionCalculator; + TimeStamp mTimeStamp; +}; + +// Used to serialize tasks related to mRootWindowHwndsOcclusionState. +class SerializedTaskDispatcher { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SerializedTaskDispatcher) + + public: + SerializedTaskDispatcher(); + + void Destroy(); + void PostTaskToMain(already_AddRefed<nsIRunnable> aTask); + void PostTaskToCalculator(already_AddRefed<nsIRunnable> aTask); + void PostDelayedTaskToCalculator(already_AddRefed<Runnable> aTask, + int aDelayMs); + bool IsOnCurrentThread(); + + private: + friend class DelayedTaskRunnable; + + ~SerializedTaskDispatcher(); + + struct Data { + std::queue<std::pair<RefPtr<nsIRunnable>, RefPtr<nsISerialEventTarget>>> + mTasks; + bool mDestroyed = false; + RefPtr<Runnable> mCurrentRunnable; + }; + + void PostTasksIfNecessary(nsISerialEventTarget* aEventTarget, + const DataMutex<Data>::AutoLock& aProofOfLock); + void HandleDelayedTask(already_AddRefed<nsIRunnable> aTask); + void HandleTasks(); + + // Hold current EventTarget during calling nsIRunnable::Run(). + RefPtr<nsISerialEventTarget> mCurrentEventTarget = nullptr; + + DataMutex<Data> mData; +}; + +class DelayedTaskRunnable : public Runnable { + public: + DelayedTaskRunnable(SerializedTaskDispatcher* aSerializedTaskDispatcher, + already_AddRefed<Runnable> aTask) + : Runnable("DelayedTaskRunnable"), + mSerializedTaskDispatcher(aSerializedTaskDispatcher), + mTask(aTask) {} + + NS_IMETHOD Run() override { + mSerializedTaskDispatcher->HandleDelayedTask(mTask.forget()); + return NS_OK; + } + + private: + RefPtr<SerializedTaskDispatcher> mSerializedTaskDispatcher; + RefPtr<Runnable> mTask; +}; + +SerializedTaskDispatcher::SerializedTaskDispatcher() + : mData("SerializedTaskDispatcher::mData") { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "SerializedTaskDispatcher::SerializedTaskDispatcher() this %p", this); +} + +SerializedTaskDispatcher::~SerializedTaskDispatcher() { +#ifdef DEBUG + auto data = mData.Lock(); + MOZ_ASSERT(data->mDestroyed); + MOZ_ASSERT(data->mTasks.empty()); +#endif +} + +void SerializedTaskDispatcher::Destroy() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "SerializedTaskDispatcher::Destroy() this %p", this); + + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + data->mDestroyed = true; + std::queue<std::pair<RefPtr<nsIRunnable>, RefPtr<nsISerialEventTarget>>> + empty; + std::swap(data->mTasks, empty); +} + +void SerializedTaskDispatcher::PostTaskToMain( + already_AddRefed<nsIRunnable> aTask) { + RefPtr<nsIRunnable> task = aTask; + + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + nsISerialEventTarget* eventTarget = GetMainThreadSerialEventTarget(); + data->mTasks.push({std::move(task), eventTarget}); + + MOZ_ASSERT_IF(!data->mCurrentRunnable, data->mTasks.size() == 1); + PostTasksIfNecessary(eventTarget, data); +} + +void SerializedTaskDispatcher::PostTaskToCalculator( + already_AddRefed<nsIRunnable> aTask) { + RefPtr<nsIRunnable> task = aTask; + + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + nsISerialEventTarget* eventTarget = + WinWindowOcclusionTracker::OcclusionCalculatorLoop()->SerialEventTarget(); + data->mTasks.push({std::move(task), eventTarget}); + + MOZ_ASSERT_IF(!data->mCurrentRunnable, data->mTasks.size() == 1); + PostTasksIfNecessary(eventTarget, data); +} + +void SerializedTaskDispatcher::PostDelayedTaskToCalculator( + already_AddRefed<Runnable> aTask, int aDelayMs) { + CALC_LOG(LogLevel::Debug, + "SerializedTaskDispatcher::PostDelayedTaskToCalculator()"); + + RefPtr<DelayedTaskRunnable> runnable = + new DelayedTaskRunnable(this, std::move(aTask)); + MessageLoop* targetLoop = + WinWindowOcclusionTracker::OcclusionCalculatorLoop(); + targetLoop->PostDelayedTask(runnable.forget(), aDelayMs); +} + +bool SerializedTaskDispatcher::IsOnCurrentThread() { + return !!mCurrentEventTarget; +} + +void SerializedTaskDispatcher::PostTasksIfNecessary( + nsISerialEventTarget* aEventTarget, + const DataMutex<Data>::AutoLock& aProofOfLock) { + MOZ_ASSERT(!aProofOfLock->mTasks.empty()); + + if (aProofOfLock->mCurrentRunnable) { + return; + } + + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<SerializedTaskDispatcher>(this), + &SerializedTaskDispatcher::HandleTasks); + aProofOfLock->mCurrentRunnable = runnable; + aEventTarget->Dispatch(runnable.forget()); +} + +void SerializedTaskDispatcher::HandleDelayedTask( + already_AddRefed<nsIRunnable> aTask) { + MOZ_ASSERT(WinWindowOcclusionTracker::IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Debug, "SerializedTaskDispatcher::HandleDelayedTask()"); + + RefPtr<nsIRunnable> task = aTask; + + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + nsISerialEventTarget* eventTarget = + WinWindowOcclusionTracker::OcclusionCalculatorLoop()->SerialEventTarget(); + data->mTasks.push({std::move(task), eventTarget}); + + MOZ_ASSERT_IF(!data->mCurrentRunnable, data->mTasks.size() == 1); + PostTasksIfNecessary(eventTarget, data); +} + +void SerializedTaskDispatcher::HandleTasks() { + RefPtr<nsIRunnable> frontTask; + + // Get front task + { + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + MOZ_RELEASE_ASSERT(data->mCurrentRunnable); + MOZ_RELEASE_ASSERT(!data->mTasks.empty()); + + frontTask = data->mTasks.front().first; + + MOZ_RELEASE_ASSERT(!mCurrentEventTarget); + mCurrentEventTarget = data->mTasks.front().second; + } + + while (frontTask) { + if (NS_IsMainThread()) { + LOG(LogLevel::Debug, "SerializedTaskDispatcher::HandleTasks()"); + } else { + CALC_LOG(LogLevel::Debug, "SerializedTaskDispatcher::HandleTasks()"); + } + + MOZ_ASSERT_IF(NS_IsMainThread(), + mCurrentEventTarget == GetMainThreadSerialEventTarget()); + MOZ_ASSERT_IF( + !NS_IsMainThread(), + mCurrentEventTarget == MessageLoop::current()->SerialEventTarget()); + + frontTask->Run(); + + // Get next task + { + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + frontTask = nullptr; + data->mTasks.pop(); + // Check if next task could be handled on current thread + if (!data->mTasks.empty() && + data->mTasks.front().second == mCurrentEventTarget) { + frontTask = data->mTasks.front().first; + } + } + } + + MOZ_ASSERT(!frontTask); + + // Post tasks to different thread if pending tasks exist. + { + auto data = mData.Lock(); + data->mCurrentRunnable = nullptr; + mCurrentEventTarget = nullptr; + + if (data->mDestroyed || data->mTasks.empty()) { + return; + } + + PostTasksIfNecessary(data->mTasks.front().second, data); + } +} + +// static +StaticRefPtr<WinWindowOcclusionTracker> WinWindowOcclusionTracker::sTracker; + +/* static */ +WinWindowOcclusionTracker* WinWindowOcclusionTracker::Get() { + MOZ_ASSERT(NS_IsMainThread()); + if (!sTracker || sTracker->mHasAttemptedShutdown) { + return nullptr; + } + return sTracker; +} + +/* static */ +void WinWindowOcclusionTracker::Ensure() { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WinWindowOcclusionTracker::Ensure()"); + + base::Thread::Options options; + options.message_loop_type = MessageLoop::TYPE_UI; + + if (sTracker) { + // Try to reuse the thread, which involves stopping and restarting it. + sTracker->mThread->Stop(); + if (sTracker->mThread->StartWithOptions(options)) { + // Success! + sTracker->mHasAttemptedShutdown = false; + + // Take this opportunity to ensure that mDisplayStatusObserver and + // mSessionChangeObserver exist. They might have failed to be + // created when sTracker was created. + sTracker->EnsureDisplayStatusObserver(); + sTracker->EnsureSessionChangeObserver(); + return; + } + // Restart failed, so null out our sTracker and try again with a new + // thread. This will cause the old singleton instance to be deallocated, + // which will destroy its mThread as well. + sTracker = nullptr; + } + + UniquePtr<base::Thread> thread = + MakeUnique<base::Thread>("WinWindowOcclusionCalc"); + + if (!thread->StartWithOptions(options)) { + return; + } + + sTracker = new WinWindowOcclusionTracker(std::move(thread)); + WindowOcclusionCalculator::CreateInstance(); + + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::Initialize); + sTracker->mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +/* static */ +void WinWindowOcclusionTracker::ShutDown() { + if (!sTracker) { + return; + } + + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WinWindowOcclusionTracker::ShutDown()"); + + sTracker->mHasAttemptedShutdown = true; + sTracker->Destroy(); + + // Our thread could hang while we're waiting for it to stop. + // Since we're shutting down, that's not a critical problem. + // We set a reasonable amount of time to wait for shutdown, + // and if it succeeds within that time, we correctly stop + // our thread by nulling out the refptr, which will cause it + // to be deallocated and join the thread. If it times out, + // we do nothing, which means that the thread will not be + // joined and sTracker memory will leak. + CVStatus status; + { + // It's important to hold the lock before posting the + // runnable. This ensures that the runnable can't begin + // until we've started our Wait, which prevents us from + // Waiting on a monitor that has already been notified. + MonitorAutoLock lock(sTracker->mMonitor); + + static const TimeDuration TIMEOUT = TimeDuration::FromSeconds(2.0); + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::Shutdown); + OcclusionCalculatorLoop()->PostTask(runnable.forget()); + + // Monitor uses SleepConditionVariableSRW, which can have + // spurious wakeups which are reported as timeouts, so we + // check timestamps to ensure that we've waited as long we + // intended to. If we wake early, we don't bother calculating + // a precise amount for the next wait; we just wait the same + // amount of time. This means timeout might happen after as + // much as 2x the TIMEOUT time. + TimeStamp timeStart = TimeStamp::NowLoRes(); + do { + status = sTracker->mMonitor.Wait(TIMEOUT); + } while ((status == CVStatus::Timeout) && + ((TimeStamp::NowLoRes() - timeStart) < TIMEOUT)); + } + + if (status == CVStatus::NoTimeout) { + WindowOcclusionCalculator::ClearInstance(); + sTracker = nullptr; + } +} + +void WinWindowOcclusionTracker::Destroy() { + if (mDisplayStatusObserver) { + mDisplayStatusObserver->Destroy(); + mDisplayStatusObserver = nullptr; + } + if (mSessionChangeObserver) { + mSessionChangeObserver->Destroy(); + mSessionChangeObserver = nullptr; + } + if (mSerializedTaskDispatcher) { + mSerializedTaskDispatcher->Destroy(); + } +} + +/* static */ +MessageLoop* WinWindowOcclusionTracker::OcclusionCalculatorLoop() { + return sTracker ? sTracker->mThread->message_loop() : nullptr; +} + +/* static */ +bool WinWindowOcclusionTracker::IsInWinWindowOcclusionThread() { + return sTracker && + sTracker->mThread->thread_id() == PlatformThread::CurrentId(); +} + +void WinWindowOcclusionTracker::EnsureDisplayStatusObserver() { + if (mDisplayStatusObserver) { + return; + } + if (StaticPrefs:: + widget_windows_window_occlusion_tracking_display_state_enabled()) { + mDisplayStatusObserver = DisplayStatusObserver::Create(this); + } +} + +void WinWindowOcclusionTracker::EnsureSessionChangeObserver() { + if (mSessionChangeObserver) { + return; + } + if (StaticPrefs:: + widget_windows_window_occlusion_tracking_session_lock_enabled()) { + mSessionChangeObserver = SessionChangeObserver::Create(this); + } +} + +void WinWindowOcclusionTracker::Enable(nsBaseWidget* aWindow, HWND aHwnd) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WinWindowOcclusionTracker::Enable() aWindow %p aHwnd %p", + aWindow, aHwnd); + + auto it = mHwndRootWindowMap.find(aHwnd); + if (it != mHwndRootWindowMap.end()) { + return; + } + + nsWeakPtr weak = do_GetWeakReference(aWindow); + mHwndRootWindowMap.emplace(aHwnd, weak); + + RefPtr<Runnable> runnable = WrapRunnable( + RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::EnableOcclusionTrackingForWindow, aHwnd); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +void WinWindowOcclusionTracker::Disable(nsBaseWidget* aWindow, HWND aHwnd) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::Disable() aWindow %p aHwnd %p", aWindow, + aHwnd); + + auto it = mHwndRootWindowMap.find(aHwnd); + if (it == mHwndRootWindowMap.end()) { + return; + } + + mHwndRootWindowMap.erase(it); + + RefPtr<Runnable> runnable = WrapRunnable( + RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::DisableOcclusionTrackingForWindow, aHwnd); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +void WinWindowOcclusionTracker::OnWindowVisibilityChanged(nsBaseWidget* aWindow, + bool aVisible) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::OnWindowVisibilityChanged() aWindow %p " + "aVisible %d", + aWindow, aVisible); + + RefPtr<Runnable> runnable = WrapRunnable( + RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::HandleVisibilityChanged, aVisible); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +WinWindowOcclusionTracker::WinWindowOcclusionTracker( + UniquePtr<base::Thread> aThread) + : mThread(std::move(aThread)), mMonitor("WinWindowOcclusionTracker") { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WinWindowOcclusionTracker::WinWindowOcclusionTracker()"); + + EnsureDisplayStatusObserver(); + EnsureSessionChangeObserver(); + + mSerializedTaskDispatcher = new SerializedTaskDispatcher(); +} + +WinWindowOcclusionTracker::~WinWindowOcclusionTracker() { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::~WinWindowOcclusionTracker()"); +} + +// static +bool WinWindowOcclusionTracker::IsWindowVisibleAndFullyOpaque( + HWND aHwnd, LayoutDeviceIntRect* aWindowRect) { + // Filter out windows that are not "visible", IsWindowVisible(). + if (!::IsWindow(aHwnd) || !::IsWindowVisible(aHwnd)) { + return false; + } + + // Filter out minimized windows. + if (::IsIconic(aHwnd)) { + return false; + } + + LONG exStyles = ::GetWindowLong(aHwnd, GWL_EXSTYLE); + // Filter out "transparent" windows, windows where the mouse clicks fall + // through them. + if (exStyles & WS_EX_TRANSPARENT) { + return false; + } + + // Filter out "tool windows", which are floating windows that do not appear on + // the taskbar or ALT-TAB. Floating windows can have larger window rectangles + // than what is visible to the user, so by filtering them out we will avoid + // incorrectly marking native windows as occluded. We do not filter out the + // Windows Taskbar. + if (exStyles & WS_EX_TOOLWINDOW) { + nsAutoString className; + if (WinUtils::GetClassName(aHwnd, className)) { + if (!className.Equals(L"Shell_TrayWnd")) { + return false; + } + } + } + + // Filter out layered windows that are not opaque or that set a transparency + // colorkey. + if (exStyles & WS_EX_LAYERED) { + BYTE alpha; + DWORD flags; + + // GetLayeredWindowAttributes only works if the application has + // previously called SetLayeredWindowAttributes on the window. + // The function will fail if the layered window was setup with + // UpdateLayeredWindow. Treat this failure as the window being transparent. + // See Remarks section of + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getlayeredwindowattributes + if (!::GetLayeredWindowAttributes(aHwnd, nullptr, &alpha, &flags)) { + return false; + } + + if (flags & LWA_ALPHA && alpha < 255) { + return false; + } + if (flags & LWA_COLORKEY) { + return false; + } + } + + // Filter out windows that do not have a simple rectangular region. + HRGN region = ::CreateRectRgn(0, 0, 0, 0); + int result = GetWindowRgn(aHwnd, region); + ::DeleteObject(region); + if (result == COMPLEXREGION) { + return false; + } + + // Windows 10 has cloaked windows, windows with WS_VISIBLE attribute but + // not displayed. explorer.exe, in particular has one that's the + // size of the desktop. It's usually behind Chrome windows in the z-order, + // but using a remote desktop can move it up in the z-order. So, ignore them. + DWORD reason; + if (SUCCEEDED(::DwmGetWindowAttribute(aHwnd, DWMWA_CLOAKED, &reason, + sizeof(reason))) && + reason != 0) { + return false; + } + + RECT winRect; + // Filter out windows that take up zero area. The call to GetWindowRect is one + // of the most expensive parts of this function, so it is last. + if (!::GetWindowRect(aHwnd, &winRect)) { + return false; + } + if (::IsRectEmpty(&winRect)) { + return false; + } + + // Ignore popup windows since they're transient unless it is the Windows + // Taskbar + // XXX Chrome Widget popup handling is removed for now. + if (::GetWindowLong(aHwnd, GWL_STYLE) & WS_POPUP) { + nsAutoString className; + if (WinUtils::GetClassName(aHwnd, className)) { + if (!className.Equals(L"Shell_TrayWnd")) { + return false; + } + } + } + + *aWindowRect = LayoutDeviceIntRect(winRect.left, winRect.top, + winRect.right - winRect.left, + winRect.bottom - winRect.top); + + WINDOWPLACEMENT windowPlacement = {0}; + windowPlacement.length = sizeof(WINDOWPLACEMENT); + ::GetWindowPlacement(aHwnd, &windowPlacement); + if (windowPlacement.showCmd == SW_MAXIMIZE) { + // If the window is maximized the window border extends beyond the visible + // region of the screen. Adjust the maximized window rect to fit the + // screen dimensions to ensure that fullscreen windows, which do not extend + // beyond the screen boundaries since they typically have no borders, will + // occlude maximized windows underneath them. + HMONITOR hmon = ::MonitorFromWindow(aHwnd, MONITOR_DEFAULTTONEAREST); + if (hmon) { + MONITORINFO mi; + mi.cbSize = sizeof(mi); + if (GetMonitorInfo(hmon, &mi)) { + LayoutDeviceIntRect workArea(mi.rcWork.left, mi.rcWork.top, + mi.rcWork.right - mi.rcWork.left, + mi.rcWork.bottom - mi.rcWork.top); + // Adjust aWindowRect to fit to monitor. + aWindowRect->width = std::min(workArea.width, aWindowRect->width); + if (aWindowRect->x < workArea.x) { + aWindowRect->x = workArea.x; + } else { + aWindowRect->x = std::min(workArea.x + workArea.width, + aWindowRect->x + aWindowRect->width) - + aWindowRect->width; + } + aWindowRect->height = std::min(workArea.height, aWindowRect->height); + if (aWindowRect->y < workArea.y) { + aWindowRect->y = workArea.y; + } else { + aWindowRect->y = std::min(workArea.y + workArea.height, + aWindowRect->y + aWindowRect->height) - + aWindowRect->height; + } + } + } + } + + return true; +} + +// static +void WinWindowOcclusionTracker::CallUpdateOcclusionState( + std::unordered_map<HWND, OcclusionState>* aMap, bool aShowAllWindows) { + MOZ_ASSERT(NS_IsMainThread()); + + auto* tracker = WinWindowOcclusionTracker::Get(); + if (!tracker) { + return; + } + tracker->UpdateOcclusionState(aMap, aShowAllWindows); +} + +void WinWindowOcclusionTracker::UpdateOcclusionState( + std::unordered_map<HWND, OcclusionState>* aMap, bool aShowAllWindows) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread()); + LOG(LogLevel::Debug, + "WinWindowOcclusionTracker::UpdateOcclusionState() aShowAllWindows %d", + aShowAllWindows); + + mNumVisibleRootWindows = 0; + for (auto& [hwnd, state] : *aMap) { + auto it = mHwndRootWindowMap.find(hwnd); + // The window was destroyed while processing occlusion. + if (it == mHwndRootWindowMap.end()) { + continue; + } + auto occlState = state; + + // If the screen is locked or off, ignore occlusion state results and + // mark the window as occluded. + if (mScreenLocked || !mDisplayOn) { + occlState = OcclusionState::OCCLUDED; + } else if (aShowAllWindows) { + occlState = OcclusionState::VISIBLE; + } + nsCOMPtr<nsIWidget> widget = do_QueryReferent(it->second); + if (!widget) { + continue; + } + auto* baseWidget = static_cast<nsBaseWidget*>(widget.get()); + baseWidget->NotifyOcclusionState(occlState); + if (baseWidget->SizeMode() != nsSizeMode_Minimized) { + mNumVisibleRootWindows++; + } + } +} + +void WinWindowOcclusionTracker::OnSessionChange(WPARAM aStatusCode, + Maybe<bool> aIsCurrentSession) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aIsCurrentSession.isNothing() || !*aIsCurrentSession) { + return; + } + + if (aStatusCode == WTS_SESSION_UNLOCK) { + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::OnSessionChange() WTS_SESSION_UNLOCK"); + + // UNLOCK will cause a foreground window change, which will + // trigger an occlusion calculation on its own. + mScreenLocked = false; + } else if (aStatusCode == WTS_SESSION_LOCK) { + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::OnSessionChange() WTS_SESSION_LOCK"); + + mScreenLocked = true; + MarkNonIconicWindowsOccluded(); + } +} + +void WinWindowOcclusionTracker::OnDisplayStateChanged(bool aDisplayOn) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::OnDisplayStateChanged() aDisplayOn %d", + aDisplayOn); + + if (mDisplayOn == aDisplayOn) { + return; + } + + mDisplayOn = aDisplayOn; + if (aDisplayOn) { + // Notify the window occlusion calculator of the display turning on + // which will schedule an occlusion calculation. This must be run + // on the WindowOcclusionCalculator thread. + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::HandleVisibilityChanged, + /* aVisible */ true); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); + } else { + MarkNonIconicWindowsOccluded(); + } +} + +void WinWindowOcclusionTracker::MarkNonIconicWindowsOccluded() { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::MarkNonIconicWindowsOccluded()"); + + // Set all visible root windows as occluded. If not visible, + // set them as hidden. + for (auto& [hwnd, weak] : mHwndRootWindowMap) { + nsCOMPtr<nsIWidget> widget = do_QueryReferent(weak); + if (!widget) { + continue; + } + auto* baseWidget = static_cast<nsBaseWidget*>(widget.get()); + auto state = (baseWidget->SizeMode() == nsSizeMode_Minimized) + ? OcclusionState::HIDDEN + : OcclusionState::OCCLUDED; + baseWidget->NotifyOcclusionState(state); + } +} + +void WinWindowOcclusionTracker::TriggerCalculation() { + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::HandleTriggerCalculation); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +// static +BOOL WinWindowOcclusionTracker::DumpOccludingWindowsCallback(HWND aHWnd, + LPARAM aLParam) { + HWND hwnd = reinterpret_cast<HWND>(aLParam); + + LayoutDeviceIntRect windowRect; + bool windowIsOccluding = IsWindowVisibleAndFullyOpaque(aHWnd, &windowRect); + if (windowIsOccluding) { + nsAutoString className; + if (WinUtils::GetClassName(aHWnd, className)) { + const auto name = NS_ConvertUTF16toUTF8(className); + printf_stderr( + "DumpOccludingWindowsCallback() aHWnd %p className %s windowRect(%d, " + "%d, %d, %d)\n", + aHWnd, name.get(), windowRect.x, windowRect.y, windowRect.width, + windowRect.height); + } + } + + if (aHWnd == hwnd) { + return false; + } + return true; +} + +void WinWindowOcclusionTracker::DumpOccludingWindows(HWND aHWnd) { + printf_stderr("DumpOccludingWindows() until aHWnd %p visible %d iconic %d\n", + aHWnd, ::IsWindowVisible(aHWnd), ::IsIconic(aHWnd)); + ::EnumWindows(&DumpOccludingWindowsCallback, reinterpret_cast<LPARAM>(aHWnd)); +} + +// static +StaticRefPtr<WinWindowOcclusionTracker::WindowOcclusionCalculator> + WinWindowOcclusionTracker::WindowOcclusionCalculator::sCalculator; + +WinWindowOcclusionTracker::WindowOcclusionCalculator:: + WindowOcclusionCalculator() + : mMonitor(WinWindowOcclusionTracker::Get()->mMonitor) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WindowOcclusionCalculator()"); + + mSerializedTaskDispatcher = + WinWindowOcclusionTracker::Get()->GetSerializedTaskDispatcher(); +} + +WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ~WindowOcclusionCalculator() {} + +// static +void WinWindowOcclusionTracker::WindowOcclusionCalculator::CreateInstance() { + MOZ_ASSERT(NS_IsMainThread()); + sCalculator = new WindowOcclusionCalculator(); +} + +// static +void WinWindowOcclusionTracker::WindowOcclusionCalculator::ClearInstance() { + MOZ_ASSERT(NS_IsMainThread()); + sCalculator = nullptr; +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator::Initialize() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_ASSERT(!mVirtualDesktopManager); + CALC_LOG(LogLevel::Info, "Initialize()"); + +#ifndef __MINGW32__ + RefPtr<IVirtualDesktopManager> desktopManager; + HRESULT hr = ::CoCreateInstance( + CLSID_VirtualDesktopManager, NULL, CLSCTX_INPROC_SERVER, + __uuidof(IVirtualDesktopManager), getter_AddRefs(desktopManager)); + if (FAILED(hr)) { + return; + } + mVirtualDesktopManager = desktopManager; +#endif +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator::Shutdown() { + MonitorAutoLock lock(mMonitor); + + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Info, "Shutdown()"); + + UnregisterEventHooks(); + if (mOcclusionUpdateRunnable) { + mOcclusionUpdateRunnable->Cancel(); + mOcclusionUpdateRunnable = nullptr; + } + mVirtualDesktopManager = nullptr; + + mMonitor.NotifyAll(); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + EnableOcclusionTrackingForWindow(HWND aHwnd) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread()); + CALC_LOG(LogLevel::Info, "EnableOcclusionTrackingForWindow() aHwnd %p", + aHwnd); + + MOZ_RELEASE_ASSERT(mRootWindowHwndsOcclusionState.find(aHwnd) == + mRootWindowHwndsOcclusionState.end()); + mRootWindowHwndsOcclusionState[aHwnd] = OcclusionState::UNKNOWN; + + if (mGlobalEventHooks.empty()) { + RegisterEventHooks(); + } + + // Schedule an occlusion calculation so that the newly tracked window does + // not have a stale occlusion status. + ScheduleOcclusionCalculationIfNeeded(); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + DisableOcclusionTrackingForWindow(HWND aHwnd) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread()); + CALC_LOG(LogLevel::Info, "DisableOcclusionTrackingForWindow() aHwnd %p", + aHwnd); + + MOZ_RELEASE_ASSERT(mRootWindowHwndsOcclusionState.find(aHwnd) != + mRootWindowHwndsOcclusionState.end()); + mRootWindowHwndsOcclusionState.erase(aHwnd); + + if (mMovingWindow == aHwnd) { + mMovingWindow = 0; + } + + if (mRootWindowHwndsOcclusionState.empty()) { + UnregisterEventHooks(); + if (mOcclusionUpdateRunnable) { + mOcclusionUpdateRunnable->Cancel(); + mOcclusionUpdateRunnable = nullptr; + } + } +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + HandleVisibilityChanged(bool aVisible) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Info, "HandleVisibilityChange() aVisible %d", aVisible); + + // May have gone from having no visible windows to having one, in + // which case we need to register event hooks, and make sure that an + // occlusion calculation is scheduled. + if (aVisible) { + MaybeRegisterEventHooks(); + ScheduleOcclusionCalculationIfNeeded(); + } +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + HandleTriggerCalculation() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Info, "HandleTriggerCalculation()"); + + MaybeRegisterEventHooks(); + ScheduleOcclusionCalculationIfNeeded(); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + MaybeRegisterEventHooks() { + if (mGlobalEventHooks.empty()) { + RegisterEventHooks(); + } +} + +// static +void CALLBACK +WinWindowOcclusionTracker::WindowOcclusionCalculator::EventHookCallback( + HWINEVENTHOOK aWinEventHook, DWORD aEvent, HWND aHwnd, LONG aIdObject, + LONG aIdChild, DWORD aEventThread, DWORD aMsEventTime) { + if (sCalculator) { + sCalculator->ProcessEventHookCallback(aWinEventHook, aEvent, aHwnd, + aIdObject, aIdChild); + } +} + +// static +BOOL CALLBACK WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ComputeNativeWindowOcclusionStatusCallback(HWND aHwnd, LPARAM aLParam) { + if (sCalculator) { + return sCalculator->ProcessComputeNativeWindowOcclusionStatusCallback( + aHwnd, reinterpret_cast<std::unordered_set<DWORD>*>(aLParam)); + } + return FALSE; +} + +// static +BOOL CALLBACK WinWindowOcclusionTracker::WindowOcclusionCalculator:: + UpdateVisibleWindowProcessIdsCallback(HWND aHwnd, LPARAM aLParam) { + if (sCalculator) { + sCalculator->ProcessUpdateVisibleWindowProcessIdsCallback(aHwnd); + return TRUE; + } + return FALSE; +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + UpdateVisibleWindowProcessIds() { + mPidsForLocationChangeHook.clear(); + ::EnumWindows(&UpdateVisibleWindowProcessIdsCallback, 0); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ComputeNativeWindowOcclusionStatus() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread()); + + if (mOcclusionUpdateRunnable) { + mOcclusionUpdateRunnable = nullptr; + } + + if (mRootWindowHwndsOcclusionState.empty()) { + return; + } + + // Set up initial conditions for occlusion calculation. + bool shouldUnregisterEventHooks = true; + + // Compute the LayoutDeviceIntRegion for the screen. + int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN); + int screenTop = ::GetSystemMetrics(SM_YVIRTUALSCREEN); + int screenWidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN); + int screenHeight = ::GetSystemMetrics(SM_CYVIRTUALSCREEN); + LayoutDeviceIntRegion screenRegion = + LayoutDeviceIntRect(screenLeft, screenTop, screenWidth, screenHeight); + mNumRootWindowsWithUnknownOcclusionState = 0; + + CALC_LOG(LogLevel::Debug, + "ComputeNativeWindowOcclusionStatus() screen(%d, %d, %d, %d)", + screenLeft, screenTop, screenWidth, screenHeight); + + for (auto& [hwnd, state] : mRootWindowHwndsOcclusionState) { + // IsIconic() checks for a minimized window. Immediately set the state of + // minimized windows to HIDDEN. + if (::IsIconic(hwnd)) { + state = OcclusionState::HIDDEN; + } else if (IsWindowOnCurrentVirtualDesktop(hwnd) == Some(false)) { + // If window is not on the current virtual desktop, immediately + // set the state of the window to OCCLUDED. + state = OcclusionState::OCCLUDED; + // Don't unregister event hooks when not on current desktop. There's no + // notification when that changes, so we can't reregister event hooks. + shouldUnregisterEventHooks = false; + } else { + state = OcclusionState::UNKNOWN; + shouldUnregisterEventHooks = false; + mNumRootWindowsWithUnknownOcclusionState++; + } + } + + // Unregister event hooks if all native windows are minimized. + if (shouldUnregisterEventHooks) { + UnregisterEventHooks(); + } else { + std::unordered_set<DWORD> currentPidsWithVisibleWindows; + mUnoccludedDesktopRegion = screenRegion; + // Calculate unoccluded region if there is a non-minimized native window. + // Also compute |current_pids_with_visible_windows| as we enumerate + // the windows. + EnumWindows(&ComputeNativeWindowOcclusionStatusCallback, + reinterpret_cast<LPARAM>(¤tPidsWithVisibleWindows)); + // Check if mPidsForLocationChangeHook has any pids of processes + // currently without visible windows. If so, unhook the win event, + // remove the pid from mPidsForLocationChangeHook and remove + // the corresponding event hook from mProcessEventHooks. + std::unordered_set<DWORD> pidsToRemove; + for (auto locChangePid : mPidsForLocationChangeHook) { + if (currentPidsWithVisibleWindows.find(locChangePid) == + currentPidsWithVisibleWindows.end()) { + // Remove the event hook from our map, and unregister the event hook. + // It's possible the eventhook will no longer be valid, but if we don't + // unregister the event hook, a process that toggles between having + // visible windows and not having visible windows could cause duplicate + // event hooks to get registered for the process. + UnhookWinEvent(mProcessEventHooks[locChangePid]); + mProcessEventHooks.erase(locChangePid); + pidsToRemove.insert(locChangePid); + } + } + if (!pidsToRemove.empty()) { + // XXX simplify + for (auto it = mPidsForLocationChangeHook.begin(); + it != mPidsForLocationChangeHook.end();) { + if (pidsToRemove.find(*it) != pidsToRemove.end()) { + it = mPidsForLocationChangeHook.erase(it); + } else { + ++it; + } + } + } + } + + std::unordered_map<HWND, OcclusionState>* map = + &mRootWindowHwndsOcclusionState; + bool showAllWindows = mShowingThumbnails; + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "CallUpdateOcclusionState", [map, showAllWindows]() { + WinWindowOcclusionTracker::CallUpdateOcclusionState(map, + showAllWindows); + }); + mSerializedTaskDispatcher->PostTaskToMain(runnable.forget()); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ScheduleOcclusionCalculationIfNeeded() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + + // OcclusionUpdateRunnable is already queued. + if (mOcclusionUpdateRunnable) { + return; + } + + CALC_LOG(LogLevel::Debug, "ScheduleOcclusionCalculationIfNeeded()"); + + RefPtr<CancelableRunnable> task = new OcclusionUpdateRunnable(this); + mOcclusionUpdateRunnable = task; + mSerializedTaskDispatcher->PostDelayedTaskToCalculator( + task.forget(), kOcclusionUpdateRunnableDelayMs); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + RegisterGlobalEventHook(DWORD aEventMin, DWORD aEventMax) { + HWINEVENTHOOK eventHook = + ::SetWinEventHook(aEventMin, aEventMax, nullptr, &EventHookCallback, 0, 0, + WINEVENT_OUTOFCONTEXT); + mGlobalEventHooks.push_back(eventHook); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + RegisterEventHookForProcess(DWORD aPid) { + mPidsForLocationChangeHook.insert(aPid); + mProcessEventHooks[aPid] = SetWinEventHook( + EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, nullptr, + &EventHookCallback, aPid, 0, WINEVENT_OUTOFCONTEXT); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + RegisterEventHooks() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_RELEASE_ASSERT(mGlobalEventHooks.empty()); + CALC_LOG(LogLevel::Info, "RegisterEventHooks()"); + + // Detects native window lost mouse capture + RegisterGlobalEventHook(EVENT_SYSTEM_CAPTUREEND, EVENT_SYSTEM_CAPTUREEND); + + // Detects native window move (drag) and resizing events. + RegisterGlobalEventHook(EVENT_SYSTEM_MOVESIZESTART, EVENT_SYSTEM_MOVESIZEEND); + + // Detects native window minimize and restore from taskbar events. + RegisterGlobalEventHook(EVENT_SYSTEM_MINIMIZESTART, EVENT_SYSTEM_MINIMIZEEND); + + // Detects foreground window changing. + RegisterGlobalEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND); + + // Detects objects getting shown and hidden. Used to know when the task bar + // and alt tab are showing preview windows so we can unocclude windows. + RegisterGlobalEventHook(EVENT_OBJECT_SHOW, EVENT_OBJECT_HIDE); + + // Detects object state changes, e.g., enable/disable state, native window + // maximize and native window restore events. + RegisterGlobalEventHook(EVENT_OBJECT_STATECHANGE, EVENT_OBJECT_STATECHANGE); + + // Cloaking and uncloaking of windows should trigger an occlusion calculation. + // In particular, switching virtual desktops seems to generate these events. + RegisterGlobalEventHook(EVENT_OBJECT_CLOAKED, EVENT_OBJECT_UNCLOAKED); + + // Determine which subset of processes to set EVENT_OBJECT_LOCATIONCHANGE on + // because otherwise event throughput is very high, as it generates events + // for location changes of all objects, including the mouse moving on top of a + // window. + UpdateVisibleWindowProcessIds(); + for (DWORD pid : mPidsForLocationChangeHook) { + RegisterEventHookForProcess(pid); + } +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + UnregisterEventHooks() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Info, "UnregisterEventHooks()"); + + for (const auto eventHook : mGlobalEventHooks) { + ::UnhookWinEvent(eventHook); + } + mGlobalEventHooks.clear(); + + for (const auto& [pid, eventHook] : mProcessEventHooks) { + ::UnhookWinEvent(eventHook); + } + mProcessEventHooks.clear(); + + mPidsForLocationChangeHook.clear(); +} + +bool WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ProcessComputeNativeWindowOcclusionStatusCallback( + HWND aHwnd, std::unordered_set<DWORD>* aCurrentPidsWithVisibleWindows) { + LayoutDeviceIntRegion currUnoccludedDestkop = mUnoccludedDesktopRegion; + LayoutDeviceIntRect windowRect; + bool windowIsOccluding = + WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(aHwnd, &windowRect); + if (windowIsOccluding) { + // Hook this window's process with EVENT_OBJECT_LOCATION_CHANGE, if we are + // not already doing so. + DWORD pid; + ::GetWindowThreadProcessId(aHwnd, &pid); + aCurrentPidsWithVisibleWindows->insert(pid); + auto it = mProcessEventHooks.find(pid); + if (it == mProcessEventHooks.end()) { + RegisterEventHookForProcess(pid); + } + + // If no more root windows to consider, return true so we can continue + // looking for windows we haven't hooked. + if (mNumRootWindowsWithUnknownOcclusionState == 0) { + return true; + } + + mUnoccludedDesktopRegion.SubOut(windowRect); + } else if (mNumRootWindowsWithUnknownOcclusionState == 0) { + // This window can't occlude other windows, but we've determined the + // occlusion state of all root windows, so we can return. + return true; + } + + // Ignore moving windows when deciding if windows under it are occluded. + if (aHwnd == mMovingWindow) { + return true; + } + + // Check if |hwnd| is a root window; if so, we're done figuring out + // if it's occluded because we've seen all the windows "over" it. + auto it = mRootWindowHwndsOcclusionState.find(aHwnd); + if (it == mRootWindowHwndsOcclusionState.end() || + it->second != OcclusionState::UNKNOWN) { + return true; + } + + CALC_LOG(LogLevel::Debug, + "ProcessComputeNativeWindowOcclusionStatusCallback() windowRect(%d, " + "%d, %d, %d) IsOccluding %d", + windowRect.x, windowRect.y, windowRect.width, windowRect.height, + windowIsOccluding); + + // On Win7, default theme makes root windows have complex regions by + // default. But we can still check if their bounding rect is occluded. + if (!windowIsOccluding) { + RECT rect; + if (::GetWindowRect(aHwnd, &rect) != 0) { + LayoutDeviceIntRect windowRect( + rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); + currUnoccludedDestkop.SubOut(windowRect); + } + } + + it->second = (mUnoccludedDesktopRegion == currUnoccludedDestkop) + ? OcclusionState::OCCLUDED + : OcclusionState::VISIBLE; + mNumRootWindowsWithUnknownOcclusionState--; + + return true; +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ProcessEventHookCallback(HWINEVENTHOOK aWinEventHook, DWORD aEvent, + HWND aHwnd, LONG aIdObject, LONG aIdChild) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + + // No need to calculate occlusion if a zero HWND generated the event. This + // happens if there is no window associated with the event, e.g., mouse move + // events. + if (!aHwnd) { + return; + } + + // We only care about events for window objects. In particular, we don't care + // about OBJID_CARET, which is spammy. + if (aIdObject != OBJID_WINDOW) { + return; + } + + CALC_LOG(LogLevel::Debug, + "WindowOcclusionCalculator::ProcessEventHookCallback() aEvent 0x%lx", + aEvent); + + // We generally ignore events for popup windows, except for when the taskbar + // is hidden or Windows Taskbar, in which case we recalculate occlusion. + // XXX Chrome Widget popup handling is removed for now. + bool calculateOcclusion = true; + if (::GetWindowLong(aHwnd, GWL_STYLE) & WS_POPUP) { + nsAutoString className; + if (WinUtils::GetClassName(aHwnd, className)) { + calculateOcclusion = className.Equals(L"Shell_TrayWnd"); + } + } + + // Detect if either the alt tab view or the task list thumbnail is being + // shown. If so, mark all non-hidden windows as occluded, and remember that + // we're in the showing_thumbnails state. This lasts until we get told that + // either the alt tab view or task list thumbnail are hidden. + if (aEvent == EVENT_OBJECT_SHOW) { + // Avoid getting the aHwnd's class name, and recomputing occlusion, if not + // needed. + if (mShowingThumbnails) { + return; + } + nsAutoString className; + if (WinUtils::GetClassName(aHwnd, className)) { + const auto name = NS_ConvertUTF16toUTF8(className); + CALC_LOG(LogLevel::Debug, + "ProcessEventHookCallback() EVENT_OBJECT_SHOW %s", name.get()); + + if (name.Equals("MultitaskingViewFrame") || + name.Equals("TaskListThumbnailWnd")) { + CALC_LOG(LogLevel::Info, + "ProcessEventHookCallback() mShowingThumbnails = true"); + mShowingThumbnails = true; + + std::unordered_map<HWND, OcclusionState>* map = + &mRootWindowHwndsOcclusionState; + bool showAllWindows = mShowingThumbnails; + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "CallUpdateOcclusionState", [map, showAllWindows]() { + WinWindowOcclusionTracker::CallUpdateOcclusionState( + map, showAllWindows); + }); + mSerializedTaskDispatcher->PostTaskToMain(runnable.forget()); + } + } + return; + } else if (aEvent == EVENT_OBJECT_HIDE) { + // Avoid getting the aHwnd's class name, and recomputing occlusion, if not + // needed. + if (!mShowingThumbnails) { + return; + } + nsAutoString className; + WinUtils::GetClassName(aHwnd, className); + const auto name = NS_ConvertUTF16toUTF8(className); + CALC_LOG(LogLevel::Debug, "ProcessEventHookCallback() EVENT_OBJECT_HIDE %s", + name.get()); + if (name.Equals("MultitaskingViewFrame") || + name.Equals("TaskListThumbnailWnd")) { + CALC_LOG(LogLevel::Info, + "ProcessEventHookCallback() mShowingThumbnails = false"); + mShowingThumbnails = false; + // Let occlusion calculation fix occlusion state, even though hwnd might + // be a popup window. + calculateOcclusion = true; + } else { + return; + } + } + // Don't continually calculate occlusion while a window is moving (unless it's + // a root window), but instead once at the beginning and once at the end. + // Remember the window being moved so if it's a root window, we can ignore + // it when deciding if windows under it are occluded. + else if (aEvent == EVENT_SYSTEM_MOVESIZESTART) { + mMovingWindow = aHwnd; + } else if (aEvent == EVENT_SYSTEM_MOVESIZEEND) { + mMovingWindow = 0; + } else if (mMovingWindow != 0) { + if (aEvent == EVENT_OBJECT_LOCATIONCHANGE || + aEvent == EVENT_OBJECT_STATECHANGE) { + // Ignore move events if it's not a root window that's being moved. If it + // is a root window, we want to calculate occlusion to support tab + // dragging to windows that were occluded when the drag was started but + // are no longer occluded. + if (mRootWindowHwndsOcclusionState.find(aHwnd) == + mRootWindowHwndsOcclusionState.end()) { + return; + } + } else { + // If we get an event that isn't a location/state change, then we probably + // missed the movesizeend notification, or got events out of order. In + // that case, we want to go back to normal occlusion calculation. + mMovingWindow = 0; + } + } + + if (!calculateOcclusion) { + return; + } + + ScheduleOcclusionCalculationIfNeeded(); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ProcessUpdateVisibleWindowProcessIdsCallback(HWND aHwnd) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + + LayoutDeviceIntRect windowRect; + if (WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(aHwnd, &windowRect)) { + DWORD pid; + ::GetWindowThreadProcessId(aHwnd, &pid); + mPidsForLocationChangeHook.insert(pid); + } +} + +bool WinWindowOcclusionTracker::WindowOcclusionCalculator:: + WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop( + HWND aHwnd, LayoutDeviceIntRect* aWindowRect) { + return IsWindowVisibleAndFullyOpaque(aHwnd, aWindowRect) && + (IsWindowOnCurrentVirtualDesktop(aHwnd) == Some(true)); +} + +Maybe<bool> WinWindowOcclusionTracker::WindowOcclusionCalculator:: + IsWindowOnCurrentVirtualDesktop(HWND aHwnd) { + if (!mVirtualDesktopManager) { + return Some(true); + } + + BOOL onCurrentDesktop; + HRESULT hr = mVirtualDesktopManager->IsWindowOnCurrentVirtualDesktop( + aHwnd, &onCurrentDesktop); + if (FAILED(hr)) { + // In this case, we do not know the window is in which virtual desktop. + return Nothing(); + } + + if (onCurrentDesktop) { + return Some(true); + } + + GUID workspaceGuid; + hr = mVirtualDesktopManager->GetWindowDesktopId(aHwnd, &workspaceGuid); + if (FAILED(hr)) { + // In this case, we do not know the window is in which virtual desktop. + return Nothing(); + } + + // IsWindowOnCurrentVirtualDesktop() is flaky for newly opened windows, + // which causes test flakiness. Occasionally, it incorrectly says a window + // is not on the current virtual desktop when it is. In this situation, + // it also returns GUID_NULL for the desktop id. + if (workspaceGuid == GUID_NULL) { + // In this case, we do not know if the window is in which virtual desktop. + // But we hanle it as on current virtual desktop. + // It does not cause a problem to window occlusion. + // Since if window is not on current virtual desktop, window size becomes + // (0, 0, 0, 0). It makes window occlusion handling explicit. It is + // necessary for gtest. + return Some(true); + } + + return Some(false); +} + +#undef LOG +#undef CALC_LOG + +} // namespace mozilla::widget diff --git a/widget/windows/WinWindowOcclusionTracker.h b/widget/windows/WinWindowOcclusionTracker.h new file mode 100644 index 0000000000..b82a41b984 --- /dev/null +++ b/widget/windows/WinWindowOcclusionTracker.h @@ -0,0 +1,333 @@ +/* -*- 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 widget_windows_WinWindowOcclusionTracker_h +#define widget_windows_WinWindowOcclusionTracker_h + +#include <string> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +#include "nsIWeakReferenceUtils.h" +#include "mozilla/Monitor.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/ThreadSafeWeakPtr.h" +#include "mozilla/widget/WindowOcclusionState.h" +#include "mozilla/widget/WinEventObserver.h" +#include "Units.h" +#include "nsThreadUtils.h" + +class nsBaseWidget; +struct IVirtualDesktopManager; +class WinWindowOcclusionTrackerTest; +class WinWindowOcclusionTrackerInteractiveTest; + +namespace base { +class Thread; +} // namespace base + +namespace mozilla { + +namespace widget { + +class OcclusionUpdateRunnable; +class SerializedTaskDispatcher; +class UpdateOcclusionStateRunnable; + +// This class handles window occlusion tracking by using HWND. +// Implementation is borrowed from chromium's NativeWindowOcclusionTrackerWin. +class WinWindowOcclusionTracker final : public DisplayStatusListener, + public SessionChangeListener { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinWindowOcclusionTracker) + + /// Can only be called from the main thread. + static WinWindowOcclusionTracker* Get(); + + /// Can only be called from the main thread. + static void Ensure(); + + /// Can only be called from the main thread. + static void ShutDown(); + + /// Can be called from any thread. + static MessageLoop* OcclusionCalculatorLoop(); + + /// Can be called from any thread. + static bool IsInWinWindowOcclusionThread(); + + /// Can only be called from the main thread. + void EnsureDisplayStatusObserver(); + + /// Can only be called from the main thread. + void EnsureSessionChangeObserver(); + + // Enables notifying to widget via NotifyOcclusionState() when the occlusion + // state has been computed. + void Enable(nsBaseWidget* aWindow, HWND aHwnd); + + // Disables notifying to widget via NotifyOcclusionState() when the occlusion + // state has been computed. + void Disable(nsBaseWidget* aWindow, HWND aHwnd); + + // Called when widget's visibility is changed + void OnWindowVisibilityChanged(nsBaseWidget* aWindow, bool aVisible); + + SerializedTaskDispatcher* GetSerializedTaskDispatcher() { + return mSerializedTaskDispatcher; + } + + void TriggerCalculation(); + + void DumpOccludingWindows(HWND aHWnd); + + private: + friend class ::WinWindowOcclusionTrackerTest; + friend class ::WinWindowOcclusionTrackerInteractiveTest; + + explicit WinWindowOcclusionTracker(UniquePtr<base::Thread> aThread); + virtual ~WinWindowOcclusionTracker(); + + // This class computes the occlusion state of the tracked windows. + // It runs on a separate thread, and notifies the main thread of + // the occlusion state of the tracked windows. + class WindowOcclusionCalculator { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowOcclusionCalculator) + public: + // Creates WindowOcclusionCalculator instance. + static void CreateInstance(); + + // Clear WindowOcclusionCalculator instance. + static void ClearInstance(); + + // Returns existing WindowOcclusionCalculator instance. + static WindowOcclusionCalculator* GetInstance() { return sCalculator; } + + void Initialize(); + void Shutdown(); + + void EnableOcclusionTrackingForWindow(HWND hwnd); + void DisableOcclusionTrackingForWindow(HWND hwnd); + + // If a window becomes visible, makes sure event hooks are registered. + void HandleVisibilityChanged(bool aVisible); + + void HandleTriggerCalculation(); + + private: + WindowOcclusionCalculator(); + ~WindowOcclusionCalculator(); + + // Registers event hooks, if not registered. + void MaybeRegisterEventHooks(); + + // This is the callback registered to get notified of various Windows + // events, like window moving/resizing. + static void CALLBACK EventHookCallback(HWINEVENTHOOK aWinEventHook, + DWORD aEvent, HWND aHwnd, + LONG aIdObject, LONG aIdChild, + DWORD aEventThread, + DWORD aMsEventTime); + + // EnumWindows callback used to iterate over all hwnds to determine + // occlusion status of all tracked root windows. Also builds up + // |current_pids_with_visible_windows_| and registers event hooks for newly + // discovered processes with visible hwnds. + static BOOL CALLBACK + ComputeNativeWindowOcclusionStatusCallback(HWND hwnd, LPARAM lParam); + + // EnumWindows callback used to update the list of process ids with + // visible hwnds, |pids_for_location_change_hook_|. + static BOOL CALLBACK UpdateVisibleWindowProcessIdsCallback(HWND aHwnd, + LPARAM aLParam); + + // Determines which processes owning visible application windows to set the + // EVENT_OBJECT_LOCATIONCHANGE event hook for and stores the pids in + // |pids_for_location_change_hook_|. + void UpdateVisibleWindowProcessIds(); + + // Computes the native window occlusion status for all tracked root gecko + // windows in |root_window_hwnds_occlusion_state_| and notifies them if + // their occlusion status has changed. + void ComputeNativeWindowOcclusionStatus(); + + // Schedules an occlusion calculation , if one isn't already scheduled. + void ScheduleOcclusionCalculationIfNeeded(); + + // Registers a global event hook (not per process) for the events in the + // range from |event_min| to |event_max|, inclusive. + void RegisterGlobalEventHook(DWORD aEventMin, DWORD aEventMax); + + // Registers the EVENT_OBJECT_LOCATIONCHANGE event hook for the process with + // passed id. The process has one or more visible, opaque windows. + void RegisterEventHookForProcess(DWORD aPid); + + // Registers/Unregisters the event hooks necessary for occlusion tracking + // via calls to RegisterEventHook. These event hooks are disabled when all + // tracked windows are minimized. + void RegisterEventHooks(); + void UnregisterEventHooks(); + + // EnumWindows callback for occlusion calculation. Returns true to + // continue enumeration, false otherwise. Currently, always returns + // true because this function also updates currentPidsWithVisibleWindows, + // and needs to see all HWNDs. + bool ProcessComputeNativeWindowOcclusionStatusCallback( + HWND aHwnd, std::unordered_set<DWORD>* aCurrentPidsWithVisibleWindows); + + // Processes events sent to OcclusionEventHookCallback. + // It generally triggers scheduling of the occlusion calculation, but + // ignores certain events in order to not calculate occlusion more than + // necessary. + void ProcessEventHookCallback(HWINEVENTHOOK aWinEventHook, DWORD aEvent, + HWND aHwnd, LONG aIdObject, LONG aIdChild); + + // EnumWindows callback for determining which processes to set the + // EVENT_OBJECT_LOCATIONCHANGE event hook for. We set that event hook for + // processes hosting fully visible, opaque windows. + void ProcessUpdateVisibleWindowProcessIdsCallback(HWND aHwnd); + + // Returns true if the window is visible, fully opaque, and on the current + // virtual desktop, false otherwise. + bool WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop( + HWND aHwnd, LayoutDeviceIntRect* aWindowRect); + + // Returns true if aHwnd is definitely on the current virtual desktop, + // false if it's definitely not on the current virtual desktop, and Nothing + // if we we can't tell for sure. + Maybe<bool> IsWindowOnCurrentVirtualDesktop(HWND aHwnd); + + static StaticRefPtr<WindowOcclusionCalculator> sCalculator; + + // Map of root app window hwnds and their occlusion state. This contains + // both visible and hidden windows. + // It is accessed from WinWindowOcclusionTracker::UpdateOcclusionState() + // without using mutex. The access is safe by using + // SerializedTaskDispatcher. + std::unordered_map<HWND, OcclusionState> mRootWindowHwndsOcclusionState; + + // Values returned by SetWinEventHook are stored so that hooks can be + // unregistered when necessary. + std::vector<HWINEVENTHOOK> mGlobalEventHooks; + + // Map from process id to EVENT_OBJECT_LOCATIONCHANGE event hook. + std::unordered_map<DWORD, HWINEVENTHOOK> mProcessEventHooks; + + // Pids of processes for which the EVENT_OBJECT_LOCATIONCHANGE event hook is + // set. + std::unordered_set<DWORD> mPidsForLocationChangeHook; + + // Used as a timer to delay occlusion update. + RefPtr<CancelableRunnable> mOcclusionUpdateRunnable; + + // Used to determine if a window is occluded. As we iterate through the + // hwnds in z-order, we subtract each opaque window's rect from + // mUnoccludedDesktopRegion. When we get to a root window, we subtract + // it from mUnoccludedDesktopRegion, and if mUnoccludedDesktopRegion + // doesn't change, the root window was already occluded. + LayoutDeviceIntRegion mUnoccludedDesktopRegion; + + // Keeps track of how many root windows we need to compute the occlusion + // state of in a call to ComputeNativeWindowOcclusionStatus. Once we've + // determined the state of all root windows, we can stop subtracting + // windows from mUnoccludedDesktopRegion;. + int mNumRootWindowsWithUnknownOcclusionState; + + // This is true if the task bar thumbnails or the alt tab thumbnails are + // showing. + bool mShowingThumbnails = false; + + // Used to keep track of the window that's currently moving. That window + // is ignored for calculation occlusion so that tab dragging won't + // ignore windows occluded by the dragged window. + HWND mMovingWindow = 0; + + // Only used on Win10+. + RefPtr<IVirtualDesktopManager> mVirtualDesktopManager; + + // Used to serialize tasks related to mRootWindowHwndsOcclusionState. + RefPtr<SerializedTaskDispatcher> mSerializedTaskDispatcher; + + // This is an alias to the singleton WinWindowOcclusionTracker mMonitor, + // and is used in ShutDown(). + Monitor& mMonitor; + + friend class OcclusionUpdateRunnable; + }; + + static BOOL CALLBACK DumpOccludingWindowsCallback(HWND aHWnd, LPARAM aLParam); + + // Returns true if we are interested in |hwnd| for purposes of occlusion + // calculation. We are interested in |hwnd| if it is a window that is + // visible, opaque, bounded, and not a popup or floating window. If we are + // interested in |hwnd|, stores the window rectangle in |window_rect|. + static bool IsWindowVisibleAndFullyOpaque(HWND aHwnd, + LayoutDeviceIntRect* aWindowRect); + + void Destroy(); + + static void CallUpdateOcclusionState( + std::unordered_map<HWND, OcclusionState>* aMap, bool aShowAllWindows); + + // Updates root windows occclusion state. If aShowAllWindows is true, + // all non-hidden windows will be marked visible. This is used to force + // rendering of thumbnails. + void UpdateOcclusionState(std::unordered_map<HWND, OcclusionState>* aMap, + bool aShowAllWindows); + + // This is called with session changed notifications. If the screen is locked + // by the current session, it marks app windows as occluded. + void OnSessionChange(WPARAM aStatusCode, + Maybe<bool> aIsCurrentSession) override; + + // This is called when the display is put to sleep. If the display is sleeping + // it marks app windows as occluded. + void OnDisplayStateChanged(bool aDisplayOn) override; + + // Marks all root windows as either occluded, or if hwnd IsIconic, hidden. + void MarkNonIconicWindowsOccluded(); + + static StaticRefPtr<WinWindowOcclusionTracker> sTracker; + + // "WinWindowOcclusionCalc" thread. + UniquePtr<base::Thread> mThread; + Monitor mMonitor; + + // Has ShutDown been called on us? We might have survived if our thread join + // timed out. + bool mHasAttemptedShutdown = false; + + // Map of HWND to widget. Maintained on main thread, and used to send + // occlusion state notifications to Windows from + // mRootWindowHwndsOcclusionState. + std::unordered_map<HWND, nsWeakPtr> mHwndRootWindowMap; + + // This is set by UpdateOcclusionState(). It is currently only used by tests. + int mNumVisibleRootWindows = 0; + + // If the screen is locked, windows are considered occluded. + bool mScreenLocked = false; + + // If the display is off, windows are considered occluded. + bool mDisplayOn = true; + + RefPtr<DisplayStatusObserver> mDisplayStatusObserver; + + RefPtr<SessionChangeObserver> mSessionChangeObserver; + + // Used to serialize tasks related to mRootWindowHwndsOcclusionState. + RefPtr<SerializedTaskDispatcher> mSerializedTaskDispatcher; + + friend class OcclusionUpdateRunnable; + friend class UpdateOcclusionStateRunnable; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_windows_WinWindowOcclusionTracker_h diff --git a/widget/windows/WindowHook.cpp b/widget/windows/WindowHook.cpp new file mode 100644 index 0000000000..06773f21b7 --- /dev/null +++ b/widget/windows/WindowHook.cpp @@ -0,0 +1,113 @@ +/* -*- 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 "WindowHook.h" +#include "nsWindow.h" +#include "nsWindowDefs.h" + +namespace mozilla { +namespace widget { + +nsresult WindowHook::AddHook(UINT nMsg, Callback callback, void* context) { + MessageData* data = LookupOrCreate(nMsg); + + if (!data) return NS_ERROR_OUT_OF_MEMORY; + + // Ensure we don't overwrite another hook + NS_ENSURE_TRUE(nullptr == data->hook.cb, NS_ERROR_UNEXPECTED); + + data->hook = CallbackData(callback, context); + + return NS_OK; +} + +nsresult WindowHook::RemoveHook(UINT nMsg, Callback callback, void* context) { + CallbackData cbdata(callback, context); + MessageData* data = Lookup(nMsg); + if (!data) return NS_ERROR_UNEXPECTED; + if (data->hook != cbdata) return NS_ERROR_UNEXPECTED; + data->hook = CallbackData(); + + DeleteIfEmpty(data); + return NS_OK; +} + +nsresult WindowHook::AddMonitor(UINT nMsg, Callback callback, void* context) { + MessageData* data = LookupOrCreate(nMsg); + return (data && data->monitors.AppendElement(CallbackData(callback, context), + fallible)) + ? NS_OK + : NS_ERROR_OUT_OF_MEMORY; +} + +nsresult WindowHook::RemoveMonitor(UINT nMsg, Callback callback, + void* context) { + CallbackData cbdata(callback, context); + MessageData* data = Lookup(nMsg); + if (!data) return NS_ERROR_UNEXPECTED; + CallbackDataArray::index_type idx = data->monitors.IndexOf(cbdata); + if (idx == CallbackDataArray::NoIndex) return NS_ERROR_UNEXPECTED; + data->monitors.RemoveElementAt(idx); + DeleteIfEmpty(data); + return NS_OK; +} + +WindowHook::MessageData* WindowHook::Lookup(UINT nMsg) { + MessageDataArray::index_type idx; + for (idx = 0; idx < mMessageData.Length(); idx++) { + MessageData& data = mMessageData[idx]; + if (data.nMsg == nMsg) return &data; + } + return nullptr; +} + +WindowHook::MessageData* WindowHook::LookupOrCreate(UINT nMsg) { + MessageData* data = Lookup(nMsg); + if (!data) { + data = mMessageData.AppendElement(); + + if (!data) return nullptr; + + data->nMsg = nMsg; + } + return data; +} + +void WindowHook::DeleteIfEmpty(MessageData* data) { + // Never remove a MessageData that has still a hook or monitor entries. + if (data->hook || !data->monitors.IsEmpty()) return; + + MessageDataArray::index_type idx; + idx = data - mMessageData.Elements(); + NS_ASSERTION( + idx < mMessageData.Length(), + "Attempted to delete MessageData that doesn't belong to this array!"); + mMessageData.RemoveElementAt(idx); +} + +bool WindowHook::Notify(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam, + MSGResult& aResult) { + MessageData* data = Lookup(nMsg); + if (!data) return false; + + uint32_t length = data->monitors.Length(); + for (uint32_t midx = 0; midx < length; midx++) { + data->monitors[midx].Invoke(hWnd, nMsg, wParam, lParam, &aResult.mResult); + } + + aResult.mConsumed = + data->hook.Invoke(hWnd, nMsg, wParam, lParam, &aResult.mResult); + return aResult.mConsumed; +} + +bool WindowHook::CallbackData::Invoke(HWND hWnd, UINT msg, WPARAM wParam, + LPARAM lParam, LRESULT* aResult) { + if (!cb) return false; + return cb(context, hWnd, msg, wParam, lParam, aResult); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/WindowHook.h b/widget/windows/WindowHook.h new file mode 100644 index 0000000000..1d1f4b02da --- /dev/null +++ b/widget/windows/WindowHook.h @@ -0,0 +1,76 @@ +/* -*- 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_WindowHook_h__ +#define __mozilla_WindowHook_h__ + +#include <windows.h> + +#include <nsHashKeys.h> +#include <nsClassHashtable.h> +#include <nsTArray.h> + +#include "nsAppShell.h" + +class nsWindow; + +namespace mozilla { +namespace widget { + +struct MSGResult; + +class WindowHook { + public: + // It is expected that most callbacks will return false + typedef bool (*Callback)(void* aContext, HWND hWnd, UINT nMsg, WPARAM wParam, + LPARAM lParam, LRESULT* aResult); + + nsresult AddHook(UINT nMsg, Callback callback, void* context); + nsresult RemoveHook(UINT nMsg, Callback callback, void* context); + nsresult AddMonitor(UINT nMsg, Callback callback, void* context); + nsresult RemoveMonitor(UINT nMsg, Callback callback, void* context); + + private: + struct CallbackData { + Callback cb; + void* context; + + CallbackData() : cb(nullptr), context(nullptr) {} + CallbackData(Callback cb, void* ctx) : cb(cb), context(ctx) {} + bool Invoke(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, + LRESULT* aResult); + bool operator==(const CallbackData& rhs) const { + return cb == rhs.cb && context == rhs.context; + } + bool operator!=(const CallbackData& rhs) const { return !(*this == rhs); } + explicit operator bool() const { return !!cb; } + }; + + typedef nsTArray<CallbackData> CallbackDataArray; + struct MessageData { + UINT nMsg; + CallbackData hook; + CallbackDataArray monitors; + }; + + bool Notify(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam, + MSGResult& aResult); + + MessageData* Lookup(UINT nMsg); + MessageData* LookupOrCreate(UINT nMsg); + void DeleteIfEmpty(MessageData* data); + + typedef nsTArray<MessageData> MessageDataArray; + MessageDataArray mMessageData; + + // For Notify + friend class ::nsWindow; +}; + +} // namespace widget +} // namespace mozilla + +#endif // __mozilla_WindowHook_h__ diff --git a/widget/windows/WindowsConsole.cpp b/widget/windows/WindowsConsole.cpp new file mode 100644 index 0000000000..c8e7eb1a11 --- /dev/null +++ b/widget/windows/WindowsConsole.cpp @@ -0,0 +1,53 @@ +/* -*- 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 "WindowsConsole.h" + +#include <windows.h> +#include <fcntl.h> +#include <cstdio> +#include <io.h> + +namespace mozilla { + +static void AssignStdHandle(const char* aPath, const char* aMode, FILE* aStream, + DWORD aStdHandle) { + // Visual Studio's _fileno() returns -2 for the standard + // streams if they aren't associated with an output stream. + const int fd = _fileno(aStream); + if (fd == -2) { + freopen(aPath, aMode, aStream); + return; + } + if (fd < 0) { + return; + } + + const HANDLE handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd)); + if (handle == INVALID_HANDLE_VALUE) { + return; + } + + const HANDLE oldHandle = GetStdHandle(aStdHandle); + if (handle == oldHandle) { + return; + } + + SetStdHandle(aStdHandle, handle); +} + +// This code attaches the process to the appropriate console. +void UseParentConsole() { + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + // Redirect the standard streams to the existing console, but + // only if they haven't been redirected to a valid file. + AssignStdHandle("CONOUT$", "w", stdout, STD_OUTPUT_HANDLE); + // There is no CONERR$, so use CONOUT$ for stderr as well. + AssignStdHandle("CONOUT$", "w", stderr, STD_ERROR_HANDLE); + AssignStdHandle("CONIN$", "r", stdin, STD_INPUT_HANDLE); + } +} + +} // namespace mozilla diff --git a/widget/windows/WindowsConsole.h b/widget/windows/WindowsConsole.h new file mode 100644 index 0000000000..4b8ecf1823 --- /dev/null +++ b/widget/windows/WindowsConsole.h @@ -0,0 +1,16 @@ +/* -*- 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 mozilla_WindowsConsole_h +#define mozilla_WindowsConsole_h + +namespace mozilla { + +// This code attaches the process to the appropriate console. +void UseParentConsole(); + +} // namespace mozilla + +#endif // mozilla_WindowsConsole_h diff --git a/widget/windows/WindowsEMF.cpp b/widget/windows/WindowsEMF.cpp new file mode 100644 index 0000000000..71e3631bea --- /dev/null +++ b/widget/windows/WindowsEMF.cpp @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 20; 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 "WindowsEMF.h" + +namespace mozilla { +namespace widget { + +WindowsEMF::WindowsEMF() : mEmf(nullptr), mDC(nullptr) {} + +WindowsEMF::~WindowsEMF() { ReleaseAllResource(); } + +bool WindowsEMF::InitForDrawing(const wchar_t* aMetafilePath /* = nullptr */) { + ReleaseAllResource(); + + mDC = ::CreateEnhMetaFile(nullptr, aMetafilePath, nullptr, nullptr); + return !!mDC; +} + +bool WindowsEMF::InitFromFileContents(const wchar_t* aMetafilePath) { + MOZ_ASSERT(aMetafilePath); + ReleaseAllResource(); + + mEmf = ::GetEnhMetaFileW(aMetafilePath); + return !!mEmf; +} + +bool WindowsEMF::InitFromFileContents(LPBYTE aBytes, UINT aSize) { + MOZ_ASSERT(aBytes && aSize != 0); + ReleaseAllResource(); + + mEmf = SetEnhMetaFileBits(aSize, aBytes); + + return !!mEmf; +} + +bool WindowsEMF::FinishDocument() { + if (mDC) { + mEmf = ::CloseEnhMetaFile(mDC); + mDC = nullptr; + } + return !!mEmf; +} + +void WindowsEMF::ReleaseEMFHandle() { + if (mEmf) { + ::DeleteEnhMetaFile(mEmf); + mEmf = nullptr; + } +} + +void WindowsEMF::ReleaseAllResource() { + FinishDocument(); + ReleaseEMFHandle(); +} + +bool WindowsEMF::Playback(HDC aDeviceContext, const RECT& aRect) { + DebugOnly<bool> result = FinishDocument(); + MOZ_ASSERT(result, "This function should be used after InitXXX."); + + return ::PlayEnhMetaFile(aDeviceContext, mEmf, &aRect) != 0; +} + +bool WindowsEMF::SaveToFile() { + DebugOnly<bool> result = FinishDocument(); + MOZ_ASSERT(result, "This function should be used after InitXXX."); + + ReleaseEMFHandle(); + return true; +} + +UINT WindowsEMF::GetEMFContentSize() { + DebugOnly<bool> result = FinishDocument(); + MOZ_ASSERT(result, "This function should be used after InitXXX."); + + return GetEnhMetaFileBits(mEmf, 0, NULL); +} + +bool WindowsEMF::GetEMFContentBits(LPBYTE aBytes) { + DebugOnly<bool> result = FinishDocument(); + MOZ_ASSERT(result, "This function should be used after InitXXX."); + + UINT emfSize = GetEMFContentSize(); + if (GetEnhMetaFileBits(mEmf, emfSize, aBytes) != emfSize) { + return false; + } + + return true; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/WindowsEMF.h b/widget/windows/WindowsEMF.h new file mode 100644 index 0000000000..3a7a20173c --- /dev/null +++ b/widget/windows/WindowsEMF.h @@ -0,0 +1,106 @@ +/* -*- 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_WINDOWSEMF_H +#define MOZILLA_WIDGET_WINDOWSEMF_H + +/* include windows.h for the HDC definitions that we need. */ +#include <windows.h> + +namespace mozilla { +namespace widget { + +/** + * Windows Enhance Metafile: https://en.wikipedia.org/wiki/Windows_Metafile + * A metafile, also called a vector image, is an image that is stored as a + * sequence of drawing commands and settings. The commands and settings + * recorded in a Metafile object can be stored in memory or saved to a file. + * + * The metafile device context is used for all drawing operations required to + * create the picture. When the system processes a GDI function associated with + * a metafile DC, it converts the function into the appropriate data and stores + * this data in a record appended to the metafile. + */ +class WindowsEMF { + public: + WindowsEMF(); + ~WindowsEMF(); + + /** + * Initializes the object with the path of a file where the EMF data stream + * should be stored. Callers are then expected to call GetDC() to draw output + * before going on to call Playback() or SaveToFile() to generate the EMF + * output. + */ + bool InitForDrawing(const wchar_t* aMetafilePath = nullptr); + + /** + * Initializes the object with an existing EMF file. Consumers cannot use + * GetDC() to obtain an HDC to modify the file. They can only use Playback(). + */ + bool InitFromFileContents(const wchar_t* aMetafilePath); + + /** + * Creates the EMF from the specified data + * + * @param aByte Pointer to a buffer that contains EMF data. + * @param aSize Specifies the size, in bytes, of aByte. + */ + bool InitFromFileContents(PBYTE aBytes, UINT aSize); + + /** + * If this object was initiaziled using InitForDrawing() then this function + * returns an HDC that can be drawn to generate the EMF output. Otherwise it + * returns null. After finishing with the HDC, consumers could call Playback() + * to draw EMF onto the given DC or call SaveToFile() to finish writing the + * EMF file. + */ + HDC GetDC() const { + MOZ_ASSERT(mDC, + "GetDC can be used only after " + "InitForDrawing/ InitFromFileContents and before" + "Playback/ SaveToFile"); + return mDC; + } + + /** + * Play the EMF's drawing commands onto the given DC. + */ + bool Playback(HDC aDeviceContext, const RECT& aRect); + + /** + * Called to generate the EMF file once a consumer has finished drawing to + * the HDC returned by GetDC(), if initializes the object with the path of a + * file. + */ + bool SaveToFile(); + + /** + * Return the size of the enhanced metafile, in bytes. + */ + UINT GetEMFContentSize(); + + /** + * Retrieves the contents of the EMF and copies them into a buffer. + * + * @param aByte the buffer to receive the data. + */ + bool GetEMFContentBits(PBYTE aBytes); + + private: + WindowsEMF(const WindowsEMF& aEMF) = delete; + bool FinishDocument(); + void ReleaseEMFHandle(); + void ReleaseAllResource(); + + /* Compiled EMF data handle. */ + HENHMETAFILE mEmf; + HDC mDC; +}; + +} // namespace widget +} // namespace mozilla + +#endif /* MOZILLA_WIDGET_WINDOWSEMF_H */ diff --git a/widget/windows/WindowsEventLog.h b/widget/windows/WindowsEventLog.h new file mode 100644 index 0000000000..e98d5077a0 --- /dev/null +++ b/widget/windows/WindowsEventLog.h @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_WindowsEventLog_h +#define mozilla_WindowsEventLog_h + +/** + * Report messages to the Windows Event Log. + */ + +#include <stdio.h> +#include <windows.h> + +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" + +/** + * This header is intended for self-contained, header-only, utility code for + * Win32. It may be used outside of xul.dll, in places such as + * default-browser-agent.exe or notificationrouter.dll. If your code creates + * dependencies on Mozilla libraries, you should put it elsewhere. + */ + +#define MOZ_WIN_EVENT_LOG_ERROR(source, hr) \ + mozilla::WriteWindowsEventLogHresult(source, hr, __FUNCTION__, __LINE__) +#define MOZ_WIN_EVENT_LOG_ERROR_MESSAGE(source, format, ...) \ + mozilla::WriteWindowsEventLogErrorMessage(source, format, __FUNCTION__, \ + __LINE__, ##__VA_ARGS__) + +namespace mozilla { + +static void WriteWindowsEventLogErrorBuffer(const wchar_t* eventSourceName, + const wchar_t* buffer, + DWORD eventId) { + HANDLE source = RegisterEventSourceW(nullptr, eventSourceName); + if (!source) { + // Not much we can do about this. + return; + } + + const wchar_t* stringsArray[] = {buffer}; + ReportEventW(source, EVENTLOG_ERROR_TYPE, 0, eventId, nullptr, 1, 0, + stringsArray, nullptr); + + DeregisterEventSource(source); +} + +inline void WriteWindowsEventLogHresult(const wchar_t* eventSourceName, + HRESULT hr, const char* sourceFile, + int sourceLine) { + const wchar_t* format = L"0x%X in %S:%d"; + int bufferSize = _scwprintf(format, hr, sourceFile, sourceLine); + ++bufferSize; // Extra character for terminating null + mozilla::UniquePtr<wchar_t[]> errorStr = + mozilla::MakeUnique<wchar_t[]>(bufferSize); + + _snwprintf_s(errorStr.get(), bufferSize, _TRUNCATE, format, hr, sourceFile, + sourceLine); + + WriteWindowsEventLogErrorBuffer(eventSourceName, errorStr.get(), hr); +} + +MOZ_FORMAT_WPRINTF(1, 4) +inline void WriteWindowsEventLogErrorMessage(const wchar_t* eventSourceName, + const wchar_t* messageFormat, + const char* sourceFile, + int sourceLine, ...) { + // First assemble the passed message + va_list ap; + va_start(ap, sourceLine); + int bufferSize = _vscwprintf(messageFormat, ap); + ++bufferSize; // Extra character for terminating null + va_end(ap); + mozilla::UniquePtr<wchar_t[]> message = + mozilla::MakeUnique<wchar_t[]>(bufferSize); + + va_start(ap, sourceLine); + vswprintf(message.get(), bufferSize, messageFormat, ap); + va_end(ap); + + // Next, assemble the complete error message to print + const wchar_t* errorFormat = L"Error: %s (%S:%d)"; + bufferSize = _scwprintf(errorFormat, message.get(), sourceFile, sourceLine); + ++bufferSize; // Extra character for terminating null + mozilla::UniquePtr<wchar_t[]> errorStr = + mozilla::MakeUnique<wchar_t[]>(bufferSize); + + _snwprintf_s(errorStr.get(), bufferSize, _TRUNCATE, errorFormat, + message.get(), sourceFile, sourceLine); + + WriteWindowsEventLogErrorBuffer(eventSourceName, errorStr.get(), 0); +} + +} // namespace mozilla + +#endif // mozilla_WindowsEventLog_h diff --git a/widget/windows/WindowsSMTCProvider.cpp b/widget/windows/WindowsSMTCProvider.cpp new file mode 100644 index 0000000000..04d833a8e7 --- /dev/null +++ b/widget/windows/WindowsSMTCProvider.cpp @@ -0,0 +1,716 @@ +/* -*- 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/. */ + +/* mingw currently doesn't support windows.media.h, so we disable + * the whole related class until this is fixed. + * @TODO: Maybe contact MinGW Team for inclusion?*/ +#ifndef __MINGW32__ + +# include "WindowsSMTCProvider.h" + +# include <windows.h> +# include <windows.media.h> +# include <wrl.h> + +# include "nsMimeTypes.h" +# include "mozilla/Assertions.h" +# include "mozilla/Logging.h" +# include "mozilla/Maybe.h" +# include "mozilla/WidgetUtils.h" +# include "mozilla/ScopeExit.h" +# include "mozilla/dom/MediaControlUtils.h" +# include "mozilla/media/MediaUtils.h" +# include "nsThreadUtils.h" + +# pragma comment(lib, "runtimeobject.lib") + +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Media; +using namespace ABI::Windows::Storage::Streams; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace mozilla; + +# ifndef RuntimeClass_Windows_Media_SystemMediaTransportControls +# define RuntimeClass_Windows_Media_SystemMediaTransportControls \ + L"Windows.Media.SystemMediaTransportControls" +# endif + +# ifndef RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference +# define RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference \ + L"Windows.Storage.Streams.RandomAccessStreamReference" +# endif + +# ifndef ISystemMediaTransportControlsInterop +EXTERN_C const IID IID_ISystemMediaTransportControlsInterop; +MIDL_INTERFACE("ddb0472d-c911-4a1f-86d9-dc3d71a95f5a") +ISystemMediaTransportControlsInterop : public IInspectable { + public: + virtual HRESULT STDMETHODCALLTYPE GetForWindow( + /* [in] */ __RPC__in HWND appWindow, + /* [in] */ __RPC__in REFIID riid, + /* [iid_is][retval][out] */ + __RPC__deref_out_opt void** mediaTransportControl) = 0; +}; +# endif /* __ISystemMediaTransportControlsInterop_INTERFACE_DEFINED__ */ + +extern mozilla::LazyLogModule gMediaControlLog; + +# undef LOG +# define LOG(msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("WindowSMTCProvider=%p, " msg, this, ##__VA_ARGS__)) + +static inline Maybe<mozilla::dom::MediaControlKey> TranslateKeycode( + SystemMediaTransportControlsButton keycode) { + switch (keycode) { + case SystemMediaTransportControlsButton_Play: + return Some(mozilla::dom::MediaControlKey::Play); + case SystemMediaTransportControlsButton_Pause: + return Some(mozilla::dom::MediaControlKey::Pause); + case SystemMediaTransportControlsButton_Next: + return Some(mozilla::dom::MediaControlKey::Nexttrack); + case SystemMediaTransportControlsButton_Previous: + return Some(mozilla::dom::MediaControlKey::Previoustrack); + case SystemMediaTransportControlsButton_Stop: + return Some(mozilla::dom::MediaControlKey::Stop); + case SystemMediaTransportControlsButton_FastForward: + return Some(mozilla::dom::MediaControlKey::Seekforward); + case SystemMediaTransportControlsButton_Rewind: + return Some(mozilla::dom::MediaControlKey::Seekbackward); + default: + return Nothing(); // Not supported Button + } +} + +static IAsyncInfo* GetIAsyncInfo(IAsyncOperation<unsigned int>* aAsyncOp) { + MOZ_ASSERT(aAsyncOp); + IAsyncInfo* asyncInfo; + HRESULT hr = aAsyncOp->QueryInterface(IID_IAsyncInfo, + reinterpret_cast<void**>(&asyncInfo)); + // The assertion always works since IAsyncOperation implements IAsyncInfo + MOZ_ASSERT(SUCCEEDED(hr)); + Unused << hr; + MOZ_ASSERT(asyncInfo); + return asyncInfo; +} + +WindowsSMTCProvider::WindowsSMTCProvider() { + LOG("Creating an empty and invisible window"); + + // In order to create a SMTC-Provider, we need a hWnd, which shall be created + // dynamically from an invisible window. This leads to the following + // boilerplate code. + WNDCLASS wnd{}; + wnd.lpszClassName = L"Firefox-MediaKeys"; + wnd.hInstance = nullptr; + wnd.lpfnWndProc = DefWindowProc; + GetLastError(); // Clear the error + RegisterClass(&wnd); + MOZ_ASSERT(!GetLastError()); + + mWindow = CreateWindowExW(0, L"Firefox-MediaKeys", L"Firefox Media Keys", 0, + CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, nullptr, + nullptr, nullptr, nullptr); + MOZ_ASSERT(mWindow); + MOZ_ASSERT(!GetLastError()); +} + +WindowsSMTCProvider::~WindowsSMTCProvider() { + // Dispose the window + MOZ_ASSERT(mWindow); + if (!DestroyWindow(mWindow)) { + LOG("Failed to destroy the hidden window. Error Code: %lu", GetLastError()); + } + if (!UnregisterClass(L"Firefox-MediaKeys", nullptr)) { + // Note that this is logged when the class wasn't even registered. + LOG("Failed to unregister the class. Error Code: %lu", GetLastError()); + } +} + +bool WindowsSMTCProvider::IsOpened() const { return mInitialized; } + +bool WindowsSMTCProvider::Open() { + LOG("Opening Source"); + MOZ_ASSERT(!mInitialized); + + if (!InitDisplayAndControls()) { + LOG("Failed to initialize the SMTC and its display"); + return false; + } + + if (!UpdateButtons()) { + LOG("Failed to initialize the buttons"); + return false; + } + + if (!RegisterEvents()) { + LOG("Failed to register SMTC key-event listener"); + return false; + } + + if (!EnableControl(true)) { + LOG("Failed to enable SMTC control"); + return false; + } + + mInitialized = true; + SetPlaybackState(mozilla::dom::MediaSessionPlaybackState::None); + return mInitialized; +} + +void WindowsSMTCProvider::Close() { + MediaControlKeySource::Close(); + // Prevent calling Set methods when init failed + if (mInitialized) { + SetPlaybackState(mozilla::dom::MediaSessionPlaybackState::None); + UnregisterEvents(); + ClearMetadata(); + // We have observed an Windows issue, if we modify `mControls` , (such as + // setting metadata, disable buttons) before disabling control, and those + // operations are not done sequentially within a same main thread task, + // then it would cause a problem where the SMTC wasn't clean up completely + // and show the executable name. + EnableControl(false); + mInitialized = false; + } +} + +void WindowsSMTCProvider::SetPlaybackState( + mozilla::dom::MediaSessionPlaybackState aState) { + MOZ_ASSERT(mInitialized); + MediaControlKeySource::SetPlaybackState(aState); + + HRESULT hr; + + // Note: we can't return the status of put_PlaybackStatus, but we can at least + // assert it. + switch (aState) { + case mozilla::dom::MediaSessionPlaybackState::Paused: + hr = mControls->put_PlaybackStatus( + ABI::Windows::Media::MediaPlaybackStatus_Paused); + break; + case mozilla::dom::MediaSessionPlaybackState::Playing: + hr = mControls->put_PlaybackStatus( + ABI::Windows::Media::MediaPlaybackStatus_Playing); + break; + case mozilla::dom::MediaSessionPlaybackState::None: + hr = mControls->put_PlaybackStatus( + ABI::Windows::Media::MediaPlaybackStatus_Stopped); + break; + // MediaPlaybackStatus still supports Closed and Changing, which we don't + // use (yet) + default: + MOZ_ASSERT_UNREACHABLE( + "Enum Inconsitency between PlaybackState and WindowsSMTCProvider"); + break; + } + + MOZ_ASSERT(SUCCEEDED(hr)); + Unused << hr; +} + +void WindowsSMTCProvider::SetMediaMetadata( + const mozilla::dom::MediaMetadataBase& aMetadata) { + MOZ_ASSERT(mInitialized); + SetMusicMetadata(aMetadata.mArtist, aMetadata.mTitle); + LoadThumbnail(aMetadata.mArtwork); +} + +void WindowsSMTCProvider::ClearMetadata() { + MOZ_ASSERT(mDisplay); + if (FAILED(mDisplay->ClearAll())) { + LOG("Failed to clear SMTC display"); + } + mImageFetchRequest.DisconnectIfExists(); + CancelPendingStoreAsyncOperation(); + mThumbnailUrl.Truncate(); + mProcessingUrl.Truncate(); + mNextImageIndex = 0; + mSupportedKeys = 0; +} + +void WindowsSMTCProvider::SetSupportedMediaKeys( + const MediaKeysArray& aSupportedKeys) { + MOZ_ASSERT(mInitialized); + + uint32_t supportedKeys = 0; + for (const mozilla::dom::MediaControlKey& key : aSupportedKeys) { + supportedKeys |= GetMediaKeyMask(key); + } + + if (supportedKeys == mSupportedKeys) { + LOG("Supported keys stay the same"); + return; + } + + LOG("Update supported keys"); + mSupportedKeys = supportedKeys; + UpdateButtons(); +} + +void WindowsSMTCProvider::UnregisterEvents() { + if (mControls && mButtonPressedToken.value != 0) { + mControls->remove_ButtonPressed(mButtonPressedToken); + } +} + +bool WindowsSMTCProvider::RegisterEvents() { + MOZ_ASSERT(mControls); + auto self = RefPtr<WindowsSMTCProvider>(this); + auto callbackbtnPressed = Callback< + ITypedEventHandler<SystemMediaTransportControls*, + SystemMediaTransportControlsButtonPressedEventArgs*>>( + [this, self](ISystemMediaTransportControls*, + ISystemMediaTransportControlsButtonPressedEventArgs* pArgs) + -> HRESULT { + MOZ_ASSERT(pArgs); + SystemMediaTransportControlsButton btn; + + if (FAILED(pArgs->get_Button(&btn))) { + LOG("SystemMediaTransportControls: ButtonPressedEvent - Could " + "not get Button."); + return S_OK; // Propagating the error probably wouldn't help. + } + + Maybe<mozilla::dom::MediaControlKey> keyCode = TranslateKeycode(btn); + if (keyCode.isSome() && IsOpened()) { + OnButtonPressed(keyCode.value()); + } + return S_OK; + }); + + if (FAILED(mControls->add_ButtonPressed(callbackbtnPressed.Get(), + &mButtonPressedToken))) { + LOG("SystemMediaTransportControls: Failed at " + "registerEvents().add_ButtonPressed()"); + return false; + } + + return true; +} + +void WindowsSMTCProvider::OnButtonPressed( + mozilla::dom::MediaControlKey aKey) const { + if (!IsKeySupported(aKey)) { + LOG("key: %s is not supported", ToMediaControlKeyStr(aKey)); + return; + } + + for (auto& listener : mListeners) { + listener->OnActionPerformed(mozilla::dom::MediaControlAction(aKey)); + } +} + +bool WindowsSMTCProvider::EnableControl(bool aEnabled) const { + MOZ_ASSERT(mControls); + return SUCCEEDED(mControls->put_IsEnabled(aEnabled)); +} + +bool WindowsSMTCProvider::UpdateButtons() const { + static const mozilla::dom::MediaControlKey kKeys[] = { + mozilla::dom::MediaControlKey::Play, mozilla::dom::MediaControlKey::Pause, + mozilla::dom::MediaControlKey::Previoustrack, + mozilla::dom::MediaControlKey::Nexttrack, + mozilla::dom::MediaControlKey::Stop}; + + bool success = true; + for (const mozilla::dom::MediaControlKey& key : kKeys) { + if (!EnableKey(key, IsKeySupported(key))) { + success = false; + LOG("Failed to set %s=%s", ToMediaControlKeyStr(key), + IsKeySupported(key) ? "true" : "false"); + } + } + + return success; +} + +bool WindowsSMTCProvider::IsKeySupported( + mozilla::dom::MediaControlKey aKey) const { + return mSupportedKeys & GetMediaKeyMask(aKey); +} + +bool WindowsSMTCProvider::EnableKey(mozilla::dom::MediaControlKey aKey, + bool aEnable) const { + MOZ_ASSERT(mControls); + switch (aKey) { + case mozilla::dom::MediaControlKey::Play: + return SUCCEEDED(mControls->put_IsPlayEnabled(aEnable)); + case mozilla::dom::MediaControlKey::Pause: + return SUCCEEDED(mControls->put_IsPauseEnabled(aEnable)); + case mozilla::dom::MediaControlKey::Previoustrack: + return SUCCEEDED(mControls->put_IsPreviousEnabled(aEnable)); + case mozilla::dom::MediaControlKey::Nexttrack: + return SUCCEEDED(mControls->put_IsNextEnabled(aEnable)); + case mozilla::dom::MediaControlKey::Stop: + return SUCCEEDED(mControls->put_IsStopEnabled(aEnable)); + default: + LOG("No button for %s", ToMediaControlKeyStr(aKey)); + return false; + } +} + +bool WindowsSMTCProvider::InitDisplayAndControls() { + // As Open() might be called multiple times, "cache" the results of the COM + // API + if (mControls && mDisplay) { + return true; + } + ComPtr<ISystemMediaTransportControlsInterop> interop; + HRESULT hr = GetActivationFactory( + HStringReference(RuntimeClass_Windows_Media_SystemMediaTransportControls) + .Get(), + interop.GetAddressOf()); + if (FAILED(hr)) { + LOG("SystemMediaTransportControls: Failed at instantiating the " + "Interop object"); + return false; + } + MOZ_ASSERT(interop); + + if (!mControls && FAILED(interop->GetForWindow( + mWindow, IID_PPV_ARGS(mControls.GetAddressOf())))) { + LOG("SystemMediaTransportControls: Failed at GetForWindow()"); + return false; + } + MOZ_ASSERT(mControls); + + if (!mDisplay && + FAILED(mControls->get_DisplayUpdater(mDisplay.GetAddressOf()))) { + LOG("SystemMediaTransportControls: Failed at get_DisplayUpdater()"); + } + + MOZ_ASSERT(mDisplay); + return true; +} + +bool WindowsSMTCProvider::SetMusicMetadata(const nsString& aArtist, + const nsString& aTitle) { + MOZ_ASSERT(mDisplay); + ComPtr<IMusicDisplayProperties> musicProps; + + HRESULT hr = mDisplay->put_Type(MediaPlaybackType::MediaPlaybackType_Music); + MOZ_ASSERT(SUCCEEDED(hr)); + Unused << hr; + hr = mDisplay->get_MusicProperties(musicProps.GetAddressOf()); + if (FAILED(hr)) { + LOG("Failed to get music properties"); + return false; + } + + hr = musicProps->put_Artist(HStringReference(aArtist.get()).Get()); + if (FAILED(hr)) { + LOG("Failed to set the music's artist"); + return false; + } + + hr = musicProps->put_Title(HStringReference(aTitle.get()).Get()); + if (FAILED(hr)) { + LOG("Failed to set the music's title"); + return false; + } + + hr = mDisplay->Update(); + if (FAILED(hr)) { + LOG("Failed to refresh the display"); + return false; + } + + return true; +} + +void WindowsSMTCProvider::LoadThumbnail( + const nsTArray<mozilla::dom::MediaImage>& aArtwork) { + MOZ_ASSERT(NS_IsMainThread()); + + // TODO: Sort the images by the preferred size or format. + mArtwork = aArtwork; + mNextImageIndex = 0; + + // Abort the loading if + // 1) thumbnail is being updated, and one in processing is in the artwork + // 2) thumbnail is not being updated, and one in use is in the artwork + if (!mProcessingUrl.IsEmpty()) { + LOG("Load thumbnail while image: %s is being processed", + NS_ConvertUTF16toUTF8(mProcessingUrl).get()); + if (mozilla::dom::IsImageIn(mArtwork, mProcessingUrl)) { + LOG("No need to load thumbnail. The one being processed is in the " + "artwork"); + return; + } + } else if (!mThumbnailUrl.IsEmpty()) { + if (mozilla::dom::IsImageIn(mArtwork, mThumbnailUrl)) { + LOG("No need to load thumbnail. The one in use is in the artwork"); + return; + } + } + + // If there is a pending image store operation, that image must be different + // from the new image will be loaded below, so the pending one should be + // cancelled. + CancelPendingStoreAsyncOperation(); + // Remove the current thumbnail on the interface + ClearThumbnail(); + // Then load the new thumbnail asynchronously + LoadImageAtIndex(mNextImageIndex++); +} + +void WindowsSMTCProvider::LoadImageAtIndex(const size_t aIndex) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aIndex >= mArtwork.Length()) { + LOG("Stop loading thumbnail. No more available images"); + mImageFetchRequest.DisconnectIfExists(); + mProcessingUrl.Truncate(); + return; + } + + const mozilla::dom::MediaImage& image = mArtwork[aIndex]; + + // TODO: No need to fetch the default image and do image processing since the + // the default image is local file and it's trustworthy. For the default + // image, we can use `CreateFromFile` to create the IRandomAccessStream. We + // should probably cache it since it could be used very often (Bug 1643102) + + if (!mozilla::dom::IsValidImageUrl(image.mSrc)) { + LOG("Skip the image with invalid URL. Try next image"); + mImageFetchRequest.DisconnectIfExists(); + LoadImageAtIndex(mNextImageIndex++); + return; + } + + mImageFetchRequest.DisconnectIfExists(); + mProcessingUrl = image.mSrc; + + mImageFetcher = mozilla::MakeUnique<mozilla::dom::FetchImageHelper>(image); + RefPtr<WindowsSMTCProvider> self = this; + mImageFetcher->FetchImage() + ->Then( + AbstractThread::MainThread(), __func__, + [this, self](const nsCOMPtr<imgIContainer>& aImage) { + LOG("The image is fetched successfully"); + mImageFetchRequest.Complete(); + + // Although IMAGE_JPEG or IMAGE_BMP are valid types as well, but a + // png image with transparent background will be converted into a + // jpeg/bmp file with a colored background. IMAGE_PNG format seems + // to be the best choice for now. + uint32_t size = 0; + char* src = nullptr; + // Only used to hold the image data + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = mozilla::dom::GetEncodedImageBuffer( + aImage, nsLiteralCString(IMAGE_PNG), + getter_AddRefs(inputStream), &size, &src); + if (NS_FAILED(rv) || !inputStream || size == 0 || !src) { + LOG("Failed to get the image buffer info. Try next image"); + LoadImageAtIndex(mNextImageIndex++); + return; + } + + LoadImage(src, size); + }, + [this, self](bool) { + LOG("Failed to fetch image. Try next image"); + mImageFetchRequest.Complete(); + LoadImageAtIndex(mNextImageIndex++); + }) + ->Track(mImageFetchRequest); +} + +void WindowsSMTCProvider::LoadImage(const char* aImageData, + uint32_t aDataSize) { + MOZ_ASSERT(NS_IsMainThread()); + + // 1. Use mImageDataWriter to write the binary data of image into mImageStream + // 2. Refer the image by mImageStreamReference and then set it to the SMTC + // In case of the race condition between they are being destroyed and the + // async operation for image loading, mImageDataWriter, mImageStream, and + // mImageStreamReference are member variables + + HRESULT hr = ActivateInstance( + HStringReference( + RuntimeClass_Windows_Storage_Streams_InMemoryRandomAccessStream) + .Get(), + mImageStream.GetAddressOf()); + if (FAILED(hr)) { + LOG("Failed to make mImageStream refer to an instance of " + "InMemoryRandomAccessStream"); + return; + } + + ComPtr<IOutputStream> outputStream; + hr = mImageStream.As(&outputStream); + if (FAILED(hr)) { + LOG("Failed when query IOutputStream interface from mImageStream"); + return; + } + + ComPtr<IDataWriterFactory> dataWriterFactory; + hr = GetActivationFactory( + HStringReference(RuntimeClass_Windows_Storage_Streams_DataWriter).Get(), + dataWriterFactory.GetAddressOf()); + if (FAILED(hr)) { + LOG("Failed to get an activation factory for IDataWriterFactory"); + return; + } + + hr = dataWriterFactory->CreateDataWriter(outputStream.Get(), + mImageDataWriter.GetAddressOf()); + if (FAILED(hr)) { + LOG("Failed to create mImageDataWriter that writes data to mImageStream"); + return; + } + + hr = mImageDataWriter->WriteBytes( + aDataSize, reinterpret_cast<BYTE*>(const_cast<char*>(aImageData))); + if (FAILED(hr)) { + LOG("Failed to write data to mImageStream"); + return; + } + + hr = mImageDataWriter->StoreAsync(&mStoreAsyncOperation); + if (FAILED(hr)) { + LOG("Failed to create a DataWriterStoreOperation for mStoreAsyncOperation"); + return; + } + + // Upon the image is stored in mImageStream, set the image to the SMTC + // interface + auto onStoreCompleted = Callback< + IAsyncOperationCompletedHandler<unsigned int>>( + [this, self = RefPtr<WindowsSMTCProvider>(this), + aImageUrl = nsString(mProcessingUrl)]( + IAsyncOperation<unsigned int>* aAsyncOp, AsyncStatus aStatus) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aStatus != AsyncStatus::Completed) { + LOG("Asynchronous operation is not completed"); + return E_ABORT; + } + + HRESULT hr = S_OK; + IAsyncInfo* asyncInfo = GetIAsyncInfo(aAsyncOp); + asyncInfo->get_ErrorCode(&hr); + if (FAILED(hr)) { + LOG("Failed to get termination status of the asynchronous operation"); + return hr; + } + + if (!UpdateThumbnail(aImageUrl)) { + LOG("Failed to update thumbnail"); + } + + // If an error occurs above: + // - If aImageUrl is not mProcessingUrl. It's fine. + // - If aImageUrl is mProcessingUrl, then mProcessingUrl won't be reset. + // Therefore the thumbnail will remain empty until a new image whose + // url is different from mProcessingUrl is loaded. + + return S_OK; + }); + + hr = mStoreAsyncOperation->put_Completed(onStoreCompleted.Get()); + if (FAILED(hr)) { + LOG("Failed to set callback on completeing the asynchronous operation"); + } +} + +bool WindowsSMTCProvider::SetThumbnail(const nsAString& aUrl) { + MOZ_ASSERT(mDisplay); + MOZ_ASSERT(mImageStream); + MOZ_ASSERT(!aUrl.IsEmpty()); + + ComPtr<IRandomAccessStreamReferenceStatics> streamRefFactory; + + HRESULT hr = GetActivationFactory( + HStringReference( + RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference) + .Get(), + streamRefFactory.GetAddressOf()); + auto cleanup = + MakeScopeExit([this, self = RefPtr<WindowsSMTCProvider>(this)] { + LOG("Clean mThumbnailUrl"); + mThumbnailUrl.Truncate(); + }); + + if (FAILED(hr)) { + LOG("Failed to get an activation factory for " + "IRandomAccessStreamReferenceStatics type"); + return false; + } + + hr = streamRefFactory->CreateFromStream(mImageStream.Get(), + mImageStreamReference.GetAddressOf()); + if (FAILED(hr)) { + LOG("Failed to create mImageStreamReference from mImageStream"); + return false; + } + + hr = mDisplay->put_Thumbnail(mImageStreamReference.Get()); + if (FAILED(hr)) { + LOG("Failed to update thumbnail"); + return false; + } + + hr = mDisplay->Update(); + if (FAILED(hr)) { + LOG("Failed to refresh display"); + return false; + } + + // No need to clean mThumbnailUrl since thumbnail is set successfully + cleanup.release(); + mThumbnailUrl = aUrl; + + return true; +} + +void WindowsSMTCProvider::ClearThumbnail() { + MOZ_ASSERT(mDisplay); + HRESULT hr = mDisplay->put_Thumbnail(nullptr); + MOZ_ASSERT(SUCCEEDED(hr)); + hr = mDisplay->Update(); + MOZ_ASSERT(SUCCEEDED(hr)); + Unused << hr; + mThumbnailUrl.Truncate(); +} + +bool WindowsSMTCProvider::UpdateThumbnail(const nsAString& aUrl) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!IsOpened()) { + LOG("Abort the thumbnail update: SMTC is closed"); + return false; + } + + if (aUrl != mProcessingUrl) { + LOG("Abort the thumbnail update: The image from %s is out of date", + NS_ConvertUTF16toUTF8(aUrl).get()); + return false; + } + + mProcessingUrl.Truncate(); + + if (!SetThumbnail(aUrl)) { + LOG("Failed to update thumbnail"); + return false; + } + + MOZ_ASSERT(mThumbnailUrl == aUrl); + LOG("The thumbnail is updated to the image from: %s", + NS_ConvertUTF16toUTF8(mThumbnailUrl).get()); + return true; +} + +void WindowsSMTCProvider::CancelPendingStoreAsyncOperation() const { + if (mStoreAsyncOperation) { + IAsyncInfo* asyncInfo = GetIAsyncInfo(mStoreAsyncOperation.Get()); + asyncInfo->Cancel(); + } +} + +#endif // __MINGW32__ diff --git a/widget/windows/WindowsSMTCProvider.h b/widget/windows/WindowsSMTCProvider.h new file mode 100644 index 0000000000..3926618d1f --- /dev/null +++ b/widget/windows/WindowsSMTCProvider.h @@ -0,0 +1,128 @@ +/* -*- 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_WINDOWSSTMCPROVIDER_H_ +#define WIDGET_WINDOWS_WINDOWSSTMCPROVIDER_H_ + +#ifndef __MINGW32__ + +# include <functional> +# include <Windows.Media.h> +# include <wrl.h> + +# include "mozilla/dom/FetchImageHelper.h" +# include "mozilla/dom/MediaController.h" +# include "mozilla/dom/MediaControlKeySource.h" +# include "mozilla/UniquePtr.h" + +using ISMTC = ABI::Windows::Media::ISystemMediaTransportControls; +using SMTCProperty = ABI::Windows::Media::SystemMediaTransportControlsProperty; +using ISMTCDisplayUpdater = + ABI::Windows::Media::ISystemMediaTransportControlsDisplayUpdater; + +using ABI::Windows::Foundation::IAsyncOperation; +using ABI::Windows::Storage::Streams::IDataWriter; +using ABI::Windows::Storage::Streams::IRandomAccessStream; +using ABI::Windows::Storage::Streams::IRandomAccessStreamReference; +using Microsoft::WRL::ComPtr; + +class WindowsSMTCProvider final : public mozilla::dom::MediaControlKeySource { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WindowsSMTCProvider, override) + + public: + WindowsSMTCProvider(); + + bool IsOpened() const override; + bool Open() override; + void Close() override; + + void SetPlaybackState( + mozilla::dom::MediaSessionPlaybackState aState) override; + + void SetMediaMetadata( + const mozilla::dom::MediaMetadataBase& aMetadata) override; + + void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override; + + private: + ~WindowsSMTCProvider(); + void UnregisterEvents(); + bool RegisterEvents(); + + void OnButtonPressed(mozilla::dom::MediaControlKey aKey) const; + // Enable the SMTC interface + bool EnableControl(bool aEnabled) const; + // Sets the play, pause, next, previous buttons on the SMTC interface by + // mSupportedKeys + bool UpdateButtons() const; + bool IsKeySupported(mozilla::dom::MediaControlKey aKey) const; + bool EnableKey(mozilla::dom::MediaControlKey aKey, bool aEnable) const; + + bool InitDisplayAndControls(); + + // Sets the Metadata for the currently playing media and sets the playback + // type to "MUSIC" + bool SetMusicMetadata(const nsString& aArtist, const nsString& aTitle); + + // Sets one of the artwork to the SMTC interface asynchronously + void LoadThumbnail(const nsTArray<mozilla::dom::MediaImage>& aArtwork); + // Stores the image at index aIndex of the mArtwork to the Thumbnail + // asynchronously + void LoadImageAtIndex(const size_t aIndex); + // Stores the raw binary data of an image to mImageStream and set it to the + // Thumbnail asynchronously + void LoadImage(const char* aImageData, uint32_t aDataSize); + // Sets the Thumbnail to the image stored in mImageStream + bool SetThumbnail(const nsAString& aUrl); + void ClearThumbnail(); + + bool UpdateThumbnail(const nsAString& aUrl); + void CancelPendingStoreAsyncOperation() const; + + void ClearMetadata(); + + bool mInitialized = false; + + // A bit table indicating what keys are enabled + uint32_t mSupportedKeys = 0; + + ComPtr<ISMTC> mControls; + ComPtr<ISMTCDisplayUpdater> mDisplay; + + // Use mImageDataWriter to write the binary data of image into mImageStream + // and refer the image by mImageStreamReference and then set it to the SMTC + // interface + ComPtr<IDataWriter> mImageDataWriter; + ComPtr<IRandomAccessStream> mImageStream; + ComPtr<IRandomAccessStreamReference> mImageStreamReference; + ComPtr<IAsyncOperation<unsigned int>> mStoreAsyncOperation; + + // mThumbnailUrl is the url of the current Thumbnail + // mProcessingUrl is the url that is being processed. The process starts from + // fetching an image from the url and then storing the fetched image to the + // mImageStream. If mProcessingUrl is not empty, it means there is an image is + // in processing + // mThumbnailUrl and mProcessingUrl won't be set at the same time and they can + // only be touched on main thread + nsString mThumbnailUrl; + nsString mProcessingUrl; + + // mArtwork can only be used in main thread in case of data racing + CopyableTArray<mozilla::dom::MediaImage> mArtwork; + size_t mNextImageIndex; + + mozilla::UniquePtr<mozilla::dom::FetchImageHelper> mImageFetcher; + mozilla::MozPromiseRequestHolder<mozilla::dom::ImagePromise> + mImageFetchRequest; + + HWND mWindow; // handle to the invisible window + + // EventRegistrationTokens are used to have a handle on a callback (to remove + // it again) + EventRegistrationToken mButtonPressedToken; +}; + +#endif // __MINGW32__ +#endif // WIDGET_WINDOWS_WINDOWSSTMCPROVIDER_H_ diff --git a/widget/windows/WindowsUIUtils.cpp b/widget/windows/WindowsUIUtils.cpp new file mode 100644 index 0000000000..3d5cff7e23 --- /dev/null +++ b/widget/windows/WindowsUIUtils.cpp @@ -0,0 +1,809 @@ +/* -*- 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 <windows.h> +#include <wrl.h> + +#include "nsServiceManagerUtils.h" + +#include "WindowsUIUtils.h" + +#include "nsIObserverService.h" +#include "nsIAppShellService.h" +#include "nsAppShellCID.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/WidgetUtils.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/media/MediaUtils.h" +#include "nsString.h" +#include "nsGlobalWindowOuter.h" +#include "nsIWidget.h" +#include "nsIWindowMediator.h" +#include "nsPIDOMWindow.h" +#include "nsWindowGfx.h" +#include "Units.h" + +/* mingw currently doesn't support windows.ui.viewmanagement.h, so we disable it + * until it's fixed. */ + +// See +// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/winrt/windows.ui.viewmanagement.h +// for the source of some of these definitions for older SDKs. +#ifndef __MINGW32__ + +# include <inspectable.h> +# include <roapi.h> +# include <windows.ui.viewmanagement.h> + +# pragma comment(lib, "runtimeobject.lib") + +using namespace ABI::Windows::UI; +using namespace ABI::Windows::UI::ViewManagement; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::ApplicationModel::DataTransfer; + +# ifndef RuntimeClass_Windows_UI_ViewManagement_UIViewSettings +# define RuntimeClass_Windows_UI_ViewManagement_UIViewSettings \ + L"Windows.UI.ViewManagement.UIViewSettings" +# endif + +# ifndef IUIViewSettingsInterop + +using IUIViewSettingsInterop = interface IUIViewSettingsInterop; + +MIDL_INTERFACE("3694dbf9-8f68-44be-8ff5-195c98ede8a6") +IUIViewSettingsInterop : public IInspectable { + public: + virtual HRESULT STDMETHODCALLTYPE GetForWindow(HWND hwnd, REFIID riid, + void** ppv) = 0; +}; +# endif + +# ifndef __IDataTransferManagerInterop_INTERFACE_DEFINED__ +# define __IDataTransferManagerInterop_INTERFACE_DEFINED__ + +using IDataTransferManagerInterop = interface IDataTransferManagerInterop; + +MIDL_INTERFACE("3A3DCD6C-3EAB-43DC-BCDE-45671CE800C8") +IDataTransferManagerInterop : public IUnknown { + public: + virtual HRESULT STDMETHODCALLTYPE GetForWindow( + HWND appWindow, REFIID riid, void** dataTransferManager) = 0; + virtual HRESULT STDMETHODCALLTYPE ShowShareUIForWindow(HWND appWindow) = 0; +}; + +# endif + +# if !defined( \ + ____x_ABI_CWindows_CApplicationModel_CDataTransfer_CIDataPackage4_INTERFACE_DEFINED__) +# define ____x_ABI_CWindows_CApplicationModel_CDataTransfer_CIDataPackage4_INTERFACE_DEFINED__ + +MIDL_INTERFACE("13a24ec8-9382-536f-852a-3045e1b29a3b") +IDataPackage4 : public IInspectable { + public: + virtual HRESULT STDMETHODCALLTYPE add_ShareCanceled( + __FITypedEventHandler_2_Windows__CApplicationModel__CDataTransfer__CDataPackage_IInspectable * + handler, + EventRegistrationToken * token) = 0; + virtual HRESULT STDMETHODCALLTYPE remove_ShareCanceled( + EventRegistrationToken token) = 0; +}; + +# endif + +# ifndef RuntimeClass_Windows_UI_ViewManagement_UISettings +# define RuntimeClass_Windows_UI_ViewManagement_UISettings \ + L"Windows.UI.ViewManagement.UISettings" +# endif +# if WINDOWS_FOUNDATION_UNIVERSALAPICONTRACT_VERSION < 0x80000 +namespace ABI { +namespace Windows { +namespace UI { +namespace ViewManagement { + +class UISettings; +class UISettingsAutoHideScrollBarsChangedEventArgs; +interface IUISettingsAutoHideScrollBarsChangedEventArgs; +MIDL_INTERFACE("87afd4b2-9146-5f02-8f6b-06d454174c0f") +IUISettingsAutoHideScrollBarsChangedEventArgs : public IInspectable{}; + +} // namespace ViewManagement +} // namespace UI +} // namespace Windows +} // namespace ABI + +namespace ABI { +namespace Windows { +namespace Foundation { + +template <> +struct __declspec(uuid("808aef30-2660-51b0-9c11-f75dd42006b4")) + ITypedEventHandler<ABI::Windows::UI::ViewManagement::UISettings*, + ABI::Windows::UI::ViewManagement:: + UISettingsAutoHideScrollBarsChangedEventArgs*> + : ITypedEventHandler_impl< + ABI::Windows::Foundation::Internal::AggregateType< + ABI::Windows::UI::ViewManagement::UISettings*, + ABI::Windows::UI::ViewManagement::IUISettings*>, + ABI::Windows::Foundation::Internal::AggregateType< + ABI::Windows::UI::ViewManagement:: + UISettingsAutoHideScrollBarsChangedEventArgs*, + ABI::Windows::UI::ViewManagement:: + IUISettingsAutoHideScrollBarsChangedEventArgs*>> { + static const wchar_t* z_get_rc_name_impl() { + return L"Windows.Foundation.TypedEventHandler`2<Windows.UI.ViewManagement." + L"UISettings, " + L"Windows.UI.ViewManagement." + L"UISettingsAutoHideScrollBarsChangedEventArgs>"; + } +}; +// Define a typedef for the parameterized interface specialization's mangled +// name. This allows code which uses the mangled name for the parameterized +// interface to access the correct parameterized interface specialization. +typedef ITypedEventHandler<ABI::Windows::UI::ViewManagement::UISettings*, + ABI::Windows::UI::ViewManagement:: + UISettingsAutoHideScrollBarsChangedEventArgs*> + __FITypedEventHandler_2_Windows__CUI__CViewManagement__CUISettings_Windows__CUI__CViewManagement__CUISettingsAutoHideScrollBarsChangedEventArgs_t; +# define __FITypedEventHandler_2_Windows__CUI__CViewManagement__CUISettings_Windows__CUI__CViewManagement__CUISettingsAutoHideScrollBarsChangedEventArgs \ + ABI::Windows::Foundation:: \ + __FITypedEventHandler_2_Windows__CUI__CViewManagement__CUISettings_Windows__CUI__CViewManagement__CUISettingsAutoHideScrollBarsChangedEventArgs_t + +} // namespace Foundation +} // namespace Windows +} // namespace ABI + +namespace ABI { +namespace Windows { +namespace UI { +namespace ViewManagement { +class UISettings; +class UISettingsAutoHideScrollBarsChangedEventArgs; +interface IUISettings5; +MIDL_INTERFACE("5349d588-0cb5-5f05-bd34-706b3231f0bd") +IUISettings5 : public IInspectable { + public: + virtual HRESULT STDMETHODCALLTYPE get_AutoHideScrollBars(boolean * value) = 0; + virtual HRESULT STDMETHODCALLTYPE add_AutoHideScrollBarsChanged( + __FITypedEventHandler_2_Windows__CUI__CViewManagement__CUISettings_Windows__CUI__CViewManagement__CUISettingsAutoHideScrollBarsChangedEventArgs * + handler, + EventRegistrationToken * token) = 0; + virtual HRESULT STDMETHODCALLTYPE remove_AutoHideScrollBarsChanged( + EventRegistrationToken token) = 0; +}; +} // namespace ViewManagement +} // namespace UI +} // namespace Windows +} // namespace ABI +# endif +#endif + +using namespace mozilla; + +enum class TabletModeState : uint8_t { Unknown, Off, On }; +static TabletModeState sInTabletModeState; + +WindowsUIUtils::WindowsUIUtils() = default; +WindowsUIUtils::~WindowsUIUtils() = default; + +NS_IMPL_ISUPPORTS(WindowsUIUtils, nsIWindowsUIUtils) + +NS_IMETHODIMP +WindowsUIUtils::GetSystemSmallIconSize(int32_t* aSize) { + NS_ENSURE_ARG(aSize); + + mozilla::LayoutDeviceIntSize size = + nsWindowGfx::GetIconMetrics(nsWindowGfx::kSmallIcon); + *aSize = std::max(size.width, size.height); + return NS_OK; +} + +NS_IMETHODIMP +WindowsUIUtils::GetSystemLargeIconSize(int32_t* aSize) { + NS_ENSURE_ARG(aSize); + + mozilla::LayoutDeviceIntSize size = + nsWindowGfx::GetIconMetrics(nsWindowGfx::kRegularIcon); + *aSize = std::max(size.width, size.height); + return NS_OK; +} + +NS_IMETHODIMP +WindowsUIUtils::SetWindowIcon(mozIDOMWindowProxy* aWindow, + imgIContainer* aSmallIcon, + imgIContainer* aBigIcon) { + NS_ENSURE_ARG(aWindow); + + nsCOMPtr<nsIWidget> widget = + nsGlobalWindowOuter::Cast(aWindow)->GetMainWidget(); + nsWindow* window = static_cast<nsWindow*>(widget.get()); + + nsresult rv; + + if (aSmallIcon) { + HICON hIcon = nullptr; + rv = nsWindowGfx::CreateIcon( + aSmallIcon, false, mozilla::LayoutDeviceIntPoint(), + nsWindowGfx::GetIconMetrics(nsWindowGfx::kSmallIcon), &hIcon); + NS_ENSURE_SUCCESS(rv, rv); + + window->SetSmallIcon(hIcon); + } + + if (aBigIcon) { + HICON hIcon = nullptr; + rv = nsWindowGfx::CreateIcon( + aBigIcon, false, mozilla::LayoutDeviceIntPoint(), + nsWindowGfx::GetIconMetrics(nsWindowGfx::kRegularIcon), &hIcon); + NS_ENSURE_SUCCESS(rv, rv); + + window->SetBigIcon(hIcon); + } + + return NS_OK; +} + +NS_IMETHODIMP +WindowsUIUtils::SetWindowIconFromExe(mozIDOMWindowProxy* aWindow, + const nsAString& aExe, uint16_t aIndex) { + NS_ENSURE_ARG(aWindow); + + nsCOMPtr<nsIWidget> widget = + nsGlobalWindowOuter::Cast(aWindow)->GetMainWidget(); + nsWindow* window = static_cast<nsWindow*>(widget.get()); + + HICON icon = ::LoadIconW(::GetModuleHandleW(PromiseFlatString(aExe).get()), + MAKEINTRESOURCEW(aIndex)); + window->SetBigIcon(icon); + window->SetSmallIcon(icon); + + return NS_OK; +} + +NS_IMETHODIMP +WindowsUIUtils::SetWindowIconNoData(mozIDOMWindowProxy* aWindow) { + NS_ENSURE_ARG(aWindow); + + nsCOMPtr<nsIWidget> widget = + nsGlobalWindowOuter::Cast(aWindow)->GetMainWidget(); + nsWindow* window = static_cast<nsWindow*>(widget.get()); + + window->SetSmallIconNoData(); + window->SetBigIconNoData(); + + return NS_OK; +} + +bool WindowsUIUtils::GetInTabletMode() { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + if (sInTabletModeState == TabletModeState::Unknown) { + UpdateInTabletMode(); + } + return sInTabletModeState == TabletModeState::On; +} + +NS_IMETHODIMP +WindowsUIUtils::GetInTabletMode(bool* aResult) { + *aResult = GetInTabletMode(); + return NS_OK; +} + +static IInspectable* GetUISettings() { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); +#ifndef __MINGW32__ + // We need to keep this alive for ~ever so that change callbacks work as + // expected, sigh. + static StaticRefPtr<IInspectable> sUiSettingsAsInspectable; + + if (!sUiSettingsAsInspectable) { + ComPtr<IInspectable> uiSettingsAsInspectable; + ::RoActivateInstance( + HStringReference(RuntimeClass_Windows_UI_ViewManagement_UISettings) + .Get(), + &uiSettingsAsInspectable); + if (NS_WARN_IF(!uiSettingsAsInspectable)) { + return nullptr; + } + + ComPtr<IUISettings5> uiSettings5; + if (SUCCEEDED(uiSettingsAsInspectable.As(&uiSettings5))) { + EventRegistrationToken unusedToken; + auto callback = Callback<ITypedEventHandler< + UISettings*, UISettingsAutoHideScrollBarsChangedEventArgs*>>( + [](auto...) { + // Scrollbar sizes change layout. + LookAndFeel::NotifyChangedAllWindows( + widget::ThemeChangeKind::StyleAndLayout); + return S_OK; + }); + (void)NS_WARN_IF(FAILED(uiSettings5->add_AutoHideScrollBarsChanged( + callback.Get(), &unusedToken))); + } + + ComPtr<IUISettings2> uiSettings2; + if (SUCCEEDED(uiSettingsAsInspectable.As(&uiSettings2))) { + EventRegistrationToken unusedToken; + auto callback = + Callback<ITypedEventHandler<UISettings*, IInspectable*>>([](auto...) { + // Text scale factor changes style and layout. + LookAndFeel::NotifyChangedAllWindows( + widget::ThemeChangeKind::StyleAndLayout); + return S_OK; + }); + (void)NS_WARN_IF(FAILED(uiSettings2->add_TextScaleFactorChanged( + callback.Get(), &unusedToken))); + } + + ComPtr<IUISettings3> uiSettings3; + if (SUCCEEDED(uiSettingsAsInspectable.As(&uiSettings3))) { + EventRegistrationToken unusedToken; + auto callback = + Callback<ITypedEventHandler<UISettings*, IInspectable*>>([](auto...) { + // System color changes change style only. + LookAndFeel::NotifyChangedAllWindows( + widget::ThemeChangeKind::Style); + return S_OK; + }); + (void)NS_WARN_IF(FAILED( + uiSettings3->add_ColorValuesChanged(callback.Get(), &unusedToken))); + } + + ComPtr<IUISettings4> uiSettings4; + if (SUCCEEDED(uiSettingsAsInspectable.As(&uiSettings4))) { + EventRegistrationToken unusedToken; + auto callback = + Callback<ITypedEventHandler<UISettings*, IInspectable*>>([](auto...) { + // Transparent effects changes change media queries only. + LookAndFeel::NotifyChangedAllWindows( + widget::ThemeChangeKind::MediaQueriesOnly); + return S_OK; + }); + (void)NS_WARN_IF(FAILED(uiSettings4->add_AdvancedEffectsEnabledChanged( + callback.Get(), &unusedToken))); + } + + sUiSettingsAsInspectable = dont_AddRef(uiSettingsAsInspectable.Detach()); + ClearOnShutdown(&sUiSettingsAsInspectable); + } + + return sUiSettingsAsInspectable.get(); +#else + return nullptr; +#endif +} + +Maybe<nscolor> WindowsUIUtils::GetAccentColor(int aTone) { + MOZ_ASSERT(aTone >= -3); + MOZ_ASSERT(aTone <= 3); +#ifndef __MINGW32__ + ComPtr<IInspectable> settings = GetUISettings(); + if (NS_WARN_IF(!settings)) { + return Nothing(); + } + ComPtr<IUISettings3> uiSettings3; + if (NS_WARN_IF(FAILED(settings.As(&uiSettings3)))) { + return Nothing(); + } + Color color; + auto colorType = UIColorType(int(UIColorType_Accent) + aTone); + if (NS_WARN_IF(FAILED(uiSettings3->GetColorValue(colorType, &color)))) { + return Nothing(); + } + return Some(NS_RGBA(color.R, color.G, color.B, color.A)); +#else + return Nothing(); +#endif +} + +Maybe<nscolor> WindowsUIUtils::GetSystemColor(ColorScheme aScheme, + int aSysColor) { +#ifndef __MINGW32__ + if (!StaticPrefs::widget_windows_uwp_system_colors_enabled()) { + return Nothing(); + } + + // https://docs.microsoft.com/en-us/windows/apps/design/style/color + // Is a useful resource to see which values have decent contrast. + if (StaticPrefs::widget_windows_uwp_system_colors_highlight_accent()) { + if (aSysColor == COLOR_HIGHLIGHT) { + int tone = aScheme == ColorScheme::Light ? 0 : -1; + if (auto c = GetAccentColor(tone)) { + return c; + } + } + if (aSysColor == COLOR_HIGHLIGHTTEXT && GetAccentColor()) { + return Some(NS_RGBA(255, 255, 255, 255)); + } + } + + if (aScheme == ColorScheme::Dark) { + // There are no explicitly dark colors in UWP, other than the highlight + // colors above. + return Nothing(); + } + + auto knownType = [&]() -> Maybe<UIElementType> { +# define MAP(_win32, _uwp) \ + case COLOR_##_win32: \ + return Some(UIElementType_##_uwp) + switch (aSysColor) { + MAP(HIGHLIGHT, Highlight); + MAP(HIGHLIGHTTEXT, HighlightText); + MAP(ACTIVECAPTION, ActiveCaption); + MAP(BTNFACE, ButtonFace); + MAP(BTNTEXT, ButtonText); + MAP(CAPTIONTEXT, CaptionText); + MAP(GRAYTEXT, GrayText); + MAP(HOTLIGHT, Hotlight); + MAP(INACTIVECAPTION, InactiveCaption); + MAP(INACTIVECAPTIONTEXT, InactiveCaptionText); + MAP(WINDOW, Window); + MAP(WINDOWTEXT, WindowText); + default: + return Nothing(); + } +# undef MAP + }(); + if (!knownType) { + return Nothing(); + } + ComPtr<IInspectable> settings = GetUISettings(); + if (NS_WARN_IF(!settings)) { + return Nothing(); + } + ComPtr<IUISettings> uiSettings; + if (NS_WARN_IF(FAILED(settings.As(&uiSettings)))) { + return Nothing(); + } + Color color; + if (NS_WARN_IF(FAILED(uiSettings->UIElementColor(*knownType, &color)))) { + return Nothing(); + } + return Some(NS_RGBA(color.R, color.G, color.B, color.A)); +#else + return Nothing(); +#endif +} +bool WindowsUIUtils::ComputeOverlayScrollbars() { +#ifndef __MINGW32__ + if (!IsWin11OrLater()) { + // While in theory Windows 10 supports overlay scrollbar settings, it's off + // by default and it's untested whether our Win10 scrollbar drawing code + // deals with it properly. + return false; + } + if (!StaticPrefs::widget_windows_overlay_scrollbars_enabled()) { + return false; + } + ComPtr<IInspectable> settings = GetUISettings(); + if (NS_WARN_IF(!settings)) { + return false; + } + ComPtr<IUISettings5> uiSettings5; + if (NS_WARN_IF(FAILED(settings.As(&uiSettings5)))) { + return false; + } + boolean autoHide = false; + if (NS_WARN_IF(FAILED(uiSettings5->get_AutoHideScrollBars(&autoHide)))) { + return false; + } + return autoHide; +#else + return false; +#endif +} + +double WindowsUIUtils::ComputeTextScaleFactor() { +#ifndef __MINGW32__ + ComPtr<IInspectable> settings = GetUISettings(); + if (NS_WARN_IF(!settings)) { + return 1.0; + } + ComPtr<IUISettings2> uiSettings2; + if (NS_WARN_IF(FAILED(settings.As(&uiSettings2)))) { + return false; + } + double scaleFactor = 1.0; + if (NS_WARN_IF(FAILED(uiSettings2->get_TextScaleFactor(&scaleFactor)))) { + return 1.0; + } + return scaleFactor; +#else + return 1.0; +#endif +} + +bool WindowsUIUtils::ComputeTransparencyEffects() { + constexpr bool kDefault = true; +#ifndef __MINGW32__ + ComPtr<IInspectable> settings = GetUISettings(); + if (NS_WARN_IF(!settings)) { + return kDefault; + } + ComPtr<IUISettings4> uiSettings4; + if (NS_WARN_IF(FAILED(settings.As(&uiSettings4)))) { + return kDefault; + } + boolean transparencyEffects = kDefault; + if (NS_WARN_IF(FAILED( + uiSettings4->get_AdvancedEffectsEnabled(&transparencyEffects)))) { + return kDefault; + } + return transparencyEffects; +#else + return kDefault; +#endif +} + +void WindowsUIUtils::UpdateInTabletMode() { +#ifndef __MINGW32__ + nsresult rv; + nsCOMPtr<nsIWindowMediator> winMediator( + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + return; + } + + nsCOMPtr<nsIWidget> widget; + nsCOMPtr<mozIDOMWindowProxy> navWin; + + rv = winMediator->GetMostRecentWindow(u"navigator:browser", + getter_AddRefs(navWin)); + if (NS_FAILED(rv) || !navWin) { + // Fall back to the hidden window + nsCOMPtr<nsIAppShellService> appShell( + do_GetService(NS_APPSHELLSERVICE_CONTRACTID)); + + rv = appShell->GetHiddenDOMWindow(getter_AddRefs(navWin)); + if (NS_FAILED(rv) || !navWin) { + return; + } + } + + nsPIDOMWindowOuter* win = nsPIDOMWindowOuter::From(navWin); + widget = widget::WidgetUtils::DOMWindowToWidget(win); + + if (!widget) { + return; + } + + HWND winPtr = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW); + ComPtr<IUIViewSettingsInterop> uiViewSettingsInterop; + + HRESULT hr = GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_ViewManagement_UIViewSettings) + .Get(), + &uiViewSettingsInterop); + if (FAILED(hr)) { + return; + } + ComPtr<IUIViewSettings> uiViewSettings; + hr = uiViewSettingsInterop->GetForWindow(winPtr, + IID_PPV_ARGS(&uiViewSettings)); + if (FAILED(hr)) { + return; + } + UserInteractionMode mode; + hr = uiViewSettings->get_UserInteractionMode(&mode); + if (FAILED(hr)) { + return; + } + + TabletModeState oldTabletModeState = sInTabletModeState; + sInTabletModeState = mode == UserInteractionMode_Touch ? TabletModeState::On + : TabletModeState::Off; + if (sInTabletModeState != oldTabletModeState) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + observerService->NotifyObservers(nullptr, "tablet-mode-change", + sInTabletModeState == TabletModeState::On + ? u"tablet-mode" + : u"normal-mode"); + } +#endif +} + +#ifndef __MINGW32__ +struct HStringDeleter { + using pointer = HSTRING; + void operator()(pointer aString) { WindowsDeleteString(aString); } +}; + +using HStringUniquePtr = UniquePtr<HSTRING, HStringDeleter>; + +Result<HStringUniquePtr, HRESULT> ConvertToWindowsString( + const nsAString& aStr) { + HSTRING rawStr; + HRESULT hr = WindowsCreateString(PromiseFlatString(aStr).get(), aStr.Length(), + &rawStr); + if (FAILED(hr)) { + return Err(hr); + } + return HStringUniquePtr(rawStr); +} + +static Result<Ok, nsresult> RequestShare( + const std::function<HRESULT(IDataRequestedEventArgs* pArgs)>& aCallback) { + HWND hwnd = GetForegroundWindow(); + if (!hwnd) { + return Err(NS_ERROR_FAILURE); + } + + ComPtr<IDataTransferManagerInterop> dtmInterop; + ComPtr<IDataTransferManager> dtm; + + HRESULT hr = RoGetActivationFactory( + HStringReference( + RuntimeClass_Windows_ApplicationModel_DataTransfer_DataTransferManager) + .Get(), + IID_PPV_ARGS(&dtmInterop)); + if (FAILED(hr) || + FAILED(dtmInterop->GetForWindow(hwnd, IID_PPV_ARGS(&dtm)))) { + return Err(NS_ERROR_FAILURE); + } + + auto callback = Callback< + ITypedEventHandler<DataTransferManager*, DataRequestedEventArgs*>>( + [aCallback](IDataTransferManager*, + IDataRequestedEventArgs* pArgs) -> HRESULT { + return aCallback(pArgs); + }); + + EventRegistrationToken dataRequestedToken; + if (FAILED(dtm->add_DataRequested(callback.Get(), &dataRequestedToken)) || + FAILED(dtmInterop->ShowShareUIForWindow(hwnd))) { + return Err(NS_ERROR_FAILURE); + } + + return Ok(); +} + +static Result<Ok, nsresult> AddShareEventListeners( + const RefPtr<mozilla::media::Refcountable<MozPromiseHolder<SharePromise>>>& + aPromiseHolder, + const ComPtr<IDataPackage>& aDataPackage) { + ComPtr<IDataPackage3> spDataPackage3; + + if (FAILED(aDataPackage.As(&spDataPackage3))) { + return Err(NS_ERROR_FAILURE); + } + + auto completedCallback = + Callback<ITypedEventHandler<DataPackage*, ShareCompletedEventArgs*>>( + [aPromiseHolder](IDataPackage*, + IShareCompletedEventArgs*) -> HRESULT { + aPromiseHolder->Resolve(true, __func__); + return S_OK; + }); + + EventRegistrationToken dataRequestedToken; + if (FAILED(spDataPackage3->add_ShareCompleted(completedCallback.Get(), + &dataRequestedToken))) { + return Err(NS_ERROR_FAILURE); + } + + ComPtr<IDataPackage4> spDataPackage4; + if (SUCCEEDED(aDataPackage.As(&spDataPackage4))) { + // Use SharedCanceled API only on supported versions of Windows + // So that the older ones can still use ShareUrl() + + auto canceledCallback = + Callback<ITypedEventHandler<DataPackage*, IInspectable*>>( + [aPromiseHolder](IDataPackage*, IInspectable*) -> HRESULT { + aPromiseHolder->Reject(NS_ERROR_FAILURE, __func__); + return S_OK; + }); + + if (FAILED(spDataPackage4->add_ShareCanceled(canceledCallback.Get(), + &dataRequestedToken))) { + return Err(NS_ERROR_FAILURE); + } + } + + return Ok(); +} +#endif + +RefPtr<SharePromise> WindowsUIUtils::Share(nsAutoString aTitle, + nsAutoString aText, + nsAutoString aUrl) { + auto promiseHolder = MakeRefPtr< + mozilla::media::Refcountable<MozPromiseHolder<SharePromise>>>(); + RefPtr<SharePromise> promise = promiseHolder->Ensure(__func__); + +#ifndef __MINGW32__ + auto result = RequestShare([promiseHolder, title = std::move(aTitle), + text = std::move(aText), url = std::move(aUrl)]( + IDataRequestedEventArgs* pArgs) { + ComPtr<IDataRequest> spDataRequest; + ComPtr<IDataPackage> spDataPackage; + ComPtr<IDataPackage2> spDataPackage2; + ComPtr<IDataPackagePropertySet> spDataPackageProperties; + + if (FAILED(pArgs->get_Request(&spDataRequest)) || + FAILED(spDataRequest->get_Data(&spDataPackage)) || + FAILED(spDataPackage.As(&spDataPackage2)) || + FAILED(spDataPackage->get_Properties(&spDataPackageProperties))) { + promiseHolder->Reject(NS_ERROR_FAILURE, __func__); + return E_FAIL; + } + + /* + * Windows always requires a title, and an empty string does not work. + * Thus we trick the API by passing a whitespace when we have no title. + * https://docs.microsoft.com/en-us/windows/uwp/app-to-app/share-data + */ + auto wTitle = ConvertToWindowsString((title.IsVoid() || title.Length() == 0) + ? nsAutoString(u" "_ns) + : title); + if (wTitle.isErr() || + FAILED(spDataPackageProperties->put_Title(wTitle.unwrap().get()))) { + promiseHolder->Reject(NS_ERROR_FAILURE, __func__); + return E_FAIL; + } + + // Assign even if empty, as Windows requires some data to share + auto wText = ConvertToWindowsString(text); + if (wText.isErr() || FAILED(spDataPackage->SetText(wText.unwrap().get()))) { + promiseHolder->Reject(NS_ERROR_FAILURE, __func__); + return E_FAIL; + } + + if (!url.IsVoid()) { + auto wUrl = ConvertToWindowsString(url); + if (wUrl.isErr()) { + promiseHolder->Reject(NS_ERROR_FAILURE, __func__); + return wUrl.unwrapErr(); + } + + ComPtr<IUriRuntimeClassFactory> uriFactory; + ComPtr<IUriRuntimeClass> uri; + + auto hr = GetActivationFactory( + HStringReference(RuntimeClass_Windows_Foundation_Uri).Get(), + &uriFactory); + + if (FAILED(hr) || + FAILED(uriFactory->CreateUri(wUrl.unwrap().get(), &uri)) || + FAILED(spDataPackage2->SetWebLink(uri.Get()))) { + promiseHolder->Reject(NS_ERROR_FAILURE, __func__); + return E_FAIL; + } + } + + if (!StaticPrefs::widget_windows_share_wait_action_enabled()) { + promiseHolder->Resolve(true, __func__); + } else if (AddShareEventListeners(promiseHolder, spDataPackage).isErr()) { + promiseHolder->Reject(NS_ERROR_FAILURE, __func__); + return E_FAIL; + } + + return S_OK; + }); + if (result.isErr()) { + promiseHolder->Reject(result.unwrapErr(), __func__); + } +#else + promiseHolder->Reject(NS_ERROR_FAILURE, __func__); +#endif + + return promise; +} + +NS_IMETHODIMP +WindowsUIUtils::ShareUrl(const nsAString& aUrlToShare, + const nsAString& aShareTitle) { + nsAutoString text; + text.SetIsVoid(true); + WindowsUIUtils::Share(nsAutoString(aShareTitle), text, + nsAutoString(aUrlToShare)); + return NS_OK; +} diff --git a/widget/windows/WindowsUIUtils.h b/widget/windows/WindowsUIUtils.h new file mode 100644 index 0000000000..a55f92c8da --- /dev/null +++ b/widget/windows/WindowsUIUtils.h @@ -0,0 +1,50 @@ +/* -*- 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_WindowsUIUtils_h__ +#define mozilla_widget_WindowsUIUtils_h__ + +#include "nsIWindowsUIUtils.h" +#include "nsString.h" +#include "nsColor.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" + +using SharePromise = + mozilla::MozPromise<bool, nsresult, /* IsExclusive */ true>; + +namespace mozilla { +enum class ColorScheme : uint8_t; +} + +class WindowsUIUtils final : public nsIWindowsUIUtils { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWINDOWSUIUTILS + + WindowsUIUtils(); + + static RefPtr<SharePromise> Share(nsAutoString aTitle, nsAutoString aText, + nsAutoString aUrl); + + static void UpdateInTabletMode(); + static bool GetInTabletMode(); + + // Gets the system accent color, or one of the darker / lighter variants + // (darker = -1/2/3, lighter=+1/2/3, values outside of that range are + // disallowed). + static mozilla::Maybe<nscolor> GetAccentColor(int aTone = 0); + static mozilla::Maybe<nscolor> GetSystemColor(mozilla::ColorScheme, int); + + // Use LookAndFeel for a cached getter. + static bool ComputeOverlayScrollbars(); + static double ComputeTextScaleFactor(); + static bool ComputeTransparencyEffects(); + + protected: + ~WindowsUIUtils(); +}; + +#endif // mozilla_widget_WindowsUIUtils_h__ diff --git a/widget/windows/components.conf b/widget/windows/components.conf new file mode 100644 index 0000000000..e5089ed9e1 --- /dev/null +++ b/widget/windows/components.conf @@ -0,0 +1,220 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Headers = [ + '/widget/windows/nsWidgetFactory.h', +] + +InitFunc = 'nsWidgetWindowsModuleCtor' +UnloadFunc = 'nsWidgetWindowsModuleDtor' + +Classes = [ + { + 'cid': '{4c9dee4a-b083-4261-8bbe-c6883d2a6bc9}', + 'contract_ids': ['@mozilla.org/gfx/parent/screenmanager;1'], + 'singleton': True, + 'type': 'mozilla::widget::ScreenManager', + 'constructor': 'mozilla::widget::ScreenManager::GetAddRefedSingleton', + 'headers': ['/widget/ScreenManager.h'], + 'processes': ProcessSelector.ALLOW_IN_GPU_AND_MAIN_PROCESS, + }, + { + 'cid': '{2d96b3df-c051-11d1-a827-0040959a28c9}', + 'contract_ids': ['@mozilla.org/widget/appshell/win;1'], + 'headers': ['/widget/windows/nsWidgetFactory.h'], + 'legacy_constructor': 'nsAppShellConstructor', + 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_UTILITY_AND_GMPLUGIN_PROCESS, + }, + { + 'cid': '{6987230e-0098-4e78-bc5f-1493ee7519fa}', + 'contract_ids': ['@mozilla.org/widget/useridleservice;1'], + 'singleton': True, + 'type': 'nsUserIdleServiceWin', + 'constructor': 'nsUserIdleServiceWin::GetInstance', + 'headers': ['/widget/windows/nsUserIdleServiceWin.h', 'nsUserIdleService.h'], + }, + { + 'cid': '{b148eed2-236d-11d3-b35c-00a0cc3c1cde}', + 'contract_ids': ['@mozilla.org/sound;1'], + 'singleton': True, + 'type': 'nsISound', + 'constructor': 'nsSound::GetInstance', + 'headers': ['/widget/windows/nsSound.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{77221d5a-1dd2-11b2-8c69-c710f15d2ed5}', + 'contract_ids': ['@mozilla.org/widget/clipboardhelper;1'], + 'type': 'nsClipboardHelper', + 'headers': ['/widget/nsClipboardHelper.h'], + }, + { + 'cid': '{b8e5bc54-a22f-4eb2-b061-24cb6d19c15f}', + 'contract_ids': ['@mozilla.org/windows-taskbar;1'], + 'type': 'mozilla::widget::WinTaskbar', + 'headers': ['/widget/windows/WinTaskbar.h'], + }, + { + 'cid': '{73a5946f-608d-454f-9d33-0b8f8c7294b6}', + 'contract_ids': ['@mozilla.org/windows-legacyjumplistbuilder;1'], + 'type': 'mozilla::widget::LegacyJumpListBuilder', + 'headers': ['/widget/windows/LegacyJumpListBuilder.h'], + }, + { + 'cid': '{2b9a1f2c-27ce-45b6-8d4e-755d0e34f8db}', + 'contract_ids': ['@mozilla.org/windows-legacyjumplistitem;1'], + 'type': 'mozilla::widget::LegacyJumpListItem', + 'headers': ['/widget/windows/LegacyJumpListItem.h'], + }, + { + 'cid': '{21f1f13b-f75a-42ad-867a-d91ad694447e}', + 'contract_ids': ['@mozilla.org/windows-legacyjumplistseparator;1'], + 'type': 'mozilla::widget::LegacyJumpListSeparator', + 'headers': ['/widget/windows/LegacyJumpListItem.h'], + }, + { + 'cid': '{f72c5dc4-5a12-47be-be28-ab105f33b08f}', + 'contract_ids': ['@mozilla.org/windows-legacyjumplistlink;1'], + 'type': 'mozilla::widget::LegacyJumpListLink', + 'headers': ['/widget/windows/LegacyJumpListItem.h'], + }, + { + 'cid': '{b16656b2-5187-498f-abf4-56346126bfdb}', + 'contract_ids': ['@mozilla.org/windows-legacyjumplistshortcut;1'], + 'type': 'mozilla::widget::LegacyJumpListShortcut', + 'headers': ['/widget/windows/LegacyJumpListItem.h'], + }, + { + 'cid': '{e04a55e8-fee3-4ea2-a98b-41d2621adc3c}', + 'contract_ids': ['@mozilla.org/windows-ui-utils;1'], + 'type': 'WindowsUIUtils', + 'headers': ['/widget/windows/WindowsUIUtils.h'], + }, + { + 'cid': '{8b5314bc-db01-11d2-96ce-0060b0fb9956}', + 'contract_ids': ['@mozilla.org/widget/transferable;1'], + 'type': 'nsTransferable', + 'headers': ['/widget/nsTransferable.h'], + }, + { + 'cid': '{948a0023-e3a7-11d2-96cf-0060b0fb9956}', + 'contract_ids': ['@mozilla.org/widget/htmlformatconverter;1'], + 'type': 'nsHTMLFormatConverter', + 'headers': ['/widget/nsHTMLFormatConverter.h'], + }, + { + 'cid': '{f92e733e-33a3-4752-90e5-25801ddeaf7b}', + 'contract_ids': ['@mozilla.org/widget/parent/dragservice;1'], + 'type': 'nsDragService', + 'headers': ['/widget/windows/nsDragService.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{9a0cb62b-d638-4faf-9588-ae96f5e29093}', + 'contract_ids': ['@mozilla.org/widget/taskbar-preview-callback;1'], + 'type': 'mozilla::widget::TaskbarPreviewCallback', + 'headers': ['/widget/windows/TaskbarPreview.h'], + }, + { + 'name': 'GfxInfo', + 'cid': '{d755a760-9f27-11df-0800-200c9a664242}', + 'contract_ids': ['@mozilla.org/gfx/info;1'], + 'type': 'mozilla::widget::GfxInfo', + 'headers': ['/widget/windows/GfxInfo.h'], + 'init_method': 'Init', + 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_SOCKET_AND_UTILITY_PROCESS, + }, + { + 'cid': '{e2fc3e45-c893-4b34-8f6d-b87faf65a897}', + 'contract_ids': ['@mozilla.org/parent/filepicker;1'], + 'type': 'nsFilePicker', + 'headers': ['/widget/windows/nsFilePicker.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{035d92f3-3802-4cf5-87cb-1758bfc5d4da}', + 'contract_ids': ['@mozilla.org/parent/colorpicker;1'], + 'type': 'nsColorPicker', + 'headers': ['/widget/windows/nsColorPicker.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{1201d357-8417-4926-a694-e6408fbedcf8}', + 'contract_ids': ['@mozilla.org/sharepicker;1'], + 'type': 'nsSharePicker', + 'headers': ['/widget/windows/nsSharePicker.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{25b4efa0-7054-4787-9cd6-630efb3fe6fa}', + 'contract_ids': ['@mozilla.org/widget/parent/clipboard;1'], + 'interfaces': ['nsIClipboard'], + 'type': 'nsIClipboard', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{b6e1a890-b2b8-4883-a65f-9476f6185313}', + 'contract_ids': ['@mozilla.org/widget/systemstatusbar;1'], + 'singleton': True, + 'init_method': 'Init', + 'type': 'mozilla::widget::SystemStatusBar', + 'constructor': 'mozilla::widget::SystemStatusBar::GetAddRefedSingleton', + 'headers': ['/widget/windows/SystemStatusBar.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, +] + +if buildconfig.substs['CC_TYPE'] == 'clang-cl': + Classes += [ + { + 'cid': '{84e11f80-ca55-11dd-ad8b-0800200c9a66}', + 'contract_ids': ['@mozilla.org/system-alerts-service;1'], + 'type': 'mozilla::widget::ToastNotification', + 'headers': ['/widget/windows/ToastNotification.h'], + 'init_method': 'Init', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{a46c385b-a45c-4b48-ab7c-aaed1252bb83}', + 'contract_ids': ['@mozilla.org/windows-alert-notification;1'], + 'type': 'mozilla::widget::WindowsAlertNotification', + 'headers': ['/widget/windows/ToastNotification.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + } + ] + +if defined('NS_PRINTING'): + Classes += [ + { + 'cid': '{d3f69889-e13a-4321-980c-a39332e21f34}', + 'contract_ids': ['@mozilla.org/gfx/devicecontextspec;1'], + 'type': 'nsDeviceContextSpecWin', + 'headers': ['/widget/windows/nsDeviceContextSpecWin.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{06beec76-a183-4d9f-85dd-085f26da565a}', + 'contract_ids': ['@mozilla.org/widget/printdialog-service;1'], + 'type': 'nsPrintDialogServiceWin', + 'headers': ['/widget/windows/nsPrintDialogWin.h'], + 'init_method': 'Init', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{841387c8-72e6-484b-9296-bf6eea80d58a}', + 'contract_ids': ['@mozilla.org/gfx/printsettings-service;1'], + 'type': 'nsPrintSettingsServiceWin', + 'headers': ['/widget/windows/nsPrintSettingsServiceWin.h'], + 'init_method': 'Init', + }, + { + 'cid': '{a6cf9129-15b3-11d2-932e-00805f8add32}', + 'contract_ids': ['@mozilla.org/gfx/printerlist;1'], + 'type': 'nsPrinterListWin', + 'headers': ['/widget/windows/nsDeviceContextSpecWin.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + ] diff --git a/widget/windows/docs/blocklist.rst b/widget/windows/docs/blocklist.rst new file mode 100644 index 0000000000..5450faaa1b --- /dev/null +++ b/widget/windows/docs/blocklist.rst @@ -0,0 +1,347 @@ +======================== +Windows DLL Blocklisting +======================== + +-------- +Overview +-------- + +There are many applications which interact with another application, which means +they run their code as a DLL in a different process. This technique is used, for +example, when an antivirus software tries to monitor/block navigation to a +malicious website, or a screen reader tries to access UI parts. If such an +application injects their code into Firefox, and if there is a bug in their code +running in our firefox.exe, it will emerge as Firefox’s bug even though it’s +not. + +Firefox for Windows has a feature to prevent DLLs from being loaded into our +processes. If we are aware that a particular DLL causes a problem in our +processes such as a crash or performance degradation, we can stop the problem by +blocking the DLL from being loaded. + +This blocklist is about a third-party application which runs outside Firefox but +interacts with Firefox. For add-ons, there is `a different process +<https://extensionworkshop.com/documentation/publish/add-ons-blocking-process/>`_. + +This page explains how to request to block a DLL which you think we should block +it as well as technical details about the feature. + +----------------------- +Two types of blocklists +----------------------- + +There are two types of blocklists in Firefox: + +1. A static blocklist that is compiled in to Firefox. This consists of DLLs + known to cause problems with Firefox, and this blocklist cannot be disabled + by the user. For more information and instructions on how to add a new DLL + to this list, see :ref:`Process for blocking a DLL in the static blocklist + <how-to-block-dll-in-static-blocklist>` below. +2. A dynamic blocklist that users can use to block DLLs that are giving them + problems. This was added in + `bug 1744362 <https://bugzilla.mozilla.org/show_bug.cgi?id=1744362>`_. + +The static blocklist has ways to specify if only certain versions of a DLL +should be blocked, or only for certain Firefox processes, etc. The dynamic +blocklist does not have this capability; if a DLL is on the list it will always +be blocked. + +Regardless of which blocklist the DLL is on, if it meets the criteria for being +blocked Firefox uses the same mechanism to block it. There are more details +below in :ref:`How the blocklist blocks a DLL <how-the-blocklist-blocks-a-dll>`. + +.. _how-to-block-dll-in-static-blocklist: + +-------------------------------------------------- +Process for blocking a DLL in the static blocklist +-------------------------------------------------- + +But wait, should we really block it? +------------------------------------ + +Blocking a DLL with the static blocklist should be our last resort to fix a +problem because doing it normally breaks functionality of an application which +installed the DLL. If there is another option, we should always go for it. +Sometimes we can safely bypass a third-party’s problem by changing our code even +though its root cause is not on our side. + +When we decide to block it, we must be certain that the issue at hand is so +great that it outweighs the user's choice to install the software, the utility +it provides, and the vendor's freedom to distribute and control their software. + +How to request to block a DLL +----------------------------- + +Our codebase has the file named +`WindowsDllBlocklistDefs.in <https://searchfox.org/mozilla-central/source/toolkit/xre/dllservices/mozglue/WindowsDllBlocklistDefs.in>`_ from which our build process generates DLL blocklists as C++ header files and compiles them. To block a new DLL, you create a patch to update WindowsDllBlocklistDefs.in and land it on our codebase, following our standard development process. Moreover, you need to fill out a form specific to the DLL blockling request so that reviewers can review the impact and risk as well as the patch itself. + +Here are the steps: + +1. File `a bug + <https://bugzilla.mozilla.org/enter_bug.cgi?format=__default__&bug_type=defect&product=Toolkit&component=Blocklist%20Policy%20Requests&op_sys=Windows&short_desc=DLL%20block%20request%3A%20%3CDLL%20name%3E&comment=Please%20go%20through%20https%3A%2F%2Fwiki.mozilla.org%2FBlocklisting%2FDLL%20before%20filing%20a%20new%20bug.>`_ + if it does not exist. +2. Answer all the questions in `this questionnaire + <https://msmania.github.io/assets/mozilla/third-party-modules/questionnaire.txt>`_, + and attach it to the bug as a plaintext. +3. Make a patch and start a code review via Phabricator as usual. + +How to edit WindowsDllBlocklistDefs.in +-------------------------------------- + +WindowsDllBlocklistDefs.in defines several variables as a Python Array. When you +add a new entry in the blocklists, you pick one of the variables and add an +entry in the following syntax: + +Syntax +****** + +:: + + Variable += [ + ... + # One-liner comment including a bug number + EntryType(Name, Version, Flags), + ... + ] + +Parameters +********** + ++-----------+--------------------------------------------------------------------------------+ +| Parameter | Value | ++===========+================================================================================+ +| Variable | ALL_PROCESSES \| BROWSER_PROCESS \| CHILD_PROCESSES \| GMPLUGIN_PROCESSES \| | +| | GPU_PROCESSES \| SOCKET_PROCESSES \| UTILITY_PROCESSES | ++-----------+--------------------------------------------------------------------------------+ +| EntryType | DllBlocklistEntry \| A11yBlocklistEntry \| RedirectToNoOpEntryPoint | ++-----------+--------------------------------------------------------------------------------+ +| Name | A case-insensitive string representing a DLL's filename to block | ++-----------+--------------------------------------------------------------------------------+ +| Version | One of the following formats: | +| | | +| | - ALL_VERSIONS \| UNVERSIONED | +| | - A tuple consisting of four digits | +| | - A 32-bit integer representing a Unix timestamp with PETimeStamp | ++-----------+--------------------------------------------------------------------------------+ + +Variable +******** + +Choose one of the following predefined variables. + +- **ALL_PROCESSES**: DLLs defined here are blocked in BROWSER_PROCESS + + CHILD_PROCESSES +- **BROWSER_PROCESS**: DLLs defined here are blocked in the browser process +- **CHILD_PROCESSES**: DLLs defined here are blocked in non-browser processes +- **GMPLUGIN_PROCESSES**: DLLs defined here are blocked in GMPlugin processes +- **GPU_PROCESSES**: DLLs defined here are blocked in GPU processes +- **SOCKET_PROCESSES**: DLLs defined here are blocked in socket processes +- **UTILITY_PROCESSES**: DLLs defined here are blocked in utility processes + +EntryType +********* +Choose one of the following predefined EntryTypes. + +- **DllBlocklistEntry**: Use this EntryType unless your case matches the other + EntryTypes. +- **A11yBlocklistEntry**: If you want to block a module only when it’s loaded by + an accessibility application such as a screen reader, you can use this + EntryType. +- **RedirectToNoOpEntryPoint**: If a modules is injected via Import Directory + Table, adding the module as DllBlocklistEntry breaks process launch, meaning + DllBlocklistEntry is not an option. You can use RedirectToNoOpEntryPoint + instead. + +Name +**** +A case-insensitive string representing a DLL's filename to block. Don’t include a directory name. + +Version +******* + +A maximum version to be blocked. If you specify a value, a module with the +specified version, older versions, and a module with no version are blocked. + +| If you want to block a module regardless of its version, use ALL_VERSIONS. +| If you want to block a module with no version, use UNVERSIONED. + + +To specify a version, you can use either of the following formats: + +- | A tuple consisting of four digits. This is compared to the version that is embedded in a DLL as a version resource. + | Example: (1, 2, 3, 4) +- | A 32-bit integer representing a Unix timestamp with PETimeStamp. This is compared to an integer of IMAGE_FILE_HEADER::TimeDateStamp. + | Example: PETimeStamp(0x12345678) + + +----------------- +Technical details +----------------- + +.. _how-the-blocklist-blocks-a-dll: + +How the blocklist blocks a DLL +------------------------------ + +Briefly speaking, we make ntdll!NtMapViewOfSection return +``STATUS_ACCESS_DENIED`` if a given module is on the blocklist, thereby a +third-party’s code, or even Firefox’s legitimate code, which tries to load a DLL +in our processes in any way such as LoadLibrary API fails and receives an +access-denied error. + +Cases where we should not block a module +---------------------------------------- + +As our blocklist works as explained above, there are the cases where we should not block a module. + +- | A module is loaded via `Import Directory Table <https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table>`_ + | Blocking this type of module blocks even a process from launching. You may be able to block this type of module with RedirectToNoOpEntryPoint. +- | A module is loaded as a `Layered Service Provider <https://docs.microsoft.com/en-us/windows/win32/winsock/categorizing-layered-service-providers-and-applications>`_ + | Blocking this type of module on Windows 8 or newer breaks networking. Blocking a LSP on Windows 7 is ok. + +(we used to have to avoid blocking modules loaded via a +`Window hook <https://docs.microsoft.com/en-us/windows/win32/winmsg/hooks>`_ because blocking this type of +module would cause repetitive attempts to load a module, resulting in slow performance +like `Bug 1633718 <https://bugzilla.mozilla.org/show_bug.cgi?id=1633718>`_, but this should be fixed +as of `Bug 1823412 <https://bugzilla.mozilla.org/show_bug.cgi?id=1823412>`_.) + +Third-party-module ping +----------------------- + +We’re collecting the :ref:`third-party-module ping <third-party-modules-ping>` +which captures a moment when a third-party module is loaded into the +Browser/Tab/RDD process. As it’s asked in the request form, it’s important to +check the third-party-module ping and see whether a module we want to block +appears in the ping or not. If it appears, you may be able to know how a module +is loaded by looking at a callstack in the ping. + +How to view callstacks in the ping +********************************** + +1. You can run a query on BigQuery console or STMO. (BigQuery console is much + faster and can handle larger data.) + + - BigQuery console (visit + `here <https://docs.telemetry.mozilla.org/cookbooks/bigquery.html#gcp-bigquery-console>`_ + to request access): https://console.cloud.google.com/bigquery + - STMO: https://sql.telemetry.mozilla.org/ + +2. Make your own query based on `this template + <https://msmania.github.io/assets/mozilla/third-party-modules/query-template.txt>`_. +3. Run the query. +4. Save the result as a JSON file. + + - In BigQuery console, click [SAVE RESULTS] and choose [JSON (local file)]. + - In STMO, click [...] at the right-top corner and select [Show API Key], + then you can download a JSON from a URL shown in the [Results in JSON format]. + +5. | Go to https://msmania.github.io/assets/mozilla/third-party-modules/ + | (A temporal link. Need to find a permanent place.) +6. Click [Upload JSON] and select the file you saved at the step 4. +7. Click a row in the table to view a callstack + + +How to see the versions of a specific module in the ping +******************************************************** + +You can use `this template query +<https://msmania.github.io/assets/mozilla/third-party-modules/query-groupby-template.txt>`_ +to query which versions of a specific module are captured in the ping. This +tells the product versions which are actively used including the crashing +versions and the working versions. + +You can also get the crashing versions by querying the crash reports or the +Socorro table. Having two version lists, you can decide whether you can specify +the Version parameter in a blocklist entry. + +Initialization +-------------- + +In order to have the most effective blocking of DLLs, the blocklist is +initialized very early during browser startup. If the :ref:`launcher process +<launcher-process>` is available, the steps are: + +- Launcher process loads dynamic blocklist from disk (see + `DynamicBlocklist::LoadFile() + <https://searchfox.org/mozilla-central/search?q=DynamicBlocklist%3A%3ALoadFile&path=&case=false®exp=false>`_) +- Launcher process puts dynamic blocklist data in shared section (see + `SharedSection::AddBlocklist() + <https://searchfox.org/mozilla-central/search?q=SharedSection%3A%3AAddBlocklist&path=&case=false®exp=false>`_) +- Launcher process creates the browser process in a suspended mode, sets up its + dynamic blocklist, then starts it. (see `LauncherMain() + <https://searchfox.org/mozilla-central/search?q=LauncherMain&path=&case=false®exp=false>`_) + + - This is so (ideally) no DLLs can be injected before the blocklist is set up. + +If the launcher process is not available, a different blocklist is used, defined +in `mozglue/WindowsDllBlocklist.cpp +<https://searchfox.org/mozilla-central/source/toolkit/xre/dllservices/mozglue/WindowsDllBlocklist.cpp>`_. +This code does not currently support the dynamic blocklist. This is intended to +only be used in testing and other non-deployed scenarios, so this shouldn't be +a problem for users. + +Note that the mozglue blocklist also has a feature to block threads that start +in ``LoadLibrary`` and variants. This code is currently only turned on in +Nightly builds because it breaks some third-party DLP products. + +Dynamic blocklist file location +------------------------------- + +Because the blocklist is loaded so early during startup, we don't have access to +what profile is going to be loaded, so the blocklist file can't be stored there. +Instead, by default the blocklist file is stored in the Windows user's roaming +app data directory, specifically + +``<Roaming AppData directory>\Mozilla\Firefox\blocklist-<install hash>`` + +Note that the install hash here is what is returned by `GetInstallHash() +<https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/common/commonupdatedir.cpp#404>`_, +and is suitable for uniquely identifying the particular Firefox installation +that is running. + +On first launch, this location will be written to the registry, and can be +overriden by setting that key to a different file location. The registry key is +``HKEY_CURRENT_USER\Software\Mozilla\Firefox\Launcher``, and the name is the +full path to firefox.exe with "\|Blocklist" appended. This code is in +`LauncherRegistryInfo +<https://searchfox.org/mozilla-central/source/toolkit/xre/LauncherRegistryInfo.cpp>`_. + +Adding to and removing from the dynamic blocklist +------------------------------------------------- + +Users can add or remove DLLs from the dynamic blocklist by navigating to +``about:third-party``, finding the entry for the DLL they are interested in, and +clicking on the dash icon. They will then be prompted to restart the browser, as +the change will only take effect after the browser restarts. + +Disabling the dynamic blocklist +------------------------------- + +It is possible that users can get Firefox into a bad state by putting a DLL on +the dynamic blocklist. One possibility is that the user blocks only one of a set +of DLLs that interact, which could make Firefox behave in unpredictable ways or +crash. + +By launching Firefox with ``--disableDynamicBlocklist``\, the dynamic blocklist +will be loaded but not used to block DLLs. This lets the user go to +``about:third-party`` and attempt to fix the problem by unblocking or blocking +DLLs. + +Similarly, in safe mode the dynamic blocklist is also disabled. + +Enterprise policy +----------------- + +The dynamic blocklist can be disabled by setting a registry key at +``HKEY_CURRENT_USER\Software\Policies\Mozilla\Firefox`` with a name of +DisableThirdPartyModuleBlocking and a DWORD value of 1. This will have the +effect of not loading the dynamic blocklist, and no icons will show up in +``about:third-party`` to allow blocking DLLs. + +------- +Contact +------- + +Any questions or feedback are welcome! + +**Matrix**: `#hardening <https://app.element.io/#/room/#hardening:mozilla.org>`_ diff --git a/widget/windows/docs/index.rst b/widget/windows/docs/index.rst new file mode 100644 index 0000000000..9a24cb9cdb --- /dev/null +++ b/widget/windows/docs/index.rst @@ -0,0 +1,9 @@ +================== +Firefox on Windows +================== + +.. toctree:: + :maxdepth: 2 + + blocklist + windows-pointing-device/index diff --git a/widget/windows/docs/windows-pointing-device/apple_vision.jpg b/widget/windows/docs/windows-pointing-device/apple_vision.jpg Binary files differnew file mode 100644 index 0000000000..9515f4d4fb --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/apple_vision.jpg diff --git a/widget/windows/docs/windows-pointing-device/apple_vision_user.webp b/widget/windows/docs/windows-pointing-device/apple_vision_user.webp Binary files differnew file mode 100644 index 0000000000..64f8afc0e5 --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/apple_vision_user.webp diff --git a/widget/windows/docs/windows-pointing-device/index.rst b/widget/windows/docs/windows-pointing-device/index.rst new file mode 100644 index 0000000000..eda552b3dd --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/index.rst @@ -0,0 +1,1384 @@ +################################################################################ +Windows Pointing Device Support in Firefox +################################################################################ + +.. contents:: Table of Contents + :depth: 4 + +================================================================================ +Introduction +================================================================================ + +This document is intended to provide the reader with a quick primer and/or +refresher on pointing devices and the various operating system APIs, user +experience guidelines, and Web standards that contribute to the way Firefox +handles input devices on Microsoft Windows. + +The documentation for these things is scattered across the web and has varying +levels of detail and completeness; some of it is missing or ambiguous and was +only determined experimentally or by reading about other people's experiences +through forum posts. An explicit goal of this document is to gather this +information into a cohesive picture. + +We will then discuss the ways in which Firefox currently (as of early 2023) +produces incorrect or suboptimal behavior when implementing those standards +and guidelines. + +Finally, we will raise some thoughts and questions to spark discussion on how +we might improve the situation and handle corner cases. Some of +these issues are intrinsically "opinion based" or "policy based", so clear +direction on these is desirable before engineering effort is invested into +reimplementation. + + +================================================================================ +Motivation +================================================================================ + +A quick look at the `pile of defects <https://bugzilla.mozilla.orgbuglist.cgi?query_format=advanced&status_whiteboard=%5Bwin%3Atouch%5D&list_id=16586149&status_whiteboard_type=allwordssubstr>`__ +on *bugzilla.mozilla.org* marked with *[win:touch]* will show anyone that +Firefox's input stack for pointer devices has issues, but the bugs recorded +there don't begin to capture the full range of unreported glitches and +difficult-to-reproduce hiccups that users run into while using touchscreen +hardware and pen digitizers on Firefox, nor does it capture the ways that +Firefox misbehaves according to various W3C standards that are (luckily) either +rarely used or worked around in web apps (and thus go undetected or +unreported). + +These bugs primarily manifest in a few ways that will each be discussed in +their own section: + +1. Firefox failing to return the proper values for the ``pointer``, + ``any-pointer``, ``hover``, and ``any-hover`` CSS Media Queries + +2. Firefox failing to fire the correct pointer-related DOM events at the + correct time (or at all) + +3. Firefox's inconsistent handling of touch-related gestures like scrolling, + where certain machines (like the Surface Pro) fail to meet the expected + behavior of scrolling inertia and overscroll. This leads to a weird touch + experience where the page comes to a choppy, dead-stop when using + single-finger scrolling + + +It's worth noting that Firefox is not alone in having these types of issues, +and that handling input devices is a notoriously difficult task for many +applications; even a substantial amount of Microsoft's own software has trouble +navigating this minefield on their own Microsoft Surface devices. Defects are +instigated by a combination of the *intrinsic complexity* of the problem domain +and the *accidential complexity* introduced by device vendors and Windows +itself. + +The *intrinsic complexity* comes from the simple fact that human-machine +interaction is difficult. A person must attempt to convey complex +and abstract goals through a series of simple movements involving a few pieces +of physical hardware. The devices can send signals that are unclear +or even contradictory, and the software must decide how to handle +this. + +As a trivial example, every software engineer that's ever written +page scrolling logic has to answer the question, "What should my +program do if the user hits 'Page Up' and 'Page Down' at the same time?". +While it may seem obvious that the answer is "Do nothing.", naively-written +keyboard input logic might assume the two are mutually-exclusive and only +process whichever key is handled first in program order. + +Occasionally, a new device will be invented that doesn't obviously map to +existing abstractions and input pipelines. There will be a period of time where +applications will want to support the new device, but it won't be well +understood by either the application developers nor the device vendor +themselves what ideal integration would look like. The new Apple Vision VR +headset is such a device; traditional VR headsets have used controllers to +point at things, but Apple insists that the entire thing should be done using +only hand tracking and eye tracking. Developers of VR video games and other +apps (like Firefox) will inevitably make many mistakes on the road to +supporting this new headset. + +A major source of defect-causing *accidental complexity* is the lack of clear +expectations and documentation from Microsoft for apps (like Firefox) that are +not using their Universal Windows Platform (UWP). The Microsoft Developer +Network (MSDN) mentions concepts like inertia, overscroll, elastic bounce, +single-finger panning, etc., but the solution is presented in the context +of UWP, and the solution for non-UWP apps is either unclear or undocumented. + +Adding to this complexity is the fact that Windows itself has gone through +several iterations of input APIs for different classes of devices, and +these APIs interact with each other in ways that are surprising or +unintuitive. Again, the advice given on MSDN pertains to UWP apps, and the +documentation about the newer "pointer" based window messages is +a mix of incomplete and inaccurate. + +Finally, individual input devices have bugs in their driver software that +would disrupt even applications that are using the Windows input APIs perfectly. +Handling all of these deviations is impossible and would result in fragile, +unmaintainable code, but Firefox inevitably has to work around common ones to +avoid alienating large portions of the userbase. + + +================================================================================ +Technical Background +================================================================================ + + +A Quick Primer on Pointing Devices +====================================== + + +Traditionally, web browsers were designed to accommodate computer mice and +devices that behave in a similar way, like trackballs and touchpads on +laptops. Generally, it was assumed that there would be one such device attached +to the computer, and it would be used to control a hovering "cursor" whose +movements would be changed by relative movement of the physical input device. + +However, modern computers can be controlled using a variety of different +pointing devices, all with different characteristics. Many allow +multiple concurrent targets to be pointed at and have multiple sensors, +buttons, and other actuators. + +For example, the screen of the Microsoft Surface Pro has dual capabilities +of being a touch sensor and a digitizer for a tablet pen. When being used as a +workstation, it's not uncommon for a user to also connect the "keyboard + +touchpad" cover and a mouse (via USB or Bluetooth) to provide the more +productivity-oriented "keyboard and mouse" setup. In that configuration, there +are 4 pointer devices connected to the machine simultaneously: a touch screen, +a pen digitizer, a touchpad, and a mouse. + +The next section will give a quick overview of common pointing devices. +Many will be familiar to the reader, but they are still mentioned to establish +common terminology and to avoid making assumptions about familiarity with every +input device. + + +Common Pointing Devices +--------------------------- + +Here are some descriptions of a few pointing device types that demonstrate +the diversity of hardware: + +**Touchscreen** + + A touchscreen is a computer display that is able to sense the + location of (possibly-multiple) fingers (or stylus) making contact with its + surface. Software can then respond to the touches by changing the displayed + objects quickly, giving the user a sense of actually physically manipulating + them on screen with their hands. + + .. image:: touchscreen.jpg + :width: 25% + + +**Digitizing Tablet + Pen Stylus** + + These advanced pointing devices tend to + exist in two forms: as an external sensing "pad" that can be plugged into a + computer and sits on a desk or in someone's lap, or as a sensor built right + into a computer display. Both use a "stylus", which is a pen-shaped + electronic device that is detectable by the surface. Common features + include the ability to distinguish proximity to the surface ("hovering") + versus actual contact, pressure sensitivity, angle/tilt detection, multiple + "ends" such as a tip and an eraser, and one-or-more buttons/switch + actuators. + + .. image:: wacom_tablet.png + :width: 25% + + +**Joystick/Pointer Stick** + + Pointer sticks are most often seen in laptop + computers made by IBM/Lenovo, where they exist as a little red nub located + between the G, H, and B keys on a standard QWERTY keyboard. They function + similarly to the analog sticks on a game controller -- The user displaces + the stick from its center position, and that is interpreted as a relative + direction to move the on-screen cursor. A greater displacement from center + is interpreted as increased velocity of movement. + + .. image:: trackpoint.jpg + :width: 25% + + +**Touchpad** + + A touchpad is a rectangular surface (often found on laptop + computers) that detects touch and motion of a finger and moves an on-screen + cursor relative to the motion. Modern touchpads often support multiple + touches simultaneously, and therefore offer functionality that is quite + similar to a touchscreen, albeit with different movement semantics because + of their physical separation from the screen (discussed below). + + .. image:: touchpad.jpg + :width: 25% + + +**VR Controllers** + + VR controllers (and other similar devices like the + Wiimote from the Nintendo Wii) allow users to point at objects in a + three-dimensional virtual world by moving a real-world controller and + "projecting" the controller's position into the virtual space. They often + also include sensors to detect the yaw, pitch, and roll of the sensors. + There are often other inputs in the controller device, like analog sticks + and buttons. + + .. image:: vrcontroller.jpg + :width: 25% + + +**Hand Tracking** + + Devices like the Apple Vision (introduced during the + time this document was being written) and (to a lesser extent) the Meta + Quest have the ability to track the wearer's hand and directly interpret + gestures and movements as input. As the human hand can assume a staggering + number of orientations and configurations, a finite list of specific shapes + and movements must be identified and labelled to allow for clear + software-user interaction. + + .. image:: apple_vision_user.webp + :width: 25% + + .. image:: apple_vision.jpg + :width: 25% + + +**Mouse** + + A pointing device that needs no introduction. Moving a physical + clam-shaped device across a surface translates to relative movement of a + cursor on screen. + + .. image:: mouse.jpg + :width: 25% + + +The Buxton Three-State Model +------------------------------- + + +Bill Buxton, an early pioneer in the field of human-computer interaction, +came up with a three-state model for pointing devices; a device can be +"Out of Range", "Tracking", or "Dragging". Not all devices support all three +states, and some devices have multiple actuators that can have the three-state +model individually applied. + +.. mermaid:: + + stateDiagram-v2 + direction LR + state "State 0" as s0 + state "State 1" as s1 + state "State 2" as s2 + s0 --> s0 : Out Of Range + s1 --> s1 : Tracking + s2 --> s2 : Dragging + s0 --> s1 : Stylus On + s1 --> s0 : Stylus Lift + s1 --> s2 : Tip Switch Close + s2 --> s1 : Tip Switch Open + + +For demonstration, here is the model applied to a few devices: + +**Computer Mouse** + + A mouse is never in the "Out of Range" state. Even though it can technically + be lifted off its surface, the mouse does not report this as a separate + condition; instead, it behaves as-if it is stationary until it can once + again sense the surface moving underneath. + + The remaining two states apply to each button individually; when a button is + not being pressed, the mouse is considered in the "tracking" state with + respect to that button. When a button is held down, the mouse is "dragging" + with respect to that button. A "click" is simply considered a zero-length + drag under this model. + + In the case of a two-button mouse, this means that the mouse can be in a + total of 4 different states: tracking, left button dragging, right button + dragging, and two-button dragging. In practice, very little software + actually does anything meaningful with two-button dragging. + +**Touch Screen** + + Applying the model to a touch screen, one can observe that current hardware + has no way to sense that a finger that is "hovering, but not quite making + contact with the screen". This means that the "Tracking" state can be ruled + out, leaving only the "Out of Range" and "Dragging" states. Since many touch + screens can support multiple fingers touching the screen concurrently, and + each finger can be in one of two states, there are potentially 2^N different + "states" that a touchscreen can be in. Windows assigns meaning to many two, + three, and four-finger gestures. + +**Tablet Digitizer** + + A tablet digitizer supports all three states: when the stylus is far away + from the surface, it is considered "out of range"; when it is located + slightly above the surface, it is "tracking"; and when it is making contact + with the surface, it is "dragging". + +The W3C standards for pointing devices are based on this three-state model, but +applied to each individual web element instead of the entire system. This +makes things like "Out-of-Range" possible for the mouse, since it can be +out of range of a web element. + +The W3C uses the terms "over" and "out" to convey the transition between +"out-of-range" and "tracking" (which the W3C calls "hover"), and the terms +"down" and "up" convey the transition between "tracking" and "dragging". + +The standard also address some of the known shortcomings of the model to +improve portability and consistency; these improvements will be discussed more +below. + +The Windows Pointer API is *supposedly* based around this model, +but unfortunately real-world testing shows that the model is not followed +very consistently with respect to the actual signals sent to the application. + + +Gestures +===================================== + + +In contrast to the sort-of "anything goes" UI designs of the past, +modern operating systems like Windows, Mac OS X, iOS, Android, and even +modern Linux DEs have an "opinionated" idea of how user interaction +should behave across all apps on the platform (the so-called "look and feel" +of the operating system). + +Users expect gestures like swipes, pinches, and taps to act the same way +across all apps for a given operating system, and they expect things like +on-screen keyboards or handwriting recognition to pop up in certain contexts. +Failing to meet those expectations makes an app look less polished, and +(especially as far as accessibility is concerned) it frustrates the user +and makes it more difficult for them to interact with the app. + +Microsoft defines guidelines for various behaviours that Windows applications +should ideally adhere to in the `Input and Interactions <https://learn.microsoft.com/en-us/windows/apps/design/input/>`__ +section on MSDN. Some of these are summarized quickly below: + +**Drag and Drop** + + Drag and drop allows a user to transfer data from one application to + another. The gesture begins when a pointer device moves into the "Dragging" + state over top of a UI element, usually as a result of holding down a mouse + button or pressing a finger on a touchscreen. The user moves the pointer + over top of the receiver of the data, and then ends the gesture by releasing + the mouse button or lifting their finger off the touchscreen. Window + interprets this transition out of the "Dragging" state as permission to + initiate the data transfer. + + Firefox has supported Drag and Drop for a very long time, so it will not be + discussed further. + + +**Pan and Zoom** + + When using touchscreens (and multi-touch touchpads), users expect to be able + to cause the viewport to "pan" left/right/up/down by pressing two fingers on + the screen (creating two pointers in "Dragging" state) and moving their + fingers in the direction of movement. When they are done, they can release + both fingers (changing both pointers to "Out of Bounds"). + + A zoom can be signalled by moving the two fingers apart or together + in a "pinch" or "reverse pinch" gesture. + + +**Single Pointer Panning** + + Applications that are based on a UI model of the user interacting with a + "page" often allow a single pointer "Dragging" over the viewport to cause + the viewport to pan, similarly to the two-finger panning discussed in the + previous section. + + Note that this gesture is not as universal as two-finger panning is -- as a + counterexample, graphics programs tend to treat one-finger dragging as + object manipulation and two-finger dragging as viewport panning. + + +**Inertia** + + When a user is done panning, they may lift their finger/pen off the screen + while the viewport is still in motion. Users expect that the page will + continue to move for a little while, as-if the user had "tossed" the page + when they let go. Effectively, the page behaves as though it has "momentum" + that needs to be gradually lost before the page comes to a full stop. + + Modern operating systems provide this behavior via their various native + widget toolkits, and the curve that objects follow as they slow to a stop + are different across OSes. In that way, they can be considered part of the + unique "look and feel" of the OS. Users expect the scrolling of pages in + their web browser to behave this way, and so when Firefox fails to provide + this behavior it can be jarring. + + +**Overscroll and Elastic Bounce** + + When a user is panning the page and reaches the outer edges, Microsoft + recommends that the app should begin an "elastic bounce" animation, where + the page will allow the user to scroll past the end ("overscroll"), + show empty space underneath the page, and then sort of "snap back" like a + rubber band that's been stretched and then released. You can see a + demonstration in `this article <https://www.windowslatest.com/2020/05/21/microsoft-is-adding-elastic-scrolling-to-chrome-on-windows-10/>`__, + which discusses Microsoft adding it to Chromium. + + +History of Web Standards and Windows APIs +=========================================== + +The World-Wide Web Consortium (W3C) and the Web Hypertext Application +Technology Working Group (WHATWG) manage the standards that detail the +interface between a user agent (like Firefox) and applications designed to run +on the Web Platform. The user agent, in turn, must rely on the operating system +(Windows, in this case) to provide the necessary APIs to implement the +standards required by the Web Platform. + +As a result of that relationship, a Web Standard is unlikely to be created +until all widely-used operating systems provide the required APIs. That allows +us to build a linear timeline with a predictable pattern: a new type of device +becomes popular, the APIs to support it are introduced into operating systems, +and eventually a cross-platform standard is introduced into the Web Platform. + +The following sections detail the history of input devices supported by +Windows and the Web Platform: + + +**1985 - Computer Mouse Support (Windows 1.0)** + + The first version of Windows (1985) supported a computer mouse. Support + for other input devices is not well-documented, but probably non-existant. + + +**1991 - Third-Party De-facto Pen Support (Wintab)** + + In the late 80s and early 90s, any tablet pen hardware vendor that wanted + to support Windows would need to write a device driver and design a + proprietary user-mode API to expose the device to user applications. In + turn, application developers would have to write and maintain code to + support the APIs of every relevant device vendor. + + In 1991, a company named LCS/Telegraphics released an API for Windows + called "Wintab", which was designed in collaboration with hardware and + software vendors to define a general API that could be targetted by + device drivers and applications. + + It would take Microsoft more than a decade to include first-party support + for tablet pens in Windows, which allowed Wintab to become the de-facto + standard for pen support on Windows. The Wintab API continues to be + supported by virtually all artist tablets to this day. Notable companies + include Wacom, Huion, XP-Pen, etc. + + +**1992 - Early Windows Pen Support (Windows for Pen Computing)** + + The earliest Windows operating system to support non-mouse pointing devices + was Windows 3.1 with the "Windows for Pen Computing" add-on (1992). + (`For the curious <https://socket3.wordpress.com/2019/07/31/windows-for-pen-computing-1-0/>`__, + and I'm certain `this book <https://www.amazon.com/Microsoft-Windows-Pen-Computing-Programmers/dp/1556154690>`__ + is a must-read!). Pen support was mostly implemented by translating actions + into the existing ``WM_MOUSExxx`` messages, but also "upgraded" any + application's ``EDIT`` controls into ``HEDIT`` controls, which looked the + same but were capable of being handwritten into using a pen. This was not + very user-friendly, as the controls stayed the same size and the UI was not + adapted to the input method. This add-on never achieved much popularity. + + It is not documented whether Netscape Navigator (the ancestor of Mozilla + Firefox) supported this add-on or not, but there is no trace of it in modern + Firefox code. + + +**1995 - Introduction of JavaScript and Mouse Events (De-facto Web Standard)** + + The introduction of JavaScript in 1995 by Netscape Communications added a + programmable, event-driven scripting environment to the Web Platform. + Browser vendors quickly added the ability for scripts to listen for and + react to mouse events. These are the well-known events like ``mouseover``, + ``mouseenter``, ``mousedown``, etc. that are ubiquitous on the web, and are + known by basically anyone who has ever written front-end JavaScript. + + This ubiquity created a de-facto standard for mouse input, which would + eventually be formally standardized by the W3C in the HTML Living Standard + in 2001. + + The Mouse Event APIs assume that the computer has one single pointing device + which is always present, has a single cursor capable of "hovering" over an + element, and has between one and three buttons. + + When support for other pointing devices like touchscreen and pen first + became available in operating systems, it was exposed to the web by + interpreting user actions into equivalent mouse events. Unfortunately, this + is unable to handle multiple concurrent pointers (like one would get from + multitouch screens) or report the kind of rich information a pen digitizer + can provide, like tilt angle, pressure, etc. This eventually lead the W3C + to develop the new "Touch Events" standard to expose touch functionality, + and eventually the "Pointer Events" to expose more of the rich information + provided by pens. + + +**2005 - Mainstream Pen Support (Windows XP Tablet PC Edition)** + + It was the release of Windows XP Tablet PC Edition (2005) that allowed + Windows applications to directly support tablet pens by using the new COM + "`Windows Tablet PC <https://learn.microsoft.com/en-us/windows/win32/tablet/tablet-pc-development-guide>`__" + APIs, most of which are provided through the main `InkCollector <https://learn.microsoft.com/en-us/windows/win32/tablet/inkcollector-class>`__ + class. The ``InkCollector`` functionality would eventually be "mainlined" + into Windows XP Professional Service Pack 2, and continues to exist in + modern Windows releases. + + The Tablet PC APIs consist of a large group of COM objects that work + together to facilitate enumerating attached pens, detecting pen movement and + pen strokes, and analyzing them to provide: + + 1. **Cursor Movement**: translates the movements of the pen into the + standard mouse events that applications expect from mouse cursor + movement, namely ``WM_NCHITTEST``, ``WM_SETCURSOR`` and + ``WM_MOUSEMOVE``. + + 2. **Gesture Recognition**: detects common user actions, like "tap", + "double-tap", "press-and-hold", and "drag". The `InkCollector` delivers + these events via COM `SystemGesture <https://learn.microsoft.com/en-us/windows/win32/tablet/inkcollector-systemgesture>`__ + events using the `InkSystemGesture <https://learn.microsoft.com/en-us/windows/win32/api/msinkaut/ne-msinkaut-inksystemgesture>`__ + enumeration. It will also translate them into common Win32 messages; for + example, a "drag" gesture would be translated into a ``WM_LBUTTONDOWN`` + message, several ``WM_MOUSEMOVE`` messages, and finally a + ``WM_LBUTTONUP`` message. + + An application that is using ``InkCollector`` will receive both types of + messages: traditional mouse input through the Win32 message queue, and + "Tablet PC API" events through COM callbacks. It is up to the + application to determine which events matter to it in a given context, + as the two types of events are not guaranteed by Microsoft to correspond + in any predictable way. + + 3. **Shape and Text Recognition**: allows the app to + recognize letters, numbers, punctuation, and other `common shapes <https://learn.microsoft.com/en-us/windows/win32/api/msinkaut/ne-msinkaut-inkapplicationgesture>`__ + the user might make using their pen. Supported shapes include circles, + squares, arrows, and motions like "scratch out" to correct a misspelled + word. Custom recognizers exist that allow recognition of other symbols, + like music notes or mathematical notation. + + 4. **Flick Recognition**: allows the user to invoke actions via quick, + linear motions that are recognized by Windows and sent to the app as + ``WM_TABLET_FLICK`` messages. The app can choose to handle the window + message or pass it on to the default window procedure, which will + translate it to scrolling messages or mouse messages. + + For example, a quick upward 'flick' corresponds to "Page up", and + a quick sideways flick in a web browser would be "back". Flicks were + never widely used by Windows apps, and they may have been removed in + more recent versions of Windows, as the existing Control Panel menus + for configuring them seem to no longer exist as of Windows 10 22H2. + + + Firefox does not appear to have ever used these APIs to allow tablet pen + input, with the exception of `one piece of code <https://searchfox.org/mozilla-central/rev/e6cb503ac22402421186e7488d4250cc1c5fecab/widget/windows/InkCollector.cpp>`__ + to detect when the pen leaves the Firefox window to solve + `Bug 1016232 <https://bugzilla.mozilla.org/show_bug.cgi?id=1016232>`__. + + +**2009 - Touch Support: WM_GESTURE (Windows 7)** + + While attempts were made with the release of Windows Vista (2007) to support + touchscreens through the existing tablet APIs, it was ultimately the release + of Windows 7 (2009) that brought first-class support for Touchscreen devices + to Windows with new Win32 APIs and two main window messages: ``WM_TOUCH`` + and ``WM_GESTURE``. + + These two messages are mutually-exclusive, and all applications are + initially set to receive only ``WM_GESTURE`` messages. Under this + configuration, Windows will attempt to recognize specific movements on a + touch digitizer and post "gesture" messages to the application's message + queue. These gestures are similar to (but, somewhat-confusingly, not + identical to) the gestures provided by the "Windows Tablet PC" APIs + mentioned above. The main gesture messages are: zoom, pan, rotate, + two-finger-tap, and press-and-tap (one finger presses, another finger + quickly taps the screen). + + In contrast to the behavior of the ``InkCollector`` APIs, which will send + both gesture events and translated mouse messages, the ``WM_GESTURE`` + message is truly "upstream" of the translated mouse messages; the translated + mouse messages will only be generated if the application forwards the + ``WM_GESTURE`` message to the default window procedure. This makes + programming against this API simpler than the ``InkCollector`` API, as + there is no need to state-fully "remember" that an action has already been + serviced by one codepath and needs to be ignored by the other. + + Firefox current supports the ``WM_GESTURE`` message when Asynchronous Pan + and Zoom (APZ) is not enabled (although we do not handle inertia in this + case, so the page comes to a dead-stop immediately when the user stops + scrolling). + + +**2009 - Touch Support: WM_TOUCH (Windows 7)** + + Also introduced in Windows 7, an application that needs full control over + touchscreen events can use `RegisterTouchWindow <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registertouchwindow>`__ + to change any of its windows to receive ``WM_TOUCH`` messages instead of the + more high-level ``WM_GESTURE`` messages. These messages explicitly notify + the application about every finger that contacts or breaks contact with the + digitizer (as well as each finger's movement over time). This provides + absolute control over touch interpretation, but also means that the burden + of handling touch behavior falls completely on the application. + + To help ease this burden, Microsoft provides two COM APIs to interpret + touch messages, ``IManipulationProcessor`` and ``IInertiaProcessor``. + + ``IManipulationProcessor`` can be considered a superset of the functionality + available through normal gestures. The application feeds ``WM_TOUCH`` data + into it (along with other state, such as pivot points and timestamps), and + it allows for manipulations like: two-finger rotation around a pivot, + single-finger rotation around a pivot, simultaneous rotation and translation + (for example, 'dragging' a single corner of a square). + `These MSDN diagrams <https://learn.microsoft.com/en-us/windows/win32/wintouch/advanced-manipulations-overview>`__ + give a good overview of the kinds of advanced manipulations an app might + support. + + ``IInertiaProcessor`` works with ``IManipulationProcessor`` to add inertia + to objects in a standard way across the operating system. It is likely that + later APIs that provide this (like DirectManipulation) are using these COM + objects under the hood to accomplish their inertia handling. + + Firefox currently handles the ``WM_TOUCH`` event when Asynchronous Pan and + Zoom (APZ) is enabled, but we do not use either the ``IInertiaProcessor`` + nor the ``IManipulationProcessor``. + + +**2012 - Unified Pointer API (Windows 8)** + + Windows 8 (2012) was Microsoft's initial attempt to make a touch-first, + mobile-first operating system that (ideally) would make it easy for app + developers to treat touch, pen, and mouse as first-class input devices. + + By this point, the Windows Tablet APIs would allow tablet pens to draw + text and shapes like squares, triangles, and music notes, and those shapes + would be recognizable by the Windows Ink subsystem. + + At the same time, Windows Touch allowed touchscreens to have advanced + manipulation, like rotate + translate, or simultaneous pan and zoom, and it + allowed objects manipulated by touch to have momentum and angular velocity. + + The shortcomings of having separate input stacks for these various devices + starts to be become apparent after a while: Why shouldn't a touchscreen be + able to recognize a circle or a triangle? Why shouldn't a pen be able to + have complex rotation and zoom functionality? How do we handle these newer + laptop touchpads that are starting to handle multi-touch gestures like a + touchscreen, but still cause relative cursor movement like a mouse? Why does + my program have to have 3 separate codepaths for different pointing devices + that are all very similar? + + The Windows Pointer Device Input Stack introduces new APIs and window + messages that generalize the various types of pointing devices under a + single API while still falling back to the legacy touch and tablet input + stacks in the event that the API is unused. (Note that the touch and tablet + stacks themselves fall back to the traditional mouse input stack when they + are unused.) + + Microsoft based their pointer APIs off the Buxton Three-State Model + (discussed earlier), where changes between "Out-of-Range" and "Tracking" are + signalled by ``WM_POINTERENTER`` AND ``WM_POINTERLEAVE`` messages, and + changes between "Tracking" and "Dragging" are signalled by + ``WM_POINTERDOWN`` and ``WM_POINTERUP``. Movement is indicated via + ``WM_POINTERUPDATE`` messages. + + If these messages are unhandled (the message is forwarded to + ``DefWindowProc``), the Win32 subsystem will translate them + into touch or gesture messages. If unhandled, those will be further + translated into mouse and system messages. + + While the Pointer API is not without some unfortunate pitfalls (which will + be discussed later), it still provides several advantages over the + previously available APIs: it can allow a mostly-unified codepath for + handling pointing devices, it circumvents many of the often-complex + interactions between the previous APIs, and it provides the ability to + simulate pointing devices to help facilitate end-to-end automated testing. + + Firefox currently uses the Pointer APIs to handle tablet stylus input only, + while other input methods still use the historical mouse and touch input + APIs above. + + +**2013 - DirectManipulation (Windows 8.1)** + + DirectManipulation is a DirectX based API that was added during the release + of Windows 8.1 (2013). This API allows an app to create a series of + "viewports" inside a window and have scrollable content within each of these + viewports. The manipulation engine will then take care of automatically + reading Pointer API messages from the window's event queue and generating + pan and zoom events to be consumed by the app. + + In the case that the app is also using DirectComposition to draw its window, + DirectManipulation can pipe the events directly into it, causing the app + to essentially get asynchronous pan and zoom with proper handling of inertia + and overscroll with very little coding. + + DirectManipulation is only used in Firefox to handle data coming from + Precision Touchpads, as Microsoft provides no other convenient API for + obtaining data from such devices. Firefox creates fake content inside of + a fake viewport to capture the incoming events from the touchpad and + translates them into the standard Asynchronous Pan and Zoom (APZ) events + that the rest of the input pipeline uses. + + +**2013 - Touch Events (Web Standard)** + + "`Touch Events <https://www.w3.org/TR/touch-events/>`__" became a W3C + recommendation in October, 2013. + + At this point, Microsoft's first operating system to include touch support + (Windows 7) was the most popular desktop operating system, and the ubiquity + of smart phones brought a huge uptick in users with touchscreen inputs. All + major browsers included some API that allowed reading touch input, + prompting the W3C to formalize a new standard to ensure interoperability. + + With the Touch Events API, multiple touch interactions may be reported + simultaneously, each with their own separate identifier for tracking and + their own coordinates within the screen, viewport, and client area. A + touch is reported by: a ``touchstart`` event with a unique ID for each + contact, zero-or-more ``touchmove`` events with that ID, and finally a + ``touchend`` event to signal the end of that specific contact. + + The API also has some amount of support for pen styluses, but it lacks + important features necessary to truly support them: hovering, pressure, + tilt, or multiple cursors like an erasure. Ultimately, its functionality + has been superceded by the newer "Pointer Events" API, discussed below. + + +**2016 - Precision Touchpads (Windows 10)** + + Early touchpads emulated a computer mouse by directly using the same IBM + PS/2 interface that most computer mice used and translating relative + movement of the user's finger into equivalent movements of a mouse on a + surface. + + As touchpad technology advanced and more powerful interface standards like + USB begun to take over the consumer market, touchpad vendors started adding + extra features to their hardware, like tap-to-click, tap-and-drag, and + tap-and-hold (to simulate a right click). These behaviors were implemented + by touchpad vendors either in hardware drivers and/or user mode "hooks" that + injected equivalent Win32 messages into the appropriate target. + + As expected, each touchpad vendor's driver had its own subtly-different + behavior from others, its own bugs, and its own negative interactions with + other software. + + During the later years of Windows 8, Microsoft and touchpad company + Synaptics co-developed the "Precision Touchpad" standard, which defines an + interface for touchpad hardware to report its physical measurements, + precision, and sensor configuration to Windows and allows it to deliver raw + touch data. Windows then interprets the data and generates gestures and + window messages in a standard way, removing the burden of implementing these + behaviors from the touchpad vendor and providing the OS with rich + information about the user's movements. + + It wasn't until the 2016 release of Windows 10 14946 that Microsoft would + support all the standard gestures through the new standard. Although + adoption by vendors has been a bit slow, the fact that + `it is a requirement for Windows 11 <https://pocketnow.com/all-windows-11-pcs-will-be-required-to-have-a-precision-touchpad-and-webcam/>`__ + means that vendor support for this standard is imminent. + + Unfortunately, there's a piece of bad news: Microsoft did not + implement the above "Unified Pointer API" for use with touchpads, as the + developers of Blender discovered when `they moved to the Pointer API <https://archive.blender.org/developer/D7660>`__. + Instead, Microsoft expects developers to either use DirectManipulation to + automatically get pan/zoom enabled for their app, or the RawInput API to + directly read touchpad data. + + +**2019 - Pointer Events (Web Standard)** + + "`Pointer Events <https://www.w3.org/TR/pointerevents/>`__" became a level 2 + W3C recommendation in April, 2019. They considered `the work done by Microsoft <https://www.w3.org/Submission/2012/SUBM-pointer-events-20120907/>`__ + as part of the design of their own Pointer API, and in many ways the W3C + standard resembles an improved, better specified, more consistent, and + easier-to-use version of the APIs provided by the Win32 subsystem. + + The Pointer Events API generalizes devices like touchscreens, mice, tablet + pens, VR controllers, etc. into a "thing that points". A pointer has + (optional) properties: a width and height (big for a finger, 1px for a + mouse), an amount of pressure, a tilt angle relative to the surface, some + buttons, etc. This helps applications maximize code reuse for handling + pointer input by having a common codebase written against these generalized + traits. If needed, the application may also have smaller, specialized + sections of code for each concrete pointer type. + + Certain types of pointers (like pens and touchscreens) have a behavior where + they are always "captured" by the first object that they interact with. For + example, if a user puts their finger on an empty part of a web page and + starts to scroll, their finger is now "captured" by the web page itself. + "Captured" means that even if their finger moves over an element in + the web page, that element will not receive events from the finger -- the + page itself will until the entire interaction stops. + + The events themselves very closely follow the Buxton Three-State Model + (discussed earlier), where ``pointerover/pointerout`` messages indicate + transitions from "Out of Range" to "Tracking" and visa-versa, and + ``pointerdown/pointerup`` messages transition between "Tracking" and + "Dragging". ``pointermove`` updates the position of the pointer, and a + special ``pointercancel`` message is sent to inform the page that the + browser is "cancelling" a ``pointerdown`` event because it has decided to + consume it for a gesture or because the operating system cancelled the + pointer for its own reasons. + + +CSS "interaction" Media Queries +========================================== + +(Note that this section is **not** about the `pointer-events <https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events>`__ +CSS property, which defines the circumstances where an element can be the target +of pointer events.) + +The W3C defines the interaction-related media queries in the +`Media Queries Level 4 - Interaction Media Features <https://www.w3.org/TR/mediaqueries-4/#mf-interaction>`__ +document. + +To summarize, the main interaction-related CSS Media Queries that Firefox must +support are ``pointer``, ``any-pointer``, ``hover`` and ``any-hover``. + + +``pointer`` + + Allows the webpage to query the existence of a pointing device on + the machine, and (if available) the assumed "pointing accuracy" of the + "primary" pointing device. The device considered "primary" on a machine with + multiple input devices is a policy decision that must be made by the web + browser; Windows simply provides the APIs to query information about + attached devices. + + The browser is expected to return one of three strings to this media query: + + ``none`` + + There is no pointing device attached to the computer. + + ``coarse`` + + The primary pointing device is capable of approximately + pointing at a relatively large target (like a finger on a + touchscreen). + + ``fine`` + + The primary pointing device is capable of near-pixel-level + accuracy (like a computer mouse or a tablet pen). + + +``any-pointer`` + + Similar to ``pointer``, but represents the union of + capabilities of all pointers attached to the system, such that the meanings + become: + + ``none`` + + There is no pointing device attached to the computer. + + ``coarse`` + + There is at-least one "coarse" pointer attached. + + ``fine`` + + There is at-least one "fine" pointer attached. + + +``hover`` + + Allows the webpage to query whether the primary pointer is + capable of "hovering" over top of elements on the page. Computer mice, + touchpad cursors, and higher-end pen tablets all support this, whereas + current touchscreens are "touch" or "no touch", and they cannot detect a + finger hovering over the screen. + + ``hover`` + + The primary pointer is capable of reporting hovering. + + ``none`` + + The primary pointer is not capable of reporting hovering. + +``any-hover`` + + Indicates whether any pointer attached to the system has the + ``hover`` capability. + + +Selection of the Primary Pointing Device +-------------------------------------------- + +To illustrate the complexity of this topic, consider the Microsoft Surface Pro. + +The Surface Pro has an advanced screen that is capable of receiving touch +input, but it can also behave like a pen digitizer and receive input from a +stylus with advanced pen capabilities, like hover sensing, pressure +sensitivity, multiple buttons, and even multiple "tips" (a pen and eraser end). + +In this case, what should Firefox consider the primary pointing device? + +Perhaps the user intends to use their Surface Pro like a touchscreen tablet, +at which point Firefox should report ``pointer: coarse`` and ``hover: none`` +capabilities. + +But what if, instead, the user wants to sketch art or take notes using a pen on +their Surface Pro? In this case, Firefox should be reporting ``pointer: fine`` +and ``hover: hover``. + +Imagine that the user then attaches the "keyboard + touchpad" cover attachment +to their Surface Pro; naturally, we will consider that the user's intent is for +the touchpad to become the primary pointing device, and so it is fairly clear +that we should return ``pointer: fine`` and ``hover: hover`` in this state. + +However, what if the user tucks the keyboard/touchpad attachment behind the +tablet and begins exclusively operating the device with their finger? + +This example shows that complex, multi-input machines can resist classification +and blur the lines between labels like "touch device", "laptop", "drawing +tablet", etc. It also illustrates that identifying the "primary" pointing +device using only machine configuration may yield unintuitive and suboptimal +results. + +While we can almost-certainly improve our hardware detection heuristics to +better answer this question (and we should, at the very least), perhaps it +makes more sense for Firefox to incorporate user intentions into the decision. +Intentions could be communicated directly by the user through some sort of +setting or indirectly through the user's actions. + +For example, if the user intends to draw on the screen with a pen, perhaps +Firefox provides something like a "drawing mode" that the user can toggle to +change the primary pointing device to the pen. Or perhaps it's better for +Firefox to interpret the mere fact of receiving pen input as evidence of the +user's intent and switch the reported primary pointing device automatically. + +If we wanted to switch automatically, there are predictable traps and pitfalls +we need to think about: we need to ensure that we don't create frustrating user +experiences where web pages may "pop" beneath the user suddenly, and +we should likely incorporate some kind of "settling time" so we don't +oscillate between devices. + +It's worth noting that Chromium doesn't seem to incorporate anything like +what's being suggested here, so if this is well-designed it may be an +opportunity for Firefox to try something novel. + + + + +================================================================================ +State of the Browser +================================================================================ + +Pan and Zoom, Inertia, Overscroll, and Elastic Bounce +========================================================= + +As can be seen in the videos below, Firefox's support for inertia, overscroll, +and elastic bounce works well on all platforms when a stylus pen is used +as the input device, and it also works just fine with the touchscreen on the +Dell XPS 15. However, it completely fails when the touchscreen is used on +the Microsoft Surface Pro. While more investigation is needed to completely +understand these issues, the fact that the correctly-behaving digitizing pens +use the Pointer API and the misbehaving input devices do not may be related. + +- `Video 1 <https://drive.google.com/file/d/1Z1QRSf2RluNhJwkKCzPb6-14vRtkqK8s/view?usp=sharing>`__ + showcasing overscroll and bounce not working on Surface Pro with touch, but + other devices/inputs are working + +- `Video 2 <https://drive.google.com/file/d/1bOgpVGBeZtwelvPJzYdA6uFRpubGtu4W/view?usp=sharing>`__ + showing that everything works just fine with an external Wacom digitizer + + +Pointer Media Queries +========================================================= + +**"any-pointer" Queries** + +Unlike the ``pointer`` media queries, which rely on the browser to make a policy +decision about what should be considered the "primary" pointer in a given +system configuration, the ``any-pointer`` queries are much more objective and +binary: the computer either has a type of device attached to it, or it +doesn't. + +**any-pointer: coarse** + +Firefox reports that there are "coarse" pointing devices present if either of +these two points is true: + +1. ``GetSystemMetrics(SM_DIGITIZER)`` reports that a device that supports + touch or pen is present. + +2. Based on heuristics, Firefox concludes that it is running on a computer it + considers a "tablet". + +Point #1 is incorrect, as a pen is not a "coarse" pointing device. Note that +this is a recent regression in `Bug 1811303 <https://bugzilla.mozilla.org/show_bug.cgi?id=1811303>`__ +that was uplifted to Firefox 112, so this actually regressed as this document +was being written! This is responsible for the incorrect "Windows 10 Desktop + +Wacom USB Tablet" issue in the table. + +Point #2 is a clear case of the `XY Problem <https://en.wikipedia.org/wiki/XY_problem>`__, +where Firefox is trying to determine if a coarse pointing device is present +by determining whether it is running on a tablet, when instead it should be +directly testing for coarse pointing devices (since, of course, those can exist +on machines that wouldn't normally be considered a "tablet"). This is +responsible for the incorrect "Windows 10 Dell XPS 15 (Touch Disabled) + Wacom +USB Tablet" issue in the table below. + +**any-pointer: fine** + +Firefox reports that there are "fine" pointing devices present if and only if +it detects a mouse. This is clearly already wrong. Firefox determines that the +computer has a mouse using the following algorithm: + +1. If ``GetSystemMetrics(SM_MOUSEPRESENT)`` returns false, report no mouse. + +2. If Firefox does not consider the current computer to be a tablet, report a + mouse if there is at-least one "mouse" device driver running on the + computer. + +3. If Firefox considers the current computer to be a tablet or a touch system, + only report a mouse if there are at-least two "mouse" device drivers + running. This exists because some tablet pens and touch digitizers report + themselves as computer mice. + +This algorithm also suffers from the XY problem -- Firefox is trying to +determine whether a fine pointing device exists by determining if there is +a computer mouse present, when instead it should be directly testing for +fine pointing devices, since mice are not the only fine pointing +devices. + +Because of this proxy question, this algorithm is completely dependent on any +attached fine pointing device (like a pen tablet) to report itself as a mouse. +Point #3 makes the problem even worse, because if a computer that resembles a +tablet fails to report its digitizers as mice, the algorithm will completely +ignore an actual computer mouse attached to the system because it expects two +of them to be reported! + +Unfortunately, the Surface Pro has both a pen digitizer and a touch digitizer, +and it reports neither as a mouse. As a result, this algorithm completely falls +apart on the Surface Pro, failing to report any "fine" pointing device even +when a computer mouse is plugged in, a pen is plugged in, or even when +the tablet is docked because its touchpad is only one mouse and it expects +at least two. + +This is also responsible for failing to report the trackpad on the Dell XPS 15 +as "fine", because the Dell XPS 15 has a touchscreen and therefore looks like +a "tablet", but doesn't report 2 mouse drivers. + +**any-pointer: hover** + + +Firefox reports that any device that is a "fine" pointer also supports "hover", +which does generally hold true, but isn't necessarily true for lower-end pens +that only support tapping. It would be better for Firefox to directly +query the operating system instead of just assuming. + +**"pointer" media query** + +As discussed previously at length, this media query relies on a "primary" +designation made by the browser. Below is the current algorithm used to +determine this: + +1. If the computer is considered a "tablet" (see below), report primary + pointer as "coarse" (this is clearly already the wrong behavior). + +2. Otherwise, if the computer has a mouse plugged in, report "fine". + +3. Otherwise, if the computer has a touchscreen or pen digitizer, report + "coarse" (this is wrong in the case of the digitizer). + +4. Otherwise, report "fine" (this is wrong; should report "None"). + +Firefox uses the following algorithm to determine if the computer is a +"tablet" for point #1 above: + +1. It is not a tablet if it's not at-least running Windows 8. + +2. If Windows "Tablet Mode" is enabled, it is a tablet no matter what. + +3. If no touch-capable digitizers are attached, it is not a tablet. + +4. If the system doesn't support auto-rotation, perhaps because it has + no rotation sensor, or perhaps because it's docked and operating in + "laptop mode" where rotation won't happen, it's not a tablet. + +5. If the vendor that made the computer reports to Windows that it supports + "convertible slate mode" and it is currently operating in "slate mode", + it's a tablet. + +6. Otherwise, it's not a tablet. + + +**Table with comparison to Chromium** + +The following table shows how Firefox and Chromium respond to various pointer +queries. The "any-pointer" and "any-hover" columns are not subjective and +therefore are always either green or red to indicate "pass" or "fail", but the +"pointer" and "hover" may also be yellow to indicate that it's "open to +interpretation" because of the aforementioned difficulty in determining the +"primary pointer". + +.. image:: touch_media_queries.png + :width: 100% + + +**Related Bugs** + +- Bug 1813979 - For Surface Pro media query "any-pointer: fine" is true only + when both the Type Cover and mouse are connected + +- Bug 1747942 - Incorrect CSS media query matches for pointer, any-pointer, + hover and any-hover on Surface Laptop + +- Bug 1528441 - @media (hover) and (any-hover) does not work on Firefox 64/65 + where certain dual inputs are present + +- Bug 1697294 - Content processes unable to detect Windows 10 Tablet Mode + +- Bug 1806259 - CSS media queries wrongly detect a Win10 desktop computer + with a mouse and a touchscreen, as a device with no mouse (hover: none) + and a touchscreen (pointer: coarse) + + +Web Events +===================== + +The pen stylus worked well on all tested systems -- The correct pointer events +were fired in the correct order, and mouse events were properly simulated in +case the default behavior was allowed. + +The touchscreen input was less reliable. On the Dell XPS 15, the +"Pointer Events" were flawless, but the "Touch Events" were missing +an important step: the ``touchstart`` and ``touchmove`` messages were sent just +fine, but Firefox never sends the ``touchend`` message! (Hopefully that isn't +too difficult to fix!) + +Unfortunately, everything really falls apart on the Surface Pro using the +touchscreen -- neither the "Pointer Events" nor the "Touch Events" fire at all! +Instead, the touch is completely absorbed by pan and zoom gestures, and nothing +is sent to the web page. The website's request for ``touch-action: none`` is +ignored, and the web page is never given any opportunity to call +``Event.preventDefault()`` to cancel the pan/zoom behavior. + + +Operating System Interfaces +================================ + +As was discussed above, Windows has multiple input APIs that were each +introduced in newer version of Windows to handle devices that were not +well-served by existing APIs. + +Backward compatibility with applications designed against older APIs is +realized when applications call the default event handler (``DefWindowProc``) +upon receiving an event type that they don't recognize (which is what apps have +always been instructed to do if they receive events they don't recognize). +The unrecognized newer events will be translated by the default event handler +into older events and sent back to the application. A very old application may +have this process repeat through several generations of APIs until it finally +sees events that it recognizes. + +Firefox currently uses a mix of the older and newer APIs, which complicates +the input handling logic and may be responsible for some of the +difficult-to-explain bugs that we see reported by users. + +Here is an explanation of the codepaths Firefox uses to handle pointer input: + +1. Firefox handles the ``WM_POINTER[LEAVE|DOWN|UP|UPDATE]`` messages if the + input device is a tablet pen and an Asynchronous Pan and Zoom (APZ) + compositor is available. Note that this already may not be ideal, as + Microsoft warns (`here <https://learn.microsoft.com/en-us/windows/win32/inputmsg/wm-pointercapturechanged>`__) + that handling some pointer messages and passing other pointer messages to + ``DefWindowProc`` has unspecified behavior (meaning that Win32 may do + something unexpected or nonsensical). + + If the above criteria aren't met, Firefox will call ``DefWindowProc``, which + will re-post the pointer messages as either touch messages or mouse + messages. + +2. If DirectManipulation is being used for APZ, it will output the + ``WM_POINTERCAPTURECHANGED`` if it detects a pan or zoom gesture it can + handle. It will then handle the rest of the gesture itself. + + DirectManipulation is used for all top-level and popup windows as long as + it isn't disabled via the ``apz.allow_zooming``, + ``apz.windows.use_direct_manipulation``, or + ``apz.windows.force_disable_direct_manipulation`` prefs. + +3. If the pointing device is touch, the next action depends on + whether an Asynchronous Pan and Zoom (APZ) compositor is available. If it + is, the window will have been registered using ``RegisterTouchWindow``, and + Firefox will receive ``WM_TOUCH`` messages, which will be sent to the + "Touch Event" API and handled directly by the APZ compositor. + + If there is no APZ compositor, it will instead be received as a + ``WM_GESTURE`` message or a mouse message, depending on the movement. Note + that these will be more basic gestures, like tap-and-hold. + +4. If none of the above apply, the message will be converted into standard + ``WM_MOUSExxx`` messages via a call to ``DefWindowProc``. + + +================================================================================ +Discussion +================================================================================ + +Here is where some of the outstanding thoughts or questions can be listed. +This can be updated as more questions come about and (hopefully) as answers to +questions become apparent. + +CSS "pointer" Media Queries +=============================== + +- The logic for the ``any-pointer`` and ``any-hover`` queries are objectively + incorrect and should be rewritten altogether. That is not as + big of a job as it sounds, as the code is fairly straightforward and + self-contained. (Note: Improvements have already been made in + `Bug 1813979 <https://bugzilla.mozilla.org/show_bug.cgi?id=1813979>`__) + +- There are a few behaviors for ``pointer`` and ``hover`` that are + objectively wrong (such as reporting a ``coarse`` pointer when the + Surface Pro is docked with a touchpad). Those should be fixable with a + code change similar to the previous bullet. + +- Do we want to continue to use only machine configuration to decide what + the "primary" pointer is, or do we also want to incorporate user intent + into the algorithm? Or, alternatively: + + 1. Do we create a way for the user to override? For example, a "Drawing + Mode" button if a tablet digitizer is sensed. + + 2. Do we attempt to change automatically in response to user action? + + - An example was used above of a docked Surface Pro computer, where + the user may use the keyboard and touchpad for a while, then perhaps + tuck that behind and use the device as a touchscreen, and then + perhaps draw on it with a tablet stylus. + + - We would need to be careful to avoid careless "popping" or + "oscillating" if we react too quickly to changing input types. + +- On a separate-but-related note, the `W3C suggested <https://www.w3.org/TR/mediaqueries-5/#descdef-media-pointer>`__ + that it might be beneficial to allow users to at-least disable all + reporting of ``fine`` pointing devices for users who may have a disability + that prevents them from being able to click small objects, even with a fine + pointing device. + + +Pan-and-Zoom, Inertia, Overscroll, and Elastic Bounce +========================================================= + +- Inertia, overscroll, and elastic bounce are just plain broken on the + Surface Pro. That should definitely be investigated. + +- We can see from the video below that Microsoft Edge has quite a bit more + overscroll and a more elastic bounce than Firefox does, and it also + allows elastic bounce in directions that the page itself doesn't scroll. + + Edge's way seems more similar to the user experience I'd expect from using + Firefox on an iPhone or Android device. Perhaps we should consider + following suit? + + (`Link to video <https://drive.google.com/file/d/14XVLT6CNn2RaXcHHCRIrQmRwoMYjj6fu/view?usp=sharing>`__) + + +Web Events +============== + +- It's worth investigating why the ``touchend`` message never seems + to be sent by Firefox on any tested devices. + +- It's very disappointing that neither the Pointer Events API nor the + Touch Events API works at all on Firefox on the Surface Pro. That should + be investigated very soon! + + +Operating System Interfaces +================================ + +- With the upcoming sun-setting of Windows 7 support, Firefox has an + opportunity to revisit the implementation of our input handling and try to + simplify our codepaths and eliminate some of the workarounds that exist to + handle some of these complex interactions, as well as fix entire classes of + bugs - both reported and unreported - that currently exist as a result. + +- Does it make sense to combine the touchscreen and pen handling together + and use the ``WM_POINTERXXX`` messages for both? + + - This would eliminate the need to handle the ``WM_TOUCH`` and + ``WM_GESTURE`` messages at all. + + - Note that there is precedent for this, as `GTK <https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/1563>`__ + has already done so. It appears that `Blender <https://archive.blender.org/developer/D7660>`__ + has plans to move toward this as well. + + - Tablet pens seemed to do very well in most of the testing, + and they are also the part of the code that mainly exercises the + ``WM_POINTERXXX`` codepaths. That may imply increased reliability in + that codepath? + + - The Pointer APIs also have good device simulation for integration + testing. + + - Would we also want to roll mouse handling into it using the + `EnableMouseInPointer <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enablemouseinpointer>` __ + call? That would allow us to also get rid of handling + ``WM_MOUSE[MOVE/WHEEL/HWHEEL]`` and ``WM_[LRM]BUTTON[UP|DOWN]`` + messages. Truly one codepath (with a few minor branches) to rule them + all! + + - Nick Rishel sent `this link <http://the-witness.net/news/2012/10/wm_touch-is-totally-bananas/>`__ + that details the troubles that the developers of The Witness (a video + game) ran into when using the ``WM_TOUCH`` API. It argues that the API + is poorly-designed, and advises that if Windows 7 support is not + needed, the API should be avoided. + +- Should we exclusively use DirectManipulation for Pan/Zoom? + + - Multitouch touchpads bypass all of the ``WM_POINTER`` machinery + for anything gesture-related and directly send their messages to + DirectManipulation. We then "capture" all the DirectManipulation events + and pump them into our events pipeline, as explained above. + + - DirectManipulation also handles "overscroll + elastic bounce" in a way + that aligns with Windows look-and-feel. + + - Perhaps it makes sense to just use DirectManipulation for all APZ + handling and eliminate any attempt at handling this through other + codepaths. + +High-Frequency Input +================================ + +"High-Frequency Input" refers to the ability for an app to be able to still +perceive input events despite them happening at a rate faster than the app +itself actually handles them. + +Consider a mouse that moves through several points: "A->B->C->D->E". If the +application processes input when the mouse is at "A" and doesn't poll again +until the mouse is at point "E", the default behavior of all modern operating +systems is to "coalesce" these events and simply report "A->E". This is fine +for the majority of use cases, but certain workloads (such as digital +handwriting and video games) can benefit from knowing the complete path that +was taken to get from the start point to the end point. + +Generally, solutions to this involve the operating system keeping a history of +pointer movements that can be retrieved through an API. For example, +Android provides the `MotionEvent <https://developer.android.com/reference/android/view/MotionEvent.html>`__ +API that batches historal movements. + +Unfortunately, the APIs to do this in Windows are terribly broken. As +`this blog <https://blog.getpaint.net/2019/11/14/paint-net-4-2-6-alpha-build-7258/>`__ +makes clear, `GetMouseMovePointsEx <https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmousemovepointsex>`__ +has so many issues that they had to remove its usage from their program because +of the burden. That same blog entry also details that the newer Pointer API has +the `GetPointerInfoHistory <https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getpointerinfohistory>`__ +that is *supposed* to support tracking pointer history, but it only ever tracks +a single entry! + +Perhaps luckily, there is currently no web standard for high-frequency input, +although it `has been asked about in the past <https://lists.w3.org/Archives/Public/public-pointer-events/2014AprJun/0057.html>`__. + +If such a standard was ever created, it would likely be very difficult for +Firefox on Windows to support it. + + +DirectManipulation and Pens +============================= + +- This is a todo item, but it needs to be investigated whether or not + DirectManipulation can directly scoop up pen input, or whether it has + to be handled by the application (and forwarded to DM if desired). diff --git a/widget/windows/docs/windows-pointing-device/mouse.jpg b/widget/windows/docs/windows-pointing-device/mouse.jpg Binary files differnew file mode 100644 index 0000000000..c4fca9ba31 --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/mouse.jpg diff --git a/widget/windows/docs/windows-pointing-device/touch_media_queries.png b/widget/windows/docs/windows-pointing-device/touch_media_queries.png Binary files differnew file mode 100644 index 0000000000..f0de661d7f --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/touch_media_queries.png diff --git a/widget/windows/docs/windows-pointing-device/touchpad.jpg b/widget/windows/docs/windows-pointing-device/touchpad.jpg Binary files differnew file mode 100644 index 0000000000..1327ec5b1c --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/touchpad.jpg diff --git a/widget/windows/docs/windows-pointing-device/touchscreen.jpg b/widget/windows/docs/windows-pointing-device/touchscreen.jpg Binary files differnew file mode 100644 index 0000000000..90246ca02e --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/touchscreen.jpg diff --git a/widget/windows/docs/windows-pointing-device/trackpoint.jpg b/widget/windows/docs/windows-pointing-device/trackpoint.jpg Binary files differnew file mode 100644 index 0000000000..9eae5b5c21 --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/trackpoint.jpg diff --git a/widget/windows/docs/windows-pointing-device/vrcontroller.jpg b/widget/windows/docs/windows-pointing-device/vrcontroller.jpg Binary files differnew file mode 100644 index 0000000000..20f2e90bfe --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/vrcontroller.jpg diff --git a/widget/windows/docs/windows-pointing-device/wacom_tablet.png b/widget/windows/docs/windows-pointing-device/wacom_tablet.png Binary files differnew file mode 100644 index 0000000000..2bc30a3b4f --- /dev/null +++ b/widget/windows/docs/windows-pointing-device/wacom_tablet.png diff --git a/widget/windows/filedialog/PWinFileDialog.ipdl b/widget/windows/filedialog/PWinFileDialog.ipdl new file mode 100644 index 0000000000..812db7e103 --- /dev/null +++ b/widget/windows/filedialog/PWinFileDialog.ipdl @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et ft=ipdl : */ +/* 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 WinFileDialogCommandsDefn; +using mozilla::WindowsHandle from "mozilla/ipc/IPCTypes.h"; +using mozilla::widget::filedialog::FileDialogType from "mozilla/widget/filedialog/WinFileDialogCommands.h"; + +namespace mozilla { +namespace widget { +namespace filedialog { + +[ChildProc=Utility] +protocol PWinFileDialog { + +child: + // Exactly one Show function should be called per instance. Further calls will + // result in IPC failure. + // + // Each will return `Nothing` iff the operation was canceled by the user. + + async ShowFileDialog(WindowsHandle parentHwnd, FileDialogType type, Command[] commands) + returns (Results? results); + async ShowFolderDialog(WindowsHandle parentHwnd, Command[] commands) + returns (nsString? path); +}; + +} // namespace filedialog +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/filedialog/WinFileDialogChild.cpp b/widget/windows/filedialog/WinFileDialogChild.cpp new file mode 100644 index 0000000000..1a2903f8ec --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogChild.cpp @@ -0,0 +1,110 @@ +/* -*- 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 "mozilla/widget/filedialog/WinFileDialogChild.h" + +#include <combaseapi.h> +#include <objbase.h> +#include <shobjidl.h> + +#include "mozilla/Assertions.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/widget/filedialog/WinFileDialogCommands.h" +#include "nsPrintfCString.h" + +namespace mozilla::widget::filedialog { + +/* extern */ mozilla::LazyLogModule sLogFileDialog("FileDialog"); + +WinFileDialogChild::WinFileDialogChild() { + MOZ_LOG(sLogFileDialog, LogLevel::Info, ("%s %p", __PRETTY_FUNCTION__, this)); +}; + +WinFileDialogChild::~WinFileDialogChild() { + MOZ_LOG(sLogFileDialog, LogLevel::Info, ("%s %p", __PRETTY_FUNCTION__, this)); +}; + +#define MOZ_ABORT_IF_ALREADY_USED() \ + do { \ + MOZ_RELEASE_ASSERT( \ + !mUsed, "called Show* twice on a single WinFileDialog instance"); \ + MOZ_LOG( \ + sLogFileDialog, LogLevel::Info, \ + ("%s %p: first call to a Show* function", __PRETTY_FUNCTION__, this)); \ + mUsed = true; \ + } while (0) + +template <size_t N> +WinFileDialogChild::IPCResult WinFileDialogChild::MakeIpcFailure( + HRESULT hr, const char (&what)[N]) { + // The crash-report annotator stringifies integer values anyway. We do so + // eagerly here to avoid questions about C int/long conversion semantics. + nsPrintfCString data("%lu", hr); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::WindowsFileDialogErrorCode, data); + + return IPC_FAIL(this, what); +} + +#define MOZ_IPC_ENSURE_HRESULT_OK(hr, what) \ + do { \ + MOZ_LOG(sLogFileDialog, LogLevel::Verbose, \ + ("checking HRESULT for %s", what)); \ + HRESULT const _hr_ = (hr); \ + if (FAILED(_hr_)) { \ + MOZ_LOG(sLogFileDialog, LogLevel::Error, \ + ("HRESULT %8lX while %s", (hr), (what))); \ + return MakeIpcFailure(_hr_, (what)); \ + } \ + } while (0) + +WinFileDialogChild::IPCResult WinFileDialogChild::RecvShowFileDialog( + uintptr_t parentHwnd, FileDialogType type, nsTArray<Command> commands, + FileResolver&& resolver) { + MOZ_ABORT_IF_ALREADY_USED(); + + SpawnFilePicker(HWND(parentHwnd), type, std::move(commands)) + ->Then( + GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__, + [resolver = std::move(resolver)](Maybe<Results> const& res) { + resolver(res); + }, + [self = RefPtr(this)](HRESULT hr) { + // this doesn't need to be returned anywhere; it'll crash the + // process as a side effect of construction + self->MakeIpcFailure(hr, "SpawnFilePicker"); + }); + + return IPC_OK(); +} + +WinFileDialogChild::IPCResult WinFileDialogChild::RecvShowFolderDialog( + uintptr_t parentHwnd, nsTArray<Command> commands, + FolderResolver&& resolver) { + MOZ_ABORT_IF_ALREADY_USED(); + + SpawnFolderPicker(HWND(parentHwnd), std::move(commands)) + ->Then( + GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__, + [resolver = std::move(resolver)](Maybe<nsString> const& res) { + resolver(res); + }, + [self = RefPtr(this), resolver](HRESULT hr) { + // this doesn't need to be returned anywhere; it'll crash the + // process as a side effect of construction + self->MakeIpcFailure(hr, "SpawnFolderPicker"); + }); + + return IPC_OK(); +} + +#undef MOZ_IPC_ENSURE_HRESULT_OK + +void WinFileDialogChild::ProcessingError(Result aCode, const char* aReason) { + detail::LogProcessingError(sLogFileDialog, this, aCode, aReason); +} + +} // namespace mozilla::widget::filedialog diff --git a/widget/windows/filedialog/WinFileDialogChild.h b/widget/windows/filedialog/WinFileDialogChild.h new file mode 100644 index 0000000000..b0939ce2ed --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogChild.h @@ -0,0 +1,52 @@ +/* -*- 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 widget_windows_filedialog_WinFileDialogChild_h__ +#define widget_windows_filedialog_WinFileDialogChild_h__ + +#include "mozilla/widget/filedialog/PWinFileDialogChild.h" + +// forward declaration of native Windows interface-struct +struct IFileDialog; + +namespace mozilla::widget::filedialog { + +class WinFileDialogChild : public PWinFileDialogChild { + public: + using Command = mozilla::widget::filedialog::Command; + using IPCResult = mozilla::ipc::IPCResult; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinFileDialogChild, override); + + WinFileDialogChild(); + + public: + using FileResolver = PWinFileDialogChild::ShowFileDialogResolver; + IPCResult RecvShowFileDialog(uintptr_t parentHwnd, FileDialogType, + nsTArray<Command>, FileResolver&&); + + using FolderResolver = PWinFileDialogChild::ShowFolderDialogResolver; + IPCResult RecvShowFolderDialog(uintptr_t parentHwnd, nsTArray<Command>, + FolderResolver&&); + + private: + ~WinFileDialogChild(); + + void ProcessingError(Result aCode, const char* aReason) override; + + // Defined and used only in WinFileDialogChild.cpp. + template <size_t N> + IPCResult MakeIpcFailure(HRESULT hr, const char (&what)[N]); + + // This flag properly _should_ be static (_i.e._, per-process) rather than + // per-instance; but we can't presently instantiate two separate utility + // processes with the same sandbox type, so we have to reuse the existing + // utility process if there is one. + bool mUsed = false; +}; + +} // namespace mozilla::widget::filedialog + +#endif // widget_windows_filedialog_WinFileDialogChild_h__ diff --git a/widget/windows/filedialog/WinFileDialogCommands.cpp b/widget/windows/filedialog/WinFileDialogCommands.cpp new file mode 100644 index 0000000000..f0503ab8f0 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogCommands.cpp @@ -0,0 +1,460 @@ +/* -*- 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 "mozilla/widget/filedialog/WinFileDialogCommands.h" + +#include <type_traits> +#include <shobjidl.h> +#include <shtypes.h> +#include <winerror.h> +#include "WinUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "mozilla/mscom/ApartmentRegion.h" +#include "nsThreadUtils.h" + +namespace mozilla::widget::filedialog { + +// Visitor to apply commands to the dialog. +struct Applicator { + IFileDialog* dialog = nullptr; + + HRESULT Visit(Command const& c) { + switch (c.type()) { + default: + case Command::T__None: + return E_INVALIDARG; + + case Command::TSetOptions: + return Apply(c.get_SetOptions()); + case Command::TSetTitle: + return Apply(c.get_SetTitle()); + case Command::TSetOkButtonLabel: + return Apply(c.get_SetOkButtonLabel()); + case Command::TSetFolder: + return Apply(c.get_SetFolder()); + case Command::TSetFileName: + return Apply(c.get_SetFileName()); + case Command::TSetDefaultExtension: + return Apply(c.get_SetDefaultExtension()); + case Command::TSetFileTypes: + return Apply(c.get_SetFileTypes()); + case Command::TSetFileTypeIndex: + return Apply(c.get_SetFileTypeIndex()); + } + } + + HRESULT Apply(SetOptions const& c) { return dialog->SetOptions(c.options()); } + HRESULT Apply(SetTitle const& c) { return dialog->SetTitle(c.title().get()); } + HRESULT Apply(SetOkButtonLabel const& c) { + return dialog->SetOkButtonLabel(c.label().get()); + } + HRESULT Apply(SetFolder const& c) { + RefPtr<IShellItem> folder; + if (SUCCEEDED(SHCreateItemFromParsingName( + c.path().get(), nullptr, IID_IShellItem, getter_AddRefs(folder)))) { + return dialog->SetFolder(folder); + } + // graciously accept that the provided path may have been nonsense + return S_OK; + } + HRESULT Apply(SetFileName const& c) { + return dialog->SetFileName(c.filename().get()); + } + HRESULT Apply(SetDefaultExtension const& c) { + return dialog->SetDefaultExtension(c.extension().get()); + } + HRESULT Apply(SetFileTypes const& c) { + std::vector<COMDLG_FILTERSPEC> vec; + for (auto const& filter : c.filterList()) { + vec.push_back( + {.pszName = filter.name().get(), .pszSpec = filter.spec().get()}); + } + return dialog->SetFileTypes(vec.size(), vec.data()); + } + HRESULT Apply(SetFileTypeIndex const& c) { + return dialog->SetFileTypeIndex(c.index()); + } +}; + +namespace { +static HRESULT GetShellItemPath(IShellItem* aItem, nsString& aResultString) { + NS_ENSURE_TRUE(aItem, E_INVALIDARG); + + mozilla::UniquePtr<wchar_t, CoTaskMemFreeDeleter> str; + HRESULT const hr = + aItem->GetDisplayName(SIGDN_FILESYSPATH, getter_Transfers(str)); + if (SUCCEEDED(hr)) { + aResultString.Assign(str.get()); + } + return hr; +} +} // namespace + +#define MOZ_ENSURE_HRESULT_OK(call_) \ + do { \ + HRESULT const _tmp_hr_ = (call_); \ + if (FAILED(_tmp_hr_)) return Err(_tmp_hr_); \ + } while (0) + +mozilla::Result<RefPtr<IFileDialog>, HRESULT> MakeFileDialog( + FileDialogType type) { + RefPtr<IFileDialog> dialog; + + CLSID const clsid = type == FileDialogType::Open ? CLSID_FileOpenDialog + : CLSID_FileSaveDialog; + HRESULT const hr = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, + IID_IFileDialog, getter_AddRefs(dialog)); + MOZ_ENSURE_HRESULT_OK(hr); + + return std::move(dialog); +} + +HRESULT ApplyCommands(::IFileDialog* dialog, + nsTArray<Command> const& commands) { + Applicator applicator{.dialog = dialog}; + for (auto const& cmd : commands) { + HRESULT const hr = applicator.Visit(cmd); + if (FAILED(hr)) { + return hr; + } + } + return S_OK; +} + +mozilla::Result<Results, HRESULT> GetFileResults(::IFileDialog* dialog) { + FILEOPENDIALOGOPTIONS fos; + MOZ_ENSURE_HRESULT_OK(dialog->GetOptions(&fos)); + + using widget::WinUtils; + + // Extract which filter type the user selected + UINT index; + MOZ_ENSURE_HRESULT_OK(dialog->GetFileTypeIndex(&index)); + + // single selection + if ((fos & FOS_ALLOWMULTISELECT) == 0) { + RefPtr<IShellItem> item; + MOZ_ENSURE_HRESULT_OK(dialog->GetResult(getter_AddRefs(item))); + if (!item) { + return Err(E_FAIL); + } + + nsAutoString path; + MOZ_ENSURE_HRESULT_OK(GetShellItemPath(item, path)); + + return Results({path}, index); + } + + // multiple selection + RefPtr<IFileOpenDialog> openDlg; + dialog->QueryInterface(IID_IFileOpenDialog, getter_AddRefs(openDlg)); + if (!openDlg) { + MOZ_ASSERT(false, "a file-save dialog was given FOS_ALLOWMULTISELECT?"); + return Err(E_UNEXPECTED); + } + + RefPtr<IShellItemArray> items; + MOZ_ENSURE_HRESULT_OK(openDlg->GetResults(getter_AddRefs(items))); + if (!items) { + return Err(E_FAIL); + } + + nsTArray<nsString> paths; + + DWORD count = 0; + MOZ_ENSURE_HRESULT_OK(items->GetCount(&count)); + for (DWORD idx = 0; idx < count; idx++) { + RefPtr<IShellItem> item; + MOZ_ENSURE_HRESULT_OK(items->GetItemAt(idx, getter_AddRefs(item))); + + nsAutoString str; + MOZ_ENSURE_HRESULT_OK(GetShellItemPath(item, str)); + + paths.EmplaceBack(str); + } + + return Results(std::move(paths), std::move(index)); +} + +mozilla::Result<nsString, HRESULT> GetFolderResults(::IFileDialog* dialog) { + RefPtr<IShellItem> item; + MOZ_ENSURE_HRESULT_OK(dialog->GetResult(getter_AddRefs(item))); + if (!item) { + // shouldn't happen -- probably a precondition failure on our part, but + // might be due to misbehaving shell extensions? + MOZ_ASSERT(false, + "unexpected lack of item: was `Show`'s return value checked?"); + return Err(E_FAIL); + } + + // If the user chose a Win7 Library, resolve to the library's + // default save folder. + RefPtr<IShellLibrary> shellLib; + RefPtr<IShellItem> folderPath; + MOZ_ENSURE_HRESULT_OK( + CoCreateInstance(CLSID_ShellLibrary, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLibrary, getter_AddRefs(shellLib))); + + if (shellLib && SUCCEEDED(shellLib->LoadLibraryFromItem(item, STGM_READ)) && + SUCCEEDED(shellLib->GetDefaultSaveFolder(DSFT_DETECT, IID_IShellItem, + getter_AddRefs(folderPath)))) { + item.swap(folderPath); + } + + // get the folder's file system path + nsAutoString str; + MOZ_ENSURE_HRESULT_OK(GetShellItemPath(item, str)); + return str; +} + +#undef MOZ_ENSURE_HRESULT_OK + +namespace detail { +void LogProcessingError(LogModule* aModule, ipc::IProtocol* aCaller, + ipc::HasResultCodes::Result aCode, + const char* aReason) { + LogLevel const level = [&]() { + switch (aCode) { + case ipc::HasResultCodes::MsgProcessed: + // Normal operation. (We probably never actually get this code.) + return LogLevel::Verbose; + + case ipc::HasResultCodes::MsgDropped: + return LogLevel::Verbose; + + default: + return LogLevel::Error; + } + }(); + + // Processing errors are sometimes unhelpfully formatted. We can't fix that + // directly because the unhelpful formatting has made its way to telemetry + // (table `telemetry.socorro_crash`, column `ipc_channel_error`) and is being + // aggregated on. :( + nsCString reason(aReason); + if (reason.Last() == '\n') { + reason.Truncate(reason.Length() - 1); + } + + if (MOZ_LOG_TEST(aModule, level)) { + const char* const side = [&]() { + switch (aCaller->GetSide()) { + case ipc::ParentSide: + return "parent"; + case ipc::ChildSide: + return "child"; + case ipc::UnknownSide: + return "unknown side"; + default: + return "<illegal value>"; + } + }(); + + const char* const errorStr = [&]() { + switch (aCode) { + case ipc::HasResultCodes::MsgProcessed: + return "Processed"; + case ipc::HasResultCodes::MsgDropped: + return "Dropped"; + case ipc::HasResultCodes::MsgNotKnown: + return "NotKnown"; + case ipc::HasResultCodes::MsgNotAllowed: + return "NotAllowed"; + case ipc::HasResultCodes::MsgPayloadError: + return "PayloadError"; + case ipc::HasResultCodes::MsgProcessingError: + return "ProcessingError"; + case ipc::HasResultCodes::MsgRouteError: + return "RouteError"; + case ipc::HasResultCodes::MsgValueError: + return "ValueError"; + default: + return "<illegal error type>"; + } + }(); + + MOZ_LOG(aModule, level, + ("%s [%s]: IPC error (%s): %s", aCaller->GetProtocolName(), side, + errorStr, reason.get())); + } + + if (level == LogLevel::Error) { + // kill the child process... + if (aCaller->GetSide() == ipc::ParentSide) { + // ... which isn't us + ipc::UtilityProcessManager::GetSingleton()->CleanShutdown( + ipc::SandboxingKind::WINDOWS_FILE_DIALOG); + } else { + // ... which (presumably) is us + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::ipc_channel_error, reason); + + MOZ_CRASH("IPC error"); + } + } +} + +// Given a (synchronous) Action returning a Result<T, HRESULT>, perform that +// action on a new single-purpose "File Dialog" thread, with COM initialized as +// STA. (The thread will be destroyed afterwards.) +// +// Returns a Promise which will resolve to T (if the action returns Ok) or +// reject with an HRESULT (if the action either returns Err or couldn't be +// performed). +template <typename Res, typename Action, size_t N> +RefPtr<Promise<Res>> SpawnFileDialogThread(const char (&where)[N], + Action action) { + RefPtr<nsIThread> thread; + { + nsresult rv = NS_NewNamedThread("File Dialog", getter_AddRefs(thread), + nullptr, {.isUiThread = true}); + if (NS_FAILED(rv)) { + return Promise<Res>::CreateAndReject((HRESULT)rv, where); + } + } + // `thread` is single-purpose, and should not perform any additional work + // after `action`. Shut it down after we've dispatched that. + auto close_thread_ = MakeScopeExit([&]() { + auto const res = thread->AsyncShutdown(); + static_assert( + std::is_same_v<uint32_t, std::underlying_type_t<decltype(res)>>); + if (NS_FAILED(res)) { + MOZ_LOG(sLogFileDialog, LogLevel::Warning, + ("thread->AsyncShutdown() failed: res=0x%08" PRIX32, + static_cast<uint32_t>(res))); + } + }); + + // our eventual return value + RefPtr promise = MakeRefPtr<typename Promise<Res>::Private>(where); + + // alias to reduce indentation depth + auto const dispatch = [&](auto closure) { + return thread->DispatchToQueue( + NS_NewRunnableFunction(where, std::move(closure)), + mozilla::EventQueuePriority::Normal); + }; + + dispatch([thread, promise, where, action = std::move(action)]() { + // Like essentially all COM UI components, the file dialog is STA: it must + // be associated with a specific thread to create its HWNDs and receive + // messages for them. If it's launched from a thread in the multithreaded + // apartment (including via implicit MTA), COM will proxy out to the + // process's main STA thread, and the file-dialog's modal loop will run + // there. + // + // This of course would completely negate any point in using a separate + // thread, since behind the scenes the dialog would still be running on the + // process's main thread. In particular, under that arrangement, file + // dialogs (and other nested modal loops, like those performed by + // `SpinEventLoopUntil`) will resolve in strictly LIFO order, effectively + // remaining suspended until all later modal loops resolve. + // + // To avoid this, we initialize COM as STA, so that it (rather than the main + // STA thread) is the file dialog's "home" thread and the IFileDialog's home + // apartment. + + mozilla::mscom::STARegion staRegion; + if (!staRegion) { + MOZ_LOG(sLogFileDialog, LogLevel::Error, + ("COM init failed on file dialog thread: hr = %08lx", + staRegion.GetHResult())); + + APTTYPE at; + APTTYPEQUALIFIER atq; + HRESULT const hr = ::CoGetApartmentType(&at, &atq); + MOZ_LOG(sLogFileDialog, LogLevel::Error, + (" current COM apartment state: hr = %08lX, APTTYPE = " + "%08X, APTTYPEQUALIFIER = %08X", + hr, at, atq)); + + // If this happens in the utility process, crash so we learn about it. + // (TODO: replace this with a telemetry ping.) + if (!XRE_IsParentProcess()) { + // Preserve relevant data on the stack for later analysis. + std::tuple volatile info{staRegion.GetHResult(), hr, at, atq}; + MOZ_CRASH("Could not initialize COM STA in utility process"); + } + + // If this happens in the parent process, don't crash; just fall back to a + // nested modal loop. This isn't ideal, but it will probably still work + // well enough for the common case, wherein no other modal loops are + // active. + // + // (TODO: replace this with a telemetry ping, too.) + } + + // Actually invoke the action and report the result. + Result<Res, HRESULT> val = action(); + if (val.isErr()) { + promise->Reject(val.unwrapErr(), where); + } else { + promise->Resolve(val.unwrap(), where); + } + }); + + return promise; +} + +// For F returning `Result<T, E>`, yields the type `T`. +template <typename F, typename... Args> +using inner_result_of = + typename std::remove_reference_t<decltype(std::declval<F>()( + std::declval<Args>()...))>::ok_type; + +template <typename ExtractorF, + typename RetT = inner_result_of<ExtractorF, IFileDialog*>> +auto SpawnPickerT(HWND parent, FileDialogType type, ExtractorF&& extractor, + nsTArray<Command> commands) -> RefPtr<Promise<Maybe<RetT>>> { + return detail::SpawnFileDialogThread<Maybe<RetT>>( + __PRETTY_FUNCTION__, + [=, commands = std::move(commands)]() -> Result<Maybe<RetT>, HRESULT> { + // On Win10, the picker doesn't support per-monitor DPI, so we create it + // with our context set temporarily to system-dpi-aware. + WinUtils::AutoSystemDpiAware dpiAwareness; + + RefPtr<IFileDialog> dialog; + MOZ_TRY_VAR(dialog, MakeFileDialog(type)); + + if (HRESULT const rv = ApplyCommands(dialog, commands); FAILED(rv)) { + return mozilla::Err(rv); + } + + if (HRESULT const rv = dialog->Show(parent); FAILED(rv)) { + if (rv == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + return Result<Maybe<RetT>, HRESULT>(Nothing()); + } + return mozilla::Err(rv); + } + + RetT res; + MOZ_TRY_VAR(res, extractor(dialog.get())); + + return Some(res); + }); +} + +} // namespace detail + +RefPtr<Promise<Maybe<Results>>> SpawnFilePicker(HWND parent, + FileDialogType type, + nsTArray<Command> commands) { + return detail::SpawnPickerT(parent, type, GetFileResults, + std::move(commands)); +} + +RefPtr<Promise<Maybe<nsString>>> SpawnFolderPicker(HWND parent, + nsTArray<Command> commands) { + return detail::SpawnPickerT(parent, FileDialogType::Open, GetFolderResults, + std::move(commands)); +} + +} // namespace mozilla::widget::filedialog diff --git a/widget/windows/filedialog/WinFileDialogCommands.h b/widget/windows/filedialog/WinFileDialogCommands.h new file mode 100644 index 0000000000..ca4561a8f2 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogCommands.h @@ -0,0 +1,74 @@ +/* -*- 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 widget_windows_filedialog_WinFileDialogCommands_h__ +#define widget_windows_filedialog_WinFileDialogCommands_h__ + +#include "ipc/EnumSerializer.h" +#include "mozilla/Logging.h" +#include "mozilla/MozPromise.h" +#include "mozilla/ipc/MessageLink.h" +#include "mozilla/widget/filedialog/WinFileDialogCommandsDefn.h" + +// Windows interface types, defined in <shobjidl.h> +struct IFileDialog; +struct IFileOpenDialog; + +namespace mozilla::widget::filedialog { + +extern LazyLogModule sLogFileDialog; + +enum class FileDialogType : uint8_t { Open, Save }; + +// Create a file-dialog of the relevant type. Requires MSCOM to be initialized. +mozilla::Result<RefPtr<IFileDialog>, HRESULT> MakeFileDialog(FileDialogType); + +// Apply the selected commands to the IFileDialog, in preparation for showing +// it. (The actual showing step is left to the caller.) +[[nodiscard]] HRESULT ApplyCommands(::IFileDialog*, + nsTArray<Command> const& commands); + +// Extract one or more results from the file-picker dialog. +// +// Requires that Show() has been called and has returned S_OK. +mozilla::Result<Results, HRESULT> GetFileResults(::IFileDialog*); + +// Extract the chosen folder from the folder-picker dialog. +// +// Requires that Show() has been called and has returned S_OK. +mozilla::Result<nsString, HRESULT> GetFolderResults(::IFileDialog*); + +namespace detail { +// Log the error. If it's a notable error, kill the child process. +void LogProcessingError(LogModule* aModule, ipc::IProtocol* aCaller, + ipc::HasResultCodes::Result aCode, const char* aReason); + +} // namespace detail + +template <typename R> +using Promise = MozPromise<R, HRESULT, true>; + +// Show a file-picker on another thread in the current process. +RefPtr<Promise<Maybe<Results>>> SpawnFilePicker(HWND parent, + FileDialogType type, + nsTArray<Command> commands); + +// Show a folder-picker on another thread in the current process. +RefPtr<Promise<Maybe<nsString>>> SpawnFolderPicker(HWND parent, + nsTArray<Command> commands); + +} // namespace mozilla::widget::filedialog + +namespace IPC { +template <> +struct ParamTraits<mozilla::widget::filedialog::FileDialogType> + : public ContiguousEnumSerializerInclusive< + mozilla::widget::filedialog::FileDialogType, + mozilla::widget::filedialog::FileDialogType::Open, + mozilla::widget::filedialog::FileDialogType::Save> {}; +} // namespace IPC + +#endif // widget_windows_filedialog_WinFileDialogCommands_h__ diff --git a/widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh b/widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh new file mode 100644 index 0000000000..dd85942f24 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogCommandsDefn.ipdlh @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et ft=ipdl : */ +/* 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/. */ + +namespace mozilla { +namespace widget { +namespace filedialog { + +// Commands corresponding to the various functions in IFileDialog (or at least +// the ones we actually make use of). +// +// All commands' semantics are direct parallels of their equivalently-named +// functions on IFileDialog, with the only changes being those necessary to use +// IPDLable representation-datatypes. (Thus, e.g., `SetOptions` effectively +// takes a `FILEOPENDIALOGOPTIONS`, and `SetFileTypeIndex` is 1-based.) +struct SetOptions { uint32_t options; }; +struct SetTitle { nsString title; }; +struct SetOkButtonLabel { nsString label; }; +struct SetFolder { nsString path; }; +struct SetFileName { nsString filename; }; +struct SetDefaultExtension { nsString extension; }; +struct ComDlgFilterSpec { nsString name; nsString spec; }; +struct SetFileTypes { ComDlgFilterSpec[] filterList; }; +struct SetFileTypeIndex { uint32_t index; }; + +// Union of the above. +union Command { + SetOptions; + SetTitle; + SetOkButtonLabel; + SetFolder; + SetFileName; + SetDefaultExtension; + SetFileTypes; + SetFileTypeIndex; +}; + +// The results from opening a file dialog. (Note that folder selection only +// returns an nsString.) +struct Results { + nsString[] paths; + uint32_t selectedFileTypeIndex; +}; + +} // namespace filedialog +} // namespace widget +} // namespace mozilla diff --git a/widget/windows/filedialog/WinFileDialogParent.cpp b/widget/windows/filedialog/WinFileDialogParent.cpp new file mode 100644 index 0000000000..2c256a1506 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogParent.cpp @@ -0,0 +1,94 @@ +/* -*- 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 "mozilla/widget/filedialog/WinFileDialogParent.h" + +#include "mozilla/Logging.h" +#include "mozilla/Result.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "nsISupports.h" + +namespace mozilla::widget::filedialog { + +// Count of currently-open file dialogs (not just open-file dialogs). +static size_t sOpenDialogActors = 0; + +WinFileDialogParent::WinFileDialogParent() { + MOZ_LOG(sLogFileDialog, LogLevel::Debug, + ("%s %p", __PRETTY_FUNCTION__, this)); +} + +WinFileDialogParent::~WinFileDialogParent() { + MOZ_LOG(sLogFileDialog, LogLevel::Debug, + ("%s %p", __PRETTY_FUNCTION__, this)); +} + +PWinFileDialogParent::nsresult WinFileDialogParent::BindToUtilityProcess( + mozilla::ipc::UtilityProcessParent* aUtilityParent) { + Endpoint<PWinFileDialogParent> parentEnd; + Endpoint<PWinFileDialogChild> childEnd; + nsresult rv = PWinFileDialog::CreateEndpoints(base::GetCurrentProcId(), + aUtilityParent->OtherPid(), + &parentEnd, &childEnd); + + if (NS_FAILED(rv)) { + MOZ_ASSERT(false, "Protocol endpoints failure"); + return NS_ERROR_FAILURE; + } + + if (!aUtilityParent->SendStartWinFileDialogService(std::move(childEnd))) { + MOZ_ASSERT(false, "SendStartWinFileDialogService failed"); + return NS_ERROR_FAILURE; + } + + if (!parentEnd.Bind(this)) { + MOZ_ASSERT(false, "parentEnd.Bind failed"); + return NS_ERROR_FAILURE; + } + + sOpenDialogActors++; + return NS_OK; +} + +void WinFileDialogParent::ProcessingError(Result aCode, const char* aReason) { + detail::LogProcessingError(sLogFileDialog, this, aCode, aReason); +} + +ProcessProxy::ProcessProxy(RefPtr<WFDP>&& obj) + : data(MakeRefPtr<Contents>(std::move(obj))) {} + +ProcessProxy::Contents::Contents(RefPtr<WFDP>&& obj) : ptr(std::move(obj)) {} + +ProcessProxy::Contents::~Contents() { + AssertIsOnMainThread(); + + // destroy the actor... + ptr->Close(); + + // ... and possibly the process + if (!--sOpenDialogActors) { + StopProcess(); + } +} + +void ProcessProxy::Contents::StopProcess() { + auto const upm = ipc::UtilityProcessManager::GetSingleton(); + if (!upm) { + // This is only possible when the UtilityProcessManager has shut down -- in + // which case the file-dialog process has also already been directed to shut + // down, and there's nothing we need to do here. + return; + } + + MOZ_LOG(sLogFileDialog, LogLevel::Debug, + ("%s: killing the WINDOWS_FILE_DIALOG process (no more live " + "actors)", + __PRETTY_FUNCTION__)); + upm->CleanShutdown(ipc::SandboxingKind::WINDOWS_FILE_DIALOG); +} + +} // namespace mozilla::widget::filedialog diff --git a/widget/windows/filedialog/WinFileDialogParent.h b/widget/windows/filedialog/WinFileDialogParent.h new file mode 100644 index 0000000000..a2c1197c55 --- /dev/null +++ b/widget/windows/filedialog/WinFileDialogParent.h @@ -0,0 +1,90 @@ +/* -*- 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 widget_windows_filedialog_WinFileDialogParent_h__ +#define widget_windows_filedialog_WinFileDialogParent_h__ + +#include "mozilla/Assertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/ProcInfo.h" +#include "mozilla/Result.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/dom/ChromeUtilsBinding.h" +#include "mozilla/ipc/UtilityProcessParent.h" +#include "mozilla/widget/filedialog/PWinFileDialogParent.h" +#include "nsISupports.h" +#include "nsStringFwd.h" + +namespace mozilla::widget::filedialog { + +class WinFileDialogParent : public PWinFileDialogParent { + public: + using UtilityActorName = ::mozilla::UtilityActorName; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WinFileDialogParent, override); + + public: + WinFileDialogParent(); + nsresult BindToUtilityProcess( + mozilla::ipc::UtilityProcessParent* aUtilityParent); + + UtilityActorName GetActorName() { + return UtilityActorName::WindowsFileDialog; + } + + private: + ~WinFileDialogParent(); + + void ProcessingError(Result aCode, const char* aReason) override; +}; + +// Proxy for the WinFileDialog process and actor. +// +// The IPC subsystem holds a strong reference to all IPC actors, so releasing +// the last RefPtr for such an actor does not actually cause the actor to be +// destroyed. Similarly, the UtilityProcessManager owns the host process for an +// actor, and merely destroying all actors within that host process will not +// cause it to be reaped. +// +// This object, then, acts as a proxy for those objects' lifetimes: when the +// last reference to `Contents` is released, the necessary explicit cleanup of +// the actor (and, if possible, the host process) will be performed. +class ProcessProxy { + public: + using WFDP = WinFileDialogParent; + + explicit ProcessProxy(RefPtr<WFDP>&& obj); + ~ProcessProxy() = default; + + explicit operator bool() const { return data->ptr && data->ptr->CanSend(); } + bool operator!() const { return !bool(*this); } + + WFDP& operator*() const { return *data->ptr; } + WFDP* operator->() const { return data->ptr; } + WFDP* get() const { return data->ptr; } + + ProcessProxy(ProcessProxy const& that) = default; + ProcessProxy(ProcessProxy&&) = default; + + private: + struct Contents { + NS_INLINE_DECL_REFCOUNTING(Contents); + + public: + explicit Contents(RefPtr<WFDP>&& obj); + RefPtr<WFDP> const ptr; + + private: + ~Contents(); + void StopProcess(); + }; + // guaranteed nonnull + RefPtr<Contents> data; +}; + +} // namespace mozilla::widget::filedialog + +#endif // widget_windows_filedialog_WinFileDialogParent_h__ diff --git a/widget/windows/filedialog/moz.build b/widget/windows/filedialog/moz.build new file mode 100644 index 0000000000..d2732faf78 --- /dev/null +++ b/widget/windows/filedialog/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +IPDL_SOURCES += [ + "PWinFileDialog.ipdl", + "WinFileDialogCommandsDefn.ipdlh", +] + +UNIFIED_SOURCES += [ + "WinFileDialogChild.cpp", + "WinFileDialogCommands.cpp", + "WinFileDialogParent.cpp", +] + +EXPORTS.mozilla.widget.filedialog += [ + "WinFileDialogChild.h", + "WinFileDialogCommands.h", + "WinFileDialogParent.h", +] + +# needed for IPC header files +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/widget/windows/metrics.yaml b/widget/windows/metrics.yaml new file mode 100644 index 0000000000..e9d3b4f5cf --- /dev/null +++ b/widget/windows/metrics.yaml @@ -0,0 +1,58 @@ +# 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/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Core :: Widget: Win32' + +file_dialog: + fallback: + type: event + description: > + Records the result of an attempt to open and use the out-of-process file + dialog when the in-process file-dialog is available as a fallback. + metadata: + # mostly technical, but includes timing data that may derive from user + # interactions + data-sensitivity: [technical, interaction] + notification_emails: + - rkraesig@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1872397 + # this event may alternatively be manually expired once bug 1677170 is + # closed + expires: 135 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1872397#c7 + extra_keys: + succeeded: + type: boolean + description: > + Whether the out-of-process dialog succeeded or failed. (Note that + user-induced cancellation is considered a form of success.) + time_remote: + type: quantity + description: > + The time between the out-of-process file dialog's instantiation + attempt and its failure, in milliseconds. + hresult_remote: + type: string + description: > + The failure code produced by the out-of-process file dialog, formatted + as eight hexdigits. Only present when `!succeeded`. + time_local: + type: quantity + description: > + The time between the in-process file dialog's instantiation attempt + and its conclusion (successfully or otherwise), in milliseconds. Only + present when `!succeeded`. + hresult_local: + type: string + description: > + The return code produced by the in-process file dialog, formatted as + eight hexdigits. Only present when `!succeeded`. diff --git a/widget/windows/moz.build b/widget/windows/moz.build new file mode 100644 index 0000000000..f19a46caf1 --- /dev/null +++ b/widget/windows/moz.build @@ -0,0 +1,213 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Widget: Win32") + SCHEDULES.exclusive = ["windows"] + +with Files("*CompositorWidget*"): + BUG_COMPONENT = ("Core", "Graphics") + +with Files("*IMEHandler*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("*IMMHandler*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("*KeyboardLayout*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("OSK*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("*TSFTextStore*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +DIRS += [ + "filedialog", +] + +TEST_DIRS += ["tests"] + +EXPORTS += [ + "nsdefs.h", + "WindowHook.h", + "WinUtils.h", +] + +EXPORTS.mozilla += [ + "ShellHeaderOnlyUtils.h", + "ToastNotificationHeaderOnlyUtils.h", + "UrlmonHeaderOnlyUtils.h", + "WindowsConsole.h", + "WindowsEventLog.h", + "WinHeaderOnlyUtils.h", +] + +EXPORTS.mozilla.widget += [ + "AudioSession.h", + "CompositorWidgetChild.h", + "CompositorWidgetParent.h", + "InProcessWinCompositorWidget.h", + "JumpListBuilder.h", + "nsWindowLoggedMessages.h", + "WinCompositorWidget.h", + "WinCompositorWindowThread.h", + "WindowsEMF.h", + "WindowsSMTCProvider.h", + "WinEventObserver.h", + "WinMessages.h", + "WinModifierKeyState.h", + "WinRegistry.h", + "WinTaskbar.h", + "WinWindowOcclusionTracker.h", +] + +UNIFIED_SOURCES += [ + "AudioSession.cpp", + "CompositorWidgetChild.cpp", + "DirectManipulationOwner.cpp", + "GfxInfo.cpp", + "IEnumFE.cpp", + "IMMHandler.cpp", + "JumpListBuilder.cpp", + "KeyboardLayout.cpp", + "LegacyJumpListItem.cpp", + "LSPAnnotator.cpp", + "nsAppShell.cpp", + "nsClipboard.cpp", + "nsColorPicker.cpp", + "nsDataObj.cpp", + "nsDataObjCollection.cpp", + "nsDragService.cpp", + "nsLookAndFeel.cpp", + "nsNativeDragSource.cpp", + "nsNativeDragTarget.cpp", + "nsNativeThemeWin.cpp", + "nsSound.cpp", + "nsToolkit.cpp", + "nsUserIdleServiceWin.cpp", + "nsUXThemeData.cpp", + "nsWindow.cpp", + "nsWindowDbg.cpp", + "nsWindowGfx.cpp", + "nsWindowLoggedMessages.cpp", + "nsWindowTaskbarConcealer.cpp", + "nsWinGesture.cpp", + "OSKTabTipManager.cpp", + "OSKVRManager.cpp", + "RemoteBackbuffer.cpp", + "ScreenHelperWin.cpp", + "SystemStatusBar.cpp", + "TaskbarPreview.cpp", + "TaskbarPreviewButton.cpp", + "TaskbarTabPreview.cpp", + "TaskbarWindowPreview.cpp", + "WidgetTraceEvent.cpp", + "WinCompositorWindowThread.cpp", + "WindowHook.cpp", + "WindowsConsole.cpp", + "WinEventObserver.cpp", + "WinIMEHandler.cpp", + "WinPointerEvents.cpp", + "WinRegistry.cpp", + "WinTaskbar.cpp", + "WinTextEventDispatcherListener.cpp", + "WinUtils.cpp", + "WinWindowOcclusionTracker.cpp", +] + +# The following files cannot be built in unified mode because of name clashes. +SOURCES += [ + "CompositorWidgetParent.cpp", + "InProcessWinCompositorWidget.cpp", + "LegacyJumpListBuilder.cpp", + "MediaKeysEventSourceFactory.cpp", + "nsBidiKeyboard.cpp", + "nsFilePicker.cpp", + "nsSharePicker.cpp", + "nsWidgetFactory.cpp", + "OSKInputPaneManager.cpp", + "WinCompositorWidget.cpp", + "WindowsSMTCProvider.cpp", + "WindowsUIUtils.cpp", + "WinMouseScrollHandler.cpp", +] + +# Needs INITGUID and we don't allow INITGUID in unified sources since bug 970429. +SOURCES += [ + "InputDeviceUtils.cpp", + "TSFTextStore.cpp", +] + +if CONFIG["NS_PRINTING"]: + UNIFIED_SOURCES += [ + "nsDeviceContextSpecWin.cpp", + "nsPrintDialogWin.cpp", + "nsPrinterWin.cpp", + "nsPrintSettingsServiceWin.cpp", + "nsPrintSettingsWin.cpp", + ] + SOURCES += [ + "nsPrintDialogUtil.cpp", + ] + +if CONFIG["MOZ_ENABLE_SKIA_PDF"]: + UNIFIED_SOURCES += [ + "WindowsEMF.cpp", + ] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +if CONFIG["MOZ_ENABLE_SKIA_PDF"]: + LOCAL_INCLUDES += CONFIG["SKIA_INCLUDES"] + +LOCAL_INCLUDES += [ + "/gfx/cairo/cairo/src", + "/layout/forms", + "/layout/generic", + "/layout/style", + "/layout/xul", + "/toolkit/components/jsoncpp/include", + "/toolkit/xre", + "/widget", + "/widget/headless", + "/xpcom/base", +] + +DEFINES["MOZ_UNICODE"] = True +DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"] +# Turn `firefox` into `Firefox`. +DEFINES["MOZ_TOAST_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"].title() + +for var in ("MOZ_ENABLE_D3D10_LAYER",): + if CONFIG[var]: + DEFINES[var] = True + +USE_LIBS += [ + "jsoncpp", +] + +OS_LIBS += [ + "ktmw32", + "rpcrt4", + "urlmon", +] + +# mingw is missing Windows toast notification definitions. +if CONFIG["CC_TYPE"] == "clang-cl": + SOURCES += [ + "ToastNotification.cpp", + "ToastNotificationHandler.cpp", + ] + +SPHINX_TREES["/widget/windows"] = "docs" diff --git a/widget/windows/nsAppShell.cpp b/widget/windows/nsAppShell.cpp new file mode 100644 index 0000000000..314176766d --- /dev/null +++ b/widget/windows/nsAppShell.cpp @@ -0,0 +1,1000 @@ +/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */ +/* 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/Attributes.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/ipc/MessageChannel.h" +#include "mozilla/ipc/WindowsMessageLoop.h" +#include "nsAppShell.h" +#include "nsToolkit.h" +#include "nsThreadUtils.h" +#include "WinUtils.h" +#include "WinTaskbar.h" +#include "WinMouseScrollHandler.h" +#include "nsWindowDefs.h" +#include "nsWindow.h" +#include "nsString.h" +#include "WinIMEHandler.h" +#include "mozilla/widget/AudioSession.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/Hal.h" +#include "nsIDOMWakeLockListener.h" +#include "nsIPowerManagerService.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/StaticPtr.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsComponentManagerUtils.h" +#include "ScreenHelperWin.h" +#include "HeadlessScreenHelper.h" +#include "mozilla/widget/ScreenManager.h" +#include "mozilla/Atomics.h" +#include "mozilla/NativeNt.h" +#include "mozilla/WindowsProcessMitigations.h" + +#include <winternl.h> + +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +#endif + +#if defined(ACCESSIBILITY) +# include "mozilla/a11y/Compatibility.h" +# include "mozilla/a11y/Platform.h" +#endif // defined(ACCESSIBILITY) + +using namespace mozilla; +using namespace mozilla::widget; + +#define WAKE_LOCK_LOG(...) \ + MOZ_LOG(gWinWakeLockLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +static mozilla::LazyLogModule gWinWakeLockLog("WinWakeLock"); + +// This wakelock listener is used for Window7 and above. +class WinWakeLockListener final : public nsIDOMMozWakeLockListener { + public: + NS_DECL_ISUPPORTS + WinWakeLockListener() { MOZ_ASSERT(XRE_IsParentProcess()); } + + private: + ~WinWakeLockListener() { + ReleaseWakelockIfNeeded(PowerRequestDisplayRequired); + ReleaseWakelockIfNeeded(PowerRequestExecutionRequired); + } + + void SetHandle(HANDLE aHandle, POWER_REQUEST_TYPE aType) { + switch (aType) { + case PowerRequestDisplayRequired: { + if (!aHandle && mDisplayHandle) { + CloseHandle(mDisplayHandle); + } + mDisplayHandle = aHandle; + return; + } + case PowerRequestExecutionRequired: { + if (!aHandle && mNonDisplayHandle) { + CloseHandle(mNonDisplayHandle); + } + mNonDisplayHandle = aHandle; + return; + } + default: + MOZ_ASSERT_UNREACHABLE("Invalid request type"); + return; + } + } + + HANDLE GetHandle(POWER_REQUEST_TYPE aType) const { + switch (aType) { + case PowerRequestDisplayRequired: + return mDisplayHandle; + case PowerRequestExecutionRequired: + return mNonDisplayHandle; + default: + MOZ_ASSERT_UNREACHABLE("Invalid request type"); + return nullptr; + } + } + + HANDLE CreateHandle(POWER_REQUEST_TYPE aType) { + MOZ_ASSERT(!GetHandle(aType)); + REASON_CONTEXT context = {0}; + context.Version = POWER_REQUEST_CONTEXT_VERSION; + context.Flags = POWER_REQUEST_CONTEXT_SIMPLE_STRING; + context.Reason.SimpleReasonString = RequestTypeLPWSTR(aType); + HANDLE handle = PowerCreateRequest(&context); + if (!handle) { + WAKE_LOCK_LOG("Failed to create handle for %s, error=%lu", + RequestTypeStr(aType), GetLastError()); + return nullptr; + } + SetHandle(handle, aType); + return handle; + } + + LPWSTR RequestTypeLPWSTR(POWER_REQUEST_TYPE aType) const { + switch (aType) { + case PowerRequestDisplayRequired: + return const_cast<LPWSTR>(L"display request"); // -Wwritable-strings + case PowerRequestExecutionRequired: + return const_cast<LPWSTR>( + L"non-display request"); // -Wwritable-strings + default: + MOZ_ASSERT_UNREACHABLE("Invalid request type"); + return const_cast<LPWSTR>(L"unknown"); // -Wwritable-strings + } + } + + const char* RequestTypeStr(POWER_REQUEST_TYPE aType) const { + switch (aType) { + case PowerRequestDisplayRequired: + return "display request"; + case PowerRequestExecutionRequired: + return "non-display request"; + default: + MOZ_ASSERT_UNREACHABLE("Invalid request type"); + return "unknown"; + } + } + + void RequestWakelockIfNeeded(POWER_REQUEST_TYPE aType) { + if (GetHandle(aType)) { + WAKE_LOCK_LOG("Already requested lock for %s", RequestTypeStr(aType)); + return; + } + + WAKE_LOCK_LOG("Prepare a wakelock for %s", RequestTypeStr(aType)); + HANDLE handle = CreateHandle(aType); + if (!handle) { + WAKE_LOCK_LOG("Failed due to no handle for %s", RequestTypeStr(aType)); + return; + } + + if (PowerSetRequest(handle, aType)) { + WAKE_LOCK_LOG("Requested %s lock", RequestTypeStr(aType)); + } else { + WAKE_LOCK_LOG("Failed to request %s lock, error=%lu", + RequestTypeStr(aType), GetLastError()); + SetHandle(nullptr, aType); + } + } + + void ReleaseWakelockIfNeeded(POWER_REQUEST_TYPE aType) { + if (!GetHandle(aType)) { + WAKE_LOCK_LOG("Already released lock for %s", RequestTypeStr(aType)); + return; + } + + WAKE_LOCK_LOG("Prepare to release wakelock for %s", RequestTypeStr(aType)); + if (!PowerClearRequest(GetHandle(aType), aType)) { + WAKE_LOCK_LOG("Failed to release %s lock, error=%lu", + RequestTypeStr(aType), GetLastError()); + return; + } + SetHandle(nullptr, aType); + WAKE_LOCK_LOG("Released wakelock for %s", RequestTypeStr(aType)); + } + + NS_IMETHOD Callback(const nsAString& aTopic, + const nsAString& aState) override { + WAKE_LOCK_LOG("topic=%s, state=%s", NS_ConvertUTF16toUTF8(aTopic).get(), + NS_ConvertUTF16toUTF8(aState).get()); + if (!aTopic.EqualsASCII("screen") && !aTopic.EqualsASCII("audio-playing") && + !aTopic.EqualsASCII("video-playing")) { + return NS_OK; + } + + const bool isNonDisplayLock = aTopic.EqualsASCII("audio-playing"); + bool requestLock = false; + if (isNonDisplayLock) { + requestLock = aState.EqualsASCII("locked-foreground") || + aState.EqualsASCII("locked-background"); + } else { + requestLock = aState.EqualsASCII("locked-foreground"); + } + + if (isNonDisplayLock) { + if (requestLock) { + RequestWakelockIfNeeded(PowerRequestExecutionRequired); + } else { + ReleaseWakelockIfNeeded(PowerRequestExecutionRequired); + } + } else { + if (requestLock) { + RequestWakelockIfNeeded(PowerRequestDisplayRequired); + } else { + ReleaseWakelockIfNeeded(PowerRequestDisplayRequired); + } + } + return NS_OK; + } + + // Handle would only exist when we request wakelock successfully. + HANDLE mDisplayHandle = nullptr; + HANDLE mNonDisplayHandle = nullptr; +}; +NS_IMPL_ISUPPORTS(WinWakeLockListener, nsIDOMMozWakeLockListener) +StaticRefPtr<nsIDOMMozWakeLockListener> sWakeLockListener; + +static void AddScreenWakeLockListener() { + nsCOMPtr<nsIPowerManagerService> sPowerManagerService = + do_GetService(POWERMANAGERSERVICE_CONTRACTID); + if (sPowerManagerService) { + sWakeLockListener = new WinWakeLockListener(); + sPowerManagerService->AddWakeLockListener(sWakeLockListener); + } else { + NS_WARNING( + "Failed to retrieve PowerManagerService, wakelocks will be broken!"); + } +} + +static void RemoveScreenWakeLockListener() { + nsCOMPtr<nsIPowerManagerService> sPowerManagerService = + do_GetService(POWERMANAGERSERVICE_CONTRACTID); + if (sPowerManagerService) { + sPowerManagerService->RemoveWakeLockListener(sWakeLockListener); + sPowerManagerService = nullptr; + sWakeLockListener = nullptr; + } +} + +class SingleNativeEventPump final : public nsIThreadObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITHREADOBSERVER + + SingleNativeEventPump() { + MOZ_ASSERT(!XRE_UseNativeEventProcessing(), + "Should only be used when not properly processing events."); + } + + private: + ~SingleNativeEventPump() {} +}; + +NS_IMPL_ISUPPORTS(SingleNativeEventPump, nsIThreadObserver) + +NS_IMETHODIMP +SingleNativeEventPump::OnDispatchedEvent() { return NS_OK; } + +NS_IMETHODIMP +SingleNativeEventPump::OnProcessNextEvent(nsIThreadInternal* aThread, + bool aMayWait) { + MSG msg; + bool gotMessage = WinUtils::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE); + if (gotMessage) { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + return NS_OK; +} + +NS_IMETHODIMP +SingleNativeEventPump::AfterProcessNextEvent(nsIThreadInternal* aThread, + bool aMayWait) { + return NS_OK; +} + +// RegisterWindowMessage values +// Native event callback message +const wchar_t* kAppShellGeckoEventId = L"nsAppShell:EventID"; +UINT sAppShellGeckoMsgId = 0x10001; // initialize to invalid message ID +// Taskbar button creation message +const wchar_t* kTaskbarButtonEventId = L"TaskbarButtonCreated"; +UINT sTaskbarButtonCreatedMsg = 0x10002; // initialize to invalid message ID + +/* static */ +UINT nsAppShell::GetTaskbarButtonCreatedMessage() { + return sTaskbarButtonCreatedMsg; +} + +namespace mozilla { +namespace crashreporter { +void LSPAnnotate(); +} // namespace crashreporter +} // namespace mozilla + +using mozilla::crashreporter::LSPAnnotate; + +//------------------------------------------------------------------------- + +// Note that since we're on x86-ish processors here, ReleaseAcquire is the +// semantics that normal loads and stores would use anyway. +static Atomic<size_t, ReleaseAcquire> sOutstandingNativeEventCallbacks; + +/*static*/ LRESULT CALLBACK nsAppShell::EventWindowProc(HWND hwnd, UINT uMsg, + WPARAM wParam, + LPARAM lParam) { + NativeEventLogger eventLogger("AppShell", hwnd, uMsg, wParam, lParam); + + if (uMsg == sAppShellGeckoMsgId) { + // The app shell might have been destroyed between this message being + // posted and being executed, so be extra careful. + if (!sOutstandingNativeEventCallbacks) { + return TRUE; + } + + nsAppShell* as = reinterpret_cast<nsAppShell*>(lParam); + as->NativeEventCallback(); + --sOutstandingNativeEventCallbacks; + return TRUE; + } + + LRESULT ret = DefWindowProc(hwnd, uMsg, wParam, lParam); + eventLogger.SetResult(ret, false); + return ret; +} + +nsAppShell::~nsAppShell() { + hal::Shutdown(); + + if (mEventWnd) { + // DestroyWindow doesn't do anything when called from a non UI thread. + // Since mEventWnd was created on the UI thread, it must be destroyed on + // the UI thread. + SendMessage(mEventWnd, WM_CLOSE, 0, 0); + } + + // Cancel any outstanding native event callbacks. + sOutstandingNativeEventCallbacks = 0; +} + +NS_IMETHODIMP +nsAppShell::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIObserverService> obsServ( + mozilla::services::GetObserverService()); + + if (!strcmp(aTopic, "sessionstore-restoring-on-startup")) { + nsWindow::SetIsRestoringSession(true); + // Now that we've handled the observer notification, we can remove it + obsServ->RemoveObserver(this, "sessionstore-restoring-on-startup"); + return NS_OK; + } + + if (!strcmp(aTopic, "sessionstore-windows-restored")) { + nsWindow::SetIsRestoringSession(false); + // Now that we've handled the observer notification, we can remove it + obsServ->RemoveObserver(this, "sessionstore-windows-restored"); + return NS_OK; + } + } + + return nsBaseAppShell::Observe(aSubject, aTopic, aData); +} + +namespace { + +// Struct storing the visible, loggable error-state of a Windows thread. +// Approximately `std:pair(::GetLastError(), ::RtlGetLastNtStatus())`. +// +// Uses sentinel values rather than a proper `Maybe` type to simplify +// minidump-analysis. +struct WinErrorState { + // Last error, as provided by ::GetLastError(). + DWORD error = ~0; + // Last NTSTATUS, as provided by the TIB. + NTSTATUS ntStatus = ~0; + + private: + // per WINE et al.; stable since NT 3.51 + constexpr static size_t kLastNtStatusOffset = + sizeof(size_t) == 8 ? 0x1250 : 0xbf4; + + static void SetLastNtStatus(NTSTATUS status) { + auto* teb = ::NtCurrentTeb(); + *reinterpret_cast<NTSTATUS*>(reinterpret_cast<char*>(teb) + + kLastNtStatusOffset) = status; + } + + static NTSTATUS GetLastNtStatus() { + auto const* teb = ::NtCurrentTeb(); + return *reinterpret_cast<NTSTATUS const*>( + reinterpret_cast<char const*>(teb) + kLastNtStatusOffset); + } + + public: + // Restore (or just set) the error state of the current thread. + static void Apply(WinErrorState const& state) { + SetLastNtStatus(state.ntStatus); + ::SetLastError(state.error); + } + + // Clear the error-state of the current thread. + static void Clear() { Apply({.error = 0, .ntStatus = 0}); } + + // Get the error-state of the current thread. + static WinErrorState Get() { + return WinErrorState{ + .error = ::GetLastError(), + .ntStatus = GetLastNtStatus(), + }; + } + + bool operator==(WinErrorState const& that) const { + return this->error == that.error && this->ntStatus == that.ntStatus; + } + + bool operator!=(WinErrorState const& that) const { return !operator==(that); } +}; + +// Struct containing information about the user atom table. (See +// DiagnoseUserAtomTable(), below.) +struct AtomTableInformation { + // Number of atoms in use. (Exactly 0x4000 == 16384, if all are.) + UINT in_use = 0; + // Number of atoms confirmed not in use. + UINT free = 0; + // Number of atoms which gave errors when checked. + UINT errors = 0; + + // Last atom which gave an unexpected error... + UINT lastErrorAtom = ~0u; + // ... and the error it gave. + WinErrorState lastErrorState; +}; + +// Return a summary of the state of the atom table. +MOZ_NEVER_INLINE static AtomTableInformation DiagnoseUserAtomTable() { + // Restore error state on exit, for the sake of automated minidump analyses. + auto const _restoreErrState = + mozilla::MakeScopeExit([oldErrState = WinErrorState::Get()]() { + WinErrorState::Apply(oldErrState); + }); + + AtomTableInformation retval; + + // Expected error-state on failure-return when the atom is assigned, but not + // enough space was provided for the full string. + constexpr WinErrorState kBufferTooSmall = { + .error = ERROR_INSUFFICIENT_BUFFER, + .ntStatus = ((NTSTATUS)0xC0000023), // == STATUS_BUFFER_TOO_SMALL + }; + // Expected error-state on failure-return when the atom is not assigned. + constexpr WinErrorState kInvalidAtom = { + .error = ERROR_INVALID_HANDLE, + .ntStatus = ((NTSTATUS)STATUS_INVALID_HANDLE), + }; + + // Iterate over only the dynamic portion of the atom table. + for (UINT atom = 0xC000; atom <= 0xFFFF; ++atom) { + // The actual atom values are PII. Don't acquire them in their entirety, and + // don't keep more information about them than is needed. + WCHAR buf[2] = {}; + // USE OF UNDOCUMENTED BEHAVIOR: The user atom table is shared by message + // names, window-class names, and clipboard-format names. Only the last has + // a documented getter-mechanism. + BOOL const ok = ::GetClipboardFormatNameW(atom, buf, 1); + WinErrorState const errState = WinErrorState::Get(); + if (ok || errState == kBufferTooSmall) { + ++retval.in_use; + } else if (errState == kInvalidAtom) { + ++retval.free; + } else { + // Unexpected error-state. + ++retval.errors; + retval.lastErrorAtom = atom; + retval.lastErrorState = errState; + } + } + + return retval; +} + +} // namespace + +#if defined(MOZ_DIAGNOSTIC_ASSERT_ENABLED) && defined(_M_X64) +MOZ_NEVER_INLINE MOZ_NAKED void EnableTrapFlag() { + asm volatile( + "pushfq;" + "orw $0x100,(%rsp);" + "popfq;" + "retq;"); +} + +MOZ_NEVER_INLINE MOZ_NAKED void DisableTrapFlag() { asm volatile("retq;"); } + +# define SSD_MAX_USER32_STEPS 0x1800 +# define SSD_MAX_ERROR_STATES 0x200 +struct SingleStepData { + uint32_t mUser32StepsLog[SSD_MAX_USER32_STEPS]{}; + WinErrorState mErrorStatesLog[SSD_MAX_ERROR_STATES]; + uint16_t mUser32StepsAtErrorState[SSD_MAX_ERROR_STATES]{}; +}; + +struct SingleStepStaticState { + SingleStepData* mData{}; + uintptr_t mUser32Start{}; + uintptr_t mUser32End{}; + uint32_t mUser32Steps{}; + uint32_t mErrorStates{}; + WinErrorState mLastRecordedErrorState; + + constexpr void Reset() { *this = SingleStepStaticState{}; } +}; + +static SingleStepStaticState sSingleStepStaticState{}; + +LONG SingleStepExceptionHandler(_EXCEPTION_POINTERS* aExceptionInfo) { + auto& state = sSingleStepStaticState; + if (state.mData && + aExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) { + auto instructionPointer = aExceptionInfo->ContextRecord->Rip; + if (instructionPointer == reinterpret_cast<uintptr_t>(&DisableTrapFlag)) { + // Stop handling any exception in this handler + state.mData = nullptr; + } else { + // Record data for the current step, if in user32 + if (state.mUser32Start <= instructionPointer && + instructionPointer < state.mUser32End) { + // We record the instruction pointer + if (state.mUser32Steps < SSD_MAX_USER32_STEPS) { + state.mData->mUser32StepsLog[state.mUser32Steps] = + static_cast<uint32_t>(instructionPointer - state.mUser32Start); + } + + // We record changes in the error state + auto currentErrorState{WinErrorState::Get()}; + if (currentErrorState != state.mLastRecordedErrorState) { + state.mLastRecordedErrorState = currentErrorState; + + if (state.mErrorStates < SSD_MAX_ERROR_STATES) { + state.mData->mErrorStatesLog[state.mErrorStates] = + currentErrorState; + state.mData->mUser32StepsAtErrorState[state.mErrorStates] = + state.mUser32Steps; + } + + ++state.mErrorStates; + } + + ++state.mUser32Steps; + } + + // Continue single-stepping + aExceptionInfo->ContextRecord->EFlags |= 0x100; + } + return EXCEPTION_CONTINUE_EXECUTION; + } + return EXCEPTION_CONTINUE_SEARCH; +} + +enum CSSD_RESULT { + CSSD_SUCCESS = 0, + CSSD_ERROR_DEBUGGER_PRESENT = 1, + CSSD_ERROR_GET_MODULE_HANDLE = 2, + CSSD_ERROR_PARSING_USER32 = 3, + CSSD_ERROR_ADD_VECTORED_EXCEPTION_HANDLER = 4, +}; + +template <typename CallbackToRun, typename PostCollectionCallback> +[[clang::optnone]] MOZ_NEVER_INLINE CSSD_RESULT +CollectSingleStepData(CallbackToRun aCallbackToRun, + PostCollectionCallback aPostCollectionCallback) { + if (::IsDebuggerPresent()) { + return CSSD_ERROR_DEBUGGER_PRESENT; + } + + MOZ_DIAGNOSTIC_ASSERT(!sSingleStepStaticState.mData, + "Single-stepping is already active"); + HANDLE user32 = ::GetModuleHandleW(L"user32.dll"); + if (!user32) { + return CSSD_ERROR_GET_MODULE_HANDLE; + } + + nt::PEHeaders user32Headers{user32}; + auto bounds = user32Headers.GetBounds(); + if (bounds.isNothing()) { + return CSSD_ERROR_PARSING_USER32; + } + + SingleStepData singleStepData{}; + + sSingleStepStaticState.Reset(); + sSingleStepStaticState.mUser32Start = + reinterpret_cast<uintptr_t>(bounds.ref().begin().get()); + sSingleStepStaticState.mUser32End = + reinterpret_cast<uintptr_t>(bounds.ref().end().get()); + sSingleStepStaticState.mData = &singleStepData; + auto veh = ::AddVectoredExceptionHandler(TRUE, SingleStepExceptionHandler); + if (!veh) { + sSingleStepStaticState.mData = nullptr; + return CSSD_ERROR_ADD_VECTORED_EXCEPTION_HANDLER; + } + + EnableTrapFlag(); + aCallbackToRun(); + DisableTrapFlag(); + ::RemoveVectoredExceptionHandler(veh); + sSingleStepStaticState.mData = nullptr; + + aPostCollectionCallback(); + + return CSSD_SUCCESS; +} +#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED && _M_X64 + +// Collect data for bug 1571516. We don't automatically send up `GetLastError` +// or `GetLastNtStatus` data for beta/release builds, so extract the relevant +// error values and store them on the stack, where they can be viewed in +// minidumps -- in fact, do so after each individual API call. This takes the +// form of various local variables whose initial character is an underscore, +// most of which are also marked [[maybe_unused]]. +// +// We tag this function `[[clang::optnone]]` to prevent the compiler from +// eliding those values as _actually_ unused, as well as to generally simplify +// the haruspex's task once the minidumps are in. (As this function should be +// called at most once per process, the minor performance hit is not a concern.) +// +[[clang::optnone]] MOZ_NEVER_INLINE nsresult nsAppShell::InitHiddenWindow() { + // note the incoming error-state; this may be relevant to errors we get later + auto _initialErr [[maybe_unused]] = WinErrorState::Get(); + // reset the error-state, to avoid ambiguity below + WinErrorState::Clear(); + + // Diagnostic variable. Only collected in the event of a failure in one of the + // functions that attempts to register an atom. + AtomTableInformation _atomTableInfo [[maybe_unused]]; + + // Attempt to register the window message. On failure, retain the initial + // value of `sAppShellGeckoMsgId`. + auto const _msgId = ::RegisterWindowMessageW(kAppShellGeckoEventId); + if (_msgId) { + sAppShellGeckoMsgId = _msgId; + } + auto const _sAppShellGeckoMsgId [[maybe_unused]] = sAppShellGeckoMsgId; + auto const _rwmErr [[maybe_unused]] = WinErrorState::Get(); + if (!_msgId) _atomTableInfo = DiagnoseUserAtomTable(); + NS_ASSERTION(sAppShellGeckoMsgId, + "Could not register hidden window event message!"); + + mLastNativeEventScheduled = TimeStamp::NowLoRes(); + + WNDCLASSW wc; + HINSTANCE const module = GetModuleHandle(nullptr); + + constexpr const wchar_t* kWindowClass = L"nsAppShell:EventWindowClass"; + // (Undocumented behavior note: on success, this will specifically be the + // window-class atom. We don't rely on this.) + BOOL const _gciwRet = ::GetClassInfoW(module, kWindowClass, &wc); + auto const _gciwErr [[maybe_unused]] = WinErrorState::Get(); + WinErrorState::Clear(); + + WinErrorState _rcErr [[maybe_unused]]; + if (!_gciwRet) { + wc.style = 0; + wc.lpfnWndProc = EventWindowProc; + 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; + + ATOM _windowClassAtom = ::RegisterClassW(&wc); + _rcErr = WinErrorState::Get(); + + if (!_windowClassAtom) _atomTableInfo = DiagnoseUserAtomTable(); + +#if defined(MOZ_DIAGNOSTIC_ASSERT_ENABLED) && defined(_M_X64) + if (!_windowClassAtom) { + // Retry with single-step data collection + auto cssdResult = CollectSingleStepData( + [&wc, &_windowClassAtom]() { + _windowClassAtom = ::RegisterClassW(&wc); + }, + [&_windowClassAtom]() { + // Crashing here gives access to the single step data on stack + MOZ_DIAGNOSTIC_ASSERT( + _windowClassAtom, + "RegisterClassW for EventWindowClass failed twice"); + }); + auto const _cssdErr [[maybe_unused]] = WinErrorState::Get(); + MOZ_DIAGNOSTIC_ASSERT( + cssdResult == CSSD_SUCCESS, + "Failed to collect single step data for RegisterClassW"); + // If we reach this point then somehow the single-stepped call succeeded + // and we can proceed + } +#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED && _M_X64 + + MOZ_DIAGNOSTIC_ASSERT(_windowClassAtom, + "RegisterClassW for EventWindowClass failed"); + WinErrorState::Clear(); + } + + mEventWnd = CreateWindowW(kWindowClass, L"nsAppShell:EventWindow", 0, 0, 0, + 10, 10, HWND_MESSAGE, nullptr, module, nullptr); + auto const _cwErr [[maybe_unused]] = WinErrorState::Get(); + +#if defined(MOZ_DIAGNOSTIC_ASSERT_ENABLED) && defined(_M_X64) + if (!mEventWnd) { + // Retry with single-step data collection + HWND eventWnd{}; + auto cssdResult = CollectSingleStepData( + [module, &eventWnd]() { + eventWnd = + CreateWindowW(kWindowClass, L"nsAppShell:EventWindow", 0, 0, 0, + 10, 10, HWND_MESSAGE, nullptr, module, nullptr); + }, + [&eventWnd]() { + // Crashing here gives access to the single step data on stack + MOZ_DIAGNOSTIC_ASSERT(eventWnd, + "CreateWindowW for EventWindow failed twice"); + }); + auto const _cssdErr [[maybe_unused]] = WinErrorState::Get(); + MOZ_DIAGNOSTIC_ASSERT( + cssdResult == CSSD_SUCCESS, + "Failed to collect single step data for CreateWindowW"); + // If we reach this point then somehow the single-stepped call succeeded and + // we can proceed + mEventWnd = eventWnd; + } +#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED && _M_X64 + + MOZ_DIAGNOSTIC_ASSERT(mEventWnd, "CreateWindowW for EventWindow failed"); + NS_ENSURE_STATE(mEventWnd); + + return NS_OK; +} + +nsresult nsAppShell::Init() { + LSPAnnotate(); + + hal::Init(); + + if (XRE_IsParentProcess()) { + sTaskbarButtonCreatedMsg = ::RegisterWindowMessageW(kTaskbarButtonEventId); + NS_ASSERTION(sTaskbarButtonCreatedMsg, + "Could not register taskbar button creation message"); + } + + // The hidden message window is used for interrupting the processing of native + // events, so that we can process gecko events. Therefore, we only need it if + // we are processing native events. Disabling this is required for win32k + // syscall lockdown. + if (XRE_UseNativeEventProcessing()) { + if (nsresult rv = this->InitHiddenWindow(); NS_FAILED(rv)) { + return rv; + } + } else if (XRE_IsContentProcess() && !IsWin32kLockedDown()) { + // We're not generally processing native events, but still using GDI and we + // still have some internal windows, e.g. from calling CoInitializeEx. + // So we use a class that will do a single event pump where previously we + // might have processed multiple events to make sure any occasional messages + // to these windows are processed. This also allows any internal Windows + // messages to be processed to ensure the GDI data remains fresh. + nsCOMPtr<nsIThreadInternal> threadInt = + do_QueryInterface(NS_GetCurrentThread()); + if (threadInt) { + threadInt->SetObserver(new SingleNativeEventPump()); + } + } + + if (XRE_IsParentProcess()) { + ScreenManager& screenManager = ScreenManager::GetSingleton(); + if (gfxPlatform::IsHeadless()) { + screenManager.SetHelper(mozilla::MakeUnique<HeadlessScreenHelper>()); + } else { + screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperWin>()); + ScreenHelperWin::RefreshScreens(); + } + + nsCOMPtr<nsIObserverService> obsServ( + mozilla::services::GetObserverService()); + + obsServ->AddObserver(this, "sessionstore-restoring-on-startup", false); + obsServ->AddObserver(this, "sessionstore-windows-restored", false); + } + + if (!WinUtils::GetTimezoneName(mTimezoneName)) { + NS_WARNING("Unable to get system timezone name, timezone may be invalid\n"); + } + + return nsBaseAppShell::Init(); +} + +NS_IMETHODIMP +nsAppShell::Run(void) { + bool wantAudio = true; + if (XRE_IsParentProcess()) { +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + wantAudio = false; + } +#endif + if (MOZ_LIKELY(wantAudio)) { + mozilla::widget::StartAudioSession(); + } + + // Add an observer that disables the screen saver when requested by Gecko. + // For example when we're playing video in the foreground tab. Whole firefox + // only needs one wakelock instance, so we would only create one listener in + // chrome process to prevent requesting unnecessary wakelock. + AddScreenWakeLockListener(); + } + + nsresult rv = nsBaseAppShell::Run(); + + if (XRE_IsParentProcess()) { + RemoveScreenWakeLockListener(); + + if (MOZ_LIKELY(wantAudio)) { + mozilla::widget::StopAudioSession(); + } + } + + return rv; +} + +void nsAppShell::DoProcessMoreGeckoEvents() { + // Called by nsBaseAppShell's NativeEventCallback() after it has finished + // processing pending gecko events and there are still gecko events pending + // for the thread. (This can happen if NS_ProcessPendingEvents reached it's + // starvation timeout limit.) The default behavior in nsBaseAppShell is to + // call ScheduleNativeEventCallback to post a follow up native event callback + // message. This triggers an additional call to NativeEventCallback for more + // gecko event processing. + + // There's a deadlock risk here with certain internal Windows modal loops. In + // our dispatch code, we prioritize messages so that input is handled first. + // However Windows modal dispatch loops often prioritize posted messages. If + // we find ourselves in a tight gecko timer loop where NS_ProcessPendingEvents + // takes longer than the timer duration, NS_HasPendingEvents(thread) will + // always be true. ScheduleNativeEventCallback will be called on every + // NativeEventCallback callback, and in a Windows modal dispatch loop, the + // callback message will be processed first -> input gets starved, dead lock. + + // To avoid, don't post native callback messages from NativeEventCallback + // when we're in a modal loop. This gets us back into the Windows modal + // dispatch loop dispatching input messages. Once we drop out of the modal + // loop, we use mNativeCallbackPending to fire off a final NativeEventCallback + // if we need it, which insures NS_ProcessPendingEvents gets called and all + // gecko events get processed. + if (mEventloopNestingLevel < 2) { + OnDispatchedEvent(); + mNativeCallbackPending = false; + } else { + mNativeCallbackPending = true; + } +} + +void nsAppShell::ScheduleNativeEventCallback() { + MOZ_ASSERT(mEventWnd, + "We should have created mEventWnd in Init, if this is called."); + + // Post a message to the hidden message window + ++sOutstandingNativeEventCallbacks; + { + MutexAutoLock lock(mLastNativeEventScheduledMutex); + // Time stamp this event so we can detect cases where the event gets + // dropping in sub classes / modal loops we do not control. + mLastNativeEventScheduled = TimeStamp::NowLoRes(); + } + ::PostMessage(mEventWnd, sAppShellGeckoMsgId, 0, + reinterpret_cast<LPARAM>(this)); +} + +bool nsAppShell::ProcessNextNativeEvent(bool mayWait) { + // Notify ipc we are spinning a (possibly nested) gecko event loop. + mozilla::ipc::MessageChannel::NotifyGeckoEventDispatch(); + + bool gotMessage = false; + + do { + MSG msg; + + // For avoiding deadlock between our process and plugin process by + // mouse wheel messages, we're handling actually when we receive one of + // following internal messages which is posted by native mouse wheel + // message handler. Any other events, especially native modifier key + // events, should not be handled between native message and posted + // internal message because it may make different modifier key state or + // mouse cursor position between them. + if (mozilla::widget::MouseScrollHandler::IsWaitingInternalMessage()) { + gotMessage = WinUtils::PeekMessage(&msg, nullptr, MOZ_WM_MOUSEWHEEL_FIRST, + MOZ_WM_MOUSEWHEEL_LAST, PM_REMOVE); + NS_ASSERTION(gotMessage, + "waiting internal wheel message, but it has not come"); + } + + if (!gotMessage) { + gotMessage = WinUtils::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE); + } + + if (gotMessage) { + if (msg.message == WM_QUIT) { + ::PostQuitMessage(msg.wParam); + Exit(); + } else { + // If we had UI activity we would be processing it now so we know we + // have either kUIActivity or kActivityNoUIAVail. + mozilla::BackgroundHangMonitor().NotifyActivity(); + + if (msg.message >= WM_KEYFIRST && msg.message <= WM_KEYLAST && + IMEHandler::ProcessRawKeyMessage(msg)) { + continue; // the message is consumed. + } + +#if defined(_X86_) + // Store Printer dialog messages for reposting on x86, because on x86 + // Windows 7 they are not processed by a window procedure, but are + // explicitly waited for in the winspool.drv code that will be further + // up the stack (winspool!WaitForCompletionMessage). These are + // undocumented Windows Message identifiers found in winspool.drv. + if (msg.message == 0x5b7a || msg.message == 0x5b7f || + msg.message == 0x5b80 || msg.message == 0x5b81) { + mMsgsToRepost.push_back(msg); + continue; + } +#endif + + // Windows documentation suggets that WM_SETTINGSCHANGE is the message + // to watch for timezone changes, but experimentation showed that it + // doesn't fire on changing the timezone, but that WM_TIMECHANGE does, + // even if there's no immediate effect on the clock (e.g., changing + // from Pacific Daylight at UTC-7 to Arizona at UTC-7). + if (msg.message == WM_TIMECHANGE) { + // The message may not give us sufficient information to determine + // if the timezone changed, so keep track of it ourselves. + wchar_t systemTimezone[128]; + bool getSystemTimeSucceeded = + WinUtils::GetTimezoneName(systemTimezone); + if (getSystemTimeSucceeded && wcscmp(systemTimezone, mTimezoneName)) { + nsBaseAppShell::OnSystemTimezoneChange(); + + wcscpy_s(mTimezoneName, 128, systemTimezone); + } + } + + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + } else if (mayWait) { + // Block and wait for any posted application message + mozilla::BackgroundHangMonitor().NotifyWait(); + { + AUTO_PROFILER_LABEL("nsAppShell::ProcessNextNativeEvent::Wait", IDLE); + WinUtils::WaitForMessage(); + } + } + } while (!gotMessage && mayWait); + + // See DoProcessNextNativeEvent, mEventloopNestingLevel will be + // one when a modal loop unwinds. + if (mNativeCallbackPending && mEventloopNestingLevel == 1) + DoProcessMoreGeckoEvents(); + + // Check for starved native callbacks. If we haven't processed one + // of these events in NATIVE_EVENT_STARVATION_LIMIT, fire one off. + static const mozilla::TimeDuration nativeEventStarvationLimit = + mozilla::TimeDuration::FromSeconds(NATIVE_EVENT_STARVATION_LIMIT); + + TimeDuration timeSinceLastNativeEventScheduled; + { + MutexAutoLock lock(mLastNativeEventScheduledMutex); + timeSinceLastNativeEventScheduled = + TimeStamp::NowLoRes() - mLastNativeEventScheduled; + } + if (timeSinceLastNativeEventScheduled > nativeEventStarvationLimit) { + ScheduleNativeEventCallback(); + } + + return gotMessage; +} + +nsresult nsAppShell::AfterProcessNextEvent(nsIThreadInternal* /* unused */, + bool /* unused */) { + if (!mMsgsToRepost.empty()) { + for (MSG msg : mMsgsToRepost) { + ::PostMessageW(msg.hwnd, msg.message, msg.wParam, msg.lParam); + } + mMsgsToRepost.clear(); + } + return NS_OK; +} diff --git a/widget/windows/nsAppShell.h b/widget/windows/nsAppShell.h new file mode 100644 index 0000000000..20ea65e834 --- /dev/null +++ b/widget/windows/nsAppShell.h @@ -0,0 +1,64 @@ +/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */ +/* 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 nsAppShell_h__ +#define nsAppShell_h__ + +#include "nsBaseAppShell.h" +#include <windows.h> +#include <vector> +#include "mozilla/TimeStamp.h" +#include "mozilla/Mutex.h" + +// The maximum time we allow before forcing a native event callback. +// In seconds. +#define NATIVE_EVENT_STARVATION_LIMIT 1 + +/** + * Native Win32 Application shell wrapper + */ +class nsAppShell final : public nsBaseAppShell { + public: + nsAppShell() + : mEventWnd(nullptr), + mNativeCallbackPending(false), + mLastNativeEventScheduledMutex( + "nsAppShell::mLastNativeEventScheduledMutex") {} + typedef mozilla::TimeStamp TimeStamp; + typedef mozilla::Mutex Mutex; + + nsresult Init(); + void DoProcessMoreGeckoEvents(); + + static UINT GetTaskbarButtonCreatedMessage(); + + NS_IMETHOD AfterProcessNextEvent(nsIThreadInternal* thread, + bool eventWasProcessed) final; + + protected: + NS_IMETHOD Run() override; + NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override; + + virtual void ScheduleNativeEventCallback(); + virtual bool ProcessNextNativeEvent(bool mayWait); + virtual ~nsAppShell(); + + static LRESULT CALLBACK EventWindowProc(HWND, UINT, WPARAM, LPARAM); + + protected: + nsresult InitHiddenWindow(); + HWND mEventWnd; + bool mNativeCallbackPending; + + Mutex mLastNativeEventScheduledMutex MOZ_UNANNOTATED; + TimeStamp mLastNativeEventScheduled; + std::vector<MSG> mMsgsToRepost; + + private: + wchar_t mTimezoneName[128]; +}; + +#endif // nsAppShell_h__ diff --git a/widget/windows/nsBidiKeyboard.cpp b/widget/windows/nsBidiKeyboard.cpp new file mode 100644 index 0000000000..87d81d458e --- /dev/null +++ b/widget/windows/nsBidiKeyboard.cpp @@ -0,0 +1,169 @@ +/* -*- 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 <stdio.h> +#include "nsBidiKeyboard.h" +#include "WidgetUtils.h" +#include "nsIWidget.h" +#include <tchar.h> + +NS_IMPL_ISUPPORTS(nsBidiKeyboard, nsIBidiKeyboard) + +nsBidiKeyboard::nsBidiKeyboard() : nsIBidiKeyboard() { Reset(); } + +nsBidiKeyboard::~nsBidiKeyboard() {} + +NS_IMETHODIMP nsBidiKeyboard::Reset() { + mInitialized = false; + mHaveBidiKeyboards = false; + mLTRKeyboard[0] = '\0'; + mRTLKeyboard[0] = '\0'; + mCurrentLocaleName[0] = '\0'; + return NS_OK; +} + +NS_IMETHODIMP nsBidiKeyboard::IsLangRTL(bool* aIsRTL) { + *aIsRTL = false; + + nsresult result = SetupBidiKeyboards(); + if (NS_FAILED(result)) return result; + + HKL currentLocale; + + currentLocale = ::GetKeyboardLayout(0); + *aIsRTL = IsRTLLanguage(currentLocale); + + if (!::GetKeyboardLayoutNameW(mCurrentLocaleName)) return NS_ERROR_FAILURE; + + NS_ASSERTION(*mCurrentLocaleName, + "GetKeyboardLayoutName return string length == 0"); + NS_ASSERTION((wcslen(mCurrentLocaleName) < KL_NAMELENGTH), + "GetKeyboardLayoutName return string length >= KL_NAMELENGTH"); + + // The language set by the user overrides the default language for that + // direction + if (*aIsRTL) { + wcsncpy(mRTLKeyboard, mCurrentLocaleName, KL_NAMELENGTH); + mRTLKeyboard[KL_NAMELENGTH - 1] = '\0'; // null terminate + } else { + wcsncpy(mLTRKeyboard, mCurrentLocaleName, KL_NAMELENGTH); + mLTRKeyboard[KL_NAMELENGTH - 1] = '\0'; // null terminate + } + + NS_ASSERTION((wcslen(mRTLKeyboard) < KL_NAMELENGTH), + "mLTRKeyboard has string length >= KL_NAMELENGTH"); + NS_ASSERTION((wcslen(mLTRKeyboard) < KL_NAMELENGTH), + "mRTLKeyboard has string length >= KL_NAMELENGTH"); + return NS_OK; +} + +NS_IMETHODIMP nsBidiKeyboard::GetHaveBidiKeyboards(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + nsresult result = SetupBidiKeyboards(); + if (NS_FAILED(result)) return result; + + *aResult = mHaveBidiKeyboards; + return NS_OK; +} + +// Get the list of keyboard layouts available in the system +// Set mLTRKeyboard to the first LTR keyboard in the list and mRTLKeyboard to +// the first RTL keyboard in the list These defaults will be used unless the +// user explicitly sets something else. +nsresult nsBidiKeyboard::SetupBidiKeyboards() { + if (mInitialized) return mHaveBidiKeyboards ? NS_OK : NS_ERROR_FAILURE; + + int keyboards; + HKL far* buf; + HKL locale; + wchar_t localeName[KL_NAMELENGTH]; + bool isLTRKeyboardSet = false; + bool isRTLKeyboardSet = false; + + // GetKeyboardLayoutList with 0 as first parameter returns the number of + // keyboard layouts available + keyboards = ::GetKeyboardLayoutList(0, nullptr); + if (!keyboards) return NS_ERROR_FAILURE; + + // allocate a buffer to hold the list + buf = (HKL far*)malloc(keyboards * sizeof(HKL)); + if (!buf) return NS_ERROR_OUT_OF_MEMORY; + + // Call again to fill the buffer + if (::GetKeyboardLayoutList(keyboards, buf) != keyboards) { + free(buf); + return NS_ERROR_UNEXPECTED; + } + + // Go through the list and pick a default LTR and RTL keyboard layout + while (keyboards--) { + locale = buf[keyboards]; + if (IsRTLLanguage(locale)) { + _snwprintf(mRTLKeyboard, KL_NAMELENGTH, L"%.*x", KL_NAMELENGTH - 1, + LANGIDFROMLCID((DWORD_PTR)locale)); + isRTLKeyboardSet = true; + } else { + _snwprintf(mLTRKeyboard, KL_NAMELENGTH, L"%.*x", KL_NAMELENGTH - 1, + LANGIDFROMLCID((DWORD_PTR)locale)); + isLTRKeyboardSet = true; + } + } + free(buf); + mInitialized = true; + + // If there is not at least one keyboard of each directionality, Bidi + // keyboard functionality will be disabled. + mHaveBidiKeyboards = (isRTLKeyboardSet && isLTRKeyboardSet); + if (!mHaveBidiKeyboards) return NS_ERROR_FAILURE; + + // Get the current keyboard layout and use it for either mRTLKeyboard or + // mLTRKeyboard as appropriate. If the user has many keyboard layouts + // installed this prevents us from arbitrarily resetting the current + // layout (bug 80274) + locale = ::GetKeyboardLayout(0); + if (!::GetKeyboardLayoutNameW(localeName)) return NS_ERROR_FAILURE; + + NS_ASSERTION(*localeName, "GetKeyboardLayoutName return string length == 0"); + NS_ASSERTION((wcslen(localeName) < KL_NAMELENGTH), + "GetKeyboardLayout return string length >= KL_NAMELENGTH"); + + if (IsRTLLanguage(locale)) { + wcsncpy(mRTLKeyboard, localeName, KL_NAMELENGTH); + mRTLKeyboard[KL_NAMELENGTH - 1] = '\0'; // null terminate + } else { + wcsncpy(mLTRKeyboard, localeName, KL_NAMELENGTH); + mLTRKeyboard[KL_NAMELENGTH - 1] = '\0'; // null terminate + } + + NS_ASSERTION(*mRTLKeyboard, "mLTRKeyboard has string length == 0"); + NS_ASSERTION(*mLTRKeyboard, "mLTRKeyboard has string length == 0"); + + return NS_OK; +} + +// Test whether the language represented by this locale identifier is a +// right-to-left language, using bit 123 of the Unicode subset bitfield in +// the LOCALESIGNATURE +// See +// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/intl/unicode_63ub.asp +bool nsBidiKeyboard::IsRTLLanguage(HKL aLocale) { + LOCALESIGNATURE localesig; + return (::GetLocaleInfoW(PRIMARYLANGID((DWORD_PTR)aLocale), + LOCALE_FONTSIGNATURE, (LPWSTR)&localesig, + (sizeof(localesig) / sizeof(WCHAR))) && + (localesig.lsUsb[3] & 0x08000000)); +} + +// static +void nsBidiKeyboard::OnLayoutChange() { + mozilla::widget::WidgetUtils::SendBidiKeyboardInfoToContent(); +} + +// static +already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboardInner() { + return do_AddRef(new nsBidiKeyboard()); +} diff --git a/widget/windows/nsBidiKeyboard.h b/widget/windows/nsBidiKeyboard.h new file mode 100644 index 0000000000..584d4d2dee --- /dev/null +++ b/widget/windows/nsBidiKeyboard.h @@ -0,0 +1,34 @@ +/* -*- 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 __nsBidiKeyboard +#define __nsBidiKeyboard +#include "nsIBidiKeyboard.h" +#include <windows.h> + +class nsBidiKeyboard : public nsIBidiKeyboard { + virtual ~nsBidiKeyboard(); + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIBIDIKEYBOARD + + nsBidiKeyboard(); + + static void OnLayoutChange(); + + protected: + nsresult SetupBidiKeyboards(); + bool IsRTLLanguage(HKL aLocale); + + bool mInitialized; + bool mHaveBidiKeyboards; + wchar_t mLTRKeyboard[KL_NAMELENGTH]; + wchar_t mRTLKeyboard[KL_NAMELENGTH]; + wchar_t mCurrentLocaleName[KL_NAMELENGTH]; +}; + +#endif // __nsBidiKeyboard diff --git a/widget/windows/nsClipboard.cpp b/widget/windows/nsClipboard.cpp new file mode 100644 index 0000000000..8783affd1e --- /dev/null +++ b/widget/windows/nsClipboard.cpp @@ -0,0 +1,1472 @@ +/* -*- 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 "nsClipboard.h" + +#include <shlobj.h> +#include <intshcut.h> + +// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN +#include <shellapi.h> + +#include <functional> +#include <thread> +#include <chrono> + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/Compatibility.h" +#endif +#include "mozilla/Logging.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_clipboard.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/WindowsVersion.h" +#include "SpecialSystemDirectory.h" + +#include "nsArrayUtils.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsDataObj.h" +#include "nsString.h" +#include "nsNativeCharsetUtils.h" +#include "nsIInputStream.h" +#include "nsITransferable.h" +#include "nsXPCOM.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsPrimitiveHelpers.h" +#include "nsIWidget.h" +#include "nsWidgetsCID.h" +#include "nsCRT.h" +#include "nsNetUtil.h" +#include "nsIFileProtocolHandler.h" +#include "nsEscape.h" +#include "nsIObserverService.h" +#include "nsMimeTypes.h" +#include "imgITools.h" +#include "imgIContainer.h" +#include "WinUtils.h" + +/* static */ +UINT nsClipboard::GetClipboardFileDescriptorFormatA() { + static UINT format = ::RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORA); + MOZ_ASSERT(format); + return format; +} + +/* static */ +UINT nsClipboard::GetClipboardFileDescriptorFormatW() { + static UINT format = ::RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORW); + MOZ_ASSERT(format); + return format; +} + +/* static */ +UINT nsClipboard::GetHtmlClipboardFormat() { + static UINT format = ::RegisterClipboardFormatW(L"HTML Format"); + return format; +} + +/* static */ +UINT nsClipboard::GetCustomClipboardFormat() { + static UINT format = + ::RegisterClipboardFormatW(L"application/x-moz-custom-clipdata"); + return format; +} + +//------------------------------------------------------------------------- +// +// nsClipboard constructor +// +//------------------------------------------------------------------------- +nsClipboard::nsClipboard() + : nsBaseClipboard(mozilla::dom::ClipboardCapabilities( + false /* supportsSelectionClipboard */, + false /* supportsFindClipboard */, + false /* supportsSelectionCache */)) { + mWindow = nullptr; + + // Register for a shutdown notification so that we can flush data + // to the OS clipboard. + nsCOMPtr<nsIObserverService> observerService = + do_GetService("@mozilla.org/observer-service;1"); + if (observerService) { + observerService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, + false); + } +} + +//------------------------------------------------------------------------- +// nsClipboard destructor +//------------------------------------------------------------------------- +nsClipboard::~nsClipboard() {} + +NS_IMPL_ISUPPORTS_INHERITED(nsClipboard, nsBaseClipboard, nsIObserver) + +NS_IMETHODIMP +nsClipboard::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + // This will be called on shutdown. + ::OleFlushClipboard(); + ::CloseClipboard(); + + return NS_OK; +} + +//------------------------------------------------------------------------- +UINT nsClipboard::GetFormat(const char* aMimeStr, bool aMapHTMLMime) { + UINT format; + + if (strcmp(aMimeStr, kTextMime) == 0) { + format = CF_UNICODETEXT; + } else if (strcmp(aMimeStr, kRTFMime) == 0) { + format = ::RegisterClipboardFormat(L"Rich Text Format"); + } else if (strcmp(aMimeStr, kJPEGImageMime) == 0 || + strcmp(aMimeStr, kJPGImageMime) == 0 || + strcmp(aMimeStr, kPNGImageMime) == 0) { + format = CF_DIBV5; + } else if (strcmp(aMimeStr, kFileMime) == 0 || + strcmp(aMimeStr, kFilePromiseMime) == 0) { + format = CF_HDROP; + } else if ((strcmp(aMimeStr, kNativeHTMLMime) == 0) || + (aMapHTMLMime && strcmp(aMimeStr, kHTMLMime) == 0)) { + format = GetHtmlClipboardFormat(); + } else if (strcmp(aMimeStr, kCustomTypesMime) == 0) { + format = GetCustomClipboardFormat(); + } else { + format = ::RegisterClipboardFormatW(NS_ConvertASCIItoUTF16(aMimeStr).get()); + } + + return format; +} + +//------------------------------------------------------------------------- +// static +nsresult nsClipboard::CreateNativeDataObject( + nsITransferable* aTransferable, IDataObject** aDataObj, nsIURI* aUri, + MightNeedToFlush* aMightNeedToFlush) { + MOZ_ASSERT(aTransferable); + if (!aTransferable) { + return NS_ERROR_FAILURE; + } + + // Create our native DataObject that implements the OLE IDataObject interface + RefPtr<nsDataObj> dataObj = new nsDataObj(aUri); + + // Now set it up with all the right data flavors & enums + nsresult res = + SetupNativeDataObject(aTransferable, dataObj, aMightNeedToFlush); + if (NS_SUCCEEDED(res)) { + dataObj.forget(aDataObj); + } + return res; +} + +static nsresult StoreValueInDataObject(nsDataObj* aObj, + LPCWSTR aClipboardFormat, DWORD value) { + HGLOBAL hGlobalMemory = ::GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD)); + if (!hGlobalMemory) { + return NS_ERROR_OUT_OF_MEMORY; + } + DWORD* pdw = (DWORD*)::GlobalLock(hGlobalMemory); + *pdw = value; + ::GlobalUnlock(hGlobalMemory); + + STGMEDIUM stg; + stg.tymed = TYMED_HGLOBAL; + stg.pUnkForRelease = nullptr; + stg.hGlobal = hGlobalMemory; + + FORMATETC fe; + SET_FORMATETC(fe, ::RegisterClipboardFormat(aClipboardFormat), 0, + DVASPECT_CONTENT, -1, TYMED_HGLOBAL) + aObj->SetData(&fe, &stg, TRUE); + + return NS_OK; +} + +//------------------------------------------------------------------------- +nsresult nsClipboard::SetupNativeDataObject( + nsITransferable* aTransferable, IDataObject* aDataObj, + MightNeedToFlush* aMightNeedToFlush) { + MOZ_ASSERT(aTransferable); + MOZ_ASSERT(aDataObj); + if (!aTransferable || !aDataObj) { + return NS_ERROR_FAILURE; + } + + auto* dObj = static_cast<nsDataObj*>(aDataObj); + if (aMightNeedToFlush) { + *aMightNeedToFlush = MightNeedToFlush::No; + } + + // Now give the Transferable to the DataObject + // for getting the data out of it + dObj->SetTransferable(aTransferable); + + // Get the transferable list of data flavors + nsTArray<nsCString> flavors; + aTransferable->FlavorsTransferableCanExport(flavors); + + // Walk through flavors that contain data and register them + // into the DataObj as supported flavors + for (uint32_t i = 0; i < flavors.Length(); i++) { + nsCString& flavorStr = flavors[i]; + + // When putting data onto the clipboard, we want to maintain kHTMLMime + // ("text/html") and not map it to CF_HTML here since this will be done + // below. + UINT format = GetFormat(flavorStr.get(), false); + + // Now tell the native IDataObject about both our mime type and + // the native data format + FORMATETC fe; + SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + dObj->AddDataFlavor(flavorStr.get(), &fe); + + // Do various things internal to the implementation, like map one + // flavor to another or add additional flavors based on what's required + // for the win32 impl. + if (flavorStr.EqualsLiteral(kTextMime)) { + // if we find text/plain, also add CF_TEXT, but we can add it for + // text/plain as well. + FORMATETC textFE; + SET_FORMATETC(textFE, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + dObj->AddDataFlavor(kTextMime, &textFE); + if (aMightNeedToFlush) { + *aMightNeedToFlush = MightNeedToFlush::Yes; + } + } else if (flavorStr.EqualsLiteral(kHTMLMime)) { + // if we find text/html, also advertise win32's html flavor (which we will + // convert on our own in nsDataObj::GetText(). + FORMATETC htmlFE; + SET_FORMATETC(htmlFE, GetHtmlClipboardFormat(), 0, DVASPECT_CONTENT, -1, + TYMED_HGLOBAL); + dObj->AddDataFlavor(kHTMLMime, &htmlFE); + } else if (flavorStr.EqualsLiteral(kURLMime)) { + // if we're a url, in addition to also being text, we need to register + // the "file" flavors so that the win32 shell knows to create an internet + // shortcut when it sees one of these beasts. + FORMATETC shortcutFE; + SET_FORMATETC(shortcutFE, + ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA), 0, + DVASPECT_CONTENT, -1, TYMED_HGLOBAL) + dObj->AddDataFlavor(kURLMime, &shortcutFE); + SET_FORMATETC(shortcutFE, + ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW), 0, + DVASPECT_CONTENT, -1, TYMED_HGLOBAL) + dObj->AddDataFlavor(kURLMime, &shortcutFE); + SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_FILECONTENTS), + 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) + dObj->AddDataFlavor(kURLMime, &shortcutFE); + SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_INETURLA), 0, + DVASPECT_CONTENT, -1, TYMED_HGLOBAL) + dObj->AddDataFlavor(kURLMime, &shortcutFE); + SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_INETURLW), 0, + DVASPECT_CONTENT, -1, TYMED_HGLOBAL) + dObj->AddDataFlavor(kURLMime, &shortcutFE); + } else if (flavorStr.EqualsLiteral(kPNGImageMime) || + flavorStr.EqualsLiteral(kJPEGImageMime) || + flavorStr.EqualsLiteral(kJPGImageMime) || + flavorStr.EqualsLiteral(kGIFImageMime) || + flavorStr.EqualsLiteral(kNativeImageMime)) { + // if we're an image, register the native bitmap flavor + FORMATETC imageFE; + // Add DIBv5 + SET_FORMATETC(imageFE, CF_DIBV5, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) + dObj->AddDataFlavor(flavorStr.get(), &imageFE); + // Add DIBv3 + SET_FORMATETC(imageFE, CF_DIB, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) + dObj->AddDataFlavor(flavorStr.get(), &imageFE); + } else if (flavorStr.EqualsLiteral(kFilePromiseMime)) { + // if we're a file promise flavor, also register the + // CFSTR_PREFERREDDROPEFFECT format. The data object + // returns a value of DROPEFFECTS_MOVE to the drop target + // when it asks for the value of this format. This causes + // the file to be moved from the temporary location instead + // of being copied. The right thing to do here is to call + // SetData() on the data object and set the value of this format + // to DROPEFFECTS_MOVE on this particular data object. But, + // since all the other clipboard formats follow the model of setting + // data on the data object only when the drop object calls GetData(), + // I am leaving this format's value hard coded in the data object. + // We can change this if other consumers of this format get added to this + // codebase and they need different values. + FORMATETC shortcutFE; + SET_FORMATETC(shortcutFE, + ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT), 0, + DVASPECT_CONTENT, -1, TYMED_HGLOBAL) + dObj->AddDataFlavor(kFilePromiseMime, &shortcutFE); + } + } + + if (!mozilla::StaticPrefs:: + clipboard_copyPrivateDataToClipboardCloudOrHistory()) { + // Let Clipboard know that data is sensitive and must not be copied to + // the Cloud Clipboard, Clipboard History and similar. + // https://docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats#cloud-clipboard-and-clipboard-history-formats + if (aTransferable->GetIsPrivateData()) { + nsresult rv = + StoreValueInDataObject(dObj, TEXT("CanUploadToCloudClipboard"), 0); + NS_ENSURE_SUCCESS(rv, rv); + rv = + StoreValueInDataObject(dObj, TEXT("CanIncludeInClipboardHistory"), 0); + NS_ENSURE_SUCCESS(rv, rv); + rv = StoreValueInDataObject( + dObj, TEXT("ExcludeClipboardContentFromMonitorProcessing"), 0); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +// See methods listed at +// <https://docs.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-idataobject#methods>. +static void IDataObjectMethodResultToString(const HRESULT aHres, + nsACString& aResult) { + switch (aHres) { + case E_INVALIDARG: + aResult = "E_INVALIDARG"; + break; + case E_UNEXPECTED: + aResult = "E_UNEXPECTED"; + break; + case E_OUTOFMEMORY: + aResult = "E_OUTOFMEMORY"; + break; + case DV_E_LINDEX: + aResult = "DV_E_LINDEX"; + break; + case DV_E_FORMATETC: + aResult = "DV_E_FORMATETC"; + break; + case DV_E_TYMED: + aResult = "DV_E_TYMED"; + break; + case DV_E_DVASPECT: + aResult = "DV_E_DVASPECT"; + break; + case OLE_E_NOTRUNNING: + aResult = "OLE_E_NOTRUNNING"; + break; + case STG_E_MEDIUMFULL: + aResult = "STG_E_MEDIUMFULL"; + break; + case DV_E_CLIPFORMAT: + aResult = "DV_E_CLIPFORMAT"; + break; + case S_OK: + aResult = "S_OK"; + break; + default: + // Explicit template instantiaton, because otherwise the call is + // ambiguous. + constexpr int kRadix = 16; + aResult = IntToCString<int32_t>(aHres, kRadix); + break; + } +} + +// See +// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olegetclipboard>. +static void OleGetClipboardResultToString(const HRESULT aHres, + nsACString& aResult) { + switch (aHres) { + case S_OK: + aResult = "S_OK"; + break; + case CLIPBRD_E_CANT_OPEN: + aResult = "CLIPBRD_E_CANT_OPEN"; + break; + case CLIPBRD_E_CANT_CLOSE: + aResult = "CLIPBRD_E_CANT_CLOSE"; + break; + default: + // Explicit template instantiaton, because otherwise the call is + // ambiguous. + constexpr int kRadix = 16; + aResult = IntToCString<int32_t>(aHres, kRadix); + break; + } +} + +// See +// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olegetclipboard>. +static void LogOleGetClipboardResult(const HRESULT aHres) { + if (MOZ_CLIPBOARD_LOG_ENABLED()) { + nsAutoCString hresString; + OleGetClipboardResultToString(aHres, hresString); + MOZ_CLIPBOARD_LOG("OleGetClipboard result: %s", hresString.get()); + } +} + +// See +// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olesetclipboard>. +static void OleSetClipboardResultToString(HRESULT aHres, nsACString& aResult) { + switch (aHres) { + case S_OK: + aResult = "S_OK"; + break; + case CLIPBRD_E_CANT_OPEN: + aResult = "CLIPBRD_E_CANT_OPEN"; + break; + case CLIPBRD_E_CANT_EMPTY: + aResult = "CLIPBRD_E_CANT_EMPTY"; + break; + case CLIPBRD_E_CANT_CLOSE: + aResult = "CLIPBRD_E_CANT_CLOSE"; + break; + case CLIPBRD_E_CANT_SET: + aResult = "CLIPBRD_E_CANT_SET"; + break; + default: + // Explicit template instantiaton, because otherwise the call is + // ambiguous. + constexpr int kRadix = 16; + aResult = IntToCString<int32_t>(aHres, kRadix); + break; + } +} + +// See +// <https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olesetclipboard>. +static void LogOleSetClipboardResult(const HRESULT aHres) { + if (MOZ_CLIPBOARD_LOG_ENABLED()) { + nsAutoCString hresString; + OleSetClipboardResultToString(aHres, hresString); + MOZ_CLIPBOARD_LOG("OleSetClipboard result: %s", hresString.get()); + } +} + +template <typename Function, typename LogFunction, typename... Args> +static HRESULT RepeatedlyTry(Function aFunction, LogFunction aLogFunction, + Args... aArgs) { + // These are magic values based on local testing. They are chosen not higher + // to avoid jank (<https://developer.mozilla.org/en-US/docs/Glossary/Jank>). + // When changing them, be careful. + static constexpr int kNumberOfTries = 3; + static constexpr int kDelayInMs = 3; + + HRESULT hres; + for (int i = 0; i < kNumberOfTries; ++i) { + hres = aFunction(aArgs...); + aLogFunction(hres); + + if (hres == S_OK) { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(kDelayInMs)); + } + + return hres; +} + +// Other apps can block access to the clipboard. This repeatedly +// calls `::OleSetClipboard` for a fixed number of times and should be called +// instead of `::OleSetClipboard`. +static void RepeatedlyTryOleSetClipboard(IDataObject* aDataObj) { + RepeatedlyTry(::OleSetClipboard, LogOleSetClipboardResult, aDataObj); +} + +//------------------------------------------------------------------------- +NS_IMETHODIMP nsClipboard::SetNativeClipboardData( + nsITransferable* aTransferable, int32_t aWhichClipboard) { + MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); + + if (aWhichClipboard != kGlobalClipboard) { + return NS_ERROR_FAILURE; + } + + // make sure we have a good transferable + if (!aTransferable) { + return NS_ERROR_FAILURE; + } + +#ifdef ACCESSIBILITY + mozilla::a11y::Compatibility::SuppressA11yForClipboardCopy(); +#endif + + RefPtr<IDataObject> dataObj; + auto mightNeedToFlush = MightNeedToFlush::No; + if (NS_SUCCEEDED(CreateNativeDataObject(aTransferable, + getter_AddRefs(dataObj), nullptr, + &mightNeedToFlush))) { + RepeatedlyTryOleSetClipboard(dataObj); + + const bool doFlush = [&] { + switch (mozilla::StaticPrefs::widget_windows_sync_clipboard_flush()) { + case 0: + return false; + case 1: + return true; + default: + // Bug 1774285: Windows Suggested Actions (introduced in Windows 11 + // 22H2) walks the entire a11y tree using UIA if something is placed + // on the clipboard using delayed rendering. (The OLE clipboard always + // uses delayed rendering.) This a11y tree walk causes an unacceptable + // hang, particularly when the a11y cache is disabled. We choose the + // lesser of the two performance/memory evils here and force immediate + // rendering as part of our workaround. + return mightNeedToFlush == MightNeedToFlush::Yes && + mozilla::IsWin1122H2OrLater(); + } + }(); + if (doFlush) { + RepeatedlyTry(::OleFlushClipboard, [](HRESULT) {}); + } + } else { + // Clear the native clipboard + RepeatedlyTryOleSetClipboard(nullptr); + } + + return NS_OK; +} + +//------------------------------------------------------------------------- +nsresult nsClipboard::GetGlobalData(HGLOBAL aHGBL, void** aData, + uint32_t* aLen) { + MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); + + // Allocate a new memory buffer and copy the data from global memory. + // Recall that win98 allocates to nearest DWORD boundary. As a safety + // precaution, allocate an extra 3 bytes (but don't report them in |aLen|!) + // and null them out to ensure that all of our NS_strlen calls will succeed. + // NS_strlen operates on char16_t, so we need 3 NUL bytes to ensure it finds + // a full NUL char16_t when |*aLen| is odd. + nsresult result = NS_ERROR_FAILURE; + if (aHGBL != nullptr) { + LPSTR lpStr = (LPSTR)GlobalLock(aHGBL); + mozilla::CheckedInt<uint32_t> allocSize = + mozilla::CheckedInt<uint32_t>(GlobalSize(aHGBL)) + 3; + if (!allocSize.isValid()) { + return NS_ERROR_INVALID_ARG; + } + char* data = static_cast<char*>(malloc(allocSize.value())); + if (data) { + uint32_t size = allocSize.value() - 3; + memcpy(data, lpStr, size); + // null terminate for safety + data[size] = data[size + 1] = data[size + 2] = '\0'; + + GlobalUnlock(aHGBL); + *aData = data; + *aLen = size; + + result = NS_OK; + } + } else { + // We really shouldn't ever get here + // but just in case + *aData = nullptr; + *aLen = 0; + LPVOID lpMsgBuf; + + FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, nullptr, + GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language + (LPWSTR)&lpMsgBuf, 0, nullptr); + + // Display the string. + MessageBoxW(nullptr, (LPCWSTR)lpMsgBuf, L"GetLastError", + MB_OK | MB_ICONINFORMATION); + + // Free the buffer. + LocalFree(lpMsgBuf); + } + + return result; +} + +//------------------------------------------------------------------------- +nsresult nsClipboard::GetNativeDataOffClipboard(nsIWidget* aWidget, + UINT /*aIndex*/, UINT aFormat, + void** aData, uint32_t* aLen) { + MOZ_CLIPBOARD_LOG("%s: overload taking nsIWidget*.", __FUNCTION__); + + HGLOBAL hglb; + nsresult result = NS_ERROR_FAILURE; + + HWND nativeWin = nullptr; + if (::OpenClipboard(nativeWin)) { + hglb = ::GetClipboardData(aFormat); + result = GetGlobalData(hglb, aData, aLen); + ::CloseClipboard(); + } + return result; +} + +// See methods listed at +// <https://docs.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-idataobject#methods>. +static void LogIDataObjectMethodResult(const HRESULT aHres, + const nsCString& aMethodName) { + if (MOZ_CLIPBOARD_LOG_ENABLED()) { + nsAutoCString hresString; + IDataObjectMethodResultToString(aHres, hresString); + MOZ_CLIPBOARD_LOG("IDataObject::%s result : %s", aMethodName.get(), + hresString.get()); + } +} + +// Other apps can block access to the clipboard. This repeatedly calls +// `GetData` for a fixed number of times and should be called instead of +// `GetData`. See +// <https://docs.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-idataobject-getdata>. +// While Microsoft's documentation doesn't include `CLIPBRD_E_CANT_OPEN` +// explicitly, it allows it implicitly and in local experiments it was indeed +// returned. +static HRESULT RepeatedlyTryGetData(IDataObject& aDataObject, LPFORMATETC pFE, + LPSTGMEDIUM pSTM) { + return RepeatedlyTry( + [&aDataObject, &pFE, &pSTM]() { return aDataObject.GetData(pFE, pSTM); }, + std::bind(LogIDataObjectMethodResult, std::placeholders::_1, + "GetData"_ns)); +} + +//------------------------------------------------------------------------- +// static +HRESULT nsClipboard::FillSTGMedium(IDataObject* aDataObject, UINT aFormat, + LPFORMATETC pFE, LPSTGMEDIUM pSTM, + DWORD aTymed) { + SET_FORMATETC(*pFE, aFormat, 0, DVASPECT_CONTENT, -1, aTymed); + + // Starting by querying for the data to see if we can get it as from global + // memory + HRESULT hres = S_FALSE; + hres = aDataObject->QueryGetData(pFE); + LogIDataObjectMethodResult(hres, "QueryGetData"_ns); + if (S_OK == hres) { + hres = RepeatedlyTryGetData(*aDataObject, pFE, pSTM); + } + return hres; +} + +//------------------------------------------------------------------------- +// If aFormat is CF_DIBV5, aMIMEImageFormat must be a type for which we have +// an image encoder (e.g. image/png). +// For other values of aFormat, it is OK to pass null for aMIMEImageFormat. +nsresult nsClipboard::GetNativeDataOffClipboard(IDataObject* aDataObject, + UINT aIndex, UINT aFormat, + const char* aMIMEImageFormat, + void** aData, uint32_t* aLen) { + MOZ_CLIPBOARD_LOG("%s: overload taking IDataObject*.", __FUNCTION__); + + nsresult result = NS_ERROR_FAILURE; + *aData = nullptr; + *aLen = 0; + + if (!aDataObject) { + return result; + } + + UINT format = aFormat; + HRESULT hres = S_FALSE; + + // XXX at the moment we only support global memory transfers + // It is here where we will add support for native images + // and IStream + FORMATETC fe; + STGMEDIUM stm; + hres = FillSTGMedium(aDataObject, format, &fe, &stm, TYMED_HGLOBAL); + + // If the format is CF_HDROP and we haven't found any files we can try looking + // for virtual files with FILEDESCRIPTOR. + if (FAILED(hres) && format == CF_HDROP) { + hres = FillSTGMedium(aDataObject, + nsClipboard::GetClipboardFileDescriptorFormatW(), &fe, + &stm, TYMED_HGLOBAL); + if (FAILED(hres)) { + hres = FillSTGMedium(aDataObject, + nsClipboard::GetClipboardFileDescriptorFormatA(), + &fe, &stm, TYMED_HGLOBAL); + } + } + + // Currently this is only handling TYMED_HGLOBAL data + // For Text, Dibs, Files, and generic data (like HTML) + if (S_OK == hres) { + static CLIPFORMAT fileDescriptorFlavorA = + ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA); + static CLIPFORMAT fileDescriptorFlavorW = + ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + static CLIPFORMAT fileFlavor = + ::RegisterClipboardFormat(CFSTR_FILECONTENTS); + static CLIPFORMAT preferredDropEffect = + ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT); + + switch (stm.tymed) { + case TYMED_HGLOBAL: { + switch (fe.cfFormat) { + case CF_TEXT: { + // Get the data out of the global data handle. The size we + // return should not include the null because the other + // platforms don't use nulls, so just return the length we get + // back from strlen(), since we know CF_TEXT is null + // terminated. Recall that GetGlobalData() returns the size of + // the allocated buffer, not the size of the data (on 98, these + // are not the same) so we can't use that. + uint32_t allocLen = 0; + if (NS_SUCCEEDED(GetGlobalData(stm.hGlobal, aData, &allocLen))) { + *aLen = strlen(reinterpret_cast<char*>(*aData)); + result = NS_OK; + } + } break; + + case CF_UNICODETEXT: { + // Get the data out of the global data handle. The size we + // return should not include the null because the other + // platforms don't use nulls, so just return the length we get + // back from strlen(), since we know CF_UNICODETEXT is null + // terminated. Recall that GetGlobalData() returns the size of + // the allocated buffer, not the size of the data (on 98, these + // are not the same) so we can't use that. + uint32_t allocLen = 0; + if (NS_SUCCEEDED(GetGlobalData(stm.hGlobal, aData, &allocLen))) { + *aLen = NS_strlen(reinterpret_cast<char16_t*>(*aData)) * 2; + result = NS_OK; + } + } break; + + case CF_DIBV5: + if (aMIMEImageFormat) { + uint32_t allocLen = 0; + const char* clipboardData; + if (NS_SUCCEEDED(GetGlobalData( + stm.hGlobal, (void**)&clipboardData, &allocLen))) { + nsCOMPtr<imgIContainer> container; + nsCOMPtr<imgITools> imgTools = + do_CreateInstance("@mozilla.org/image/tools;1"); + result = imgTools->DecodeImageFromBuffer( + clipboardData, allocLen, + nsLiteralCString(IMAGE_BMP_MS_CLIPBOARD), + getter_AddRefs(container)); + if (NS_FAILED(result)) { + break; + } + + nsAutoCString mimeType; + if (strcmp(aMIMEImageFormat, kJPGImageMime) == 0) { + mimeType.Assign(IMAGE_JPEG); + } else { + mimeType.Assign(aMIMEImageFormat); + } + + nsCOMPtr<nsIInputStream> inputStream; + result = imgTools->EncodeImage(container, mimeType, u""_ns, + getter_AddRefs(inputStream)); + if (NS_FAILED(result)) { + break; + } + + if (!inputStream) { + result = NS_ERROR_FAILURE; + break; + } + + *aData = inputStream.forget().take(); + *aLen = sizeof(nsIInputStream*); + } + } + break; + + case CF_HDROP: { + // in the case of a file drop, multiple files are stashed within a + // single data object. In order to match mozilla's D&D apis, we + // just pull out the file at the requested index, pretending as + // if there really are multiple drag items. + HDROP dropFiles = (HDROP)GlobalLock(stm.hGlobal); + + UINT numFiles = ::DragQueryFileW(dropFiles, 0xFFFFFFFF, nullptr, 0); + NS_ASSERTION(numFiles > 0, + "File drop flavor, but no files...hmmmm"); + NS_ASSERTION(aIndex < numFiles, + "Asked for a file index out of range of list"); + if (numFiles > 0) { + UINT fileNameLen = + ::DragQueryFileW(dropFiles, aIndex, nullptr, 0); + wchar_t* buffer = reinterpret_cast<wchar_t*>( + moz_xmalloc((fileNameLen + 1) * sizeof(wchar_t))); + ::DragQueryFileW(dropFiles, aIndex, buffer, fileNameLen + 1); + *aData = buffer; + *aLen = fileNameLen * sizeof(char16_t); + result = NS_OK; + } + GlobalUnlock(stm.hGlobal); + + } break; + + default: { + if (fe.cfFormat == fileDescriptorFlavorA || + fe.cfFormat == fileDescriptorFlavorW) { + nsAutoString tempPath; + + LPFILEGROUPDESCRIPTOR fgdesc = + static_cast<LPFILEGROUPDESCRIPTOR>(GlobalLock(stm.hGlobal)); + if (fgdesc) { + result = GetTempFilePath( + nsDependentString((fgdesc->fgd)[aIndex].cFileName), + tempPath); + GlobalUnlock(stm.hGlobal); + } + if (NS_FAILED(result)) { + break; + } + result = SaveStorageOrStream(aDataObject, aIndex, tempPath); + if (NS_FAILED(result)) { + break; + } + wchar_t* buffer = reinterpret_cast<wchar_t*>( + moz_xmalloc((tempPath.Length() + 1) * sizeof(wchar_t))); + wcscpy(buffer, tempPath.get()); + *aData = buffer; + *aLen = tempPath.Length() * sizeof(wchar_t); + result = NS_OK; + } else if (fe.cfFormat == fileFlavor) { + NS_WARNING( + "Mozilla doesn't yet understand how to read this type of " + "file flavor"); + } else { + // Get the data out of the global data handle. The size we + // return should not include the null because the other + // platforms don't use nulls, so just return the length we get + // back from strlen(), since we know CF_UNICODETEXT is null + // terminated. Recall that GetGlobalData() returns the size of + // the allocated buffer, not the size of the data (on 98, these + // are not the same) so we can't use that. + // + // NOTE: we are assuming that anything that falls into this + // default case is unicode. As we start to get more + // kinds of binary data, this may become an incorrect + // assumption. Stay tuned. + uint32_t allocLen = 0; + if (NS_SUCCEEDED(GetGlobalData(stm.hGlobal, aData, &allocLen))) { + if (fe.cfFormat == GetHtmlClipboardFormat()) { + // CF_HTML is actually UTF8, not unicode, so disregard the + // assumption above. We have to check the header for the + // actual length, and we'll do that in FindPlatformHTML(). + // For now, return the allocLen. This case is mostly to + // ensure we don't try to call strlen on the buffer. + *aLen = allocLen; + } else if (fe.cfFormat == GetCustomClipboardFormat()) { + // Binary data + *aLen = allocLen; + } else if (fe.cfFormat == preferredDropEffect) { + // As per the MSDN doc entitled: "Shell Clipboard Formats" + // CFSTR_PREFERREDDROPEFFECT should return a DWORD + // Reference: + // http://msdn.microsoft.com/en-us/library/bb776902(v=vs.85).aspx + NS_ASSERTION( + allocLen == sizeof(DWORD), + "CFSTR_PREFERREDDROPEFFECT should return a DWORD"); + *aLen = allocLen; + } else { + *aLen = NS_strlen(reinterpret_cast<char16_t*>(*aData)) * + sizeof(char16_t); + } + result = NS_OK; + } + } + } break; + } // switch + } break; + + case TYMED_GDI: { +#ifdef DEBUG + MOZ_CLIPBOARD_LOG("*********************** TYMED_GDI"); +#endif + } break; + + default: + break; + } // switch + + ReleaseStgMedium(&stm); + } + + return result; +} + +//------------------------------------------------------------------------- +nsresult nsClipboard::GetDataFromDataObject(IDataObject* aDataObject, + UINT anIndex, nsIWidget* aWindow, + nsITransferable* aTransferable) { + MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); + + // make sure we have a good transferable + if (!aTransferable) { + return NS_ERROR_INVALID_ARG; + } + + nsresult res = NS_ERROR_FAILURE; + + // get flavor list that includes all flavors that can be written (including + // ones obtained through conversion) + nsTArray<nsCString> flavors; + res = aTransferable->FlavorsTransferableCanImport(flavors); + if (NS_FAILED(res)) { + return NS_ERROR_FAILURE; + } + + // Walk through flavors and see which flavor is on the clipboard them on the + // native clipboard, + for (uint32_t i = 0; i < flavors.Length(); i++) { + nsCString& flavorStr = flavors[i]; + UINT format = GetFormat(flavorStr.get()); + + // Try to get the data using the desired flavor. This might fail, but all is + // not lost. + void* data = nullptr; + uint32_t dataLen = 0; + bool dataFound = false; + if (nullptr != aDataObject) { + if (NS_SUCCEEDED(GetNativeDataOffClipboard(aDataObject, anIndex, format, + flavorStr.get(), &data, + &dataLen))) { + dataFound = true; + } + } else if (nullptr != aWindow) { + if (NS_SUCCEEDED(GetNativeDataOffClipboard(aWindow, anIndex, format, + &data, &dataLen))) { + dataFound = true; + } + } + + // This is our second chance to try to find some data, having not found it + // when directly asking for the flavor. Let's try digging around in other + // flavors to help satisfy our craving for data. + if (!dataFound) { + if (flavorStr.EqualsLiteral(kTextMime)) { + dataFound = + FindUnicodeFromPlainText(aDataObject, anIndex, &data, &dataLen); + } else if (flavorStr.EqualsLiteral(kURLMime)) { + // drags from other windows apps expose the native + // CFSTR_INETURL{A,W} flavor + dataFound = FindURLFromNativeURL(aDataObject, anIndex, &data, &dataLen); + if (!dataFound) { + dataFound = + FindURLFromLocalFile(aDataObject, anIndex, &data, &dataLen); + } + } + } // if we try one last ditch effort to find our data + + // Hopefully by this point we've found it and can go about our business + if (dataFound) { + nsCOMPtr<nsISupports> genericDataWrapper; + if (flavorStr.EqualsLiteral(kFileMime)) { + // we have a file path in |data|. Create an nsLocalFile object. + nsDependentString filepath(reinterpret_cast<char16_t*>(data)); + nsCOMPtr<nsIFile> file; + if (NS_SUCCEEDED( + NS_NewLocalFile(filepath, false, getter_AddRefs(file)))) { + genericDataWrapper = do_QueryInterface(file); + } + free(data); + } else if (flavorStr.EqualsLiteral(kNativeHTMLMime)) { + uint32_t dummy; + // the editor folks want CF_HTML exactly as it's on the clipboard, no + // conversions, no fancy stuff. Pull it off the clipboard, stuff it into + // a wrapper and hand it back to them. + if (FindPlatformHTML(aDataObject, anIndex, &data, &dummy, &dataLen)) { + nsPrimitiveHelpers::CreatePrimitiveForData( + flavorStr, data, dataLen, getter_AddRefs(genericDataWrapper)); + } else { + free(data); + continue; // something wrong with this flavor, keep looking for other + // data + } + free(data); + } else if (flavorStr.EqualsLiteral(kHTMLMime)) { + uint32_t startOfData = 0; + // The JS folks want CF_HTML exactly as it is on the clipboard, but + // minus the CF_HTML header index information. + // It also needs to be converted to UTF16 and have linebreaks changed. + if (FindPlatformHTML(aDataObject, anIndex, &data, &startOfData, + &dataLen)) { + dataLen -= startOfData; + nsPrimitiveHelpers::CreatePrimitiveForCFHTML( + static_cast<char*>(data) + startOfData, &dataLen, + getter_AddRefs(genericDataWrapper)); + } else { + free(data); + continue; // something wrong with this flavor, keep looking for other + // data + } + free(data); + } else if (flavorStr.EqualsLiteral(kJPEGImageMime) || + flavorStr.EqualsLiteral(kJPGImageMime) || + flavorStr.EqualsLiteral(kPNGImageMime)) { + nsIInputStream* imageStream = reinterpret_cast<nsIInputStream*>(data); + genericDataWrapper = do_QueryInterface(imageStream); + NS_IF_RELEASE(imageStream); + } else { + // Treat custom types as a string of bytes. + if (!flavorStr.EqualsLiteral(kCustomTypesMime)) { + bool isRTF = flavorStr.EqualsLiteral(kRTFMime); + // we probably have some form of text. The DOM only wants LF, so + // convert from Win32 line endings to DOM line endings. + int32_t signedLen = static_cast<int32_t>(dataLen); + nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(isRTF, &data, + &signedLen); + dataLen = signedLen; + + if (isRTF) { + // RTF on Windows is known to sometimes deliver an extra null byte. + if (dataLen > 0 && static_cast<char*>(data)[dataLen - 1] == '\0') { + dataLen--; + } + } + } + + nsPrimitiveHelpers::CreatePrimitiveForData( + flavorStr, data, dataLen, getter_AddRefs(genericDataWrapper)); + free(data); + } + + NS_ASSERTION(genericDataWrapper, + "About to put null data into the transferable"); + aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper); + res = NS_OK; + + // we found one, get out of the loop + break; + } + } // foreach flavor + + return res; +} + +// +// FindPlatformHTML +// +// Someone asked for the OS CF_HTML flavor. We give it back to them exactly +// as-is. +// +bool nsClipboard ::FindPlatformHTML(IDataObject* inDataObject, UINT inIndex, + void** outData, uint32_t* outStartOfData, + uint32_t* outDataLen) { + // Reference: MSDN doc entitled "HTML Clipboard Format" + // http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx#unknown_854 + // CF_HTML is UTF8, not unicode. We also can't rely on it being + // null-terminated so we have to check the CF_HTML header for the correct + // length. The length we return is the bytecount from the beginning of the + // selected data to the end of the selected data, without the null + // termination. Because it's UTF8, we're guaranteed the header is ASCII. + + if (!outData || !*outData) { + return false; + } + + char version[8] = {0}; + int32_t startOfData = 0; + int32_t endOfData = 0; + int numFound = + sscanf((char*)*outData, "Version:%7s\nStartHTML:%d\nEndHTML:%d", version, + &startOfData, &endOfData); + + if (numFound != 3 || startOfData < -1 || endOfData < -1) { + return false; + } + + // Fixup the start and end markers if they have no context (set to -1) + if (startOfData == -1) { + startOfData = 0; + } + if (endOfData == -1) { + endOfData = *outDataLen; + } + + // Make sure we were passed sane values within our buffer size. + // (Note that we've handled all cases of negative endOfData above, so we can + // safely cast it to be unsigned here.) + if (!endOfData || startOfData >= endOfData || + static_cast<uint32_t>(endOfData) > *outDataLen) { + return false; + } + + // We want to return the buffer not offset by startOfData because it will be + // parsed out later (probably by HTMLEditor::ParseCFHTML) when it is still + // in CF_HTML format. + + // We return the byte offset from the start of the data buffer to where the + // HTML data starts. The caller might want to extract the HTML only. + *outStartOfData = startOfData; + *outDataLen = endOfData; + return true; +} + +// +// FindUnicodeFromPlainText +// +// Looks for CF_TEXT on the clipboard and converts it into an UTF-16 string +// if present. Returns this string in outData, and its length in outDataLen. +// XXXndeakin Windows converts between CF_UNICODE and CF_TEXT automatically +// so it doesn't seem like this is actually needed. +// +bool nsClipboard ::FindUnicodeFromPlainText(IDataObject* inDataObject, + UINT inIndex, void** outData, + uint32_t* outDataLen) { + MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); + + // We are looking for text/plain and we failed to find it on the clipboard + // first, so try again with CF_TEXT. If that is present, convert it to + // unicode. + nsresult rv = GetNativeDataOffClipboard(inDataObject, inIndex, CF_TEXT, + nullptr, outData, outDataLen); + if (NS_FAILED(rv) || !*outData) { + return false; + } + + const char* castedText = static_cast<char*>(*outData); + nsAutoString tmp; + rv = NS_CopyNativeToUnicode(nsDependentCSubstring(castedText, *outDataLen), + tmp); + if (NS_FAILED(rv)) { + return false; + } + + // out with the old, in with the new + free(*outData); + *outData = ToNewUnicode(tmp); + *outDataLen = tmp.Length() * sizeof(char16_t); + + return true; + +} // FindUnicodeFromPlainText + +// +// FindURLFromLocalFile +// +// we are looking for a URL and couldn't find it, try again with looking for +// a local file. If we have one, it may either be a normal file or an internet +// shortcut. In both cases, however, we can get a URL (it will be a file:// url +// in the local file case). +// +bool nsClipboard ::FindURLFromLocalFile(IDataObject* inDataObject, UINT inIndex, + void** outData, uint32_t* outDataLen) { + MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); + + bool dataFound = false; + + nsresult loadResult = + GetNativeDataOffClipboard(inDataObject, inIndex, GetFormat(kFileMime), + nullptr, outData, outDataLen); + if (NS_SUCCEEDED(loadResult) && *outData) { + // we have a file path in |data|. Is it an internet shortcut or a normal + // file? + const nsDependentString filepath(static_cast<char16_t*>(*outData)); + nsCOMPtr<nsIFile> file; + nsresult rv = NS_NewLocalFile(filepath, true, getter_AddRefs(file)); + if (NS_FAILED(rv)) { + free(*outData); + return dataFound; + } + + if (IsInternetShortcut(filepath)) { + free(*outData); + nsAutoCString url; + ResolveShortcut(file, url); + if (!url.IsEmpty()) { + // convert it to unicode and pass it out + NS_ConvertUTF8toUTF16 urlString(url); + // the internal mozilla URL format, text/x-moz-url, contains + // URL\ntitle. We can guess the title from the file's name. + nsAutoString title; + file->GetLeafName(title); + // We rely on IsInternetShortcut check that file has a .url extension. + title.SetLength(title.Length() - 4); + if (title.IsEmpty()) { + title = urlString; + } + *outData = ToNewUnicode(urlString + u"\n"_ns + title); + *outDataLen = + NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t); + + dataFound = true; + } + } else { + // we have a normal file, use some Necko objects to get our file path + nsAutoCString urlSpec; + NS_GetURLSpecFromFile(file, urlSpec); + + // convert it to unicode and pass it out + free(*outData); + *outData = UTF8ToNewUnicode(urlSpec); + *outDataLen = + NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t); + dataFound = true; + } // else regular file + } + + return dataFound; +} // FindURLFromLocalFile + +// +// FindURLFromNativeURL +// +// we are looking for a URL and couldn't find it using our internal +// URL flavor, so look for it using the native URL flavor, +// CF_INETURLSTRW (We don't handle CF_INETURLSTRA currently) +// +bool nsClipboard ::FindURLFromNativeURL(IDataObject* inDataObject, UINT inIndex, + void** outData, uint32_t* outDataLen) { + MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); + + bool dataFound = false; + + void* tempOutData = nullptr; + uint32_t tempDataLen = 0; + + nsresult loadResult = GetNativeDataOffClipboard( + inDataObject, inIndex, ::RegisterClipboardFormat(CFSTR_INETURLW), nullptr, + &tempOutData, &tempDataLen); + if (NS_SUCCEEDED(loadResult) && tempOutData) { + nsDependentString urlString(static_cast<char16_t*>(tempOutData)); + // the internal mozilla URL format, text/x-moz-url, contains + // URL\ntitle. Since we don't actually have a title here, + // just repeat the URL to fake it. + *outData = ToNewUnicode(urlString + u"\n"_ns + urlString); + *outDataLen = + NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t); + free(tempOutData); + dataFound = true; + } else { + loadResult = GetNativeDataOffClipboard( + inDataObject, inIndex, ::RegisterClipboardFormat(CFSTR_INETURLA), + nullptr, &tempOutData, &tempDataLen); + if (NS_SUCCEEDED(loadResult) && tempOutData) { + // CFSTR_INETURLA is (currently) equal to CFSTR_SHELLURL which is equal to + // CF_TEXT which is by definition ANSI encoded. + nsCString urlUnescapedA; + bool unescaped = + NS_UnescapeURL(static_cast<char*>(tempOutData), tempDataLen, + esc_OnlyNonASCII | esc_SkipControl, urlUnescapedA); + + nsString urlString; + if (unescaped) { + NS_CopyNativeToUnicode(urlUnescapedA, urlString); + } else { + NS_CopyNativeToUnicode( + nsDependentCString(static_cast<char*>(tempOutData), tempDataLen), + urlString); + } + + // the internal mozilla URL format, text/x-moz-url, contains + // URL\ntitle. Since we don't actually have a title here, + // just repeat the URL to fake it. + *outData = ToNewUnicode(urlString + u"\n"_ns + urlString); + *outDataLen = + NS_strlen(static_cast<char16_t*>(*outData)) * sizeof(char16_t); + free(tempOutData); + dataFound = true; + } + } + + return dataFound; +} // FindURLFromNativeURL + +// Other apps can block access to the clipboard. This repeatedly +// calls `::OleGetClipboard` for a fixed number of times and should be called +// instead of `::OleGetClipboard`. +static HRESULT RepeatedlyTryOleGetClipboard(IDataObject** aDataObj) { + return RepeatedlyTry(::OleGetClipboard, LogOleGetClipboardResult, aDataObj); +} + +// +// ResolveShortcut +// +void nsClipboard ::ResolveShortcut(nsIFile* aFile, nsACString& outURL) { + nsCOMPtr<nsIFileProtocolHandler> fph; + nsresult rv = NS_GetFileProtocolHandler(getter_AddRefs(fph)); + if (NS_FAILED(rv)) { + return; + } + + nsCOMPtr<nsIURI> uri; + rv = fph->ReadURLFile(aFile, getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return; + } + + uri->GetSpec(outURL); +} // ResolveShortcut + +// +// IsInternetShortcut +// +// A file is an Internet Shortcut if it ends with .URL +// +bool nsClipboard ::IsInternetShortcut(const nsAString& inFileName) { + return StringEndsWith(inFileName, u".url"_ns, + nsCaseInsensitiveStringComparator); +} // IsInternetShortcut + +//------------------------------------------------------------------------- +NS_IMETHODIMP +nsClipboard::GetNativeClipboardData(nsITransferable* aTransferable, + int32_t aWhichClipboard) { + MOZ_DIAGNOSTIC_ASSERT(aTransferable); + MOZ_DIAGNOSTIC_ASSERT( + nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); + + MOZ_CLIPBOARD_LOG("%s aWhichClipboard=%i", __FUNCTION__, aWhichClipboard); + + nsresult res; + // This makes sure we can use the OLE functionality for the clipboard + IDataObject* dataObj; + if (S_OK == RepeatedlyTryOleGetClipboard(&dataObj)) { + // Use OLE IDataObject for clipboard operations + MOZ_CLIPBOARD_LOG(" use OLE IDataObject:"); + if (MOZ_CLIPBOARD_LOG_ENABLED()) { + IEnumFORMATETC* pEnum = nullptr; + if (S_OK == dataObj->EnumFormatEtc(DATADIR_GET, &pEnum)) { + FORMATETC fEtc; + while (S_OK == pEnum->Next(1, &fEtc, nullptr)) { + nsAutoString format; + mozilla::widget::WinUtils::GetClipboardFormatAsString(fEtc.cfFormat, + format); + MOZ_CLIPBOARD_LOG(" FORMAT %s", + NS_ConvertUTF16toUTF8(format).get()); + } + } + pEnum->Release(); + } + + res = GetDataFromDataObject(dataObj, 0, nullptr, aTransferable); + dataObj->Release(); + } else { + // do it the old manual way + res = GetDataFromDataObject(nullptr, 0, mWindow, aTransferable); + } + return res; +} + +nsresult nsClipboard::EmptyNativeClipboardData(int32_t aWhichClipboard) { + MOZ_DIAGNOSTIC_ASSERT( + nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); + // Some programs such as ZoneAlarm monitor clipboard usage and then open the + // clipboard to scan it. If we i) empty and then ii) set data, then the + // 'set data' can sometimes fail with access denied becacuse another program + // has the clipboard open. So to avoid this race condition for OpenClipboard + // we do not empty the clipboard when we're setting it. + RepeatedlyTryOleSetClipboard(nullptr); + return NS_OK; +} + +mozilla::Result<int32_t, nsresult> +nsClipboard::GetNativeClipboardSequenceNumber(int32_t aWhichClipboard) { + MOZ_DIAGNOSTIC_ASSERT(kGlobalClipboard == aWhichClipboard); + return (int32_t)::GetClipboardSequenceNumber(); +} + +//------------------------------------------------------------------------- +mozilla::Result<bool, nsresult> +nsClipboard::HasNativeClipboardDataMatchingFlavors( + const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) { + MOZ_DIAGNOSTIC_ASSERT( + nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); + for (const auto& flavor : aFlavorList) { + UINT format = GetFormat(flavor.get()); + if (IsClipboardFormatAvailable(format)) { + return true; + } + } + return false; +} + +//------------------------------------------------------------------------- +nsresult nsClipboard::GetTempFilePath(const nsAString& aFileName, + nsAString& aFilePath) { + nsresult result = NS_OK; + + nsCOMPtr<nsIFile> tmpFile; + result = + GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(tmpFile)); + NS_ENSURE_SUCCESS(result, result); + + result = tmpFile->Append(aFileName); + NS_ENSURE_SUCCESS(result, result); + + result = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660); + NS_ENSURE_SUCCESS(result, result); + result = tmpFile->GetPath(aFilePath); + + return result; +} + +//------------------------------------------------------------------------- +nsresult nsClipboard::SaveStorageOrStream(IDataObject* aDataObject, UINT aIndex, + const nsAString& aFileName) { + NS_ENSURE_ARG_POINTER(aDataObject); + + FORMATETC fe = {0}; + SET_FORMATETC(fe, RegisterClipboardFormat(CFSTR_FILECONTENTS), 0, + DVASPECT_CONTENT, aIndex, TYMED_ISTORAGE | TYMED_ISTREAM); + + STGMEDIUM stm = {0}; + HRESULT hres = aDataObject->GetData(&fe, &stm); + if (FAILED(hres)) { + return NS_ERROR_FAILURE; + } + + auto releaseMediumGuard = + mozilla::MakeScopeExit([&] { ReleaseStgMedium(&stm); }); + + // We do this check because, even though we *asked* for IStorage or IStream, + // it seems that IDataObject providers can just hand us back whatever they + // feel like. See Bug 1824644 for a fun example of that! + if (stm.tymed != TYMED_ISTORAGE && stm.tymed != TYMED_ISTREAM) { + return NS_ERROR_FAILURE; + } + + if (stm.tymed == TYMED_ISTORAGE) { + RefPtr<IStorage> file; + hres = StgCreateStorageEx( + aFileName.Data(), STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE, + STGFMT_STORAGE, 0, NULL, NULL, IID_IStorage, getter_AddRefs(file)); + if (FAILED(hres)) { + return NS_ERROR_FAILURE; + } + + hres = stm.pstg->CopyTo(0, NULL, NULL, file); + if (FAILED(hres)) { + return NS_ERROR_FAILURE; + } + + file->Commit(STGC_DEFAULT); + + return NS_OK; + } + + MOZ_ASSERT(stm.tymed == TYMED_ISTREAM); + + HANDLE handle = CreateFile(aFileName.Data(), GENERIC_WRITE, FILE_SHARE_READ, + NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (handle == INVALID_HANDLE_VALUE) { + return NS_ERROR_FAILURE; + } + + auto fileCloseGuard = mozilla::MakeScopeExit([&] { CloseHandle(handle); }); + + const ULONG bufferSize = 4096; + char buffer[bufferSize] = {0}; + ULONG bytesRead = 0; + DWORD bytesWritten = 0; + while (true) { + HRESULT result = stm.pstm->Read(buffer, bufferSize, &bytesRead); + if (FAILED(result)) { + return NS_ERROR_FAILURE; + } + if (bytesRead == 0) { + break; + } + if (!WriteFile(handle, buffer, static_cast<DWORD>(bytesRead), &bytesWritten, + NULL)) { + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} diff --git a/widget/windows/nsClipboard.h b/widget/windows/nsClipboard.h new file mode 100644 index 0000000000..b0afb9ee40 --- /dev/null +++ b/widget/windows/nsClipboard.h @@ -0,0 +1,113 @@ +/* -*- 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 nsClipboard_h__ +#define nsClipboard_h__ + +#include "nsBaseClipboard.h" +#include "nsIObserver.h" +#include "nsIURI.h" + +#include <ole2.h> +#include <windows.h> + +class nsITransferable; +class nsIWidget; +class nsIFile; +struct IDataObject; + +/** + * Native Win32 Clipboard wrapper + */ + +class nsClipboard : public nsBaseClipboard, public nsIObserver { + virtual ~nsClipboard(); + + public: + nsClipboard(); + + NS_DECL_ISUPPORTS_INHERITED + + // nsIObserver + NS_DECL_NSIOBSERVER + + // Internal Native Routines + enum class MightNeedToFlush : bool { No, Yes }; + static nsresult CreateNativeDataObject(nsITransferable* aTransferable, + IDataObject** aDataObj, nsIURI* aUri, + MightNeedToFlush* = nullptr); + static nsresult SetupNativeDataObject(nsITransferable* aTransferable, + IDataObject* aDataObj, + MightNeedToFlush* = nullptr); + static nsresult GetDataFromDataObject(IDataObject* aDataObject, UINT anIndex, + nsIWidget* aWindow, + nsITransferable* aTransferable); + static nsresult GetNativeDataOffClipboard(nsIWidget* aWindow, UINT aIndex, + UINT aFormat, void** aData, + uint32_t* aLen); + static nsresult GetNativeDataOffClipboard(IDataObject* aDataObject, + UINT aIndex, UINT aFormat, + const char* aMIMEImageFormat, + void** aData, uint32_t* aLen); + static nsresult GetGlobalData(HGLOBAL aHGBL, void** aData, uint32_t* aLen); + + // This function returns the internal Windows clipboard format identifier + // for a given Mime string. The default is to map kHTMLMime ("text/html") + // to the clipboard format CF_HTML ("HTLM Format"), but it can also be + // registered as clipboard format "text/html" to support previous versions + // of Gecko. + static UINT GetFormat(const char* aMimeStr, bool aMapHTMLMime = true); + + static UINT GetClipboardFileDescriptorFormatA(); + static UINT GetClipboardFileDescriptorFormatW(); + static UINT GetHtmlClipboardFormat(); + static UINT GetCustomClipboardFormat(); + + protected: + // @param aDataObject must be non-nullptr. + static HRESULT FillSTGMedium(IDataObject* aDataObject, UINT aFormat, + LPFORMATETC pFE, LPSTGMEDIUM pSTM, DWORD aTymed); + + // Implement the native clipboard behavior. + NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable, + int32_t aWhichClipboard) override; + NS_IMETHOD GetNativeClipboardData(nsITransferable* aTransferable, + int32_t aWhichClipboard) override; + nsresult EmptyNativeClipboardData(int32_t aWhichClipboard) override; + mozilla::Result<int32_t, nsresult> GetNativeClipboardSequenceNumber( + int32_t aWhichClipboard) override; + mozilla::Result<bool, nsresult> HasNativeClipboardDataMatchingFlavors( + const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) override; + + static bool IsInternetShortcut(const nsAString& inFileName); + static bool FindURLFromLocalFile(IDataObject* inDataObject, UINT inIndex, + void** outData, uint32_t* outDataLen); + static bool FindURLFromNativeURL(IDataObject* inDataObject, UINT inIndex, + void** outData, uint32_t* outDataLen); + static bool FindUnicodeFromPlainText(IDataObject* inDataObject, UINT inIndex, + void** outData, uint32_t* outDataLen); + static bool FindPlatformHTML(IDataObject* inDataObject, UINT inIndex, + void** outData, uint32_t* outStartOfData, + uint32_t* outDataLen); + + static void ResolveShortcut(nsIFile* inFileName, nsACString& outURL); + static nsresult GetTempFilePath(const nsAString& aFileName, + nsAString& aFilePath); + static nsresult SaveStorageOrStream(IDataObject* aDataObject, UINT aIndex, + const nsAString& aFileName); + + nsIWidget* mWindow; +}; + +#define SET_FORMATETC(fe, cf, td, asp, li, med) \ + { \ + (fe).cfFormat = cf; \ + (fe).ptd = td; \ + (fe).dwAspect = asp; \ + (fe).lindex = li; \ + (fe).tymed = med; \ + } + +#endif // nsClipboard_h__ diff --git a/widget/windows/nsColorPicker.cpp b/widget/windows/nsColorPicker.cpp new file mode 100644 index 0000000000..5074b620f5 --- /dev/null +++ b/widget/windows/nsColorPicker.cpp @@ -0,0 +1,202 @@ +/* -*- 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 "nsColorPicker.h" + +#include <shlwapi.h> + +#include "mozilla/ArrayUtils.h" +#include "mozilla/AutoRestore.h" +#include "nsIWidget.h" +#include "nsString.h" +#include "WidgetUtils.h" +#include "WinUtils.h" +#include "nsPIDOMWindow.h" + +using namespace mozilla::widget; + +namespace { +static DWORD ColorStringToRGB(const nsAString& aColor) { + DWORD result = 0; + + for (uint32_t i = 1; i < aColor.Length(); ++i) { + result *= 16; + + char16_t c = aColor[i]; + if (c >= '0' && c <= '9') { + result += c - '0'; + } else if (c >= 'a' && c <= 'f') { + result += 10 + (c - 'a'); + } else { + result += 10 + (c - 'A'); + } + } + + DWORD r = result & 0x00FF0000; + DWORD g = result & 0x0000FF00; + DWORD b = result & 0x000000FF; + + r = r >> 16; + b = b << 16; + + result = r | g | b; + + return result; +} + +static nsString ToHexString(BYTE n) { + nsString result; + if (n <= 0x0F) { + result.Append('0'); + } + result.AppendInt(n, 16); + return result; +} + +static void BGRIntToRGBString(DWORD color, nsAString& aResult) { + BYTE r = GetRValue(color); + BYTE g = GetGValue(color); + BYTE b = GetBValue(color); + + aResult.Assign('#'); + aResult.Append(ToHexString(r)); + aResult.Append(ToHexString(g)); + aResult.Append(ToHexString(b)); +} +} // namespace + +static AsyncColorChooser* gColorChooser; + +AsyncColorChooser::AsyncColorChooser(COLORREF aInitialColor, + const nsTArray<nsString>& aDefaultColors, + nsIWidget* aParentWidget, + nsIColorPickerShownCallback* aCallback) + : mozilla::Runnable("AsyncColorChooser"), + mInitialColor(aInitialColor), + mDefaultColors(aDefaultColors.Clone()), + mColor(aInitialColor), + mParentWidget(aParentWidget), + mCallback(aCallback) {} + +NS_IMETHODIMP +AsyncColorChooser::Run() { + MOZ_ASSERT(NS_IsMainThread(), + "Color pickers can only be opened from main thread currently"); + + // Allow only one color picker to be opened at a time, to workaround bug + // 944737 + if (!gColorChooser) { + mozilla::AutoRestore<AsyncColorChooser*> restoreColorChooser(gColorChooser); + gColorChooser = this; + + ScopedRtlShimWindow shim(mParentWidget.get()); + + COLORREF customColors[16]; + for (size_t i = 0; i < mozilla::ArrayLength(customColors); i++) { + if (i < mDefaultColors.Length()) { + customColors[i] = ColorStringToRGB(mDefaultColors[i]); + } else { + customColors[i] = 0x00FFFFFF; + } + } + + CHOOSECOLOR options; + options.lStructSize = sizeof(options); + options.hwndOwner = shim.get(); + options.Flags = CC_RGBINIT | CC_FULLOPEN | CC_ENABLEHOOK; + options.rgbResult = mInitialColor; + options.lpCustColors = customColors; + options.lpfnHook = HookProc; + + mColor = ChooseColor(&options) ? options.rgbResult : mInitialColor; + } else { + NS_WARNING( + "Currently, it's not possible to open more than one color " + "picker at a time"); + mColor = mInitialColor; + } + + if (mCallback) { + nsAutoString colorStr; + BGRIntToRGBString(mColor, colorStr); + mCallback->Done(colorStr); + } + + return NS_OK; +} + +void AsyncColorChooser::Update(COLORREF aColor) { + if (mColor != aColor) { + mColor = aColor; + + nsAutoString colorStr; + BGRIntToRGBString(mColor, colorStr); + mCallback->Update(colorStr); + } +} + +/* static */ UINT_PTR CALLBACK AsyncColorChooser::HookProc(HWND aDialog, + UINT aMsg, + WPARAM aWParam, + LPARAM aLParam) { + if (!gColorChooser) { + return 0; + } + + if (aMsg == WM_INITDIALOG) { + // "The default dialog box procedure processes the WM_INITDIALOG message + // before passing it to the hook procedure. + // For all other messages, the hook procedure receives the message first." + // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nc-commdlg-lpcchookproc + // "The dialog box procedure should return TRUE to direct the system to + // set the keyboard focus to the control specified by wParam." + // https://docs.microsoft.com/en-us/windows/win32/dlgbox/wm-initdialog + return 1; + } + + if (aMsg == WM_CTLCOLORSTATIC) { + // The color picker does not expose a proper way to retrieve the current + // color, so we need to obtain it from the static control displaying the + // current color instead. + const int kCurrentColorBoxID = 709; + if ((HWND)aLParam == GetDlgItem(aDialog, kCurrentColorBoxID)) { + gColorChooser->Update(GetPixel((HDC)aWParam, 0, 0)); + } + } + + // Let the default dialog box procedure processes the message. + return 0; +} + +/////////////////////////////////////////////////////////////////////////////// +// nsIColorPicker + +nsColorPicker::nsColorPicker() {} + +nsColorPicker::~nsColorPicker() {} + +NS_IMPL_ISUPPORTS(nsColorPicker, nsIColorPicker) + +NS_IMETHODIMP +nsColorPicker::Init(mozIDOMWindowProxy* parent, const nsAString& title, + const nsAString& aInitialColor, + const nsTArray<nsString>& aDefaultColors) { + MOZ_ASSERT(parent, + "Null parent passed to colorpicker, no color picker for you!"); + mParentWidget = + WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(parent)); + mInitialColor = ColorStringToRGB(aInitialColor); + mDefaultColors.Assign(aDefaultColors); + return NS_OK; +} + +NS_IMETHODIMP +nsColorPicker::Open(nsIColorPickerShownCallback* aCallback) { + NS_ENSURE_ARG(aCallback); + nsCOMPtr<nsIRunnable> event = new AsyncColorChooser( + mInitialColor, mDefaultColors, mParentWidget, aCallback); + return NS_DispatchToMainThread(event); +} diff --git a/widget/windows/nsColorPicker.h b/widget/windows/nsColorPicker.h new file mode 100644 index 0000000000..ff34b9bd48 --- /dev/null +++ b/widget/windows/nsColorPicker.h @@ -0,0 +1,55 @@ +/* -*- 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 nsColorPicker_h__ +#define nsColorPicker_h__ + +#include <windows.h> +#include <commdlg.h> + +#include "nsCOMPtr.h" +#include "nsIColorPicker.h" +#include "nsThreadUtils.h" + +class nsIWidget; + +class AsyncColorChooser : public mozilla::Runnable { + public: + AsyncColorChooser(COLORREF aInitialColor, + const nsTArray<nsString>& aDefaultColors, + nsIWidget* aParentWidget, + nsIColorPickerShownCallback* aCallback); + NS_IMETHOD Run() override; + + private: + void Update(COLORREF aColor); + + static UINT_PTR CALLBACK HookProc(HWND aDialog, UINT aMsg, WPARAM aWParam, + LPARAM aLParam); + + COLORREF mInitialColor; + nsTArray<nsString> mDefaultColors; + COLORREF mColor; + nsCOMPtr<nsIWidget> mParentWidget; + nsCOMPtr<nsIColorPickerShownCallback> mCallback; +}; + +class nsColorPicker : public nsIColorPicker { + virtual ~nsColorPicker(); + + public: + nsColorPicker(); + + NS_DECL_ISUPPORTS + NS_DECL_NSICOLORPICKER + + private: + COLORREF mInitialColor; + nsTArray<nsString> mDefaultColors; + nsCOMPtr<nsIWidget> mParentWidget; +}; + +#endif // nsColorPicker_h__ diff --git a/widget/windows/nsDataObj.cpp b/widget/windows/nsDataObj.cpp new file mode 100644 index 0000000000..88a2a2ad09 --- /dev/null +++ b/widget/windows/nsDataObj.cpp @@ -0,0 +1,2276 @@ +/* -*- 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 "mozilla/TextUtils.h" + +#include <ole2.h> +#include <shlobj.h> + +#include "nsComponentManagerUtils.h" +#include "nsDataObj.h" +#include "nsArrayUtils.h" +#include "nsClipboard.h" +#include "nsReadableUtils.h" +#include "nsICookieJarSettings.h" +#include "nsIHttpChannel.h" +#include "nsISupportsPrimitives.h" +#include "nsITransferable.h" +#include "IEnumFE.h" +#include "nsPrimitiveHelpers.h" +#include "nsString.h" +#include "nsCRT.h" +#include "nsPrintfCString.h" +#include "nsIStringBundle.h" +#include "nsEscape.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "mozilla/Components.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/Unused.h" +#include "nsProxyRelease.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nscore.h" +#include "nsDirectoryServiceDefs.h" +#include "nsITimer.h" +#include "nsThreadUtils.h" +#include "mozilla/Preferences.h" +#include "nsContentUtils.h" +#include "nsIPrincipal.h" +#include "nsNativeCharsetUtils.h" +#include "nsMimeTypes.h" +#include "nsIMIMEService.h" +#include "imgIEncoder.h" +#include "imgITools.h" +#include "WinUtils.h" +#include "nsLocalFile.h" + +#include "mozilla/LazyIdleThread.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::glue; +using namespace mozilla::widget; + +#define BFH_LENGTH 14 +#define DEFAULT_THREAD_TIMEOUT_MS 30000 + +//----------------------------------------------------------------------------- +// CStreamBase implementation +nsDataObj::CStreamBase::CStreamBase() : mStreamRead(0) {} + +//----------------------------------------------------------------------------- +nsDataObj::CStreamBase::~CStreamBase() {} + +NS_IMPL_ISUPPORTS(nsDataObj::CStream, nsIStreamListener) + +//----------------------------------------------------------------------------- +// CStream implementation +nsDataObj::CStream::CStream() : mChannelRead(false) {} + +//----------------------------------------------------------------------------- +nsDataObj::CStream::~CStream() {} + +//----------------------------------------------------------------------------- +// helper - initializes the stream +nsresult nsDataObj::CStream::Init(nsIURI* pSourceURI, + nsContentPolicyType aContentPolicyType, + nsIPrincipal* aRequestingPrincipal, + nsICookieJarSettings* aCookieJarSettings, + nsIReferrerInfo* aReferrerInfo) { + // we can not create a channel without a requestingPrincipal + if (!aRequestingPrincipal) { + return NS_ERROR_FAILURE; + } + nsresult rv; + rv = NS_NewChannel(getter_AddRefs(mChannel), pSourceURI, aRequestingPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + aContentPolicyType, aCookieJarSettings, + nullptr, // PerformanceStorage + nullptr, // loadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_FROM_CACHE); + NS_ENSURE_SUCCESS(rv, rv); + + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel)) { + rv = httpChannel->SetReferrerInfo(aReferrerInfo); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + + rv = mChannel->AsyncOpen(this); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// IUnknown's QueryInterface, nsISupport's AddRef and Release are shared by +// IUnknown and nsIStreamListener. +STDMETHODIMP nsDataObj::CStream::QueryInterface(REFIID refiid, + void** ppvResult) { + *ppvResult = nullptr; + if (IID_IUnknown == refiid || refiid == IID_IStream) + + { + *ppvResult = this; + } + + if (nullptr != *ppvResult) { + ((LPUNKNOWN)*ppvResult)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +// nsIStreamListener implementation +NS_IMETHODIMP +nsDataObj::CStream::OnDataAvailable( + nsIRequest* aRequest, nsIInputStream* aInputStream, + uint64_t aOffset, // offset within the stream + uint32_t aCount) // bytes available on this call +{ + // If we've been asked to read zero bytes, call `Read` once, just to ensure + // any side-effects take place, and return immediately. + if (aCount == 0) { + char buffer[1] = {0}; + uint32_t bytesReadByCall = 0; + nsresult rv = aInputStream->Read(buffer, 0, &bytesReadByCall); + MOZ_ASSERT(bytesReadByCall == 0); + return rv; + } + + // Extend the write buffer for the incoming data. + size_t oldLength = mChannelData.Length(); + char* buffer = + reinterpret_cast<char*>(mChannelData.AppendElements(aCount, fallible)); + if (!buffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + MOZ_ASSERT(mChannelData.Length() == (aOffset + aCount), + "stream length mismatch w/write buffer"); + + // Read() may not return aCount on a single call, so loop until we've + // accumulated all the data OnDataAvailable has promised. + uint32_t bytesRead = 0; + while (bytesRead < aCount) { + uint32_t bytesReadByCall = 0; + nsresult rv = aInputStream->Read(buffer + bytesRead, aCount - bytesRead, + &bytesReadByCall); + bytesRead += bytesReadByCall; + + if (bytesReadByCall == 0) { + // A `bytesReadByCall` of zero indicates EOF without failure... but we + // were promised `aCount` elements and haven't gotten them. Return a + // generic failure. + rv = NS_ERROR_FAILURE; + } + + if (NS_FAILED(rv)) { + // Drop any trailing uninitialized elements before erroring out. + mChannelData.RemoveElementsAt(oldLength + bytesRead, aCount - bytesRead); + return rv; + } + } + return NS_OK; +} + +NS_IMETHODIMP nsDataObj::CStream::OnStartRequest(nsIRequest* aRequest) { + mChannelResult = NS_OK; + return NS_OK; +} + +NS_IMETHODIMP nsDataObj::CStream::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + mChannelRead = true; + mChannelResult = aStatusCode; + return NS_OK; +} + +// Pumps thread messages while waiting for the async listener operation to +// complete. Failing this call will fail the stream incall from Windows +// and cancel the operation. +nsresult nsDataObj::CStream::WaitForCompletion() { + // We are guaranteed OnStopRequest will get called, so this should be ok. + SpinEventLoopUntil("widget:nsDataObj::CStream::WaitForCompletion"_ns, + [&]() { return mChannelRead; }); + + if (!mChannelData.Length()) mChannelResult = NS_ERROR_FAILURE; + + return mChannelResult; +} + +//----------------------------------------------------------------------------- +// IStream +STDMETHODIMP nsDataObj::CStreamBase::Clone(IStream** ppStream) { + return E_NOTIMPL; +} + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStreamBase::Commit(DWORD dwFrags) { return E_NOTIMPL; } + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStreamBase::CopyTo(IStream* pDestStream, + ULARGE_INTEGER nBytesToCopy, + ULARGE_INTEGER* nBytesRead, + ULARGE_INTEGER* nBytesWritten) { + return E_NOTIMPL; +} + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStreamBase::LockRegion(ULARGE_INTEGER nStart, + ULARGE_INTEGER nBytes, + DWORD dwFlags) { + return E_NOTIMPL; +} + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStream::Read(void* pvBuffer, ULONG nBytesToRead, + ULONG* nBytesRead) { + // Wait for the write into our buffer to complete via the stream listener. + // We can't respond to this by saying "call us back later". + if (NS_FAILED(WaitForCompletion())) return E_FAIL; + + // Bytes left for Windows to read out of our buffer + ULONG bytesLeft = mChannelData.Length() - mStreamRead; + // Let Windows know what we will hand back, usually this is the entire buffer + *nBytesRead = std::min(bytesLeft, nBytesToRead); + // Copy the buffer data over + memcpy(pvBuffer, ((char*)mChannelData.Elements() + mStreamRead), *nBytesRead); + // Update our bytes read tracking + mStreamRead += *nBytesRead; + return S_OK; +} + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStreamBase::Revert(void) { return E_NOTIMPL; } + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStreamBase::Seek(LARGE_INTEGER nMove, DWORD dwOrigin, + ULARGE_INTEGER* nNewPos) { + if (nNewPos == nullptr) return STG_E_INVALIDPOINTER; + + if (nMove.LowPart == 0 && nMove.HighPart == 0 && + (dwOrigin == STREAM_SEEK_SET || dwOrigin == STREAM_SEEK_CUR)) { + nNewPos->LowPart = 0; + nNewPos->HighPart = 0; + return S_OK; + } + + return E_NOTIMPL; +} + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStreamBase::SetSize(ULARGE_INTEGER nNewSize) { + return E_NOTIMPL; +} + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStream::Stat(STATSTG* statstg, DWORD dwFlags) { + if (statstg == nullptr) return STG_E_INVALIDPOINTER; + + if (!mChannel || NS_FAILED(WaitForCompletion())) return E_FAIL; + + memset((void*)statstg, 0, sizeof(STATSTG)); + + if (dwFlags != STATFLAG_NONAME) { + nsCOMPtr<nsIURI> sourceURI; + if (NS_FAILED(mChannel->GetURI(getter_AddRefs(sourceURI)))) { + return E_FAIL; + } + + nsAutoCString strFileName; + nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(sourceURI); + sourceURL->GetFileName(strFileName); + + if (strFileName.IsEmpty()) return E_FAIL; + + NS_UnescapeURL(strFileName); + NS_ConvertUTF8toUTF16 wideFileName(strFileName); + + uint32_t nMaxNameLength = (wideFileName.Length() * 2) + 2; + void* retBuf = CoTaskMemAlloc(nMaxNameLength); // freed by caller + if (!retBuf) return STG_E_INSUFFICIENTMEMORY; + + ZeroMemory(retBuf, nMaxNameLength); + memcpy(retBuf, wideFileName.get(), wideFileName.Length() * 2); + statstg->pwcsName = (LPOLESTR)retBuf; + } + + SYSTEMTIME st; + + statstg->type = STGTY_STREAM; + + GetSystemTime(&st); + SystemTimeToFileTime((const SYSTEMTIME*)&st, (LPFILETIME)&statstg->mtime); + statstg->ctime = statstg->atime = statstg->mtime; + + statstg->cbSize.QuadPart = mChannelData.Length(); + statstg->grfMode = STGM_READ; + statstg->grfLocksSupported = LOCK_ONLYONCE; + statstg->clsid = CLSID_NULL; + + return S_OK; +} + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStreamBase::UnlockRegion(ULARGE_INTEGER nStart, + ULARGE_INTEGER nBytes, + DWORD dwFlags) { + return E_NOTIMPL; +} + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CStreamBase::Write(const void* pvBuffer, + ULONG nBytesToRead, + ULONG* nBytesRead) { + return E_NOTIMPL; +} + +//----------------------------------------------------------------------------- +HRESULT nsDataObj::CreateStream(IStream** outStream) { + NS_ENSURE_TRUE(outStream, E_INVALIDARG); + + nsresult rv = NS_ERROR_FAILURE; + nsAutoString wideFileName; + nsCOMPtr<nsIURI> sourceURI; + HRESULT res; + + res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName); + if (FAILED(res)) return res; + + nsDataObj::CStream* pStream = new nsDataObj::CStream(); + NS_ENSURE_TRUE(pStream, E_OUTOFMEMORY); + + pStream->AddRef(); + + // query the requestingPrincipal from the transferable and add it to the new + // channel + nsCOMPtr<nsIPrincipal> requestingPrincipal = + mTransferable->GetRequestingPrincipal(); + MOZ_ASSERT(requestingPrincipal, "can not create channel without a principal"); + + // Note that the cookieJarSettings could be null if the data object is for the + // image copy. We will fix this in Bug 1690532. + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = + mTransferable->GetCookieJarSettings(); + + // The referrer is optional. + nsCOMPtr<nsIReferrerInfo> referrerInfo = mTransferable->GetReferrerInfo(); + + nsContentPolicyType contentPolicyType = mTransferable->GetContentPolicyType(); + rv = pStream->Init(sourceURI, contentPolicyType, requestingPrincipal, + cookieJarSettings, referrerInfo); + if (NS_FAILED(rv)) { + pStream->Release(); + return E_FAIL; + } + *outStream = pStream; + + return S_OK; +} + +//----------------------------------------------------------------------------- +// AutoCloseEvent implementation +nsDataObj::AutoCloseEvent::AutoCloseEvent() + : mEvent(::CreateEventW(nullptr, TRUE, FALSE, nullptr)) {} + +bool nsDataObj::AutoCloseEvent::IsInited() const { return !!mEvent; } + +void nsDataObj::AutoCloseEvent::Signal() const { ::SetEvent(mEvent); } + +DWORD nsDataObj::AutoCloseEvent::Wait(DWORD aMillisec) const { + return ::WaitForSingleObject(mEvent, aMillisec); +} + +//----------------------------------------------------------------------------- +// AutoSetEvent implementation +nsDataObj::AutoSetEvent::AutoSetEvent(NotNull<AutoCloseEvent*> aEvent) + : mEvent(aEvent) {} + +nsDataObj::AutoSetEvent::~AutoSetEvent() { Signal(); } + +void nsDataObj::AutoSetEvent::Signal() const { mEvent->Signal(); } + +bool nsDataObj::AutoSetEvent::IsWaiting() const { + return mEvent->Wait(0) == WAIT_TIMEOUT; +} + +//----------------------------------------------------------------------------- +// CMemStream implementation +Win32SRWLock nsDataObj::CMemStream::mLock; + +//----------------------------------------------------------------------------- +nsDataObj::CMemStream::CMemStream(nsHGLOBAL aGlobalMem, uint32_t aTotalLength, + already_AddRefed<AutoCloseEvent> aEvent) + : mGlobalMem(aGlobalMem), mEvent(aEvent), mTotalLength(aTotalLength) { + ::CoCreateFreeThreadedMarshaler(this, getter_AddRefs(mMarshaler)); +} + +//----------------------------------------------------------------------------- +nsDataObj::CMemStream::~CMemStream() {} + +//----------------------------------------------------------------------------- +// IUnknown +STDMETHODIMP nsDataObj::CMemStream::QueryInterface(REFIID refiid, + void** ppvResult) { + *ppvResult = nullptr; + if (refiid == IID_IUnknown || refiid == IID_IStream || + refiid == IID_IAgileObject) { + *ppvResult = this; + } else if (refiid == IID_IMarshal && mMarshaler) { + return mMarshaler->QueryInterface(refiid, ppvResult); + } + + if (nullptr != *ppvResult) { + ((LPUNKNOWN)*ppvResult)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +void nsDataObj::CMemStream::WaitForCompletion() { + if (!mEvent) { + // We are not waiting for obtaining the icon cache. + return; + } + if (!NS_IsMainThread()) { + mEvent->Wait(INFINITE); + } else { + // We should not block the main thread. + mEvent->Signal(); + } + // mEvent will always be in the signaled state here. +} + +//----------------------------------------------------------------------------- +// IStream +STDMETHODIMP nsDataObj::CMemStream::Read(void* pvBuffer, ULONG nBytesToRead, + ULONG* nBytesRead) { + // Wait until the event is signaled. + WaitForCompletion(); + + AutoExclusiveLock lock(mLock); + char* contents = reinterpret_cast<char*>(GlobalLock(mGlobalMem.get())); + if (!contents) { + return E_OUTOFMEMORY; + } + + // Bytes left for Windows to read out of our buffer + ULONG bytesLeft = mTotalLength - mStreamRead; + // Let Windows know what we will hand back, usually this is the entire buffer + *nBytesRead = std::min(bytesLeft, nBytesToRead); + // Copy the buffer data over + memcpy(pvBuffer, contents + mStreamRead, *nBytesRead); + // Update our bytes read tracking + mStreamRead += *nBytesRead; + + GlobalUnlock(mGlobalMem.get()); + return S_OK; +} + +//----------------------------------------------------------------------------- +STDMETHODIMP nsDataObj::CMemStream::Stat(STATSTG* statstg, DWORD dwFlags) { + if (statstg == nullptr) return STG_E_INVALIDPOINTER; + + memset((void*)statstg, 0, sizeof(STATSTG)); + + if (dwFlags != STATFLAG_NONAME) { + constexpr size_t kMaxNameLength = sizeof(wchar_t); + void* retBuf = CoTaskMemAlloc(kMaxNameLength); // freed by caller + if (!retBuf) return STG_E_INSUFFICIENTMEMORY; + + ZeroMemory(retBuf, kMaxNameLength); + statstg->pwcsName = (LPOLESTR)retBuf; + } + + SYSTEMTIME st; + + statstg->type = STGTY_STREAM; + + GetSystemTime(&st); + SystemTimeToFileTime((const SYSTEMTIME*)&st, (LPFILETIME)&statstg->mtime); + statstg->ctime = statstg->atime = statstg->mtime; + + statstg->cbSize.QuadPart = mTotalLength; + statstg->grfMode = STGM_READ; + statstg->grfLocksSupported = LOCK_ONLYONCE; + statstg->clsid = CLSID_NULL; + + return S_OK; +} + +/* + * Class nsDataObj + */ + +//----------------------------------------------------- +// construction +//----------------------------------------------------- +nsDataObj::nsDataObj(nsIURI* uri) + : m_cRef(0), + mTransferable(nullptr), + mIsAsyncMode(FALSE), + mIsInOperation(FALSE) { + mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, "nsDataObj", + LazyIdleThread::ManualShutdown); + m_enumFE = new CEnumFormatEtc(); + m_enumFE->AddRef(); + + if (uri) { + // A URI was obtained, so pass this through to the DataObject + // so it can create a SourceURL for CF_HTML flavour + uri->GetSpec(mSourceURL); + } +} +//----------------------------------------------------- +// destruction +//----------------------------------------------------- +nsDataObj::~nsDataObj() { + NS_IF_RELEASE(mTransferable); + + mDataFlavors.Clear(); + + m_enumFE->Release(); + + // Free arbitrary system formats + for (uint32_t idx = 0; idx < mDataEntryList.Length(); idx++) { + CoTaskMemFree(mDataEntryList[idx]->fe.ptd); + ReleaseStgMedium(&mDataEntryList[idx]->stgm); + CoTaskMemFree(mDataEntryList[idx]); + } +} + +//----------------------------------------------------- +// IUnknown interface methods - see inknown.h for documentation +//----------------------------------------------------- +STDMETHODIMP nsDataObj::QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + + if ((IID_IUnknown == riid) || (IID_IDataObject == riid)) { + *ppv = this; + AddRef(); + return S_OK; + } else if (IID_IDataObjectAsyncCapability == riid) { + *ppv = static_cast<IDataObjectAsyncCapability*>(this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +//----------------------------------------------------- +STDMETHODIMP_(ULONG) nsDataObj::AddRef() { + ++m_cRef; + NS_LOG_ADDREF(this, m_cRef, "nsDataObj", sizeof(*this)); + + // When the first reference is taken, hold our own internal reference. + if (m_cRef == 1) { + mKeepAlive = this; + } + + return m_cRef; +} + +namespace { +class RemoveTempFileHelper final : public nsIObserver, public nsINamed { + public: + explicit RemoveTempFileHelper(nsIFile* aTempFile) : mTempFile(aTempFile) { + MOZ_ASSERT(mTempFile); + } + + // The attach method is seperate from the constructor as we may be addref-ing + // ourself, and we want to be sure someone has a strong reference to us. + void Attach() { + // We need to listen to both the xpcom shutdown message and our timer, and + // fire when the first of either of these two messages is received. + nsresult rv; + rv = NS_NewTimerWithObserver(getter_AddRefs(mTimer), this, 500, + nsITimer::TYPE_ONE_SHOT); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsIObserverService> observerService = + do_GetService("@mozilla.org/observer-service;1"); + if (NS_WARN_IF(!observerService)) { + mTimer->Cancel(); + mTimer = nullptr; + return; + } + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + } + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSINAMED + + private: + ~RemoveTempFileHelper() { + if (mTempFile) { + mTempFile->Remove(false); + } + } + + nsCOMPtr<nsIFile> mTempFile; + nsCOMPtr<nsITimer> mTimer; +}; + +NS_IMPL_ISUPPORTS(RemoveTempFileHelper, nsIObserver, nsINamed); + +NS_IMETHODIMP +RemoveTempFileHelper::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + // Let's be careful and make sure that we don't die immediately + RefPtr<RemoveTempFileHelper> grip = this; + + // Make sure that we aren't called again by destroying references to ourself. + nsCOMPtr<nsIObserverService> observerService = + do_GetService("@mozilla.org/observer-service;1"); + if (observerService) { + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + // Remove the tempfile + if (mTempFile) { + mTempFile->Remove(false); + mTempFile = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +RemoveTempFileHelper::GetName(nsACString& aName) { + aName.AssignLiteral("RemoveTempFileHelper"); + return NS_OK; +} +} // namespace + +//----------------------------------------------------- +STDMETHODIMP_(ULONG) nsDataObj::Release() { + --m_cRef; + + NS_LOG_RELEASE(this, m_cRef, "nsDataObj"); + + // If we hold the last reference, submit release of it to the main thread. + if (m_cRef == 1 && mKeepAlive) { + NS_ReleaseOnMainThread("nsDataObj release", mKeepAlive.forget(), true); + } + + if (0 != m_cRef) return m_cRef; + + // We have released our last ref on this object and need to delete the + // temp file. External app acting as drop target may still need to open the + // temp file. Addref a timer so it can delay deleting file and destroying + // this object. + if (mCachedTempFile) { + RefPtr<RemoveTempFileHelper> helper = + new RemoveTempFileHelper(mCachedTempFile); + mCachedTempFile = nullptr; + helper->Attach(); + } + + // In case the destructor ever AddRef/Releases, ensure we don't delete twice + // or take mKeepAlive as another reference. + m_cRef = 1; + + delete this; + + return 0; +} + +//----------------------------------------------------- +BOOL nsDataObj::FormatsMatch(const FORMATETC& source, + const FORMATETC& target) const { + if ((source.cfFormat == target.cfFormat) && + (source.dwAspect & target.dwAspect) && (source.tymed & target.tymed)) { + return TRUE; + } else { + return FALSE; + } +} + +//----------------------------------------------------- +// IDataObject methods +//----------------------------------------------------- +STDMETHODIMP nsDataObj::GetData(LPFORMATETC aFormat, LPSTGMEDIUM pSTM) { + if (!mTransferable) return DV_E_FORMATETC; + + // Hold an extra reference in case we end up spinning the event loop. + RefPtr<nsDataObj> keepAliveDuringGetData(this); + + uint32_t dfInx = 0; + + static CLIPFORMAT fileDescriptorFlavorA = + ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA); + static CLIPFORMAT fileDescriptorFlavorW = + ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + static CLIPFORMAT uniformResourceLocatorA = + ::RegisterClipboardFormat(CFSTR_INETURLA); + static CLIPFORMAT uniformResourceLocatorW = + ::RegisterClipboardFormat(CFSTR_INETURLW); + static CLIPFORMAT fileFlavor = ::RegisterClipboardFormat(CFSTR_FILECONTENTS); + static CLIPFORMAT PreferredDropEffect = + ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT); + + // Arbitrary system formats are used for image feedback during drag + // and drop. We are responsible for storing these internally during + // drag operations. + LPDATAENTRY pde; + if (LookupArbitraryFormat(aFormat, &pde, FALSE)) { + return CopyMediumData(pSTM, &pde->stgm, aFormat, FALSE) ? S_OK + : E_UNEXPECTED; + } + + // Firefox internal formats + ULONG count; + FORMATETC fe; + m_enumFE->Reset(); + while (NOERROR == m_enumFE->Next(1, &fe, &count) && + dfInx < mDataFlavors.Length()) { + nsCString& df = mDataFlavors.ElementAt(dfInx); + if (FormatsMatch(fe, *aFormat)) { + pSTM->pUnkForRelease = + nullptr; // caller is responsible for deleting this data + CLIPFORMAT format = aFormat->cfFormat; + switch (format) { + // Someone is asking for plain or unicode text + case CF_TEXT: + case CF_UNICODETEXT: + return GetText(df, *aFormat, *pSTM); + + // Some 3rd party apps that receive drag and drop files from the browser + // window require support for this. + case CF_HDROP: + return GetFile(*aFormat, *pSTM); + + // Someone is asking for an image + case CF_DIBV5: + case CF_DIB: + return GetDib(df, *aFormat, *pSTM); + + default: + if (format == fileDescriptorFlavorA) + return GetFileDescriptor(*aFormat, *pSTM, false); + if (format == fileDescriptorFlavorW) + return GetFileDescriptor(*aFormat, *pSTM, true); + if (format == uniformResourceLocatorA) + return GetUniformResourceLocator(*aFormat, *pSTM, false); + if (format == uniformResourceLocatorW) + return GetUniformResourceLocator(*aFormat, *pSTM, true); + if (format == fileFlavor) return GetFileContents(*aFormat, *pSTM); + if (format == PreferredDropEffect) + return GetPreferredDropEffect(*aFormat, *pSTM); + // MOZ_LOG(gWindowsLog, LogLevel::Info, + // ("***** nsDataObj::GetData - Unknown format %u\n", format)); + return GetText(df, *aFormat, *pSTM); + } // switch + } // if + dfInx++; + } // while + + return DATA_E_FORMATETC; +} + +//----------------------------------------------------- +STDMETHODIMP nsDataObj::GetDataHere(LPFORMATETC pFE, LPSTGMEDIUM pSTM) { + return E_FAIL; +} + +//----------------------------------------------------- +// Other objects querying to see if we support a +// particular format +//----------------------------------------------------- +STDMETHODIMP nsDataObj::QueryGetData(LPFORMATETC pFE) { + // Arbitrary system formats are used for image feedback during drag + // and drop. We are responsible for storing these internally during + // drag operations. + LPDATAENTRY pde; + if (LookupArbitraryFormat(pFE, &pde, FALSE)) return S_OK; + + // Firefox internal formats + ULONG count; + FORMATETC fe; + m_enumFE->Reset(); + while (NOERROR == m_enumFE->Next(1, &fe, &count)) { + if (fe.cfFormat == pFE->cfFormat) { + return S_OK; + } + } + return E_FAIL; +} + +//----------------------------------------------------- +STDMETHODIMP nsDataObj::GetCanonicalFormatEtc(LPFORMATETC pFEIn, + LPFORMATETC pFEOut) { + return E_NOTIMPL; +} + +//----------------------------------------------------- +STDMETHODIMP nsDataObj::SetData(LPFORMATETC aFormat, LPSTGMEDIUM aMedium, + BOOL shouldRel) { + // Arbitrary system formats are used for image feedback during drag + // and drop. We are responsible for storing these internally during + // drag operations. + LPDATAENTRY pde; + if (LookupArbitraryFormat(aFormat, &pde, TRUE)) { + // Release the old data the lookup handed us for this format. This + // may have been set in CopyMediumData when we originally stored the + // data. + if (pde->stgm.tymed) { + ReleaseStgMedium(&pde->stgm); + memset(&pde->stgm, 0, sizeof(STGMEDIUM)); + } + + bool result = true; + if (shouldRel) { + // If shouldRel is TRUE, the data object called owns the storage medium + // after the call returns. Store the incoming data in our data array for + // release when we are destroyed. This is the common case with arbitrary + // data from explorer. + pde->stgm = *aMedium; + } else { + // Copy the incoming data into our data array. (AFAICT, this never gets + // called with arbitrary formats for drag images.) + result = CopyMediumData(&pde->stgm, aMedium, aFormat, TRUE); + } + pde->fe.tymed = pde->stgm.tymed; + + return result ? S_OK : DV_E_TYMED; + } + + if (shouldRel) ReleaseStgMedium(aMedium); + + return S_OK; +} + +bool nsDataObj::LookupArbitraryFormat(FORMATETC* aFormat, + LPDATAENTRY* aDataEntry, + BOOL aAddorUpdate) { + *aDataEntry = nullptr; + + if (aFormat->ptd != nullptr) return false; + + // See if it's already in our list. If so return the data entry. + for (uint32_t idx = 0; idx < mDataEntryList.Length(); idx++) { + if (mDataEntryList[idx]->fe.cfFormat == aFormat->cfFormat && + mDataEntryList[idx]->fe.dwAspect == aFormat->dwAspect && + mDataEntryList[idx]->fe.lindex == aFormat->lindex) { + if (aAddorUpdate || (mDataEntryList[idx]->fe.tymed & aFormat->tymed)) { + // If the caller requests we update, or if the + // medium type matches, return the entry. + *aDataEntry = mDataEntryList[idx]; + return true; + } else { + // Medium does not match, not found. + return false; + } + } + } + + if (!aAddorUpdate) return false; + + // Add another entry to mDataEntryList + LPDATAENTRY dataEntry = (LPDATAENTRY)CoTaskMemAlloc(sizeof(DATAENTRY)); + if (!dataEntry) return false; + + dataEntry->fe = *aFormat; + *aDataEntry = dataEntry; + memset(&dataEntry->stgm, 0, sizeof(STGMEDIUM)); + + // Add this to our IEnumFORMATETC impl. so we can return it when + // it's requested. + m_enumFE->AddFormatEtc(aFormat); + + // Store a copy internally in the arbitrary formats array. + mDataEntryList.AppendElement(dataEntry); + + return true; +} + +bool nsDataObj::CopyMediumData(STGMEDIUM* aMediumDst, STGMEDIUM* aMediumSrc, + LPFORMATETC aFormat, BOOL aSetData) { + STGMEDIUM stgmOut = *aMediumSrc; + + switch (stgmOut.tymed) { + case TYMED_ISTREAM: + stgmOut.pstm->AddRef(); + break; + case TYMED_ISTORAGE: + stgmOut.pstg->AddRef(); + break; + case TYMED_HGLOBAL: + if (!aMediumSrc->pUnkForRelease) { + if (aSetData) { + if (aMediumSrc->tymed != TYMED_HGLOBAL) return false; + stgmOut.hGlobal = + OleDuplicateData(aMediumSrc->hGlobal, aFormat->cfFormat, 0); + if (!stgmOut.hGlobal) return false; + } else { + // We are returning this data from LookupArbitraryFormat, indicate to + // the shell we hold it and will free it. + stgmOut.pUnkForRelease = static_cast<IDataObject*>(this); + } + } + break; + default: + return false; + } + + if (stgmOut.pUnkForRelease) stgmOut.pUnkForRelease->AddRef(); + + *aMediumDst = stgmOut; + + return true; +} + +//----------------------------------------------------- +STDMETHODIMP nsDataObj::EnumFormatEtc(DWORD dwDir, LPENUMFORMATETC* ppEnum) { + switch (dwDir) { + case DATADIR_GET: + m_enumFE->Clone(ppEnum); + break; + case DATADIR_SET: + // fall through + default: + *ppEnum = nullptr; + } // switch + + if (nullptr == *ppEnum) return E_FAIL; + + (*ppEnum)->Reset(); + // Clone already AddRefed the result so don't addref it again. + return NOERROR; +} + +//----------------------------------------------------- +STDMETHODIMP nsDataObj::DAdvise(LPFORMATETC pFE, DWORD dwFlags, + LPADVISESINK pIAdviseSink, DWORD* pdwConn) { + return OLE_E_ADVISENOTSUPPORTED; +} + +//----------------------------------------------------- +STDMETHODIMP nsDataObj::DUnadvise(DWORD dwConn) { + return OLE_E_ADVISENOTSUPPORTED; +} + +//----------------------------------------------------- +STDMETHODIMP nsDataObj::EnumDAdvise(LPENUMSTATDATA* ppEnum) { + return OLE_E_ADVISENOTSUPPORTED; +} + +// IDataObjectAsyncCapability methods +STDMETHODIMP nsDataObj::EndOperation(HRESULT hResult, IBindCtx* pbcReserved, + DWORD dwEffects) { + mIsInOperation = FALSE; + return S_OK; +} + +STDMETHODIMP nsDataObj::GetAsyncMode(BOOL* pfIsOpAsync) { + *pfIsOpAsync = mIsAsyncMode; + + return S_OK; +} + +STDMETHODIMP nsDataObj::InOperation(BOOL* pfInAsyncOp) { + *pfInAsyncOp = mIsInOperation; + + return S_OK; +} + +STDMETHODIMP nsDataObj::SetAsyncMode(BOOL fDoOpAsync) { + mIsAsyncMode = fDoOpAsync; + return S_OK; +} + +STDMETHODIMP nsDataObj::StartOperation(IBindCtx* pbcReserved) { + mIsInOperation = TRUE; + return S_OK; +} + +// +// GetDIB +// +// Someone is asking for a bitmap. The data in the transferable will be a +// straight imgIContainer, so just QI it. +// +HRESULT +nsDataObj::GetDib(const nsACString& inFlavor, FORMATETC& aFormat, + STGMEDIUM& aSTG) { + nsCOMPtr<nsISupports> genericDataWrapper; + if (NS_FAILED( + mTransferable->GetTransferData(PromiseFlatCString(inFlavor).get(), + getter_AddRefs(genericDataWrapper)))) { + return E_FAIL; + } + + nsCOMPtr<imgIContainer> image = do_QueryInterface(genericDataWrapper); + if (!image) { + return E_FAIL; + } + + nsCOMPtr<imgITools> imgTools = + do_CreateInstance("@mozilla.org/image/tools;1"); + + nsAutoString options(u"bpp=32;"_ns); + if (aFormat.cfFormat == CF_DIBV5) { + options.AppendLiteral("version=5"); + } else { + options.AppendLiteral("version=3"); + } + + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = imgTools->EncodeImage(image, nsLiteralCString(IMAGE_BMP), + options, getter_AddRefs(inputStream)); + if (NS_FAILED(rv) || !inputStream) { + return E_FAIL; + } + + nsCOMPtr<imgIEncoder> encoder = do_QueryInterface(inputStream); + if (!encoder) { + return E_FAIL; + } + + uint32_t size = 0; + rv = encoder->GetImageBufferUsed(&size); + if (NS_FAILED(rv) || size <= BFH_LENGTH) { + return E_FAIL; + } + + char* src = nullptr; + rv = encoder->GetImageBuffer(&src); + if (NS_FAILED(rv) || !src) { + return E_FAIL; + } + + // We don't want the file header. + src += BFH_LENGTH; + size -= BFH_LENGTH; + + HGLOBAL glob = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, size); + if (!glob) { + return E_FAIL; + } + + char* dst = (char*)::GlobalLock(glob); + ::CopyMemory(dst, src, size); + ::GlobalUnlock(glob); + + aSTG.hGlobal = glob; + aSTG.tymed = TYMED_HGLOBAL; + return S_OK; +} + +// +// GetFileDescriptor +// + +HRESULT +nsDataObj ::GetFileDescriptor(FORMATETC& aFE, STGMEDIUM& aSTG, + bool aIsUnicode) { + HRESULT res = S_OK; + + // How we handle this depends on if we're dealing with an internet + // shortcut, since those are done under the covers. + if (IsFlavourPresent(kFilePromiseMime) || IsFlavourPresent(kFileMime)) { + if (aIsUnicode) + return GetFileDescriptor_IStreamW(aFE, aSTG); + else + return GetFileDescriptor_IStreamA(aFE, aSTG); + } else if (IsFlavourPresent(kURLMime)) { + if (aIsUnicode) + res = GetFileDescriptorInternetShortcutW(aFE, aSTG); + else + res = GetFileDescriptorInternetShortcutA(aFE, aSTG); + } else + NS_WARNING("Not yet implemented\n"); + + return res; +} // GetFileDescriptor + +// +HRESULT +nsDataObj ::GetFileContents(FORMATETC& aFE, STGMEDIUM& aSTG) { + HRESULT res = S_OK; + + // How we handle this depends on if we're dealing with an internet + // shortcut, since those are done under the covers. + if (IsFlavourPresent(kFilePromiseMime) || IsFlavourPresent(kFileMime)) + return GetFileContents_IStream(aFE, aSTG); + else if (IsFlavourPresent(kURLMime)) + return GetFileContentsInternetShortcut(aFE, aSTG); + else + NS_WARNING("Not yet implemented\n"); + + return res; + +} // GetFileContents + +// Ensure that the supplied name doesn't have invalid characters. +static void ValidateFilename(nsString& aFilename, bool isShortcut) { + nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1"); + if (NS_WARN_IF(!mimeService)) { + aFilename.Truncate(); + return; + } + + uint32_t flags = nsIMIMEService::VALIDATE_SANITIZE_ONLY; + if (isShortcut) { + flags |= nsIMIMEService::VALIDATE_ALLOW_INVALID_FILENAMES; + } + + nsAutoString outFilename; + mimeService->ValidateFileNameForSaving(aFilename, EmptyCString(), flags, + outFilename); + aFilename = outFilename; +} + +// +// Given a unicode string, convert it to a valid local charset filename +// and append the .url extension to be used for a shortcut file. +// This ensures that we do not cut MBCS characters in the middle. +// +// It would seem that this is more functionality suited to being in nsIFile. +// +static bool CreateURLFilenameFromTextA(nsAutoString& aText, char* aFilename) { + if (aText.IsEmpty()) { + return false; + } + aText.AppendLiteral(".url"); + ValidateFilename(aText, true); + if (aText.IsEmpty()) { + return false; + } + + // ValidateFilename should already be checking the filename length, but do + // an extra check to verify for the local code page that the converted text + // doesn't go over MAX_PATH and just return false if it does. + char defaultChar = '_'; + int currLen = WideCharToMultiByte(CP_ACP, WC_COMPOSITECHECK | WC_DEFAULTCHAR, + aText.get(), -1, aFilename, MAX_PATH, + &defaultChar, nullptr); + return currLen != 0; +} + +// Wide character version of CreateURLFilenameFromTextA +static bool CreateURLFilenameFromTextW(nsAutoString& aText, + wchar_t* aFilename) { + if (aText.IsEmpty()) { + return false; + } + aText.AppendLiteral(".url"); + ValidateFilename(aText, true); + if (aText.IsEmpty() || aText.Length() >= MAX_PATH) { + return false; + } + + wcscpy(&aFilename[0], aText.get()); + return true; +} + +#define PAGEINFO_PROPERTIES "chrome://navigator/locale/pageInfo.properties" + +static bool GetLocalizedString(const char* aName, nsAString& aString) { + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::components::StringBundle::Service(); + if (!stringService) return false; + + nsCOMPtr<nsIStringBundle> stringBundle; + nsresult rv = stringService->CreateBundle(PAGEINFO_PROPERTIES, + getter_AddRefs(stringBundle)); + if (NS_FAILED(rv)) return false; + + rv = stringBundle->GetStringFromName(aName, aString); + return NS_SUCCEEDED(rv); +} + +// +// GetFileDescriptorInternetShortcut +// +// Create the special format for an internet shortcut and build up the data +// structures the shell is expecting. +// +HRESULT +nsDataObj ::GetFileDescriptorInternetShortcutA(FORMATETC& aFE, + STGMEDIUM& aSTG) { + // get the title of the shortcut + nsAutoString title; + if (NS_FAILED(ExtractShortcutTitle(title))) return E_OUTOFMEMORY; + + HGLOBAL fileGroupDescHandle = + ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORA)); + if (!fileGroupDescHandle) return E_OUTOFMEMORY; + + LPFILEGROUPDESCRIPTORA fileGroupDescA = + reinterpret_cast<LPFILEGROUPDESCRIPTORA>( + ::GlobalLock(fileGroupDescHandle)); + if (!fileGroupDescA) { + ::GlobalFree(fileGroupDescHandle); + return E_OUTOFMEMORY; + } + + // get a valid filename in the following order: 1) from the page title, + // 2) localized string for an untitled page, 3) just use "Untitled.url" + if (!CreateURLFilenameFromTextA(title, fileGroupDescA->fgd[0].cFileName)) { + nsAutoString untitled; + if (!GetLocalizedString("noPageTitle", untitled) || + !CreateURLFilenameFromTextA(untitled, + fileGroupDescA->fgd[0].cFileName)) { + strcpy(fileGroupDescA->fgd[0].cFileName, "Untitled.url"); + } + } + + // one file in the file block + fileGroupDescA->cItems = 1; + fileGroupDescA->fgd[0].dwFlags = FD_LINKUI; + + ::GlobalUnlock(fileGroupDescHandle); + aSTG.hGlobal = fileGroupDescHandle; + aSTG.tymed = TYMED_HGLOBAL; + + return S_OK; +} // GetFileDescriptorInternetShortcutA + +HRESULT +nsDataObj ::GetFileDescriptorInternetShortcutW(FORMATETC& aFE, + STGMEDIUM& aSTG) { + // get the title of the shortcut + nsAutoString title; + if (NS_FAILED(ExtractShortcutTitle(title))) return E_OUTOFMEMORY; + + HGLOBAL fileGroupDescHandle = + ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORW)); + if (!fileGroupDescHandle) return E_OUTOFMEMORY; + + LPFILEGROUPDESCRIPTORW fileGroupDescW = + reinterpret_cast<LPFILEGROUPDESCRIPTORW>( + ::GlobalLock(fileGroupDescHandle)); + if (!fileGroupDescW) { + ::GlobalFree(fileGroupDescHandle); + return E_OUTOFMEMORY; + } + + // get a valid filename in the following order: 1) from the page title, + // 2) localized string for an untitled page, 3) just use "Untitled.url" + if (!CreateURLFilenameFromTextW(title, fileGroupDescW->fgd[0].cFileName)) { + nsAutoString untitled; + if (!GetLocalizedString("noPageTitle", untitled) || + !CreateURLFilenameFromTextW(untitled, + fileGroupDescW->fgd[0].cFileName)) { + wcscpy(fileGroupDescW->fgd[0].cFileName, L"Untitled.url"); + } + } + + // one file in the file block + fileGroupDescW->cItems = 1; + fileGroupDescW->fgd[0].dwFlags = FD_LINKUI; + + ::GlobalUnlock(fileGroupDescHandle); + aSTG.hGlobal = fileGroupDescHandle; + aSTG.tymed = TYMED_HGLOBAL; + + return S_OK; +} // GetFileDescriptorInternetShortcutW + +// +// GetFileContentsInternetShortcut +// +// Create the special format for an internet shortcut and build up the data +// structures the shell is expecting. +// +HRESULT +nsDataObj ::GetFileContentsInternetShortcut(FORMATETC& aFE, STGMEDIUM& aSTG) { + static const char* kShellIconPref = "browser.shell.shortcutFavicons"; + nsAutoString url; + if (NS_FAILED(ExtractShortcutURL(url))) return E_OUTOFMEMORY; + + nsCOMPtr<nsIURI> aUri; + nsresult rv = NS_NewURI(getter_AddRefs(aUri), url); + if (NS_FAILED(rv)) { + return E_FAIL; + } + + nsAutoCString asciiUrl; + rv = aUri->GetAsciiSpec(asciiUrl); + if (NS_FAILED(rv)) { + return E_FAIL; + } + + RefPtr<AutoCloseEvent> event; + + const char* shortcutFormatStr; + int totalLen; + nsCString asciiPath; + if (!Preferences::GetBool(kShellIconPref, true)) { + shortcutFormatStr = "[InternetShortcut]\r\nURL=%s\r\n"; + const int formatLen = strlen(shortcutFormatStr) - 2; // don't include %s + totalLen = formatLen + asciiUrl.Length(); // don't include null character + } else { + nsCOMPtr<nsIFile> icoFile; + + nsAutoString aUriHash; + + event = new AutoCloseEvent(); + if (!event->IsInited()) { + return E_FAIL; + } + + RefPtr<AutoSetEvent> e = new AutoSetEvent(WrapNotNull(event)); + mozilla::widget::FaviconHelper::ObtainCachedIconFile( + aUri, aUriHash, mIOThread, true, + NS_NewRunnableFunction( + "FaviconHelper::RefreshDesktop", [e = std::move(e)] { + if (e->IsWaiting()) { + // Unblock IStream:::Read. + e->Signal(); + } else { + // We could not wait until the favicon was available. We have + // to refresh to refect the favicon. + SendNotifyMessage(HWND_BROADCAST, WM_SETTINGCHANGE, + SPI_SETNONCLIENTMETRICS, 0); + } + })); + + rv = mozilla::widget::FaviconHelper::GetOutputIconPath(aUri, icoFile, true); + NS_ENSURE_SUCCESS(rv, E_FAIL); + nsString path; + rv = icoFile->GetPath(path); + NS_ENSURE_SUCCESS(rv, E_FAIL); + + if (IsAsciiNullTerminated(static_cast<const char16_t*>(path.get()))) { + LossyCopyUTF16toASCII(path, asciiPath); + shortcutFormatStr = + "[InternetShortcut]\r\nURL=%s\r\n" + "IDList=\r\nHotKey=0\r\nIconFile=%s\r\n" + "IconIndex=0\r\n"; + } else { + int len = + WideCharToMultiByte(CP_UTF7, 0, char16ptr_t(path.BeginReading()), + path.Length(), nullptr, 0, nullptr, nullptr); + NS_ENSURE_TRUE(len > 0, E_FAIL); + asciiPath.SetLength(len); + WideCharToMultiByte(CP_UTF7, 0, char16ptr_t(path.BeginReading()), + path.Length(), asciiPath.BeginWriting(), len, nullptr, + nullptr); + shortcutFormatStr = + "[InternetShortcut]\r\nURL=%s\r\n" + "IDList=\r\nHotKey=0\r\nIconIndex=0\r\n" + "[InternetShortcut.W]\r\nIconFile=%s\r\n"; + } + const int formatLen = strlen(shortcutFormatStr) - 2 * 2; // no %s twice + totalLen = formatLen + asciiUrl.Length() + + asciiPath.Length(); // we don't want a null character on the end + } + + // create a global memory area and build up the file contents w/in it + nsAutoGlobalMem globalMem(nsHGLOBAL(::GlobalAlloc(GMEM_SHARE, totalLen))); + if (!globalMem) return E_OUTOFMEMORY; + + char* contents = reinterpret_cast<char*>(::GlobalLock(globalMem.get())); + if (!contents) { + return E_OUTOFMEMORY; + } + + // NOTE: we intentionally use the Microsoft version of snprintf here because + // it does NOT null + // terminate strings which reach the maximum size of the buffer. Since we know + // that the formatted length here is totalLen, this call to _snprintf will + // format the string into the buffer without appending the null character. + + if (!Preferences::GetBool(kShellIconPref, true)) { + _snprintf(contents, totalLen, shortcutFormatStr, asciiUrl.get()); + } else { + _snprintf(contents, totalLen, shortcutFormatStr, asciiUrl.get(), + asciiPath.get()); + } + + ::GlobalUnlock(globalMem.get()); + + if (aFE.tymed & TYMED_ISTREAM) { + if (!mIsInOperation) { + // The drop target didn't initiate an async operation. + // We can't block CMemStream::Read. + event = nullptr; + } + RefPtr<IStream> stream = + new CMemStream(globalMem.disown(), totalLen, event.forget()); + stream.forget(&aSTG.pstm); + aSTG.tymed = TYMED_ISTREAM; + } else { + if (event && event->IsInited()) { + event->Signal(); // We can't block reading the global memory + } + aSTG.hGlobal = globalMem.disown(); + aSTG.tymed = TYMED_HGLOBAL; + } + + return S_OK; +} // GetFileContentsInternetShortcut + +// check if specified flavour is present in the transferable +bool nsDataObj ::IsFlavourPresent(const char* inFlavour) { + bool retval = false; + NS_ENSURE_TRUE(mTransferable, false); + + // get the list of flavors available in the transferable + nsTArray<nsCString> flavors; + nsresult rv = mTransferable->FlavorsTransferableCanExport(flavors); + NS_ENSURE_SUCCESS(rv, false); + + // try to find requested flavour + for (uint32_t i = 0; i < flavors.Length(); ++i) { + if (flavors[i].Equals(inFlavour)) { + retval = true; // found it! + break; + } + } // for each flavor + + return retval; +} + +HRESULT nsDataObj::GetPreferredDropEffect(FORMATETC& aFE, STGMEDIUM& aSTG) { + HRESULT res = S_OK; + aSTG.tymed = TYMED_HGLOBAL; + aSTG.pUnkForRelease = nullptr; + HGLOBAL hGlobalMemory = nullptr; + hGlobalMemory = ::GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD)); + if (hGlobalMemory) { + DWORD* pdw = (DWORD*)GlobalLock(hGlobalMemory); + // The PreferredDropEffect clipboard format is only registered if a + // drag/drop of an image happens from Mozilla to the desktop. We want its + // value to be DROPEFFECT_MOVE in that case so that the file is moved from + // the temporary location, not copied. This value should, ideally, be set on + // the data object via SetData() but our IDataObject implementation doesn't + // implement SetData. It adds data to the data object lazily only when the + // drop target asks for it. + *pdw = (DWORD)DROPEFFECT_MOVE; + GlobalUnlock(hGlobalMemory); + } else { + res = E_OUTOFMEMORY; + } + aSTG.hGlobal = hGlobalMemory; + return res; +} + +//----------------------------------------------------- +HRESULT nsDataObj::GetText(const nsACString& aDataFlavor, FORMATETC& aFE, + STGMEDIUM& aSTG) { + void* data = nullptr; + + const nsPromiseFlatCString& flavorStr = PromiseFlatCString(aDataFlavor); + + // NOTE: CreateDataFromPrimitive creates new memory, that needs to be deleted + nsCOMPtr<nsISupports> genericDataWrapper; + nsresult rv = mTransferable->GetTransferData( + flavorStr.get(), getter_AddRefs(genericDataWrapper)); + if (NS_FAILED(rv) || !genericDataWrapper) { + return E_FAIL; + } + + uint32_t len; + nsPrimitiveHelpers::CreateDataFromPrimitive( + nsDependentCString(flavorStr.get()), genericDataWrapper, &data, &len); + if (!data) return E_FAIL; + + HGLOBAL hGlobalMemory = nullptr; + + aSTG.tymed = TYMED_HGLOBAL; + aSTG.pUnkForRelease = nullptr; + + // We play games under the hood and advertise flavors that we know we + // can support, only they require a bit of conversion or munging of the data. + // Do that here. + // + // The transferable gives us data that is null-terminated, but this isn't + // reflected in the |len| parameter. Windoze apps expect this null to be there + // so bump our data buffer by the appropriate size to account for the null + // (one char for CF_TEXT, one char16_t for CF_UNICODETEXT). + DWORD allocLen = (DWORD)len; + if (aFE.cfFormat == CF_TEXT) { + // Someone is asking for text/plain; convert the unicode (assuming it's + // present) to text with the correct platform encoding. + size_t bufferSize = sizeof(char) * (len + 2); + char* plainTextData = static_cast<char*>(moz_xmalloc(bufferSize)); + char16_t* castedUnicode = reinterpret_cast<char16_t*>(data); + int32_t plainTextLen = + WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)castedUnicode, len / 2 + 1, + plainTextData, bufferSize, NULL, NULL); + // replace the unicode data with our plaintext data. Recall that + // |plainTextLen| doesn't include the null in the length. + free(data); + if (plainTextLen) { + data = plainTextData; + allocLen = plainTextLen; + } else { + free(plainTextData); + NS_WARNING("Oh no, couldn't convert unicode to plain text"); + return S_OK; + } + } else if (aFE.cfFormat == nsClipboard::GetHtmlClipboardFormat()) { + // Someone is asking for win32's HTML flavor. Convert our html fragment + // from unicode to UTF-8 then put it into a format specified by msft. + NS_ConvertUTF16toUTF8 converter(reinterpret_cast<char16_t*>(data)); + char* utf8HTML = nullptr; + nsresult rv = + BuildPlatformHTML(converter.get(), &utf8HTML); // null terminates + + free(data); + if (NS_SUCCEEDED(rv) && utf8HTML) { + // replace the unicode data with our HTML data. Don't forget the null. + data = utf8HTML; + allocLen = strlen(utf8HTML) + sizeof(char); + } else { + NS_WARNING("Oh no, couldn't convert to HTML"); + return S_OK; + } + } else if (aFE.cfFormat != nsClipboard::GetCustomClipboardFormat()) { + // we assume that any data that isn't caught above is unicode. This may + // be an erroneous assumption, but is true so far. + allocLen += sizeof(char16_t); + } + + hGlobalMemory = (HGLOBAL)GlobalAlloc(GMEM_MOVEABLE, allocLen); + + // Copy text to Global Memory Area + if (hGlobalMemory) { + char* dest = reinterpret_cast<char*>(GlobalLock(hGlobalMemory)); + char* source = reinterpret_cast<char*>(data); + memcpy(dest, source, allocLen); // copies the null as well + GlobalUnlock(hGlobalMemory); + } + aSTG.hGlobal = hGlobalMemory; + + // Now, delete the memory that was created by CreateDataFromPrimitive (or our + // text/plain data) + free(data); + + return S_OK; +} + +//----------------------------------------------------- +HRESULT nsDataObj::GetFile(FORMATETC& aFE, STGMEDIUM& aSTG) { + uint32_t dfInx = 0; + ULONG count; + FORMATETC fe; + m_enumFE->Reset(); + while (NOERROR == m_enumFE->Next(1, &fe, &count) && + dfInx < mDataFlavors.Length()) { + if (mDataFlavors[dfInx].EqualsLiteral(kNativeImageMime)) + return DropImage(aFE, aSTG); + if (mDataFlavors[dfInx].EqualsLiteral(kFileMime)) + return DropFile(aFE, aSTG); + if (mDataFlavors[dfInx].EqualsLiteral(kFilePromiseMime)) + return DropTempFile(aFE, aSTG); + dfInx++; + } + return E_FAIL; +} + +HRESULT nsDataObj::DropFile(FORMATETC& aFE, STGMEDIUM& aSTG) { + nsresult rv; + nsCOMPtr<nsISupports> genericDataWrapper; + + if (NS_FAILED(mTransferable->GetTransferData( + kFileMime, getter_AddRefs(genericDataWrapper)))) { + return E_FAIL; + } + nsCOMPtr<nsIFile> file(do_QueryInterface(genericDataWrapper)); + if (!file) return E_FAIL; + + aSTG.tymed = TYMED_HGLOBAL; + aSTG.pUnkForRelease = nullptr; + + nsAutoString path; + rv = file->GetPath(path); + if (NS_FAILED(rv)) return E_FAIL; + + uint32_t allocLen = path.Length() + 2; + HGLOBAL hGlobalMemory = nullptr; + char16_t* dest; + + hGlobalMemory = GlobalAlloc(GMEM_MOVEABLE, + sizeof(DROPFILES) + allocLen * sizeof(char16_t)); + if (!hGlobalMemory) return E_FAIL; + + DROPFILES* pDropFile = (DROPFILES*)GlobalLock(hGlobalMemory); + + // First, populate the drop file structure + pDropFile->pFiles = sizeof(DROPFILES); // Offset to start of file name string + pDropFile->fNC = 0; + pDropFile->pt.x = 0; + pDropFile->pt.y = 0; + pDropFile->fWide = TRUE; + + // Copy the filename right after the DROPFILES structure + dest = (char16_t*)(((char*)pDropFile) + pDropFile->pFiles); + memcpy(dest, path.get(), (allocLen - 1) * sizeof(char16_t)); + + // Two null characters are needed at the end of the file name. + // Lookup the CF_HDROP shell clipboard format for more info. + // Add the second null character right after the first one. + dest[allocLen - 1] = L'\0'; + + GlobalUnlock(hGlobalMemory); + + aSTG.hGlobal = hGlobalMemory; + + return S_OK; +} + +HRESULT nsDataObj::DropImage(FORMATETC& aFE, STGMEDIUM& aSTG) { + nsresult rv; + if (!mCachedTempFile) { + nsCOMPtr<nsISupports> genericDataWrapper; + + if (NS_FAILED(mTransferable->GetTransferData( + kNativeImageMime, getter_AddRefs(genericDataWrapper)))) { + return E_FAIL; + } + nsCOMPtr<imgIContainer> image(do_QueryInterface(genericDataWrapper)); + if (!image) return E_FAIL; + + nsCOMPtr<imgITools> imgTools = + do_CreateInstance("@mozilla.org/image/tools;1"); + nsCOMPtr<nsIInputStream> inputStream; + rv = imgTools->EncodeImage(image, nsLiteralCString(IMAGE_BMP), + u"bpp=32;version=3"_ns, + getter_AddRefs(inputStream)); + if (NS_FAILED(rv) || !inputStream) { + return E_FAIL; + } + + nsCOMPtr<imgIEncoder> encoder = do_QueryInterface(inputStream); + if (!encoder) { + return E_FAIL; + } + + uint32_t size = 0; + rv = encoder->GetImageBufferUsed(&size); + if (NS_FAILED(rv)) { + return E_FAIL; + } + + char* src = nullptr; + rv = encoder->GetImageBuffer(&src); + if (NS_FAILED(rv) || !src) { + return E_FAIL; + } + + // Save the bitmap to a temporary location. + nsCOMPtr<nsIFile> dropFile; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dropFile)); + if (!dropFile) { + return E_FAIL; + } + + // Filename must be random so as not to confuse apps like + // Photoshop which handle multiple drags into a single window. + char buf[13]; + nsCString filename; + NS_MakeRandomString(buf, 8); + memcpy(buf + 8, ".bmp", 5); + filename.Append(nsDependentCString(buf, 12)); + dropFile->AppendNative(filename); + rv = dropFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660); + if (NS_FAILED(rv)) { + return E_FAIL; + } + + // Cache the temp file so we can delete it later and so + // it doesn't get recreated over and over on multiple calls + // which does occur from windows shell. + dropFile->Clone(getter_AddRefs(mCachedTempFile)); + + // Write the data to disk. + nsCOMPtr<nsIOutputStream> outStream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(outStream), dropFile); + if (NS_FAILED(rv)) { + return E_FAIL; + } + + uint32_t written = 0; + rv = outStream->Write(src, size, &written); + if (NS_FAILED(rv) || written != size) { + return E_FAIL; + } + + outStream->Close(); + } + + // Pass the file name back to the drop target so that it can access the file. + nsAutoString path; + rv = mCachedTempFile->GetPath(path); + if (NS_FAILED(rv)) return E_FAIL; + + // Two null characters are needed to terminate the file name list. + HGLOBAL hGlobalMemory = nullptr; + + uint32_t allocLen = path.Length() + 2; + + aSTG.tymed = TYMED_HGLOBAL; + aSTG.pUnkForRelease = nullptr; + + hGlobalMemory = GlobalAlloc(GMEM_MOVEABLE, + sizeof(DROPFILES) + allocLen * sizeof(char16_t)); + if (!hGlobalMemory) return E_FAIL; + + DROPFILES* pDropFile = (DROPFILES*)GlobalLock(hGlobalMemory); + + // First, populate the drop file structure. + pDropFile->pFiles = + sizeof(DROPFILES); // Offset to start of file name char array. + pDropFile->fNC = 0; + pDropFile->pt.x = 0; + pDropFile->pt.y = 0; + pDropFile->fWide = TRUE; + + // Copy the filename right after the DROPFILES structure. + char16_t* dest = (char16_t*)(((char*)pDropFile) + pDropFile->pFiles); + memcpy(dest, path.get(), + (allocLen - 1) * + sizeof(char16_t)); // Copies the null character in path as well. + + // Two null characters are needed at the end of the file name. + // Lookup the CF_HDROP shell clipboard format for more info. + // Add the second null character right after the first one. + dest[allocLen - 1] = L'\0'; + + GlobalUnlock(hGlobalMemory); + + aSTG.hGlobal = hGlobalMemory; + + return S_OK; +} + +HRESULT nsDataObj::DropTempFile(FORMATETC& aFE, STGMEDIUM& aSTG) { + nsresult rv; + if (!mCachedTempFile) { + // Tempfile will need a temporary location. + nsCOMPtr<nsIFile> dropFile; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dropFile)); + if (!dropFile) return E_FAIL; + + // Filename must be random + nsCString filename; + nsAutoString wideFileName; + nsCOMPtr<nsIURI> sourceURI; + HRESULT res; + res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName); + if (FAILED(res)) return res; + NS_CopyUnicodeToNative(wideFileName, filename); + + dropFile->AppendNative(filename); + rv = dropFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660); + if (NS_FAILED(rv)) return E_FAIL; + + // Cache the temp file so we can delete it later and so + // it doesn't get recreated over and over on multiple calls + // which does occur from windows shell. + dropFile->Clone(getter_AddRefs(mCachedTempFile)); + + // Write the data to disk. + nsCOMPtr<nsIOutputStream> outStream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(outStream), dropFile); + if (NS_FAILED(rv)) return E_FAIL; + + IStream* pStream = nullptr; + nsDataObj::CreateStream(&pStream); + NS_ENSURE_TRUE(pStream, E_FAIL); + + char buffer[512]; + ULONG readCount = 0; + uint32_t writeCount = 0; + while (1) { + HRESULT hres = pStream->Read(buffer, sizeof(buffer), &readCount); + if (FAILED(hres)) return E_FAIL; + if (readCount == 0) break; + rv = outStream->Write(buffer, readCount, &writeCount); + if (NS_FAILED(rv)) return E_FAIL; + } + outStream->Close(); + pStream->Release(); + } + + // Pass the file name back to the drop target so that it can access the file. + nsAutoString path; + rv = mCachedTempFile->GetPath(path); + if (NS_FAILED(rv)) return E_FAIL; + + uint32_t allocLen = path.Length() + 2; + + // Two null characters are needed to terminate the file name list. + HGLOBAL hGlobalMemory = nullptr; + + aSTG.tymed = TYMED_HGLOBAL; + aSTG.pUnkForRelease = nullptr; + + hGlobalMemory = GlobalAlloc(GMEM_MOVEABLE, + sizeof(DROPFILES) + allocLen * sizeof(char16_t)); + if (!hGlobalMemory) return E_FAIL; + + DROPFILES* pDropFile = (DROPFILES*)GlobalLock(hGlobalMemory); + + // First, populate the drop file structure. + pDropFile->pFiles = + sizeof(DROPFILES); // Offset to start of file name char array. + pDropFile->fNC = 0; + pDropFile->pt.x = 0; + pDropFile->pt.y = 0; + pDropFile->fWide = TRUE; + + // Copy the filename right after the DROPFILES structure. + char16_t* dest = (char16_t*)(((char*)pDropFile) + pDropFile->pFiles); + memcpy(dest, path.get(), + (allocLen - 1) * + sizeof(char16_t)); // Copies the null character in path as well. + + // Two null characters are needed at the end of the file name. + // Lookup the CF_HDROP shell clipboard format for more info. + // Add the second null character right after the first one. + dest[allocLen - 1] = L'\0'; + + GlobalUnlock(hGlobalMemory); + + aSTG.hGlobal = hGlobalMemory; + + return S_OK; +} + +//----------------------------------------------------- +// Registers the DataFlavor/FE pair. +//----------------------------------------------------- +void nsDataObj::AddDataFlavor(const char* aDataFlavor, LPFORMATETC aFE) { + // These two lists are the mapping to and from data flavors and FEs. + // Later, OLE will tell us it needs a certain type of FORMATETC (text, + // unicode, etc) unicode, etc), so we will look up the data flavor that + // corresponds to the FE and then ask the transferable for that type of data. + mDataFlavors.AppendElement(aDataFlavor); + m_enumFE->AddFormatEtc(aFE); +} + +//----------------------------------------------------- +// Sets the transferable object +//----------------------------------------------------- +void nsDataObj::SetTransferable(nsITransferable* aTransferable) { + NS_IF_RELEASE(mTransferable); + + mTransferable = aTransferable; + if (nullptr == mTransferable) { + return; + } + + NS_ADDREF(mTransferable); + + return; +} + +// +// ExtractURL +// +// Roots around in the transferable for the appropriate flavor that indicates +// a url and pulls out the url portion of the data. Used mostly for creating +// internet shortcuts on the desktop. The url flavor is of the format: +// +// <url> <linefeed> <page title> +// +nsresult nsDataObj ::ExtractShortcutURL(nsString& outURL) { + NS_ASSERTION(mTransferable, "We don't have a good transferable"); + nsresult rv = NS_ERROR_FAILURE; + + nsCOMPtr<nsISupports> genericURL; + if (NS_SUCCEEDED(mTransferable->GetTransferData( + kURLMime, getter_AddRefs(genericURL)))) { + nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL)); + if (urlObject) { + nsAutoString url; + urlObject->GetData(url); + outURL = url; + + // find the first linefeed in the data, that's where the url ends. trunc + // the result string at that point. + int32_t lineIndex = outURL.FindChar('\n'); + NS_ASSERTION(lineIndex > 0, + "Format for url flavor is <url> <linefeed> <page title>"); + if (lineIndex > 0) { + outURL.Truncate(lineIndex); + rv = NS_OK; + } + } + } else if (NS_SUCCEEDED(mTransferable->GetTransferData( + kURLDataMime, getter_AddRefs(genericURL))) || + NS_SUCCEEDED(mTransferable->GetTransferData( + kURLPrivateMime, getter_AddRefs(genericURL)))) { + nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL)); + if (urlObject) { + nsAutoString url; + urlObject->GetData(url); + outURL = url; + + rv = NS_OK; + } + + } // if found flavor + + return rv; + +} // ExtractShortcutURL + +// +// ExtractShortcutTitle +// +// Roots around in the transferable for the appropriate flavor that indicates +// a url and pulls out the title portion of the data. Used mostly for creating +// internet shortcuts on the desktop. The url flavor is of the format: +// +// <url> <linefeed> <page title> +// +nsresult nsDataObj ::ExtractShortcutTitle(nsString& outTitle) { + NS_ASSERTION(mTransferable, "We'd don't have a good transferable"); + nsresult rv = NS_ERROR_FAILURE; + + nsCOMPtr<nsISupports> genericURL; + if (NS_SUCCEEDED(mTransferable->GetTransferData( + kURLMime, getter_AddRefs(genericURL)))) { + nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL)); + if (urlObject) { + nsAutoString url; + urlObject->GetData(url); + + // find the first linefeed in the data, that's where the url ends. we want + // everything after that linefeed. FindChar() returns -1 if we can't find + int32_t lineIndex = url.FindChar('\n'); + NS_ASSERTION(lineIndex != -1, + "Format for url flavor is <url> <linefeed> <page title>"); + if (lineIndex != -1) { + url.Mid(outTitle, lineIndex + 1, url.Length() - (lineIndex + 1)); + rv = NS_OK; + } + } + } // if found flavor + + return rv; + +} // ExtractShortcutTitle + +// +// BuildPlatformHTML +// +// Munge our HTML data to win32's CF_HTML spec. Basically, put the requisite +// header information on it. This will null-terminate |outPlatformHTML|. See +// https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format +// for details. +// +// We assume that |inOurHTML| is already a fragment (ie, doesn't have <HTML> +// or <BODY> tags). We'll wrap the fragment with them to make other apps +// happy. +// +nsresult nsDataObj ::BuildPlatformHTML(const char* inOurHTML, + char** outPlatformHTML) { + *outPlatformHTML = nullptr; + nsDependentCString inHTMLString(inOurHTML); + + // Do we already have mSourceURL from a drag? + if (mSourceURL.IsEmpty()) { + nsAutoString url; + ExtractShortcutURL(url); + + AppendUTF16toUTF8(url, mSourceURL); + } + + constexpr auto kStartHTMLPrefix = "Version:0.9\r\nStartHTML:"_ns; + constexpr auto kEndHTMLPrefix = "\r\nEndHTML:"_ns; + constexpr auto kStartFragPrefix = "\r\nStartFragment:"_ns; + constexpr auto kEndFragPrefix = "\r\nEndFragment:"_ns; + constexpr auto kStartSourceURLPrefix = "\r\nSourceURL:"_ns; + constexpr auto kEndFragTrailer = "\r\n"_ns; + + // The CF_HTML's size is embedded in the fragment, in such a way that the + // number of digits in the size is part of the size itself. While it _is_ + // technically possible to compute the necessary size of the size-field + // precisely -- by trial and error, if nothing else -- it's simpler just to + // pick a rough but generous estimate and zero-pad it. (Zero-padding is + // explicitly permitted by the format definition.) + // + // Originally, in 2001, the "rough but generous estimate" was 8 digits. While + // a maximum size of (10**9 - 1) bytes probably would have covered all + // possible use-cases at the time, it's somewhat more likely to overflow + // nowadays. Nonetheless, for the sake of backwards compatibility with any + // misbehaving consumers of our existing CF_HTML output, we retain exactly + // that padding for (most) fragments where it suffices. (No such misbehaving + // consumers are actually known, so this is arguably paranoia.) + // + // It is now 2022. A padding size of 16 will cover up to about 8.8 petabytes, + // which should be enough for at least the next few years or so. + const size_t numberLength = inHTMLString.Length() < 9999'0000 ? 8 : 16; + + const size_t sourceURLLength = mSourceURL.Length(); + + const size_t fixedHeaderLen = + kStartHTMLPrefix.Length() + kEndHTMLPrefix.Length() + + kStartFragPrefix.Length() + kEndFragPrefix.Length() + + kEndFragTrailer.Length() + (4 * numberLength); + + const size_t totalHeaderLen = + fixedHeaderLen + (sourceURLLength > 0 + ? kStartSourceURLPrefix.Length() + sourceURLLength + : 0); + + constexpr auto kHeaderString = "<html><body>\r\n<!--StartFragment-->"_ns; + constexpr auto kTrailingString = + "<!--EndFragment-->\r\n" + "</body>\r\n" + "</html>"_ns; + + // calculate the offsets + size_t startHTMLOffset = totalHeaderLen; + size_t startFragOffset = startHTMLOffset + kHeaderString.Length(); + + size_t endFragOffset = startFragOffset + inHTMLString.Length(); + size_t endHTMLOffset = endFragOffset + kTrailingString.Length(); + + // now build the final version + nsCString clipboardString; + clipboardString.SetCapacity(endHTMLOffset); + + const int numberLengthInt = static_cast<int>(numberLength); + clipboardString.Append(kStartHTMLPrefix); + clipboardString.AppendPrintf("%0*zu", numberLengthInt, startHTMLOffset); + + clipboardString.Append(kEndHTMLPrefix); + clipboardString.AppendPrintf("%0*zu", numberLengthInt, endHTMLOffset); + + clipboardString.Append(kStartFragPrefix); + clipboardString.AppendPrintf("%0*zu", numberLengthInt, startFragOffset); + + clipboardString.Append(kEndFragPrefix); + clipboardString.AppendPrintf("%0*zu", numberLengthInt, endFragOffset); + + if (sourceURLLength > 0) { + clipboardString.Append(kStartSourceURLPrefix); + clipboardString.Append(mSourceURL); + } + + clipboardString.Append(kEndFragTrailer); + + // Assert that the positional values were correct as we pass by their + // corresponding positions. + MOZ_ASSERT(clipboardString.Length() == startHTMLOffset); + clipboardString.Append(kHeaderString); + MOZ_ASSERT(clipboardString.Length() == startFragOffset); + clipboardString.Append(inHTMLString); + MOZ_ASSERT(clipboardString.Length() == endFragOffset); + clipboardString.Append(kTrailingString); + MOZ_ASSERT(clipboardString.Length() == endHTMLOffset); + + *outPlatformHTML = ToNewCString(clipboardString, mozilla::fallible); + if (!*outPlatformHTML) return NS_ERROR_OUT_OF_MEMORY; + + return NS_OK; +} + +HRESULT +nsDataObj ::GetUniformResourceLocator(FORMATETC& aFE, STGMEDIUM& aSTG, + bool aIsUnicode) { + HRESULT res = S_OK; + if (IsFlavourPresent(kURLMime)) { + if (aIsUnicode) + res = ExtractUniformResourceLocatorW(aFE, aSTG); + else + res = ExtractUniformResourceLocatorA(aFE, aSTG); + } else + NS_WARNING("Not yet implemented\n"); + return res; +} + +HRESULT +nsDataObj::ExtractUniformResourceLocatorA(FORMATETC& aFE, STGMEDIUM& aSTG) { + HRESULT result = S_OK; + + nsAutoString url; + if (NS_FAILED(ExtractShortcutURL(url))) return E_OUTOFMEMORY; + + NS_LossyConvertUTF16toASCII asciiUrl(url); + const int totalLen = asciiUrl.Length() + 1; + HGLOBAL hGlobalMemory = GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, totalLen); + if (!hGlobalMemory) return E_OUTOFMEMORY; + + char* contents = reinterpret_cast<char*>(GlobalLock(hGlobalMemory)); + if (!contents) { + GlobalFree(hGlobalMemory); + return E_OUTOFMEMORY; + } + + strcpy(contents, asciiUrl.get()); + GlobalUnlock(hGlobalMemory); + aSTG.hGlobal = hGlobalMemory; + aSTG.tymed = TYMED_HGLOBAL; + + return result; +} + +HRESULT +nsDataObj::ExtractUniformResourceLocatorW(FORMATETC& aFE, STGMEDIUM& aSTG) { + HRESULT result = S_OK; + + nsAutoString url; + if (NS_FAILED(ExtractShortcutURL(url))) return E_OUTOFMEMORY; + + const int totalLen = (url.Length() + 1) * sizeof(char16_t); + HGLOBAL hGlobalMemory = GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, totalLen); + if (!hGlobalMemory) return E_OUTOFMEMORY; + + wchar_t* contents = reinterpret_cast<wchar_t*>(GlobalLock(hGlobalMemory)); + if (!contents) { + GlobalFree(hGlobalMemory); + return E_OUTOFMEMORY; + } + + wcscpy(contents, url.get()); + GlobalUnlock(hGlobalMemory); + aSTG.hGlobal = hGlobalMemory; + aSTG.tymed = TYMED_HGLOBAL; + + return result; +} + +// Gets the filename from the kFilePromiseURLMime flavour +HRESULT nsDataObj::GetDownloadDetails(nsIURI** aSourceURI, + nsAString& aFilename) { + *aSourceURI = nullptr; + + NS_ENSURE_TRUE(mTransferable, E_FAIL); + + // get the URI from the kFilePromiseURLMime flavor + nsCOMPtr<nsISupports> urlPrimitive; + nsresult rv = mTransferable->GetTransferData(kFilePromiseURLMime, + getter_AddRefs(urlPrimitive)); + NS_ENSURE_SUCCESS(rv, E_FAIL); + nsCOMPtr<nsISupportsString> srcUrlPrimitive = do_QueryInterface(urlPrimitive); + NS_ENSURE_TRUE(srcUrlPrimitive, E_FAIL); + + nsAutoString srcUri; + srcUrlPrimitive->GetData(srcUri); + if (srcUri.IsEmpty()) return E_FAIL; + nsCOMPtr<nsIURI> sourceURI; + NS_NewURI(getter_AddRefs(sourceURI), srcUri); + + nsAutoString srcFileName; + nsCOMPtr<nsISupports> fileNamePrimitive; + Unused << mTransferable->GetTransferData(kFilePromiseDestFilename, + getter_AddRefs(fileNamePrimitive)); + nsCOMPtr<nsISupportsString> srcFileNamePrimitive = + do_QueryInterface(fileNamePrimitive); + if (srcFileNamePrimitive) { + srcFileNamePrimitive->GetData(srcFileName); + } else { + nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(sourceURI); + if (!sourceURL) return E_FAIL; + + nsAutoCString urlFileName; + sourceURL->GetFileName(urlFileName); + NS_UnescapeURL(urlFileName); + CopyUTF8toUTF16(urlFileName, srcFileName); + } + + // make the name safe for the filesystem + ValidateFilename(srcFileName, false); + if (srcFileName.IsEmpty()) return E_FAIL; + + sourceURI.swap(*aSourceURI); + aFilename = srcFileName; + return S_OK; +} + +HRESULT nsDataObj::GetFileDescriptor_IStreamA(FORMATETC& aFE, STGMEDIUM& aSTG) { + HGLOBAL fileGroupDescHandle = + ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORW)); + NS_ENSURE_TRUE(fileGroupDescHandle, E_OUTOFMEMORY); + + LPFILEGROUPDESCRIPTORA fileGroupDescA = + reinterpret_cast<LPFILEGROUPDESCRIPTORA>(GlobalLock(fileGroupDescHandle)); + if (!fileGroupDescA) { + ::GlobalFree(fileGroupDescHandle); + return E_OUTOFMEMORY; + } + + nsAutoString wideFileName; + HRESULT res; + nsCOMPtr<nsIURI> sourceURI; + res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName); + if (FAILED(res)) { + ::GlobalFree(fileGroupDescHandle); + return res; + } + + nsAutoCString nativeFileName; + NS_CopyUnicodeToNative(wideFileName, nativeFileName); + + strncpy(fileGroupDescA->fgd[0].cFileName, nativeFileName.get(), MAX_PATH - 1); + fileGroupDescA->fgd[0].cFileName[MAX_PATH - 1] = '\0'; + + // one file in the file block + fileGroupDescA->cItems = 1; + fileGroupDescA->fgd[0].dwFlags = FD_PROGRESSUI; + + GlobalUnlock(fileGroupDescHandle); + aSTG.hGlobal = fileGroupDescHandle; + aSTG.tymed = TYMED_HGLOBAL; + + return S_OK; +} + +HRESULT nsDataObj::GetFileDescriptor_IStreamW(FORMATETC& aFE, STGMEDIUM& aSTG) { + HGLOBAL fileGroupDescHandle = + ::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORW)); + NS_ENSURE_TRUE(fileGroupDescHandle, E_OUTOFMEMORY); + + LPFILEGROUPDESCRIPTORW fileGroupDescW = + reinterpret_cast<LPFILEGROUPDESCRIPTORW>(GlobalLock(fileGroupDescHandle)); + if (!fileGroupDescW) { + ::GlobalFree(fileGroupDescHandle); + return E_OUTOFMEMORY; + } + + nsAutoString wideFileName; + HRESULT res; + nsCOMPtr<nsIURI> sourceURI; + res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName); + if (FAILED(res)) { + ::GlobalFree(fileGroupDescHandle); + return res; + } + + wcsncpy(fileGroupDescW->fgd[0].cFileName, wideFileName.get(), MAX_PATH - 1); + fileGroupDescW->fgd[0].cFileName[MAX_PATH - 1] = '\0'; + // one file in the file block + fileGroupDescW->cItems = 1; + fileGroupDescW->fgd[0].dwFlags = FD_PROGRESSUI; + + GlobalUnlock(fileGroupDescHandle); + aSTG.hGlobal = fileGroupDescHandle; + aSTG.tymed = TYMED_HGLOBAL; + + return S_OK; +} + +HRESULT nsDataObj::GetFileContents_IStream(FORMATETC& aFE, STGMEDIUM& aSTG) { + IStream* pStream = nullptr; + + nsDataObj::CreateStream(&pStream); + NS_ENSURE_TRUE(pStream, E_FAIL); + + aSTG.tymed = TYMED_ISTREAM; + aSTG.pstm = pStream; + aSTG.pUnkForRelease = nullptr; + + return S_OK; +} diff --git a/widget/windows/nsDataObj.h b/widget/windows/nsDataObj.h new file mode 100644 index 0000000000..17683e371a --- /dev/null +++ b/widget/windows/nsDataObj.h @@ -0,0 +1,315 @@ +/* -*- 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 _NSDATAOBJ_H_ +#define _NSDATAOBJ_H_ + +#include <oleidl.h> +#include <shldisp.h> + +#include "mozilla/glue/WinUtils.h" +#include "mozilla/LazyIdleThread.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIFile.h" +#include "nsIURI.h" +#include "nsIStreamListener.h" +#include "nsIChannel.h" +#include "nsCOMArray.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsString.h" +#include "nsWindowsHelpers.h" + +class nsICookieJarSettings; +class nsIPrincipal; +class nsIReferrerInfo; +class nsIThread; +class nsITransferable; +class CEnumFormatEtc; + +/* + * This ole registered class is used to facilitate drag-drop of objects which + * can be adapted by an object derived from CfDragDrop. The CfDragDrop is + * associated with instances via SetDragDrop(). + */ +class nsDataObj : public IDataObject, public IDataObjectAsyncCapability { + RefPtr<mozilla::LazyIdleThread> mIOThread; + + public: // construction, destruction + explicit nsDataObj(nsIURI* uri = nullptr); + + protected: + virtual ~nsDataObj(); + + public: // IUnknown methods - see iunknown.h for documentation + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP QueryInterface(REFIID, void**) override; + STDMETHODIMP_(ULONG) Release() override; + + // support for clipboard + virtual void AddDataFlavor(const char* aDataFlavor, LPFORMATETC aFE); + void SetTransferable(nsITransferable* aTransferable); + + public: // IDataObject methods - these are general comments. see CfDragDrop + // for overriding behavior + // Store data in pSTM according to the format specified by pFE, if the + // format is supported (supported formats are specified in CfDragDrop:: + // GetFormats) and return NOERROR; otherwise return DATA_E_FORMATETC. It + // is the callers responsibility to free pSTM if NOERROR is returned. + STDMETHODIMP GetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM) override; + + // Similar to GetData except that the caller allocates the structure + // referenced by pSTM. + STDMETHODIMP GetDataHere(LPFORMATETC pFE, LPSTGMEDIUM pSTM) override; + + // Returns S_TRUE if this object supports the format specified by pSTM, + // S_FALSE otherwise. + STDMETHODIMP QueryGetData(LPFORMATETC pFE) override; + + // Set pCanonFE to the canonical format of pFE if one exists and return + // NOERROR, otherwise return DATA_S_SAMEFORMATETC. A canonical format + // implies an identical rendering. + STDMETHODIMP GetCanonicalFormatEtc(LPFORMATETC pFE, + LPFORMATETC pCanonFE) final; + + // Set this objects data according to the format specified by pFE and + // the storage medium specified by pSTM and return NOERROR, if the format + // is supported. If release is TRUE this object must release the storage + // associated with pSTM. + STDMETHODIMP SetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM, + BOOL release) override; + + // Set ppEnum to an IEnumFORMATETC object which will iterate all of the + // data formats that this object supports. direction is either DATADIR_GET + // or DATADIR_SET. + STDMETHODIMP EnumFormatEtc(DWORD direction, LPENUMFORMATETC* ppEnum) final; + + // Set up an advisory connection to this object based on the format specified + // by pFE, flags, and the pAdvise. Set pConn to the established advise + // connection. + STDMETHODIMP DAdvise(LPFORMATETC pFE, DWORD flags, LPADVISESINK pAdvise, + DWORD* pConn) final; + + // Turn off advising of a previous call to DAdvise which set pConn. + STDMETHODIMP DUnadvise(DWORD pConn) final; + + // Set ppEnum to an IEnumSTATDATA object which will iterate over the + // existing objects which have established advisory connections to this + // object. + STDMETHODIMP EnumDAdvise(LPENUMSTATDATA* ppEnum) final; + + // IDataObjectAsyncCapability methods + STDMETHODIMP EndOperation(HRESULT hResult, IBindCtx* pbcReserved, + DWORD dwEffects) final; + STDMETHODIMP GetAsyncMode(BOOL* pfIsOpAsync) final; + STDMETHODIMP InOperation(BOOL* pfInAsyncOp) final; + STDMETHODIMP SetAsyncMode(BOOL fDoOpAsync) final; + STDMETHODIMP StartOperation(IBindCtx* pbcReserved) final; + + private: // other methods + // Gets the filename from the kFilePromiseURLMime flavour + HRESULT GetDownloadDetails(nsIURI** aSourceURI, nsAString& aFilename); + + // help determine the kind of drag + bool IsFlavourPresent(const char* inFlavour); + + protected: + HRESULT GetFile(FORMATETC& aFE, STGMEDIUM& aSTG); + HRESULT GetText(const nsACString& aDF, FORMATETC& aFE, STGMEDIUM& aSTG); + + private: + HRESULT GetDib(const nsACString& inFlavor, FORMATETC&, STGMEDIUM& aSTG); + + HRESULT DropImage(FORMATETC& aFE, STGMEDIUM& aSTG); + HRESULT DropFile(FORMATETC& aFE, STGMEDIUM& aSTG); + HRESULT DropTempFile(FORMATETC& aFE, STGMEDIUM& aSTG); + + HRESULT GetUniformResourceLocator(FORMATETC& aFE, STGMEDIUM& aSTG, + bool aIsUnicode); + HRESULT ExtractUniformResourceLocatorA(FORMATETC& aFE, STGMEDIUM& aSTG); + HRESULT ExtractUniformResourceLocatorW(FORMATETC& aFE, STGMEDIUM& aSTG); + HRESULT GetFileDescriptor(FORMATETC& aFE, STGMEDIUM& aSTG, bool aIsUnicode); + + protected: + HRESULT GetFileContents(FORMATETC& aFE, STGMEDIUM& aSTG); + + private: + HRESULT GetPreferredDropEffect(FORMATETC& aFE, STGMEDIUM& aSTG); + + // Provide the structures needed for an internet shortcut by the shell + HRESULT GetFileDescriptorInternetShortcutA(FORMATETC& aFE, STGMEDIUM& aSTG); + HRESULT GetFileDescriptorInternetShortcutW(FORMATETC& aFE, STGMEDIUM& aSTG); + HRESULT GetFileContentsInternetShortcut(FORMATETC& aFE, STGMEDIUM& aSTG); + + // IStream implementation + HRESULT GetFileDescriptor_IStreamA(FORMATETC& aFE, STGMEDIUM& aSTG); + HRESULT GetFileDescriptor_IStreamW(FORMATETC& aFE, STGMEDIUM& aSTG); + HRESULT GetFileContents_IStream(FORMATETC& aFE, STGMEDIUM& aSTG); + + nsresult ExtractShortcutURL(nsString& outURL); + nsresult ExtractShortcutTitle(nsString& outTitle); + + // munge our HTML data to win32's CF_HTML spec. Will null terminate + nsresult BuildPlatformHTML(const char* inOurHTML, char** outPlatformHTML); + + // Used for the SourceURL part of CF_HTML + nsCString mSourceURL; + + protected: + BOOL FormatsMatch(const FORMATETC& source, const FORMATETC& target) const; + + ULONG m_cRef; // the reference count + + private: + nsTArray<nsCString> mDataFlavors; + + nsITransferable* mTransferable; // nsDataObj owns and ref counts + // nsITransferable, the nsITransferable does + // know anything about the nsDataObj + + protected: + CEnumFormatEtc* m_enumFE; // Ownership Rules: + // nsDataObj owns and ref counts CEnumFormatEtc, + + private: + nsCOMPtr<nsIFile> mCachedTempFile; + RefPtr<nsDataObj> mKeepAlive; + + BOOL mIsAsyncMode; + BOOL mIsInOperation; + /////////////////////////////////////////////////////////////////////////////// + // CStream class implementation + // this class is used in Drag and drop with download sample + // called from IDataObject::GetData + class CStreamBase : public IStream { + // IStream + STDMETHODIMP Clone(IStream** ppStream) final; + STDMETHODIMP Commit(DWORD dwFrags) final; + STDMETHODIMP CopyTo(IStream* pDestStream, ULARGE_INTEGER nBytesToCopy, + ULARGE_INTEGER* nBytesRead, + ULARGE_INTEGER* nBytesWritten) final; + STDMETHODIMP LockRegion(ULARGE_INTEGER nStart, ULARGE_INTEGER nBytes, + DWORD dwFlags) final; + STDMETHODIMP Revert(void) final; + STDMETHODIMP Seek(LARGE_INTEGER nMove, DWORD dwOrigin, + ULARGE_INTEGER* nNewPos) final; + STDMETHODIMP SetSize(ULARGE_INTEGER nNewSize) final; + STDMETHODIMP UnlockRegion(ULARGE_INTEGER nStart, ULARGE_INTEGER nBytes, + DWORD dwFlags) final; + STDMETHODIMP Write(const void* pvBuffer, ULONG nBytesToRead, + ULONG* nBytesRead) final; + + protected: + uint32_t mStreamRead; + + CStreamBase(); + virtual ~CStreamBase(); + }; + + class CStream final : public CStreamBase, public nsIStreamListener { + nsCOMPtr<nsIChannel> mChannel; + FallibleTArray<uint8_t> mChannelData; + nsresult mChannelResult; + bool mChannelRead; + + virtual ~CStream(); + nsresult WaitForCompletion(); + + // IUnknown + STDMETHOD(QueryInterface)(REFIID refiid, void** ppvResult) final; + + // IStream + STDMETHODIMP Read(void* pvBuffer, ULONG nBytesToRead, + ULONG* nBytesRead) final; + STDMETHODIMP Stat(STATSTG* statstg, DWORD dwFlags) final; + + public: + CStream(); + nsresult Init(nsIURI* pSourceURI, nsContentPolicyType aContentPolicyType, + nsIPrincipal* aRequestingPrincipal, + nsICookieJarSettings* aCookieJarSettings, + nsIReferrerInfo* aReferrerInfo); + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + }; + + HRESULT CreateStream(IStream** outStream); + + // This class must be thread-safe. + class AutoCloseEvent final { + const nsAutoHandle mEvent; + + AutoCloseEvent(const AutoCloseEvent&) = delete; + void operator=(const AutoCloseEvent&) = delete; + ~AutoCloseEvent() = default; + + public: + AutoCloseEvent(); + bool IsInited() const; + void Signal() const; + DWORD Wait(DWORD aMillisec) const; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AutoCloseEvent) + }; + + // This class must be thread-safe. + class AutoSetEvent final { + const RefPtr<AutoCloseEvent> mEvent; + + AutoSetEvent(const AutoSetEvent&) = delete; + void operator=(const AutoSetEvent&) = delete; + ~AutoSetEvent(); + + public: + explicit AutoSetEvent(mozilla::NotNull<AutoCloseEvent*> aEvent); + void Signal() const; + bool IsWaiting() const; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AutoSetEvent) + }; + + // This class must be thread-safe. + class CMemStream final : public CStreamBase { + static mozilla::glue::Win32SRWLock mLock; + const nsAutoGlobalMem mGlobalMem; + const RefPtr<AutoCloseEvent> mEvent; + const uint32_t mTotalLength; + RefPtr<IUnknown> mMarshaler; + + virtual ~CMemStream(); + void WaitForCompletion(); + + // IStream + STDMETHODIMP Read(void* pvBuffer, ULONG nBytesToRead, + ULONG* nBytesRead) final; + STDMETHODIMP Stat(STATSTG* statstg, DWORD dwFlags) final; + + public: + CMemStream(nsHGLOBAL aGlobalMem, uint32_t mTotalLength, + already_AddRefed<AutoCloseEvent> aEvent); + + // IUnknown + STDMETHOD(QueryInterface)(REFIID refiid, void** ppvResult) final; + NS_INLINE_DECL_THREADSAFE_VIRTUAL_REFCOUNTING(CMemStream, final) + }; + + private: + // Drag and drop helper data for implementing drag and drop image support + typedef struct { + FORMATETC fe; + STGMEDIUM stgm; + } DATAENTRY, *LPDATAENTRY; + + nsTArray<LPDATAENTRY> mDataEntryList; + nsCOMPtr<nsITimer> mTimer; + + bool LookupArbitraryFormat(FORMATETC* aFormat, LPDATAENTRY* aDataEntry, + BOOL aAddorUpdate); + bool CopyMediumData(STGMEDIUM* aMediumDst, STGMEDIUM* aMediumSrc, + LPFORMATETC aFormat, BOOL aSetData); +}; + +#endif // _NSDATAOBJ_H_ diff --git a/widget/windows/nsDataObjCollection.cpp b/widget/windows/nsDataObjCollection.cpp new file mode 100644 index 0000000000..8750563602 --- /dev/null +++ b/widget/windows/nsDataObjCollection.cpp @@ -0,0 +1,370 @@ +/* -*- 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 <shlobj.h> + +#include "nsDataObjCollection.h" +#include "nsClipboard.h" +#include "IEnumFE.h" + +#include <ole2.h> + +// {25589C3E-1FAC-47b9-BF43-CAEA89B79533} +const IID IID_IDataObjCollection = { + 0x25589c3e, + 0x1fac, + 0x47b9, + {0xbf, 0x43, 0xca, 0xea, 0x89, 0xb7, 0x95, 0x33}}; + +/* + * Class nsDataObjCollection + */ + +nsDataObjCollection::nsDataObjCollection() {} + +nsDataObjCollection::~nsDataObjCollection() { mDataObjects.Clear(); } + +// IUnknown interface methods - see iunknown.h for documentation +STDMETHODIMP nsDataObjCollection::QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + + if ((IID_IUnknown == riid) || (IID_IDataObject == riid)) { + *ppv = static_cast<IDataObject*>(this); + AddRef(); + return NOERROR; + } + + if (IID_IDataObjCollection == riid) { + *ppv = static_cast<nsIDataObjCollection*>(this); + AddRef(); + return NOERROR; + } + // offer to operate asynchronously (required by nsDragService) + if (IID_IDataObjectAsyncCapability == riid) { + *ppv = static_cast<IDataObjectAsyncCapability*>(this); + AddRef(); + return NOERROR; + } + + return E_NOINTERFACE; +} + +STDMETHODIMP_(ULONG) nsDataObjCollection::AddRef() { return ++m_cRef; } + +STDMETHODIMP_(ULONG) nsDataObjCollection::Release() { + if (0 != --m_cRef) return m_cRef; + + delete this; + + return 0; +} + +// IDataObject methods +STDMETHODIMP nsDataObjCollection::GetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM) { + static CLIPFORMAT fileDescriptorFlavorA = + ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA); + static CLIPFORMAT fileDescriptorFlavorW = + ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + static CLIPFORMAT fileFlavor = ::RegisterClipboardFormat(CFSTR_FILECONTENTS); + + switch (pFE->cfFormat) { + case CF_TEXT: + case CF_UNICODETEXT: + return GetText(pFE, pSTM); + case CF_HDROP: + return GetFile(pFE, pSTM); + default: + if (pFE->cfFormat == fileDescriptorFlavorA || + pFE->cfFormat == fileDescriptorFlavorW) { + return GetFileDescriptors(pFE, pSTM); + } + if (pFE->cfFormat == fileFlavor) { + return GetFileContents(pFE, pSTM); + } + } + return GetFirstSupporting(pFE, pSTM); +} + +STDMETHODIMP nsDataObjCollection::GetDataHere(LPFORMATETC pFE, + LPSTGMEDIUM pSTM) { + return E_FAIL; +} + +// Other objects querying to see if we support a particular format +STDMETHODIMP nsDataObjCollection::QueryGetData(LPFORMATETC pFE) { + UINT format = nsClipboard::GetFormat(MULTI_MIME); + + if (format == pFE->cfFormat) { + return S_OK; + } + + for (uint32_t i = 0; i < mDataObjects.Length(); ++i) { + IDataObject* dataObj = mDataObjects.ElementAt(i); + if (S_OK == dataObj->QueryGetData(pFE)) { + return S_OK; + } + } + + return DV_E_FORMATETC; +} + +STDMETHODIMP nsDataObjCollection::SetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM, + BOOL fRelease) { + // Set arbitrary data formats on the first object in the collection and let + // it handle the heavy lifting + if (mDataObjects.Length() == 0) return E_FAIL; + return mDataObjects.ElementAt(0)->SetData(pFE, pSTM, fRelease); +} + +// Registers a DataFlavor/FE pair +void nsDataObjCollection::AddDataFlavor(const char* aDataFlavor, + LPFORMATETC aFE) { + // Add the FormatEtc to our list if it's not already there. We don't care + // about the internal aDataFlavor because nsDataObj handles that. + IEnumFORMATETC* ifEtc; + FORMATETC fEtc; + ULONG num; + if (S_OK != this->EnumFormatEtc(DATADIR_GET, &ifEtc)) return; + while (S_OK == ifEtc->Next(1, &fEtc, &num)) { + NS_ASSERTION( + 1 == num, + "Bit off more than we can chew in nsDataObjCollection::AddDataFlavor"); + if (FormatsMatch(fEtc, *aFE)) { + ifEtc->Release(); + return; + } + } // If we didn't find a matching format, add this one + ifEtc->Release(); + m_enumFE->AddFormatEtc(aFE); +} + +// We accept ownership of the nsDataObj which we free on destruction +void nsDataObjCollection::AddDataObject(IDataObject* aDataObj) { + nsDataObj* dataObj = reinterpret_cast<nsDataObj*>(aDataObj); + mDataObjects.AppendElement(dataObj); +} + +// Methods for getting data +HRESULT nsDataObjCollection::GetFile(LPFORMATETC pFE, LPSTGMEDIUM pSTM) { + STGMEDIUM workingmedium; + FORMATETC fe = *pFE; + HGLOBAL hGlobalMemory; + HRESULT hr; + // Make enough space for the header and the trailing null + uint32_t buffersize = sizeof(DROPFILES) + sizeof(char16_t); + uint32_t alloclen = 0; + char16_t* realbuffer; + nsAutoString filename; + + hGlobalMemory = GlobalAlloc(GHND, buffersize); + + for (uint32_t i = 0; i < mDataObjects.Length(); ++i) { + nsDataObj* dataObj = mDataObjects.ElementAt(i); + hr = dataObj->GetData(&fe, &workingmedium); + if (hr != S_OK) { + switch (hr) { + case DV_E_FORMATETC: + continue; + default: + return hr; + } + } + // Now we need to pull out the filename + char16_t* buffer = (char16_t*)GlobalLock(workingmedium.hGlobal); + if (buffer == nullptr) return E_FAIL; + buffer += sizeof(DROPFILES) / sizeof(char16_t); + filename = buffer; + GlobalUnlock(workingmedium.hGlobal); + ReleaseStgMedium(&workingmedium); + // Now put the filename into our buffer + alloclen = (filename.Length() + 1) * sizeof(char16_t); + hGlobalMemory = ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, GHND); + if (hGlobalMemory == nullptr) return E_FAIL; + realbuffer = (char16_t*)((char*)GlobalLock(hGlobalMemory) + buffersize); + if (!realbuffer) return E_FAIL; + realbuffer--; // Overwrite the preceding null + memcpy(realbuffer, filename.get(), alloclen); + GlobalUnlock(hGlobalMemory); + buffersize += alloclen; + } + // We get the last null (on the double null terminator) for free since we used + // the zero memory flag when we allocated. All we need to do is fill the + // DROPFILES structure + DROPFILES* df = (DROPFILES*)GlobalLock(hGlobalMemory); + if (!df) return E_FAIL; + df->pFiles = sizeof(DROPFILES); // Offset to start of file name string + df->fNC = 0; + df->pt.x = 0; + df->pt.y = 0; + df->fWide = TRUE; // utf-16 chars + GlobalUnlock(hGlobalMemory); + // Finally fill out the STGMEDIUM struct + pSTM->tymed = TYMED_HGLOBAL; + pSTM->pUnkForRelease = nullptr; // Caller gets to free the data + pSTM->hGlobal = hGlobalMemory; + return S_OK; +} + +HRESULT nsDataObjCollection::GetText(LPFORMATETC pFE, LPSTGMEDIUM pSTM) { + STGMEDIUM workingmedium; + FORMATETC fe = *pFE; + HGLOBAL hGlobalMemory; + HRESULT hr; + uint32_t buffersize = 1; + uint32_t alloclen = 0; + + hGlobalMemory = GlobalAlloc(GHND, buffersize); + + if (pFE->cfFormat == CF_TEXT) { + nsAutoCString text; + for (uint32_t i = 0; i < mDataObjects.Length(); ++i) { + nsDataObj* dataObj = mDataObjects.ElementAt(i); + hr = dataObj->GetData(&fe, &workingmedium); + if (hr != S_OK) { + switch (hr) { + case DV_E_FORMATETC: + continue; + default: + return hr; + } + } + // Now we need to pull out the text + char* buffer = (char*)GlobalLock(workingmedium.hGlobal); + if (buffer == nullptr) return E_FAIL; + text = buffer; + GlobalUnlock(workingmedium.hGlobal); + ReleaseStgMedium(&workingmedium); + // Now put the text into our buffer + alloclen = text.Length(); + hGlobalMemory = + ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, GHND); + if (hGlobalMemory == nullptr) return E_FAIL; + buffer = ((char*)GlobalLock(hGlobalMemory) + buffersize); + if (!buffer) return E_FAIL; + buffer--; // Overwrite the preceding null + memcpy(buffer, text.get(), alloclen); + GlobalUnlock(hGlobalMemory); + buffersize += alloclen; + } + pSTM->tymed = TYMED_HGLOBAL; + pSTM->pUnkForRelease = nullptr; // Caller gets to free the data + pSTM->hGlobal = hGlobalMemory; + return S_OK; + } + if (pFE->cfFormat == CF_UNICODETEXT) { + buffersize = sizeof(char16_t); + nsAutoString text; + for (uint32_t i = 0; i < mDataObjects.Length(); ++i) { + nsDataObj* dataObj = mDataObjects.ElementAt(i); + hr = dataObj->GetData(&fe, &workingmedium); + if (hr != S_OK) { + switch (hr) { + case DV_E_FORMATETC: + continue; + default: + return hr; + } + } + // Now we need to pull out the text + char16_t* buffer = (char16_t*)GlobalLock(workingmedium.hGlobal); + if (buffer == nullptr) return E_FAIL; + text = buffer; + GlobalUnlock(workingmedium.hGlobal); + ReleaseStgMedium(&workingmedium); + // Now put the text into our buffer + alloclen = text.Length() * sizeof(char16_t); + hGlobalMemory = + ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, GHND); + if (hGlobalMemory == nullptr) return E_FAIL; + buffer = (char16_t*)((char*)GlobalLock(hGlobalMemory) + buffersize); + if (!buffer) return E_FAIL; + buffer--; // Overwrite the preceding null + memcpy(buffer, text.get(), alloclen); + GlobalUnlock(hGlobalMemory); + buffersize += alloclen; + } + pSTM->tymed = TYMED_HGLOBAL; + pSTM->pUnkForRelease = nullptr; // Caller gets to free the data + pSTM->hGlobal = hGlobalMemory; + return S_OK; + } + + return E_FAIL; +} + +HRESULT nsDataObjCollection::GetFileDescriptors(LPFORMATETC pFE, + LPSTGMEDIUM pSTM) { + STGMEDIUM workingmedium; + FORMATETC fe = *pFE; + HGLOBAL hGlobalMemory; + HRESULT hr; + uint32_t buffersize = sizeof(UINT); + uint32_t alloclen = sizeof(FILEDESCRIPTOR); + + hGlobalMemory = GlobalAlloc(GHND, buffersize); + + for (uint32_t i = 0; i < mDataObjects.Length(); ++i) { + nsDataObj* dataObj = mDataObjects.ElementAt(i); + hr = dataObj->GetData(&fe, &workingmedium); + if (hr != S_OK) { + switch (hr) { + case DV_E_FORMATETC: + continue; + default: + return hr; + } + } + // Now we need to pull out the filedescriptor + FILEDESCRIPTOR* buffer = + (FILEDESCRIPTOR*)((char*)GlobalLock(workingmedium.hGlobal) + + sizeof(UINT)); + if (buffer == nullptr) return E_FAIL; + hGlobalMemory = ::GlobalReAlloc(hGlobalMemory, buffersize + alloclen, GHND); + if (hGlobalMemory == nullptr) return E_FAIL; + FILEGROUPDESCRIPTOR* realbuffer = + (FILEGROUPDESCRIPTOR*)GlobalLock(hGlobalMemory); + if (!realbuffer) return E_FAIL; + FILEDESCRIPTOR* copyloc = (FILEDESCRIPTOR*)((char*)realbuffer + buffersize); + memcpy(copyloc, buffer, alloclen); + realbuffer->cItems++; + GlobalUnlock(hGlobalMemory); + GlobalUnlock(workingmedium.hGlobal); + ReleaseStgMedium(&workingmedium); + buffersize += alloclen; + } + pSTM->tymed = TYMED_HGLOBAL; + pSTM->pUnkForRelease = nullptr; // Caller gets to free the data + pSTM->hGlobal = hGlobalMemory; + return S_OK; +} + +HRESULT nsDataObjCollection::GetFileContents(LPFORMATETC pFE, + LPSTGMEDIUM pSTM) { + ULONG num = 0; + ULONG numwanted = (pFE->lindex == -1) ? 0 : pFE->lindex; + FORMATETC fEtc = *pFE; + fEtc.lindex = -1; // We're lying to the data object so it thinks it's alone + + // The key for this data type is to figure out which data object the index + // corresponds to and then just pass it along + for (uint32_t i = 0; i < mDataObjects.Length(); ++i) { + nsDataObj* dataObj = mDataObjects.ElementAt(i); + if (dataObj->QueryGetData(&fEtc) != S_OK) continue; + if (num == numwanted) return dataObj->GetData(pFE, pSTM); + num++; + } + return DV_E_LINDEX; +} + +HRESULT nsDataObjCollection::GetFirstSupporting(LPFORMATETC pFE, + LPSTGMEDIUM pSTM) { + // There is no way to pass more than one of this, so just find the first data + // object that supports it and pass it along + for (uint32_t i = 0; i < mDataObjects.Length(); ++i) { + if (mDataObjects.ElementAt(i)->QueryGetData(pFE) == S_OK) + return mDataObjects.ElementAt(i)->GetData(pFE, pSTM); + } + return DV_E_FORMATETC; +} diff --git a/widget/windows/nsDataObjCollection.h b/widget/windows/nsDataObjCollection.h new file mode 100644 index 0000000000..02ec7e8916 --- /dev/null +++ b/widget/windows/nsDataObjCollection.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 _NSDATAOBJCOLLECTION_H_ +#define _NSDATAOBJCOLLECTION_H_ + +#include <oleidl.h> + +#include "mozilla/RefPtr.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsDataObj.h" +#include "mozilla/Attributes.h" + +#define MULTI_MIME "Mozilla/IDataObjectCollectionFormat" + +EXTERN_C const IID IID_IDataObjCollection; + +// An interface to make sure we have the right kind of object for D&D +// this way we can filter out collection objects that aren't ours +class nsIDataObjCollection : public IUnknown { + public: +}; + +/* + * This ole registered class is used to facilitate drag-drop of objects which + * can be adapted by an object derived from CfDragDrop. The CfDragDrop is + * associated with instances via SetDragDrop(). + */ + +class nsDataObjCollection final : public nsIDataObjCollection, + public nsDataObj { + public: + nsDataObjCollection(); + + private: + ~nsDataObjCollection() final; + + public: // IUnknown methods - see iunknown.h for documentation + STDMETHODIMP_(ULONG) AddRef() final; + STDMETHODIMP QueryInterface(REFIID, void**) final; + STDMETHODIMP_(ULONG) Release() final; + + private: // DataGet and DataSet helper methods + HRESULT GetFile(LPFORMATETC pFE, LPSTGMEDIUM pSTM); + HRESULT GetText(LPFORMATETC pFE, LPSTGMEDIUM pSTM); + HRESULT GetFileDescriptors(LPFORMATETC pFE, LPSTGMEDIUM pSTM); + HRESULT GetFileContents(LPFORMATETC pFE, LPSTGMEDIUM pSTM); + HRESULT GetFirstSupporting(LPFORMATETC pFE, LPSTGMEDIUM pSTM); + + using nsDataObj::GetFile; + using nsDataObj::GetFileContents; + using nsDataObj::GetText; + + // support for clipboard + void AddDataFlavor(const char* aDataFlavor, LPFORMATETC aFE) final; + + public: // from nsPIDataObjCollection + void AddDataObject(IDataObject* aDataObj); + int32_t GetNumDataObjects() { return mDataObjects.Length(); } + nsDataObj* GetDataObjectAt(uint32_t aItem) { + return mDataObjects.SafeElementAt(aItem, RefPtr<nsDataObj>()); + } + + public: + // Store data in pSTM according to the format specified by pFE, if the + // format is supported (supported formats are specified in CfDragDrop:: + // GetFormats) and return NOERROR; otherwise return DATA_E_FORMATETC. It + // is the callers responsibility to free pSTM if NOERROR is returned. + STDMETHODIMP GetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM) final; + + // Similar to GetData except that the caller allocates the structure + // referenced by pSTM. + STDMETHODIMP GetDataHere(LPFORMATETC pFE, LPSTGMEDIUM pSTM) final; + + // Returns S_TRUE if this object supports the format specified by pSTM, + // S_FALSE otherwise. + STDMETHODIMP QueryGetData(LPFORMATETC pFE) final; + + // Set this objects data according to the format specified by pFE and + // the storage medium specified by pSTM and return NOERROR, if the format + // is supported. If release is TRUE this object must release the storage + // associated with pSTM. + STDMETHODIMP SetData(LPFORMATETC pFE, LPSTGMEDIUM pSTM, BOOL release) final; + + private: + nsTArray<RefPtr<nsDataObj> > mDataObjects; +}; + +#endif // diff --git a/widget/windows/nsDeviceContextSpecWin.cpp b/widget/windows/nsDeviceContextSpecWin.cpp new file mode 100644 index 0000000000..ac3bf6f6ed --- /dev/null +++ b/widget/windows/nsDeviceContextSpecWin.cpp @@ -0,0 +1,680 @@ +/* -*- 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 "nsDeviceContextSpecWin.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/gfx/PrintPromise.h" +#include "mozilla/gfx/PrintTargetPDF.h" +#include "mozilla/gfx/PrintTargetWindows.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Telemetry.h" +#include "nsAnonymousTemporaryFile.h" + +#include <wchar.h> +#include <windef.h> +#include <winspool.h> + +#include "nsIWidget.h" + +#include "nsTArray.h" +#include "nsIPrintSettingsWin.h" + +#include "nsComponentManagerUtils.h" +#include "nsPrinterWin.h" +#include "nsReadableUtils.h" +#include "nsString.h" + +#include "gfxWindowsSurface.h" + +#include "nsIFileStreams.h" +#include "nsWindowsHelpers.h" + +#include "mozilla/gfx/Logging.h" + +#ifdef MOZ_ENABLE_SKIA_PDF +# include "mozilla/gfx/PrintTargetSkPDF.h" +# include "mozilla/gfx/PrintTargetEMF.h" +# include "nsIUUIDGenerator.h" +# include "nsDirectoryServiceDefs.h" +# include "nsPrintfCString.h" +# include "nsThreadUtils.h" +#endif + +extern mozilla::LazyLogModule gPrintingLog; +#define PR_PL(_p1) MOZ_LOG(gPrintingLog, mozilla::LogLevel::Debug, _p1) + +using namespace mozilla; +using namespace mozilla::gfx; + +#ifdef MOZ_ENABLE_SKIA_PDF +using namespace mozilla::widget; +#endif + +static const wchar_t kDriverName[] = L"WINSPOOL"; + +//---------------------------------------------------------------------------------- +//--------------- +// static members +//---------------------------------------------------------------------------------- +nsDeviceContextSpecWin::nsDeviceContextSpecWin() = default; + +//---------------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsDeviceContextSpecWin, nsIDeviceContextSpec) + +nsDeviceContextSpecWin::~nsDeviceContextSpecWin() { + SetDevMode(nullptr); + + if (mTempFile) { + mTempFile->Remove(/* recursive = */ false); + } + + if (nsCOMPtr<nsIPrintSettingsWin> ps = do_QueryInterface(mPrintSettings)) { + ps->SetDeviceName(u""_ns); + ps->SetDriverName(u""_ns); + ps->SetDevMode(nullptr); + } +} + +static bool GetDefaultPrinterName(nsAString& aDefaultPrinterName) { + DWORD length = 0; + GetDefaultPrinterW(nullptr, &length); + + if (length) { + aDefaultPrinterName.SetLength(length); + if (GetDefaultPrinterW((LPWSTR)aDefaultPrinterName.BeginWriting(), + &length)) { + // `length` includes the terminating null, so we subtract that from our + // string length. + aDefaultPrinterName.SetLength(length - 1); + PR_PL(("DEFAULT PRINTER [%s]\n", + NS_ConvertUTF16toUTF8(aDefaultPrinterName).get())); + return true; + } + } + + aDefaultPrinterName.Truncate(); + PR_PL(("NO DEFAULT PRINTER\n")); + return false; +} + +//---------------------------------------------------------------------------------- +NS_IMETHODIMP nsDeviceContextSpecWin::Init(nsIPrintSettings* aPrintSettings, + bool aIsPrintPreview) { + mPrintSettings = aPrintSettings; + + // Get the Printer Name to be used and output format. + nsAutoString printerName; + if (mPrintSettings) { + mOutputFormat = mPrintSettings->GetOutputFormat(); + mPrintSettings->GetPrinterName(printerName); + } + + // If there is no name then use the default printer + if (printerName.IsEmpty()) { + GetDefaultPrinterName(printerName); + } + + // Gather telemetry on the print target type. + // + // Unfortunately, if we're not using our own internal save-to-pdf codepaths, + // there isn't a good way to determine whether a print is going to be to a + // physical printer or to a file or some other non-physical output. We do our + // best by checking for what seems to be the most common save-to-PDF virtual + // printers. + // + // We use StringBeginsWith below, since printer names are often followed by a + // version number or other product differentiating string. (True for doPDF, + // novaPDF, PDF-XChange and Soda PDF, for example.) + if (mOutputFormat == nsIPrintSettings::kOutputFormatPDF) { + Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE, + u"pdf_file"_ns, 1); + } else if (StringBeginsWith(printerName, u"Microsoft Print to PDF"_ns) || + StringBeginsWith(printerName, u"Adobe PDF"_ns) || + StringBeginsWith(printerName, u"Bullzip PDF Printer"_ns) || + StringBeginsWith(printerName, u"CutePDF Writer"_ns) || + StringBeginsWith(printerName, u"doPDF"_ns) || + StringBeginsWith(printerName, u"Foxit Reader PDF Printer"_ns) || + StringBeginsWith(printerName, u"Nitro PDF Creator"_ns) || + StringBeginsWith(printerName, u"novaPDF"_ns) || + StringBeginsWith(printerName, u"PDF-XChange"_ns) || + StringBeginsWith(printerName, u"PDF24 PDF"_ns) || + StringBeginsWith(printerName, u"PDFCreator"_ns) || + StringBeginsWith(printerName, u"PrimoPDF"_ns) || + StringBeginsWith(printerName, u"Soda PDF"_ns) || + StringBeginsWith(printerName, u"Solid PDF Creator"_ns) || + StringBeginsWith(printerName, + u"Universal Document Converter"_ns)) { + Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE, + u"pdf_file"_ns, 1); + } else if (printerName.EqualsLiteral("Microsoft XPS Document Writer")) { + Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE, + u"xps_file"_ns, 1); + } else { + nsAString::const_iterator start, end; + printerName.BeginReading(start); + printerName.EndReading(end); + if (CaseInsensitiveFindInReadable(u"pdf"_ns, start, end)) { + Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE, + u"pdf_unknown"_ns, 1); + } else { + Telemetry::ScalarAdd(Telemetry::ScalarID::PRINTING_TARGET_TYPE, + u"unknown"_ns, 1); + } + } + + nsresult rv = NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE; + if (aPrintSettings) { +#ifdef MOZ_ENABLE_SKIA_PDF + nsAutoString printViaPdf; + Preferences::GetString("print.print_via_pdf_encoder", printViaPdf); + if (printViaPdf.EqualsLiteral("skia-pdf")) { + mPrintViaSkPDF = true; + } +#endif + + // If we're in the child or we're printing to PDF we only need information + // from the print settings. + if (XRE_IsContentProcess() || + mOutputFormat == nsIPrintSettings::kOutputFormatPDF) { + return NS_OK; + } + + nsCOMPtr<nsIPrintSettingsWin> psWin(do_QueryInterface(aPrintSettings)); + if (psWin) { + nsAutoString deviceName; + nsAutoString driverName; + psWin->GetDeviceName(deviceName); + psWin->GetDriverName(driverName); + + LPDEVMODEW devMode; + psWin->GetDevMode(&devMode); // creates new memory (makes a copy) + + if (!deviceName.IsEmpty() && !driverName.IsEmpty() && devMode) { + // Scaling is special, it is one of the few + // devMode items that we control in layout + if (devMode->dmFields & DM_SCALE) { + double scale = double(devMode->dmScale) / 100.0f; + if (scale != 1.0) { + aPrintSettings->SetScaling(scale); + devMode->dmScale = 100; + } + } + + SetDeviceName(deviceName); + SetDriverName(driverName); + SetDevMode(devMode); + + return NS_OK; + } else { + PR_PL( + ("***** nsDeviceContextSpecWin::Init - " + "deviceName/driverName/devMode was NULL!\n")); + if (devMode) ::HeapFree(::GetProcessHeap(), 0, devMode); + } + } + } else { + PR_PL(("***** nsDeviceContextSpecWin::Init - aPrintSettingswas NULL!\n")); + } + + if (printerName.IsEmpty()) { + return rv; + } + + return GetDataFromPrinter(printerName, mPrintSettings); +} + +//---------------------------------------------------------- + +already_AddRefed<PrintTarget> nsDeviceContextSpecWin::MakePrintTarget() { + NS_ASSERTION(mDevMode || mOutputFormat == nsIPrintSettings::kOutputFormatPDF, + "DevMode can't be NULL here unless we're printing to PDF."); + +#ifdef MOZ_ENABLE_SKIA_PDF + if (mPrintViaSkPDF) { + double width, height; + mPrintSettings->GetEffectivePageSize(&width, &height); + if (width <= 0 || height <= 0) { + return nullptr; + } + + // convert twips to points + width /= TWIPS_PER_POINT_FLOAT; + height /= TWIPS_PER_POINT_FLOAT; + IntSize size = IntSize::Ceil(width, height); + + if (mOutputFormat == nsIPrintSettings::kOutputFormatPDF) { + nsString filename; + // TODO(dshin): + // - Does this handle bug 1659470? + // - Should this code path be enabled, we should use temporary files and + // then move the file in `EndDocument()`. + mPrintSettings->GetToFileName(filename); + + nsAutoCString printFile(NS_ConvertUTF16toUTF8(filename).get()); + auto skStream = MakeUnique<SkFILEWStream>(printFile.get()); + return PrintTargetSkPDF::CreateOrNull(std::move(skStream), size); + } + + if (mDevMode) { + NS_WARNING_ASSERTION(!mDriverName.IsEmpty(), "No driver!"); + HDC dc = + ::CreateDCW(mDriverName.get(), mDeviceName.get(), nullptr, mDevMode); + if (!dc) { + gfxCriticalError(gfxCriticalError::DefaultOptions(false)) + << "Failed to create device context in GetSurfaceForPrinter"; + return nullptr; + } + return PrintTargetEMF::CreateOrNull(dc, size); + } + } +#endif + + if (mOutputFormat == nsIPrintSettings::kOutputFormatPDF) { + double width, height; + mPrintSettings->GetEffectiveSheetSize(&width, &height); + if (width <= 0 || height <= 0) { + return nullptr; + } + + // convert twips to points + width /= TWIPS_PER_POINT_FLOAT; + height /= TWIPS_PER_POINT_FLOAT; + + auto stream = [&]() -> nsCOMPtr<nsIOutputStream> { + if (mPrintSettings->GetOutputDestination() == + nsIPrintSettings::kOutputDestinationStream) { + nsCOMPtr<nsIOutputStream> out; + mPrintSettings->GetOutputStream(getter_AddRefs(out)); + return out; + } + + // Even if the destination may be a named path, write to a temp file - + // this is consistent with behaviour of `PrintTarget` on other platforms. + nsCOMPtr<nsIFile> file; + nsresult rv = NS_OpenAnonymousTemporaryNsIFile(getter_AddRefs(mTempFile)); + file = mTempFile; + + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsCOMPtr<nsIFileOutputStream> stream = + do_CreateInstance("@mozilla.org/network/file-output-stream;1"); + if (NS_FAILED(stream->Init(file, -1, -1, 0))) { + return nullptr; + } + return stream; + }(); + + return PrintTargetPDF::CreateOrNull(stream, IntSize::Ceil(width, height)); + } + + if (mDevMode) { + NS_WARNING_ASSERTION(!mDriverName.IsEmpty(), "No driver!"); + HDC dc = + ::CreateDCW(mDriverName.get(), mDeviceName.get(), nullptr, mDevMode); + if (!dc) { + gfxCriticalError(gfxCriticalError::DefaultOptions(false)) + << "Failed to create device context in GetSurfaceForPrinter"; + return nullptr; + } + + // The PrintTargetWindows takes over ownership of this DC + return PrintTargetWindows::CreateOrNull(dc); + } + + return nullptr; +} + +RefPtr<PrintEndDocumentPromise> nsDeviceContextSpecWin::EndDocument() { + if (mPrintSettings->GetOutputDestination() != + nsIPrintSettings::kOutputDestinationFile || + mOutputFormat != nsIPrintSettings::kOutputFormatPDF) { + return PrintEndDocumentPromise::CreateAndResolve(true, __func__); + } + +#ifdef MOZ_ENABLE_SKIA_PDF + if (mPrintViaSkPDF) { + return PrintEndDocumentPromise::CreateAndResolve(true, __func__); + } +#endif + + MOZ_ASSERT(mTempFile, "No handle to temporary PDF file."); + + nsAutoString targetPath; + mPrintSettings->GetToFileName(targetPath); + + if (targetPath.IsEmpty()) { + return PrintEndDocumentPromise::CreateAndResolve(true, __func__); + } + + // We still need to move the file to its actual destination. + nsCOMPtr<nsIFile> destFile; + auto rv = NS_NewLocalFile(targetPath, false, getter_AddRefs(destFile)); + if (NS_FAILED(rv)) { + return PrintEndDocumentPromise::CreateAndReject(rv, __func__); + } + + return nsIDeviceContextSpec::EndDocumentAsync( + __func__, + [destFile = std::move(destFile), + tempFile = std::move(mTempFile)]() -> nsresult { + nsAutoString destLeafName; + MOZ_TRY(destFile->GetLeafName(destLeafName)); + + nsCOMPtr<nsIFile> destDir; + MOZ_TRY(destFile->GetParent(getter_AddRefs(destDir))); + + // This should be fine - Windows API calls usually prevent moving + // between different volumes (See Win32 API's `MOVEFILE_COPY_ALLOWED` + // flag), but we handle that down this call. + MOZ_TRY(tempFile->MoveTo(destDir, destLeafName)); + return NS_OK; + }); +} + +//---------------------------------------------------------------------------------- +void nsDeviceContextSpecWin::SetDeviceName(const nsAString& aDeviceName) { + mDeviceName = aDeviceName; +} + +//---------------------------------------------------------------------------------- +void nsDeviceContextSpecWin::SetDriverName(const nsAString& aDriverName) { + mDriverName = aDriverName; +} + +//---------------------------------------------------------------------------------- +void nsDeviceContextSpecWin::SetDevMode(LPDEVMODEW aDevMode) { + if (mDevMode) { + ::HeapFree(::GetProcessHeap(), 0, mDevMode); + } + + mDevMode = aDevMode; +} + +//------------------------------------------------------------------ +void nsDeviceContextSpecWin::GetDevMode(LPDEVMODEW& aDevMode) { + aDevMode = mDevMode; +} + +#define DISPLAY_LAST_ERROR + +//---------------------------------------------------------------------------------- +// Setup the object's data member with the selected printer's data +nsresult nsDeviceContextSpecWin::GetDataFromPrinter(const nsAString& aName, + nsIPrintSettings* aPS) { + nsresult rv = NS_ERROR_FAILURE; + + nsHPRINTER hPrinter = nullptr; + const nsString& flat = PromiseFlatString(aName); + wchar_t* name = + (wchar_t*)flat.get(); // Windows APIs use non-const name argument + + BOOL status = ::OpenPrinterW(name, &hPrinter, nullptr); + if (status) { + nsAutoPrinter autoPrinter(hPrinter); + + LPDEVMODEW pDevMode; + + // Allocate a buffer of the correct size. + LONG needed = + ::DocumentPropertiesW(nullptr, hPrinter, name, nullptr, nullptr, 0); + if (needed < 0) { + PR_PL( + ("**** nsDeviceContextSpecWin::GetDataFromPrinter - Couldn't get " + "size of DEVMODE using DocumentPropertiesW(pDeviceName = \"%s\"). " + "GetLastEror() = %08lx\n", + NS_ConvertUTF16toUTF8(aName).get(), GetLastError())); + return NS_ERROR_FAILURE; + } + + // Some drivers do not return the correct size for their DEVMODE, so we + // over-allocate to try and compensate. + // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1664530#c5) + needed *= 2; + pDevMode = + (LPDEVMODEW)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, needed); + if (!pDevMode) return NS_ERROR_FAILURE; + + // Get the default DevMode for the printer and modify it for our needs. + LONG ret = ::DocumentPropertiesW(nullptr, hPrinter, name, pDevMode, nullptr, + DM_OUT_BUFFER); + + if (ret == IDOK && aPS) { + nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(aPS); + MOZ_ASSERT(psWin); + psWin->CopyToNative(pDevMode); + // Sets back the changes we made to the DevMode into the Printer Driver + ret = ::DocumentPropertiesW(nullptr, hPrinter, name, pDevMode, pDevMode, + DM_IN_BUFFER | DM_OUT_BUFFER); + + // We need to copy the final DEVMODE settings back to our print settings, + // because they may have been set from invalid prefs. + if (ret == IDOK) { + // We need to get information from the device as well. + nsAutoHDC printerDC(::CreateICW(kDriverName, name, nullptr, pDevMode)); + if (NS_WARN_IF(!printerDC)) { + ::HeapFree(::GetProcessHeap(), 0, pDevMode); + return NS_ERROR_FAILURE; + } + + psWin->CopyFromNative(printerDC, pDevMode); + } + } + + if (ret != IDOK) { + ::HeapFree(::GetProcessHeap(), 0, pDevMode); + PR_PL( + ("***** nsDeviceContextSpecWin::GetDataFromPrinter - " + "DocumentProperties call failed code: %ld/0x%lx\n", + ret, ret)); + DISPLAY_LAST_ERROR + return NS_ERROR_FAILURE; + } + + SetDevMode( + pDevMode); // cache the pointer and takes responsibility for the memory + + SetDeviceName(aName); + + SetDriverName(nsDependentString(kDriverName)); + + rv = NS_OK; + } else { + rv = NS_ERROR_GFX_PRINTER_NAME_NOT_FOUND; + PR_PL( + ("***** nsDeviceContextSpecWin::GetDataFromPrinter - Couldn't open " + "printer: [%s]\n", + NS_ConvertUTF16toUTF8(aName).get())); + DISPLAY_LAST_ERROR + } + return rv; +} + +//*********************************************************** +// Printer List +//*********************************************************** + +nsPrinterListWin::~nsPrinterListWin() = default; + +// Helper to get the array of PRINTER_INFO_4 records from the OS into a +// caller-supplied byte array; returns the number of records present. +static unsigned GetPrinterInfo4(nsTArray<BYTE>& aBuffer) { + const DWORD kLevel = 4; + DWORD needed = 0; + DWORD count = 0; + const DWORD kFlags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS; + BOOL ok = ::EnumPrintersW(kFlags, + nullptr, // Name + kLevel, // Level + nullptr, // pPrinterEnum + 0, // cbBuf (buffer size) + &needed, // Bytes needed in buffer + &count); + if (needed > 0) { + if (!aBuffer.SetLength(needed, fallible)) { + return 0; + } + ok = ::EnumPrintersW(kFlags, nullptr, kLevel, aBuffer.Elements(), + aBuffer.Length(), &needed, &count); + } + if (!ok) { + return 0; + } + return count; +} + +nsTArray<nsPrinterListBase::PrinterInfo> nsPrinterListWin::Printers() const { + PR_PL(("nsPrinterListWin::Printers\n")); + + AutoTArray<BYTE, 1024> buffer; + unsigned count = GetPrinterInfo4(buffer); + + if (!count) { + PR_PL(("[No printers found]\n")); + return {}; + } + + const auto* printers = + reinterpret_cast<const _PRINTER_INFO_4W*>(buffer.Elements()); + nsTArray<PrinterInfo> list; + for (unsigned i = 0; i < count; i++) { + // For LOCAL printers, we check whether OpenPrinter succeeds, and omit + // them from the list if not. This avoids presenting printers that the + // user cannot actually use (e.g. due to Windows permissions). + // For NETWORK printers, this check may block for a long time (waiting for + // network timeout), so we skip it; if the user tries to access a printer + // that isn't available, we'll have to show an error later. + // (We always need to be able to handle an error, anyhow, as the printer + // could get disconnected after we've created the list, for example.) + bool isAvailable = false; + if (printers[i].Attributes & PRINTER_ATTRIBUTE_NETWORK) { + isAvailable = true; + } else if (printers[i].Attributes & PRINTER_ATTRIBUTE_LOCAL) { + HANDLE handle; + if (::OpenPrinterW(printers[i].pPrinterName, &handle, nullptr)) { + ::ClosePrinter(handle); + isAvailable = true; + } + } + if (isAvailable) { + list.AppendElement(PrinterInfo{nsString(printers[i].pPrinterName)}); + PR_PL(("Printer Name: %s\n", + NS_ConvertUTF16toUTF8(printers[i].pPrinterName).get())); + } + } + + if (list.IsEmpty()) { + PR_PL(("[No usable printers found]\n")); + return {}; + } + + list.Sort([](const PrinterInfo& a, const PrinterInfo& b) { + size_t len = std::min(a.mName.Length(), b.mName.Length()); + int result = CaseInsensitiveCompare(a.mName.BeginReading(), + b.mName.BeginReading(), len); + return result ? result : int(a.mName.Length()) - int(b.mName.Length()); + }); + + return list; +} + +Maybe<nsPrinterListBase::PrinterInfo> nsPrinterListWin::PrinterByName( + nsString aName) const { + Maybe<PrinterInfo> rv; + + AutoTArray<BYTE, 1024> buffer; + unsigned count = GetPrinterInfo4(buffer); + + const auto* printers = + reinterpret_cast<const _PRINTER_INFO_4W*>(buffer.Elements()); + for (unsigned i = 0; i < count; ++i) { + if (aName.Equals(nsString(printers[i].pPrinterName))) { + rv.emplace(PrinterInfo{aName}); + break; + } + } + + return rv; +} + +Maybe<nsPrinterListBase::PrinterInfo> nsPrinterListWin::PrinterBySystemName( + nsString aName) const { + return PrinterByName(std::move(aName)); +} + +RefPtr<nsIPrinter> nsPrinterListWin::CreatePrinter(PrinterInfo aInfo) const { + return nsPrinterWin::Create(mCommonPaperInfo, std::move(aInfo.mName)); +} + +nsresult nsPrinterListWin::SystemDefaultPrinterName(nsAString& aName) const { + if (!GetDefaultPrinterName(aName)) { + NS_WARNING("Uh oh, GetDefaultPrinterName failed"); + // Indicate failure by leaving aName untouched, i.e. the empty string. + } + return NS_OK; +} + +NS_IMETHODIMP +nsPrinterListWin::InitPrintSettingsFromPrinter( + const nsAString& aPrinterName, nsIPrintSettings* aPrintSettings) { + NS_ENSURE_ARG_POINTER(aPrintSettings); + + if (aPrinterName.IsEmpty()) { + return NS_OK; + } + + // When printing to PDF on Windows there is no associated printer driver. + int16_t outputFormat = aPrintSettings->GetOutputFormat(); + if (outputFormat == nsIPrintSettings::kOutputFormatPDF) { + return NS_OK; + } + + RefPtr<nsDeviceContextSpecWin> devSpecWin = new nsDeviceContextSpecWin(); + if (!devSpecWin) return NS_ERROR_OUT_OF_MEMORY; + + // If the settings have already been initialized from prefs then pass these to + // GetDataFromPrinter, so that they are saved to the printer. + bool initializedFromPrefs; + nsresult rv = + aPrintSettings->GetIsInitializedFromPrefs(&initializedFromPrefs); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (initializedFromPrefs) { + // If we pass in print settings to GetDataFromPrinter it already copies + // things back to the settings, so we can return here. + return devSpecWin->GetDataFromPrinter(aPrinterName, aPrintSettings); + } + + devSpecWin->GetDataFromPrinter(aPrinterName); + + LPDEVMODEW devmode; + devSpecWin->GetDevMode(devmode); + if (NS_WARN_IF(!devmode)) { + return NS_ERROR_FAILURE; + } + + aPrintSettings->SetPrinterName(aPrinterName); + + // We need to get information from the device as well. + const nsString& flat = PromiseFlatString(aPrinterName); + char16ptr_t printerName = flat.get(); + HDC dc = ::CreateICW(kDriverName, printerName, nullptr, devmode); + if (NS_WARN_IF(!dc)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(aPrintSettings); + MOZ_ASSERT(psWin); + psWin->CopyFromNative(dc, devmode); + ::DeleteDC(dc); + + return NS_OK; +} diff --git a/widget/windows/nsDeviceContextSpecWin.h b/widget/windows/nsDeviceContextSpecWin.h new file mode 100644 index 0000000000..fff6472d62 --- /dev/null +++ b/widget/windows/nsDeviceContextSpecWin.h @@ -0,0 +1,98 @@ +/* -*- 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 nsDeviceContextSpecWin_h___ +#define nsDeviceContextSpecWin_h___ + +#include "nsCOMPtr.h" +#include "nsIDeviceContextSpec.h" +#include "nsPrinterListBase.h" +#include "nsIPrintSettings.h" +#include <windows.h> +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "mozilla/gfx/PrintPromise.h" + +class nsIFile; +class nsIWidget; + +class nsDeviceContextSpecWin : public nsIDeviceContextSpec { + public: + nsDeviceContextSpecWin(); + + NS_DECL_ISUPPORTS + + already_AddRefed<PrintTarget> MakePrintTarget() final; + NS_IMETHOD BeginDocument(const nsAString& aTitle, + const nsAString& aPrintToFileName, + int32_t aStartPage, int32_t aEndPage) override { + return NS_OK; + } + RefPtr<mozilla::gfx::PrintEndDocumentPromise> EndDocument() override; + NS_IMETHOD BeginPage(const IntSize& aSizeInPoints) override { return NS_OK; } + NS_IMETHOD EndPage() override { return NS_OK; } + + NS_IMETHOD Init(nsIPrintSettings* aPS, bool aIsPrintPreview) override; + + void GetDriverName(nsAString& aDriverName) const { + aDriverName = mDriverName; + } + void GetDeviceName(nsAString& aDeviceName) const { + aDeviceName = mDeviceName; + } + + // The GetDevMode will return a pointer to a DevMode + // whether it is from the Global memory handle or just the DevMode + // To get the DevMode from the Global memory Handle it must lock it + // So this call must be paired with a call to UnlockGlobalHandle + void GetDevMode(LPDEVMODEW& aDevMode); + + // helper functions + nsresult GetDataFromPrinter(const nsAString& aName, + nsIPrintSettings* aPS = nullptr); + + protected: + void SetDeviceName(const nsAString& aDeviceName); + void SetDriverName(const nsAString& aDriverName); + void SetDevMode(LPDEVMODEW aDevMode); + + virtual ~nsDeviceContextSpecWin(); + + nsString mDriverName; + nsString mDeviceName; + LPDEVMODEW mDevMode = nullptr; + + int16_t mOutputFormat = nsIPrintSettings::kOutputFormatNative; + + // A temporary file to create an "anonymous" print target. See bug 1664253, + // this should ideally not be needed. + nsCOMPtr<nsIFile> mTempFile; +}; + +//------------------------------------------------------------------------- +// Printer List +//------------------------------------------------------------------------- +class nsPrinterListWin final : public nsPrinterListBase { + public: + NS_IMETHOD InitPrintSettingsFromPrinter(const nsAString&, + nsIPrintSettings*) final; + + nsTArray<PrinterInfo> Printers() const final; + RefPtr<nsIPrinter> CreatePrinter(PrinterInfo) const final; + + nsPrinterListWin() = default; + + protected: + nsresult SystemDefaultPrinterName(nsAString&) const final; + + mozilla::Maybe<PrinterInfo> PrinterByName(nsString) const final; + mozilla::Maybe<PrinterInfo> PrinterBySystemName( + nsString aPrinterName) const final; + + private: + ~nsPrinterListWin(); +}; + +#endif diff --git a/widget/windows/nsDragService.cpp b/widget/windows/nsDragService.cpp new file mode 100644 index 0000000000..80f545d9a7 --- /dev/null +++ b/widget/windows/nsDragService.cpp @@ -0,0 +1,664 @@ +/* -*- 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 <ole2.h> +#include <oleidl.h> +#include <shlobj.h> +#include <shlwapi.h> + +// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN +#include <shellapi.h> + +#include "mozilla/RefPtr.h" +#include "nsDragService.h" +#include "nsITransferable.h" +#include "nsDataObj.h" + +#include "nsWidgetsCID.h" +#include "nsNativeDragTarget.h" +#include "nsNativeDragSource.h" +#include "nsClipboard.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "nsDataObjCollection.h" + +#include "nsArrayUtils.h" +#include "nsString.h" +#include "nsEscape.h" +#include "nsIScreenManager.h" +#include "nsToolkit.h" +#include "nsCRT.h" +#include "nsDirectoryServiceDefs.h" +#include "nsUnicharUtils.h" +#include "nsRect.h" +#include "nsMathUtils.h" +#include "WinUtils.h" +#include "KeyboardLayout.h" +#include "gfxContext.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" +#include "mozilla/gfx/Tools.h" +#include "mozilla/ScopeExit.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::widget; + +//------------------------------------------------------------------------- +// +// DragService constructor +// +//------------------------------------------------------------------------- +nsDragService::nsDragService() + : mDataObject(nullptr), mSentLocalDropEvent(false) {} + +//------------------------------------------------------------------------- +// +// DragService destructor +// +//------------------------------------------------------------------------- +nsDragService::~nsDragService() { NS_IF_RELEASE(mDataObject); } + +bool nsDragService::CreateDragImage(nsINode* aDOMNode, + const Maybe<CSSIntRegion>& aRegion, + SHDRAGIMAGE* psdi) { + if (!psdi) return false; + + memset(psdi, 0, sizeof(SHDRAGIMAGE)); + if (!aDOMNode) return false; + + // Prepare the drag image + LayoutDeviceIntRect dragRect; + RefPtr<SourceSurface> surface; + nsPresContext* pc; + DrawDrag(aDOMNode, aRegion, mScreenPosition, &dragRect, &surface, &pc); + if (!surface) return false; + + uint32_t bmWidth = dragRect.Width(), bmHeight = dragRect.Height(); + + if (bmWidth == 0 || bmHeight == 0) return false; + + psdi->crColorKey = CLR_NONE; + + RefPtr<DataSourceSurface> dataSurface = Factory::CreateDataSourceSurface( + IntSize(bmWidth, bmHeight), SurfaceFormat::B8G8R8A8); + NS_ENSURE_TRUE(dataSurface, false); + + DataSourceSurface::MappedSurface map; + if (!dataSurface->Map(DataSourceSurface::MapType::READ_WRITE, &map)) { + return false; + } + + RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData( + BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride, + dataSurface->GetFormat()); + if (!dt) { + dataSurface->Unmap(); + return false; + } + + dt->DrawSurface( + surface, + Rect(0, 0, dataSurface->GetSize().width, dataSurface->GetSize().height), + Rect(0, 0, surface->GetSize().width, surface->GetSize().height), + DrawSurfaceOptions(), DrawOptions(1.0f, CompositionOp::OP_SOURCE)); + dt->Flush(); + + BITMAPV5HEADER bmih; + memset((void*)&bmih, 0, sizeof(BITMAPV5HEADER)); + bmih.bV5Size = sizeof(BITMAPV5HEADER); + bmih.bV5Width = bmWidth; + bmih.bV5Height = -(int32_t)bmHeight; // flip vertical + bmih.bV5Planes = 1; + bmih.bV5BitCount = 32; + bmih.bV5Compression = BI_BITFIELDS; + bmih.bV5RedMask = 0x00FF0000; + bmih.bV5GreenMask = 0x0000FF00; + bmih.bV5BlueMask = 0x000000FF; + bmih.bV5AlphaMask = 0xFF000000; + + HDC hdcSrc = CreateCompatibleDC(nullptr); + void* lpBits = nullptr; + if (hdcSrc) { + psdi->hbmpDragImage = + ::CreateDIBSection(hdcSrc, (BITMAPINFO*)&bmih, DIB_RGB_COLORS, + (void**)&lpBits, nullptr, 0); + if (psdi->hbmpDragImage && lpBits) { + CopySurfaceDataToPackedArray(map.mData, static_cast<uint8_t*>(lpBits), + dataSurface->GetSize(), map.mStride, + BytesPerPixel(dataSurface->GetFormat())); + } + + psdi->sizeDragImage.cx = bmWidth; + psdi->sizeDragImage.cy = bmHeight; + + const auto screenPoint = + LayoutDeviceIntPoint::Round(mScreenPosition * pc->CSSToDevPixelScale()); + psdi->ptOffset.x = screenPoint.x - dragRect.X(); + psdi->ptOffset.y = screenPoint.y - dragRect.Y(); + + DeleteDC(hdcSrc); + } + + dataSurface->Unmap(); + + return psdi->hbmpDragImage != nullptr; +} + +//------------------------------------------------------------------------- +nsresult nsDragService::InvokeDragSessionImpl( + nsIArray* anArrayTransferables, const Maybe<CSSIntRegion>& aRegion, + uint32_t aActionType) { + // Try and get source URI of the items that are being dragged + nsIURI* uri = nullptr; + + RefPtr<dom::Document> doc(mSourceDocument); + if (doc) { + uri = doc->GetDocumentURI(); + } + + uint32_t numItemsToDrag = 0; + nsresult rv = anArrayTransferables->GetLength(&numItemsToDrag); + if (!numItemsToDrag) return NS_ERROR_FAILURE; + + // The clipboard class contains some static utility methods that we + // can use to create an IDataObject from the transferable + + // if we're dragging more than one item, we need to create a + // "collection" object to fake out the OS. This collection contains + // one |IDataObject| for each transferable. If there is just the one + // (most cases), only pass around the native |IDataObject|. + RefPtr<IDataObject> itemToDrag; + if (numItemsToDrag > 1) { + nsDataObjCollection* dataObjCollection = new nsDataObjCollection(); + if (!dataObjCollection) return NS_ERROR_OUT_OF_MEMORY; + itemToDrag = dataObjCollection; + for (uint32_t i = 0; i < numItemsToDrag; ++i) { + nsCOMPtr<nsITransferable> trans = + do_QueryElementAt(anArrayTransferables, i); + if (trans) { + RefPtr<IDataObject> dataObj; + rv = nsClipboard::CreateNativeDataObject(trans, getter_AddRefs(dataObj), + uri); + NS_ENSURE_SUCCESS(rv, rv); + // Add the flavors to the collection object too + rv = nsClipboard::SetupNativeDataObject(trans, dataObjCollection); + NS_ENSURE_SUCCESS(rv, rv); + + dataObjCollection->AddDataObject(dataObj); + } + } + } // if dragging multiple items + else { + nsCOMPtr<nsITransferable> trans = + do_QueryElementAt(anArrayTransferables, 0); + if (trans) { + rv = nsClipboard::CreateNativeDataObject(trans, + getter_AddRefs(itemToDrag), uri); + NS_ENSURE_SUCCESS(rv, rv); + } + } // else dragging a single object + + // Create a drag image if support is available + IDragSourceHelper* pdsh; + if (SUCCEEDED(CoCreateInstance(CLSID_DragDropHelper, nullptr, + CLSCTX_INPROC_SERVER, IID_IDragSourceHelper, + (void**)&pdsh))) { + SHDRAGIMAGE sdi; + if (CreateDragImage(mSourceNode, aRegion, &sdi)) { + if (FAILED(pdsh->InitializeFromBitmap(&sdi, itemToDrag))) + DeleteObject(sdi.hbmpDragImage); + } + pdsh->Release(); + } + + // Kick off the native drag session + return StartInvokingDragSession(itemToDrag, aActionType); +} + +static HWND GetSourceWindow(dom::Document* aSourceDocument) { + if (!aSourceDocument) { + return nullptr; + } + + auto* pc = aSourceDocument->GetPresContext(); + if (!pc) { + return nullptr; + } + + nsCOMPtr<nsIWidget> widget = pc->GetRootWidget(); + if (!widget) { + return nullptr; + } + + return (HWND)widget->GetNativeData(NS_NATIVE_WINDOW); +} + +//------------------------------------------------------------------------- +nsresult nsDragService::StartInvokingDragSession(IDataObject* aDataObj, + uint32_t aActionType) { + // To do the drag we need to create an object that + // implements the IDataObject interface (for OLE) + RefPtr<nsNativeDragSource> nativeDragSrc = + new nsNativeDragSource(mDataTransfer); + + // Now figure out what the native drag effect should be + DWORD winDropRes; + DWORD effects = DROPEFFECT_SCROLL; + if (aActionType & DRAGDROP_ACTION_COPY) { + effects |= DROPEFFECT_COPY; + } + if (aActionType & DRAGDROP_ACTION_MOVE) { + effects |= DROPEFFECT_MOVE; + } + if (aActionType & DRAGDROP_ACTION_LINK) { + effects |= DROPEFFECT_LINK; + } + + // XXX not sure why we bother to cache this, it can change during + // the drag + mDragAction = aActionType; + mSentLocalDropEvent = false; + + // Start dragging + StartDragSession(); + OpenDragPopup(); + + RefPtr<IDataObjectAsyncCapability> pAsyncOp; + // Offer to do an async drag + if (SUCCEEDED(aDataObj->QueryInterface(IID_IDataObjectAsyncCapability, + getter_AddRefs(pAsyncOp)))) { + pAsyncOp->SetAsyncMode(VARIANT_TRUE); + } else { + MOZ_ASSERT_UNREACHABLE("When did our data object stop being async"); + } + + // Call the native D&D method + HRESULT res = ::DoDragDrop(aDataObj, nativeDragSrc, effects, &winDropRes); + + // In cases where the drop operation completed outside the application, + // update the source node's DataTransfer dropEffect value so it is up to date. + if (!mSentLocalDropEvent) { + uint32_t dropResult; + // Order is important, since multiple flags can be returned. + if (winDropRes & DROPEFFECT_COPY) + dropResult = DRAGDROP_ACTION_COPY; + else if (winDropRes & DROPEFFECT_LINK) + dropResult = DRAGDROP_ACTION_LINK; + else if (winDropRes & DROPEFFECT_MOVE) + dropResult = DRAGDROP_ACTION_MOVE; + else + dropResult = DRAGDROP_ACTION_NONE; + + if (mDataTransfer) { + if (res == DRAGDROP_S_DROP) // Success + mDataTransfer->SetDropEffectInt(dropResult); + else + mDataTransfer->SetDropEffectInt(DRAGDROP_ACTION_NONE); + } + } + + mUserCancelled = nativeDragSrc->UserCancelled(); + + // We're done dragging, get the cursor position and end the drag + // Use GetMessagePos to get the position of the mouse at the last message + // seen by the event loop. (Bug 489729) + // Note that we must convert this from device pixels back to Windows logical + // pixels (bug 818927). + DWORD pos = ::GetMessagePos(); + POINT cpos; + cpos.x = GET_X_LPARAM(pos); + cpos.y = GET_Y_LPARAM(pos); + if (auto wnd = GetSourceWindow(mSourceDocument)) { + // Convert from screen to client coordinates like nsWindow::InitEvent does. + ::ScreenToClient(wnd, &cpos); + } + SetDragEndPoint(LayoutDeviceIntPoint(cpos.x, cpos.y)); + + ModifierKeyState modifierKeyState; + EndDragSession(true, modifierKeyState.GetModifiers()); + + mDoingDrag = false; + + return DRAGDROP_S_DROP == res ? NS_OK : NS_ERROR_FAILURE; +} + +//------------------------------------------------------------------------- +// Make Sure we have the right kind of object +nsDataObjCollection* nsDragService::GetDataObjCollection( + IDataObject* aDataObj) { + nsDataObjCollection* dataObjCol = nullptr; + if (aDataObj) { + nsIDataObjCollection* dataObj; + if (aDataObj->QueryInterface(IID_IDataObjCollection, (void**)&dataObj) == + S_OK) { + dataObjCol = static_cast<nsDataObjCollection*>(aDataObj); + dataObj->Release(); + } + } + + return dataObjCol; +} + +//------------------------------------------------------------------------- +NS_IMETHODIMP +nsDragService::GetNumDropItems(uint32_t* aNumItems) { + if (!mDataObject) { + *aNumItems = 0; + return NS_OK; + } + + if (IsCollectionObject(mDataObject)) { + nsDataObjCollection* dataObjCol = GetDataObjCollection(mDataObject); + // If the count cannot be determined just return 0. + // This can happen if we have collection data of type + // MULTI_MIME ("Mozilla/IDataObjectCollectionFormat") on the clipboard + // from another process but we can't obtain an IID_IDataObjCollection + // from this process. + *aNumItems = dataObjCol ? dataObjCol->GetNumDataObjects() : 0; + return NS_OK; + } + // Next check if we have a file drop. Return the number of files in + // the file drop as the number of items we have, pretending like we + // actually have > 1 drag item. + FORMATETC fe2; + SET_FORMATETC(fe2, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (SUCCEEDED(mDataObject->QueryGetData(&fe2))) { + STGMEDIUM stm; + if (FAILED(mDataObject->GetData(&fe2, &stm))) { + *aNumItems = 1; + return NS_OK; + } + HDROP hdrop = static_cast<HDROP>(GlobalLock(stm.hGlobal)); + MOZ_ASSERT(hdrop != NULL); + *aNumItems = ::DragQueryFileW(hdrop, 0xFFFFFFFF, nullptr, 0); + ::GlobalUnlock(stm.hGlobal); + ::ReleaseStgMedium(&stm); + // Data may be provided later, so assume we have 1 item + if (*aNumItems == 0) { + *aNumItems = 1; + } + return NS_OK; + } + // Next check if we have a virtual file drop. + SET_FORMATETC(fe2, nsClipboard::GetClipboardFileDescriptorFormatW(), 0, + DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + STGMEDIUM stm; + + if (SUCCEEDED(mDataObject->GetData(&fe2, &stm))) { + LPFILEGROUPDESCRIPTOR pDesc = + static_cast<LPFILEGROUPDESCRIPTOR>(GlobalLock(stm.hGlobal)); + if (pDesc) { + *aNumItems = pDesc->cItems; + } + GlobalUnlock(stm.hGlobal); + ReleaseStgMedium(&stm); + return NS_OK; + } + *aNumItems = 1; + return NS_OK; +} + +//------------------------------------------------------------------------- +NS_IMETHODIMP +nsDragService::GetData(nsITransferable* aTransferable, uint32_t anItem) { + // This typcially happens on a drop, the target would be asking + // for it's transferable to be filled in + // Use a static clipboard utility method for this + if (!mDataObject) return NS_ERROR_FAILURE; + + nsresult dataFound = NS_ERROR_FAILURE; + + if (IsCollectionObject(mDataObject)) { + // multiple items, use |anItem| as an index into our collection + nsDataObjCollection* dataObjCol = GetDataObjCollection(mDataObject); + uint32_t cnt = dataObjCol->GetNumDataObjects(); + if (anItem < cnt) { + IDataObject* dataObj = dataObjCol->GetDataObjectAt(anItem); + dataFound = nsClipboard::GetDataFromDataObject(dataObj, 0, nullptr, + aTransferable); + } else + NS_WARNING("Index out of range!"); + } else { + // If they are asking for item "0", we can just get it... + if (anItem == 0) { + dataFound = nsClipboard::GetDataFromDataObject(mDataObject, anItem, + nullptr, aTransferable); + } else { + // It better be a file drop, or else non-zero indexes are invalid! + FORMATETC fe2; + FORMATETC fe3; + SET_FORMATETC(fe2, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + SET_FORMATETC(fe3, nsClipboard::GetClipboardFileDescriptorFormatW(), 0, + DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (SUCCEEDED(mDataObject->QueryGetData(&fe2)) || + SUCCEEDED(mDataObject->QueryGetData(&fe3))) + dataFound = nsClipboard::GetDataFromDataObject(mDataObject, anItem, + nullptr, aTransferable); + else + NS_WARNING( + "Reqesting non-zero index, but clipboard data is not a " + "collection!"); + } + } + return dataFound; +} + +//--------------------------------------------------------- +NS_IMETHODIMP +nsDragService::SetIDataObject(IDataObject* aDataObj) { + // When the native drag starts the DragService gets + // the IDataObject that is being dragged + NS_IF_RELEASE(mDataObject); + mDataObject = aDataObj; + NS_IF_ADDREF(mDataObject); + + if (MOZ_DRAGSERVICE_LOG_ENABLED()) { + MOZ_DRAGSERVICE_LOG("nsDragService::SetIDataObject (%p)", mDataObject); + IEnumFORMATETC* pEnum = nullptr; + if (mDataObject && + S_OK == mDataObject->EnumFormatEtc(DATADIR_GET, &pEnum)) { + MOZ_DRAGSERVICE_LOG(" formats in DataObject:"); + + FORMATETC fEtc; + while (S_OK == pEnum->Next(1, &fEtc, nullptr)) { + nsAutoString format; + WinUtils::GetClipboardFormatAsString(fEtc.cfFormat, format); + MOZ_DRAGSERVICE_LOG(" FORMAT %s", + NS_ConvertUTF16toUTF8(format).get()); + } + pEnum->Release(); + } + } + + return NS_OK; +} + +//--------------------------------------------------------- +void nsDragService::SetDroppedLocal() { + // Sent from the native drag handler, letting us know + // a drop occurred within the application vs. outside of it. + mSentLocalDropEvent = true; + return; +} + +//------------------------------------------------------------------------- +NS_IMETHODIMP +nsDragService::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) { + if (!aDataFlavor || !mDataObject || !_retval) { + MOZ_DRAGSERVICE_LOG("%s: error", __PRETTY_FUNCTION__); + return NS_ERROR_FAILURE; + } + + *_retval = false; + auto logging = MakeScopeExit([&] { + MOZ_DRAGSERVICE_LOG("IsDataFlavorSupported: %s is%s found", aDataFlavor, + *_retval ? "" : " not"); + }); + + FORMATETC fe; + UINT format = 0; + + if (IsCollectionObject(mDataObject)) { + // We know we have one of our special collection objects. + format = nsClipboard::GetFormat(aDataFlavor); + SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, + TYMED_HGLOBAL | TYMED_FILE | TYMED_GDI); + + // See if any one of the IDataObjects in the collection supports + // this data type + nsDataObjCollection* dataObjCol = GetDataObjCollection(mDataObject); + if (dataObjCol) { + uint32_t cnt = dataObjCol->GetNumDataObjects(); + for (uint32_t i = 0; i < cnt; ++i) { + IDataObject* dataObj = dataObjCol->GetDataObjectAt(i); + if (S_OK == dataObj->QueryGetData(&fe)) { + *_retval = true; // found it! + } + } + } + return NS_OK; + } + + // Ok, so we have a single object. Check to see if has the correct + // data type. Since this can come from an outside app, we also + // need to see if we need to perform text->unicode conversion if + // the client asked for unicode and it wasn't available. + format = nsClipboard::GetFormat(aDataFlavor); + SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, + TYMED_HGLOBAL | TYMED_FILE | TYMED_GDI); + if (mDataObject->QueryGetData(&fe) == S_OK) { + *_retval = true; // found it! + return NS_OK; + } + + // We haven't found the exact flavor the client asked for, but + // maybe we can still find it from something else that's in the + // data object. + if (strcmp(aDataFlavor, kTextMime) == 0) { + // If unicode wasn't there, it might exist as CF_TEXT, client asked + // for unicode and it wasn't present, check if we + // have CF_TEXT. We'll handle the actual data substitution in + // the data object. + SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, + TYMED_HGLOBAL | TYMED_FILE | TYMED_GDI); + if (mDataObject->QueryGetData(&fe) == S_OK) { + *_retval = true; // found it! + } + return NS_OK; + } + + if (strcmp(aDataFlavor, kURLMime) == 0) { + // client asked for a url and it wasn't present, but if we + // have a file, then we have a URL to give them (the path, or + // the internal URL if an InternetShortcut). + format = nsClipboard::GetFormat(kFileMime); + SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, + TYMED_HGLOBAL | TYMED_FILE | TYMED_GDI); + if (mDataObject->QueryGetData(&fe) == S_OK) { + *_retval = true; // found it! + } + return NS_OK; + } + + if (format == CF_HDROP) { + // Dragging a link from browsers creates both a URL and a FILE which is a + // *.url shortcut in the data object. The file is useful when dropping in + // Windows Explorer to create a internet shortcut. But when dropping in the + // browser, users do not expect to have this file. So do not try to look up + // virtal file if there is a URL in the data object. + format = nsClipboard::GetFormat(kURLMime); + SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (mDataObject->QueryGetData(&fe) == S_OK) { + return NS_OK; + } + + // If the client wants a file, maybe we find a virtual file. + format = nsClipboard::GetClipboardFileDescriptorFormatW(); + SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (mDataObject->QueryGetData(&fe) == S_OK) { + *_retval = true; // found it! + } + + // XXX should we fall back to CFSTR_FILEDESCRIPTORA? + return NS_OK; + } + + return NS_OK; +} + +// +// IsCollectionObject +// +// Determine if this is a single |IDataObject| or one of our private +// collection objects. We know the difference because our collection +// object will respond to supporting the private |MULTI_MIME| format. +// +bool nsDragService::IsCollectionObject(IDataObject* inDataObj) { + bool isCollection = false; + + // setup the format object to ask for the MULTI_MIME format. We only + // need to do this once + static UINT sFormat = 0; + static FORMATETC sFE; + if (!sFormat) { + sFormat = nsClipboard::GetFormat(MULTI_MIME); + SET_FORMATETC(sFE, sFormat, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + } + + // ask the object if it supports it. If yes, we have a collection + // object + if (inDataObj->QueryGetData(&sFE) == S_OK) isCollection = true; + + return isCollection; + +} // IsCollectionObject + +// +// EndDragSession +// +// Override the default to make sure that we release the data object +// when the drag ends. It seems that OLE doesn't like to let apps quit +// w/out crashing when we're still holding onto their data +// +NS_IMETHODIMP +nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) { + // Bug 100180: If we've got mouse events captured, make sure we release it - + // that way, if we happen to call EndDragSession before diving into a nested + // event loop, we can still respond to mouse events. + if (::GetCapture()) { + ::ReleaseCapture(); + } + + nsBaseDragService::EndDragSession(aDoneDrag, aKeyModifiers); + NS_IF_RELEASE(mDataObject); + + return NS_OK; +} + +NS_IMETHODIMP +nsDragService::UpdateDragImage(nsINode* aImage, int32_t aImageX, + int32_t aImageY) { + if (!mDataObject) { + return NS_OK; + } + + nsBaseDragService::UpdateDragImage(aImage, aImageX, aImageY); + + IDragSourceHelper* pdsh; + if (SUCCEEDED(CoCreateInstance(CLSID_DragDropHelper, nullptr, + CLSCTX_INPROC_SERVER, IID_IDragSourceHelper, + (void**)&pdsh))) { + SHDRAGIMAGE sdi; + if (CreateDragImage(mSourceNode, Nothing(), &sdi)) { + nsNativeDragTarget::DragImageChanged(); + if (FAILED(pdsh->InitializeFromBitmap(&sdi, mDataObject))) + DeleteObject(sdi.hbmpDragImage); + } + pdsh->Release(); + } + + return NS_OK; +} diff --git a/widget/windows/nsDragService.h b/widget/windows/nsDragService.h new file mode 100644 index 0000000000..5a86a74f76 --- /dev/null +++ b/widget/windows/nsDragService.h @@ -0,0 +1,67 @@ +/* -*- 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 nsDragService_h__ +#define nsDragService_h__ + +#include "nsBaseDragService.h" +#include <windows.h> +#include <shlobj.h> + +struct IDataObject; +class nsDataObjCollection; + +/** + * Native Win32 DragService wrapper + */ + +class nsDragService : public nsBaseDragService { + public: + nsDragService(); + virtual ~nsDragService(); + + // nsBaseDragService + MOZ_CAN_RUN_SCRIPT virtual nsresult InvokeDragSessionImpl( + nsIArray* anArrayTransferables, + const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion, + uint32_t aActionType); + + // nsIDragSession + NS_IMETHOD GetData(nsITransferable* aTransferable, uint32_t anItem) override; + NS_IMETHOD GetNumDropItems(uint32_t* aNumItems) override; + NS_IMETHOD IsDataFlavorSupported(const char* aDataFlavor, + bool* _retval) override; + MOZ_CAN_RUN_SCRIPT NS_IMETHOD EndDragSession(bool aDoneDrag, + uint32_t aKeyModifiers) override; + NS_IMETHOD UpdateDragImage(nsINode* aImage, int32_t aImageX, + int32_t aImageY) override; + + // native impl. + NS_IMETHOD SetIDataObject(IDataObject* aDataObj); + MOZ_CAN_RUN_SCRIPT nsresult StartInvokingDragSession(IDataObject* aDataObj, + uint32_t aActionType); + + // A drop occurred within the application vs. outside of it. + void SetDroppedLocal(); + + IDataObject* GetDataObject() { return mDataObject; } + + protected: + nsDataObjCollection* GetDataObjCollection(IDataObject* aDataObj); + + // determine if we have a single data object or one of our private + // collections + bool IsCollectionObject(IDataObject* inDataObj); + + // Create a bitmap for drag operations + bool CreateDragImage(nsINode* aDOMNode, + const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion, + SHDRAGIMAGE* psdi); + + IDataObject* mDataObject; + bool mSentLocalDropEvent; +}; + +#endif // nsDragService_h__ diff --git a/widget/windows/nsFilePicker.cpp b/widget/windows/nsFilePicker.cpp new file mode 100644 index 0000000000..310c54bb40 --- /dev/null +++ b/widget/windows/nsFilePicker.cpp @@ -0,0 +1,1078 @@ +/* -*- 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 "nsFilePicker.h" + +#include <cderr.h> +#include <shlobj.h> +#include <shlwapi.h> +#include <sysinfoapi.h> +#include <winerror.h> +#include <winuser.h> +#include <utility> + +#include "ContentAnalysis.h" +#include "mozilla/Assertions.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/Components.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/Logging.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsCRT.h" +#include "nsEnumeratorUtils.h" +#include "nsIContentAnalysis.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsToolkit.h" +#include "nsWindow.h" +#include "WinUtils.h" + +#include "mozilla/glean/GleanMetrics.h" + +#include "mozilla/widget/filedialog/WinFileDialogCommands.h" +#include "mozilla/widget/filedialog/WinFileDialogParent.h" + +using mozilla::UniquePtr; + +using namespace mozilla::widget; + +UniquePtr<char16_t[], nsFilePicker::FreeDeleter> + nsFilePicker::sLastUsedUnicodeDirectory; + +using mozilla::LogLevel; + +#define MAX_EXTENSION_LENGTH 10 + +/////////////////////////////////////////////////////////////////////////////// +// Helper classes + +// Manages matching PickerOpen/PickerClosed calls on the parent widget. +class AutoWidgetPickerState { + static RefPtr<nsWindow> GetWindowForWidget(nsIWidget* aWidget) { + MOZ_ASSERT(NS_IsMainThread()); + if (!aWidget) { + return nullptr; + } + HWND hwnd = (HWND)aWidget->GetNativeData(NS_NATIVE_WINDOW); + return RefPtr(WinUtils::GetNSWindowPtr(hwnd)); + } + + public: + explicit AutoWidgetPickerState(nsIWidget* aWidget) + : mWindow(GetWindowForWidget(aWidget)) { + MOZ_ASSERT(mWindow); + if (mWindow) mWindow->PickerOpen(); + } + ~AutoWidgetPickerState() { + // may be null if moved-from + if (mWindow) mWindow->PickerClosed(); + } + + AutoWidgetPickerState(AutoWidgetPickerState const&) = delete; + AutoWidgetPickerState(AutoWidgetPickerState&& that) noexcept = default; + + private: + RefPtr<nsWindow> mWindow; +}; + +/////////////////////////////////////////////////////////////////////////////// +// nsIFilePicker + +nsFilePicker::nsFilePicker() : mSelectedType(1) {} + +NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) + +NS_IMETHODIMP nsFilePicker::Init( + mozIDOMWindowProxy* aParent, const nsAString& aTitle, + nsIFilePicker::Mode aMode, + mozilla::dom::BrowsingContext* aBrowsingContext) { + // Don't attempt to open a real file-picker in headless mode. + if (gfxPlatform::IsHeadless()) { + return nsresult::NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(aParent); + nsIDocShell* docShell = window ? window->GetDocShell() : nullptr; + mLoadContext = do_QueryInterface(docShell); + + return nsBaseFilePicker::Init(aParent, aTitle, aMode, aBrowsingContext); +} + +namespace mozilla::detail { +// Boilerplate for remotely showing a file dialog. +template <typename ActionType, + typename ReturnType = typename decltype(std::declval<ActionType>()( + nullptr))::element_type::ResolveValueType> +static auto ShowRemote(ActionType&& action) + -> RefPtr<MozPromise<ReturnType, HRESULT, true>> { + using RetPromise = MozPromise<ReturnType, HRESULT, true>; + + constexpr static const auto fail = []() { + return RetPromise::CreateAndReject(E_FAIL, __PRETTY_FUNCTION__); + }; + + auto mgr = mozilla::ipc::UtilityProcessManager::GetSingleton(); + if (!mgr) { + MOZ_ASSERT(false); + return fail(); + } + + auto wfda = mgr->CreateWinFileDialogActor(); + if (!wfda) { + return fail(); + } + + using mozilla::widget::filedialog::sLogFileDialog; + + return wfda->Then( + mozilla::GetMainThreadSerialEventTarget(), + "nsFilePicker ShowRemote acquire", + [action = std::forward<ActionType>(action)]( + filedialog::ProcessProxy const& p) -> RefPtr<RetPromise> { + MOZ_LOG(sLogFileDialog, LogLevel::Info, + ("nsFilePicker ShowRemote first callback: p = [%p]", p.get())); + + // false positive: not actually redundant + // NOLINTNEXTLINE(readability-redundant-smartptr-get) + return action(p.get())->Then( + mozilla::GetMainThreadSerialEventTarget(), + "nsFilePicker ShowRemote call", + [p](ReturnType ret) { + return RetPromise::CreateAndResolve(std::move(ret), + __PRETTY_FUNCTION__); + }, + [](mozilla::ipc::ResponseRejectReason error) { + MOZ_LOG(sLogFileDialog, LogLevel::Error, + ("IPC call rejected: %zu", size_t(error))); + return fail(); + }); + }, + [](nsresult error) -> RefPtr<RetPromise> { + MOZ_LOG(sLogFileDialog, LogLevel::Error, + ("could not acquire WinFileDialog: %zu", size_t(error))); + return fail(); + }); +} + +// fd_async +// +// Wrapper-namespace for the AsyncExecute() and AsyncAll() functions. +namespace fd_async { + +// Implementation details of, specifically, the AsyncExecute() and AsyncAll() +// functions. +namespace details { +// Helper for generically copying ordinary types and nsTArray (which lacks a +// copy constructor) in the same breath. +template <typename T> +static T Copy(T const& val) { + return val; +} +template <typename T> +static nsTArray<T> Copy(nsTArray<T> const& arr) { + return arr.Clone(); +} + +// The possible execution strategies of AsyncExecute. +enum Strategy { Local, Remote, RemoteWithFallback }; + +// Decode the relevant preference to determine the desired execution- +// strategy. +static Strategy GetStrategy() { + int32_t const pref = + mozilla::StaticPrefs::widget_windows_utility_process_file_picker(); + switch (pref) { + case -1: + return Local; + case 2: + return Remote; + case 1: + return RemoteWithFallback; + + default: +#ifdef NIGHTLY_BUILD + // on Nightly builds, fall back to local on failure + return RemoteWithFallback; +#else + // on release and beta, remain local-only for now + return Local; +#endif + } +}; + +template <typename T> +class AsyncAllIterator final { + public: + NS_INLINE_DECL_REFCOUNTING(AsyncAllIterator) + AsyncAllIterator( + nsTArray<T> aItems, + std::function< + RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)> + aPredicate, + RefPtr<mozilla::MozPromise<bool, nsresult, true>::Private> aPromise) + : mItems(std::move(aItems)), + mNextIndex(0), + mPredicate(std::move(aPredicate)), + mPromise(std::move(aPromise)) {} + + void StartIterating() { ContinueIterating(); } + + private: + ~AsyncAllIterator() = default; + void ContinueIterating() { + if (mNextIndex >= mItems.Length()) { + mPromise->Resolve(true, __func__); + return; + } + mPredicate(mItems.ElementAt(mNextIndex)) + ->Then( + mozilla::GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr{this}](bool aResult) { + if (!aResult) { + self->mPromise->Resolve(false, __func__); + return; + } + ++self->mNextIndex; + self->ContinueIterating(); + }, + [self = RefPtr{this}](nsresult aError) { + self->mPromise->Reject(aError, __func__); + }); + } + nsTArray<T> mItems; + uint32_t mNextIndex; + std::function<RefPtr<mozilla::MozPromise<bool, nsresult, true>>( + const T& item)> + mPredicate; + RefPtr<mozilla::MozPromise<bool, nsresult, true>::Private> mPromise; +}; + +namespace telemetry { +static uint32_t Delta(uint64_t tb, uint64_t ta) { + // FILETIMEs are 100ns intervals; we reduce that to 1ms. + // (`u32::max()` milliseconds is roughly 47.91 days.) + return uint32_t((tb - ta) / 10'000); +}; +static nsCString HexString(HRESULT val) { + return nsPrintfCString("%08lX", val); +}; + +static void RecordSuccess(uint64_t (&&time)[2]) { + auto [t0, t1] = time; + + namespace glean_fd = mozilla::glean::file_dialog; + glean_fd::FallbackExtra extra{ + .hresultLocal = Nothing(), + .hresultRemote = Nothing(), + .succeeded = Some(true), + .timeLocal = Nothing(), + .timeRemote = Some(Delta(t1, t0)), + }; + glean_fd::fallback.Record(Some(extra)); +} + +static void RecordFailure(uint64_t (&&time)[3], HRESULT hrRemote, + HRESULT hrLocal) { + auto [t0, t1, t2] = time; + + { + namespace glean_fd = mozilla::glean::file_dialog; + glean_fd::FallbackExtra extra{ + .hresultLocal = Some(HexString(hrLocal)), + .hresultRemote = Some(HexString(hrRemote)), + .succeeded = Some(false), + .timeLocal = Some(Delta(t2, t1)), + .timeRemote = Some(Delta(t1, t0)), + }; + glean_fd::fallback.Record(Some(extra)); + } +} + +} // namespace telemetry +} // namespace details + +// Invoke either or both of a "do locally" and "do remotely" function with the +// provided arguments, depending on the relevant preference-value and whether +// or not the remote version fails. +// +// Both functions must be asynchronous, returning a `RefPtr<MozPromise<...>>`. +// "Failure" is defined as the promise being rejected. +template <typename Fn1, typename Fn2, typename... Args> +static auto AsyncExecute(Fn1 local, Fn2 remote, Args const&... args) + -> std::invoke_result_t<Fn1, Args...> { + using namespace details; + + static_assert(std::is_same_v<std::invoke_result_t<Fn1, Args...>, + std::invoke_result_t<Fn2, Args...>>); + using PromiseT = typename std::invoke_result_t<Fn1, Args...>::element_type; + + constexpr static char kFunctionName[] = "LocalAndOrRemote::AsyncExecute"; + + switch (GetStrategy()) { + case Local: + return local(args...); + + case Remote: + return remote(args...); + + case RemoteWithFallback: + // more complicated; continue below + break; + } + + // capture time for telemetry + constexpr static const auto GetTime = []() -> uint64_t { + FILETIME t; + ::GetSystemTimeAsFileTime(&t); + return (uint64_t(t.dwHighDateTime) << 32) | t.dwLowDateTime; + }; + uint64_t const t0 = GetTime(); + + return remote(args...)->Then( + NS_GetCurrentThread(), kFunctionName, + [t0](typename PromiseT::ResolveValueType result) -> RefPtr<PromiseT> { + // success; stop here + auto const t1 = GetTime(); + // record success + telemetry::RecordSuccess({t0, t1}); + return PromiseT::CreateAndResolve(result, kFunctionName); + }, + // initialized lambda pack captures are C++20 (clang 9, gcc 9); + // `make_tuple` is just a C++17 workaround + [=, tuple = std::make_tuple(Copy(args)...)]( + typename PromiseT::RejectValueType err) mutable -> RefPtr<PromiseT> { + // failure; record time + auto const t1 = GetTime(); + HRESULT const hrRemote = err; + + // retry locally... + auto p0 = std::apply(local, std::move(tuple)); + // ...then record the telemetry event + return p0->Then( + NS_GetCurrentThread(), kFunctionName, + [t0, t1, + hrRemote](typename PromiseT::ResolveOrRejectValue const& val) + -> RefPtr<PromiseT> { + auto const t2 = GetTime(); + HRESULT const hrLocal = val.IsReject() ? val.RejectValue() : S_OK; + telemetry::RecordFailure({t0, t1, t2}, hrRemote, hrLocal); + + return PromiseT::CreateAndResolveOrReject(val, kFunctionName); + }); + }); +} + +// Asynchronously invokes `aPredicate` on each member of `aItems`. +// Yields `false` (and stops immediately) if any invocation of +// `predicate` yielded `false`; otherwise yields `true`. +template <typename T> +static RefPtr<mozilla::MozPromise<bool, nsresult, true>> AsyncAll( + nsTArray<T> aItems, + std::function< + RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)> + aPredicate) { + auto promise = + mozilla::MakeRefPtr<mozilla::MozPromise<bool, nsresult, true>::Private>( + __func__); + auto iterator = mozilla::MakeRefPtr<details::AsyncAllIterator<T>>( + std::move(aItems), aPredicate, promise); + iterator->StartIterating(); + return promise; +} +} // namespace fd_async + +using fd_async::AsyncAll; +using fd_async::AsyncExecute; + +} // namespace mozilla::detail + +/* static */ +nsFilePicker::FPPromise<filedialog::Results> nsFilePicker::ShowFilePickerRemote( + HWND parent, filedialog::FileDialogType type, + nsTArray<filedialog::Command> const& commands) { + using mozilla::widget::filedialog::sLogFileDialog; + return mozilla::detail::ShowRemote( + [parent, type, + commands = commands.Clone()](filedialog::WinFileDialogParent* p) { + MOZ_LOG(sLogFileDialog, LogLevel::Info, + ("%s: p = [%p]", __PRETTY_FUNCTION__, p)); + return p->SendShowFileDialog((uintptr_t)parent, type, commands); + }); +} + +/* static */ +nsFilePicker::FPPromise<nsString> nsFilePicker::ShowFolderPickerRemote( + HWND parent, nsTArray<filedialog::Command> const& commands) { + using mozilla::widget::filedialog::sLogFileDialog; + return mozilla::detail::ShowRemote([parent, commands = commands.Clone()]( + filedialog::WinFileDialogParent* p) { + MOZ_LOG(sLogFileDialog, LogLevel::Info, + ("%s: p = [%p]", __PRETTY_FUNCTION__, p)); + return p->SendShowFolderDialog((uintptr_t)parent, commands); + }); +} + +/* static */ +nsFilePicker::FPPromise<filedialog::Results> nsFilePicker::ShowFilePickerLocal( + HWND parent, filedialog::FileDialogType type, + nsTArray<filedialog::Command> const& commands) { + return filedialog::SpawnFilePicker(parent, type, commands.Clone()); +} + +/* static */ +nsFilePicker::FPPromise<nsString> nsFilePicker::ShowFolderPickerLocal( + HWND parent, nsTArray<filedialog::Command> const& commands) { + return filedialog::SpawnFolderPicker(parent, commands.Clone()); +} + +/* + * Folder picker invocation + */ + +/* + * Show a folder picker. + * + * @param aInitialDir The initial directory. The last-used directory will be + * used if left blank. + * @return A promise which: + * - resolves to true if a file was selected successfully (in which + * case mUnicodeFile will be updated); + * - resolves to false if the dialog was cancelled by the user; + * - is rejected with the associated HRESULT if some error occurred. + */ +RefPtr<mozilla::MozPromise<bool, HRESULT, true>> nsFilePicker::ShowFolderPicker( + const nsString& aInitialDir) { + using Promise = mozilla::MozPromise<bool, HRESULT, true>; + constexpr static auto Ok = [](bool val) { + return Promise::CreateAndResolve(val, "nsFilePicker::ShowFolderPicker"); + }; + constexpr static auto NotOk = [](HRESULT val = E_FAIL) { + return Promise::CreateAndReject(val, "nsFilePicker::ShowFolderPicker"); + }; + + namespace fd = ::mozilla::widget::filedialog; + nsTArray<fd::Command> commands = { + fd::SetOptions(FOS_PICKFOLDERS), + fd::SetTitle(mTitle), + }; + + if (!mOkButtonLabel.IsEmpty()) { + commands.AppendElement(fd::SetOkButtonLabel(mOkButtonLabel)); + } + + if (!aInitialDir.IsEmpty()) { + commands.AppendElement(fd::SetFolder(aInitialDir)); + } + + ScopedRtlShimWindow shim(mParentWidget.get()); + AutoWidgetPickerState awps(mParentWidget); + + return mozilla::detail::AsyncExecute(&ShowFolderPickerLocal, + &ShowFolderPickerRemote, shim.get(), + commands) + ->Then( + NS_GetCurrentThread(), __PRETTY_FUNCTION__, + [self = RefPtr(this), shim = std::move(shim), + awps = std::move(awps)](Maybe<nsString> val) { + if (val) { + self->mUnicodeFile = val.extract(); + return Ok(true); + } + return Ok(false); + }, + [](HRESULT err) { + NS_WARNING("ShowFolderPicker failed"); + return NotOk(err); + }); +} + +/* + * File open and save picker invocation + */ + +/* + * Show a file picker. + * + * @param aInitialDir The initial directory. The last-used directory will be + * used if left blank. + * @return A promise which: + * - resolves to true if one or more files were selected successfully + * (in which case mUnicodeFile and/or mFiles will be updated); + * - resolves to false if the dialog was cancelled by the user; + * - is rejected with the associated HRESULT if some error occurred. + */ +RefPtr<mozilla::MozPromise<bool, HRESULT, true>> nsFilePicker::ShowFilePicker( + const nsString& aInitialDir) { + AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER); + + using Promise = mozilla::MozPromise<bool, HRESULT, true>; + constexpr static auto Ok = [](bool val) { + return Promise::CreateAndResolve(val, "nsFilePicker::ShowFilePicker"); + }; + constexpr static auto NotOk = [](HRESULT val = E_FAIL) { + return Promise::CreateAndReject(val, "nsFilePicker::ShowFilePicker"); + }; + + namespace fd = ::mozilla::widget::filedialog; + nsTArray<fd::Command> commands; + // options + { + FILEOPENDIALOGOPTIONS fos = 0; + fos |= FOS_SHAREAWARE | FOS_OVERWRITEPROMPT | FOS_FORCEFILESYSTEM; + + // Handle add to recent docs settings + if (IsPrivacyModeEnabled() || !mAddToRecentDocs) { + fos |= FOS_DONTADDTORECENT; + } + + // mode specific + switch (mMode) { + case modeOpen: + fos |= FOS_FILEMUSTEXIST; + break; + + case modeOpenMultiple: + fos |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT; + break; + + case modeSave: + fos |= FOS_NOREADONLYRETURN; + // Don't follow shortcuts when saving a shortcut, this can be used + // to trick users (bug 271732) + if (IsDefaultPathLink()) fos |= FOS_NODEREFERENCELINKS; + break; + + case modeGetFolder: + MOZ_ASSERT(false, "file-picker opened in directory-picker mode"); + return NotOk(E_FAIL); + } + + commands.AppendElement(fd::SetOptions(fos)); + } + + // initial strings + + // title + commands.AppendElement(fd::SetTitle(mTitle)); + + // default filename + if (!mDefaultFilename.IsEmpty()) { + // Prevent the shell from expanding environment variables by removing + // the % characters that are used to delimit them. + nsAutoString sanitizedFilename(mDefaultFilename); + sanitizedFilename.ReplaceChar('%', '_'); + + commands.AppendElement(fd::SetFileName(sanitizedFilename)); + } + + // default extension to append to new files + if (!mDefaultExtension.IsEmpty()) { + // We don't want environment variables expanded in the extension either. + nsAutoString sanitizedExtension(mDefaultExtension); + sanitizedExtension.ReplaceChar('%', '_'); + + commands.AppendElement(fd::SetDefaultExtension(sanitizedExtension)); + } else if (IsDefaultPathHtml()) { + commands.AppendElement(fd::SetDefaultExtension(u"html"_ns)); + } + + // initial location + if (!aInitialDir.IsEmpty()) { + commands.AppendElement(fd::SetFolder(aInitialDir)); + } + + // filter types and the default index + if (!mFilterList.IsEmpty()) { + nsTArray<fd::ComDlgFilterSpec> fileTypes; + for (auto const& filter : mFilterList) { + fileTypes.EmplaceBack(filter.title, filter.filter); + } + commands.AppendElement(fd::SetFileTypes(std::move(fileTypes))); + commands.AppendElement(fd::SetFileTypeIndex(mSelectedType)); + } + + ScopedRtlShimWindow shim(mParentWidget.get()); + AutoWidgetPickerState awps(mParentWidget); + + mozilla::BackgroundHangMonitor().NotifyWait(); + auto type = mMode == modeSave ? FileDialogType::Save : FileDialogType::Open; + + auto promise = mozilla::detail::AsyncExecute( + &ShowFilePickerLocal, &ShowFilePickerRemote, shim.get(), type, commands); + + return promise->Then( + mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__, + [self = RefPtr(this), mode = mMode, shim = std::move(shim), + awps = std::move(awps)](Maybe<Results> res_opt) { + if (!res_opt) { + return Ok(false); + } + auto result = res_opt.extract(); + + // Remember what filter type the user selected + self->mSelectedType = int32_t(result.selectedFileTypeIndex()); + + auto const& paths = result.paths(); + + // single selection + if (mode != modeOpenMultiple) { + if (!paths.IsEmpty()) { + MOZ_ASSERT(paths.Length() == 1); + self->mUnicodeFile = paths[0]; + return Ok(true); + } + return Ok(false); + } + + // multiple selection + for (auto const& str : paths) { + nsCOMPtr<nsIFile> file; + if (NS_SUCCEEDED(NS_NewLocalFile(str, false, getter_AddRefs(file)))) { + self->mFiles.AppendObject(file); + } + } + + return Ok(true); + }, + [](HRESULT err) { + NS_WARNING("ShowFilePicker failed"); + return NotOk(err); + }); +} + +void nsFilePicker::ClearFiles() { + mUnicodeFile.Truncate(); + mFiles.Clear(); +} + +namespace { +class GetFilesInDirectoryCallback final + : public mozilla::dom::GetFilesCallback { + public: + explicit GetFilesInDirectoryCallback( + RefPtr<mozilla::MozPromise<nsTArray<mozilla::PathString>, nsresult, + true>::Private> + aPromise) + : mPromise(std::move(aPromise)) {} + void Callback( + nsresult aStatus, + const FallibleTArray<RefPtr<mozilla::dom::BlobImpl>>& aBlobImpls) { + if (NS_FAILED(aStatus)) { + mPromise->Reject(aStatus, __func__); + return; + } + nsTArray<mozilla::PathString> filePaths; + filePaths.SetCapacity(aBlobImpls.Length()); + for (const auto& blob : aBlobImpls) { + if (blob->IsFile()) { + mozilla::PathString pathString; + mozilla::ErrorResult error; + blob->GetMozFullPathInternal(pathString, error); + nsresult rv = error.StealNSResult(); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPromise->Reject(rv, __func__); + return; + } + filePaths.AppendElement(pathString); + } else { + NS_WARNING("Got a non-file blob, can't do content analysis on it"); + } + } + mPromise->Resolve(std::move(filePaths), __func__); + } + + private: + RefPtr<mozilla::MozPromise<nsTArray<mozilla::PathString>, nsresult, + true>::Private> + mPromise; +}; +} // anonymous namespace + +RefPtr<nsFilePicker::ContentAnalysisResponse> +nsFilePicker::CheckContentAnalysisService() { + nsresult rv; + nsCOMPtr<nsIContentAnalysis> contentAnalysis = + mozilla::components::nsIContentAnalysis::Service(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__); + } + bool contentAnalysisIsActive = false; + rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__); + } + if (!contentAnalysisIsActive) { + return nsFilePicker::ContentAnalysisResponse::CreateAndResolve(true, + __func__); + } + + nsCOMPtr<nsIURI> uri = mBrowsingContext->Canonical()->GetCurrentURI(); + + auto processOneItem = [self = RefPtr{this}, + contentAnalysis = std::move(contentAnalysis), + uri = + std::move(uri)](const mozilla::PathString& aItem) { + nsCString emptyDigestString; + auto* windowGlobal = + self->mBrowsingContext->Canonical()->GetCurrentWindowGlobal(); + nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest( + new mozilla::contentanalysis::ContentAnalysisRequest( + nsIContentAnalysisRequest::AnalysisType::eFileAttached, aItem, true, + std::move(emptyDigestString), uri, + nsIContentAnalysisRequest::OperationType::eCustomDisplayString, + windowGlobal)); + + auto promise = + mozilla::MakeRefPtr<nsFilePicker::ContentAnalysisResponse::Private>( + __func__); + auto contentAnalysisCallback = + mozilla::MakeRefPtr<mozilla::contentanalysis::ContentAnalysisCallback>( + [promise](nsIContentAnalysisResponse* aResponse) { + promise->Resolve(aResponse->GetShouldAllowContent(), __func__); + }, + [promise](nsresult aError) { promise->Reject(aError, __func__); }); + + nsresult rv = contentAnalysis->AnalyzeContentRequestCallback( + contentAnalysisRequest, /* aAutoAcknowledge */ true, + contentAnalysisCallback); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->Reject(rv, __func__); + } + return promise; + }; + + // Since getting the files to analyze might be asynchronous, use a MozPromise + // to unify the logic below. + auto getFilesToAnalyzePromise = mozilla::MakeRefPtr<mozilla::MozPromise< + nsTArray<mozilla::PathString>, nsresult, true>::Private>(__func__); + if (mMode == modeGetFolder) { + nsCOMPtr<nsISupports> tmp; + nsresult rv = GetDomFileOrDirectory(getter_AddRefs(tmp)); + if (NS_WARN_IF(NS_FAILED(rv))) { + getFilesToAnalyzePromise->Reject(rv, __func__); + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, + __func__); + } + auto* directory = static_cast<mozilla::dom::Directory*>(tmp.get()); + mozilla::dom::OwningFileOrDirectory owningDirectory; + owningDirectory.SetAsDirectory() = directory; + nsTArray<mozilla::dom::OwningFileOrDirectory> directoryArray{ + std::move(owningDirectory)}; + + mozilla::ErrorResult error; + RefPtr<mozilla::dom::GetFilesHelper> helper = + mozilla::dom::GetFilesHelper::Create(directoryArray, true, error); + rv = error.StealNSResult(); + if (NS_WARN_IF(NS_FAILED(rv))) { + getFilesToAnalyzePromise->Reject(rv, __func__); + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, + __func__); + } + auto getFilesCallback = mozilla::MakeRefPtr<GetFilesInDirectoryCallback>( + getFilesToAnalyzePromise); + helper->AddCallback(getFilesCallback); + } else { + nsCOMArray<nsIFile> files; + if (!mUnicodeFile.IsEmpty()) { + nsCOMPtr<nsIFile> file; + rv = GetFile(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + getFilesToAnalyzePromise->Reject(rv, __func__); + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, + __func__); + } + files.AppendElement(file); + } else { + files.AppendElements(mFiles); + } + nsTArray<mozilla::PathString> paths(files.Length()); + std::transform(files.begin(), files.end(), MakeBackInserter(paths), + [](auto* entry) { return entry->NativePath(); }); + getFilesToAnalyzePromise->Resolve(std::move(paths), __func__); + } + + return getFilesToAnalyzePromise->Then( + mozilla::GetMainThreadSerialEventTarget(), __func__, + [processOneItem](nsTArray<mozilla::PathString> aPaths) mutable { + return mozilla::detail::AsyncAll<mozilla::PathString>(std::move(aPaths), + processOneItem); + }, + [](nsresult aError) { + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(aError, + __func__); + }); +}; + +/////////////////////////////////////////////////////////////////////////////// +// nsIFilePicker impl. + +nsresult nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) { + NS_ENSURE_ARG_POINTER(aCallback); + + if (MaybeBlockFilePicker(aCallback)) { + return NS_OK; + } + + // Don't attempt to open a real file-picker in headless mode. + if (gfxPlatform::IsHeadless()) { + return nsresult::NS_ERROR_NOT_AVAILABLE; + } + + nsAutoString initialDir; + if (mDisplayDirectory) mDisplayDirectory->GetPath(initialDir); + + // If no display directory, re-use the last one. + if (initialDir.IsEmpty()) { + // Allocate copy of last used dir. + initialDir = sLastUsedUnicodeDirectory.get(); + } + + // Clear previous file selections + ClearFiles(); + + auto promise = mMode == modeGetFolder ? ShowFolderPicker(initialDir) + : ShowFilePicker(initialDir); + + auto p2 = promise->Then( + mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__, + [self = RefPtr(this), + callback = RefPtr(aCallback)](bool selectionMade) -> void { + if (!selectionMade) { + callback->Done(ResultCode::returnCancel); + return; + } + + self->RememberLastUsedDirectory(); + + nsIFilePicker::ResultCode retValue = ResultCode::returnOK; + + if (self->mMode == modeSave) { + // Windows does not return resultReplace; we must check whether the + // file already exists. + nsCOMPtr<nsIFile> file; + nsresult rv = + NS_NewLocalFile(self->mUnicodeFile, false, getter_AddRefs(file)); + + bool flag = false; + if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(file->Exists(&flag)) && flag) { + retValue = ResultCode::returnReplace; + } + } + + if (self->mBrowsingContext && !self->mBrowsingContext->IsChrome() && + self->mMode != modeSave && retValue != ResultCode::returnCancel) { + self->CheckContentAnalysisService()->Then( + mozilla::GetMainThreadSerialEventTarget(), __func__, + [retValue, callback, self = RefPtr{self}](bool aAllowContent) { + if (aAllowContent) { + callback->Done(retValue); + } else { + self->ClearFiles(); + callback->Done(ResultCode::returnCancel); + } + }, + [callback, self = RefPtr{self}](nsresult aError) { + self->ClearFiles(); + callback->Done(ResultCode::returnCancel); + }); + return; + } + + callback->Done(retValue); + }, + [callback = RefPtr(aCallback)](HRESULT err) { + using mozilla::widget::filedialog::sLogFileDialog; + MOZ_LOG(sLogFileDialog, LogLevel::Error, + ("nsFilePicker: Show failed with hr=0x%08lX", err)); + callback->Done(ResultCode::returnCancel); + }); + + return NS_OK; +} + +nsresult nsFilePicker::Show(nsIFilePicker::ResultCode* aReturnVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsFilePicker::GetFile(nsIFile** aFile) { + NS_ENSURE_ARG_POINTER(aFile); + *aFile = nullptr; + + if (mUnicodeFile.IsEmpty()) return NS_OK; + + nsCOMPtr<nsIFile> file; + nsresult rv = NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + file.forget(aFile); + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::GetFileURL(nsIURI** aFileURL) { + *aFileURL = nullptr; + nsCOMPtr<nsIFile> file; + nsresult rv = GetFile(getter_AddRefs(file)); + if (!file) return rv; + + return NS_NewFileURI(aFileURL, file); +} + +NS_IMETHODIMP +nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) { + NS_ENSURE_ARG_POINTER(aFiles); + return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile)); +} + +// Get the file + path +NS_IMETHODIMP +nsBaseWinFilePicker::SetDefaultString(const nsAString& aString) { + mDefaultFilePath = aString; + + // First, make sure the file name is not too long. + int32_t nameLength; + int32_t nameIndex = mDefaultFilePath.RFind(u"\\"); + if (nameIndex == kNotFound) + nameIndex = 0; + else + nameIndex++; + nameLength = mDefaultFilePath.Length() - nameIndex; + mDefaultFilename.Assign(Substring(mDefaultFilePath, nameIndex)); + + if (nameLength > MAX_PATH) { + int32_t extIndex = mDefaultFilePath.RFind(u"."); + if (extIndex == kNotFound) extIndex = mDefaultFilePath.Length(); + + // Let's try to shave the needed characters from the name part. + int32_t charsToRemove = nameLength - MAX_PATH; + if (extIndex - nameIndex >= charsToRemove) { + mDefaultFilePath.Cut(extIndex - charsToRemove, charsToRemove); + } + } + + // Then, we need to replace illegal characters. At this stage, we cannot + // replace the backslash as the string might represent a file path. + mDefaultFilePath.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-'); + mDefaultFilename.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-'); + + return NS_OK; +} + +NS_IMETHODIMP +nsBaseWinFilePicker::GetDefaultString(nsAString& aString) { + return NS_ERROR_FAILURE; +} + +// The default extension to use for files +NS_IMETHODIMP +nsBaseWinFilePicker::GetDefaultExtension(nsAString& aExtension) { + aExtension = mDefaultExtension; + return NS_OK; +} + +NS_IMETHODIMP +nsBaseWinFilePicker::SetDefaultExtension(const nsAString& aExtension) { + mDefaultExtension = aExtension; + return NS_OK; +} + +// Set the filter index +NS_IMETHODIMP +nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) { + // Windows' filter index is 1-based, we use a 0-based system. + *aFilterIndex = mSelectedType - 1; + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::SetFilterIndex(int32_t aFilterIndex) { + // Windows' filter index is 1-based, we use a 0-based system. + mSelectedType = aFilterIndex + 1; + return NS_OK; +} + +void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) { + mParentWidget = aParent; + mTitle.Assign(aTitle); +} + +NS_IMETHODIMP +nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) { + nsString sanitizedFilter(aFilter); + sanitizedFilter.ReplaceChar('%', '_'); + + if (sanitizedFilter == u"..apps"_ns) { + sanitizedFilter = u"*.exe;*.com"_ns; + } else { + sanitizedFilter.StripWhitespace(); + if (sanitizedFilter == u"*"_ns) { + sanitizedFilter = u"*.*"_ns; + } + } + mFilterList.AppendElement( + Filter{.title = nsString(aTitle), .filter = std::move(sanitizedFilter)}); + return NS_OK; +} + +void nsFilePicker::RememberLastUsedDirectory() { + if (IsPrivacyModeEnabled()) { + // Don't remember the directory if private browsing was in effect + return; + } + + nsCOMPtr<nsIFile> file; + if (NS_FAILED(NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file)))) { + NS_WARNING("RememberLastUsedDirectory failed to init file path."); + return; + } + + nsCOMPtr<nsIFile> dir; + nsAutoString newDir; + if (NS_FAILED(file->GetParent(getter_AddRefs(dir))) || + !(mDisplayDirectory = dir) || + NS_FAILED(mDisplayDirectory->GetPath(newDir)) || newDir.IsEmpty()) { + NS_WARNING("RememberLastUsedDirectory failed to get parent directory."); + return; + } + + sLastUsedUnicodeDirectory.reset(ToNewUnicode(newDir)); +} + +bool nsFilePicker::IsPrivacyModeEnabled() { + return mLoadContext && mLoadContext->UsePrivateBrowsing(); +} + +bool nsFilePicker::IsDefaultPathLink() { + NS_ConvertUTF16toUTF8 ext(mDefaultFilePath); + ext.Trim(" .", false, true); // watch out for trailing space and dots + ToLowerCase(ext); + return StringEndsWith(ext, ".lnk"_ns) || StringEndsWith(ext, ".pif"_ns) || + StringEndsWith(ext, ".url"_ns); +} + +bool nsFilePicker::IsDefaultPathHtml() { + int32_t extIndex = mDefaultFilePath.RFind(u"."); + if (extIndex >= 0) { + nsAutoString ext; + mDefaultFilePath.Right(ext, mDefaultFilePath.Length() - extIndex); + if (ext.LowerCaseEqualsLiteral(".htm") || + ext.LowerCaseEqualsLiteral(".html") || + ext.LowerCaseEqualsLiteral(".shtml")) + return true; + } + return false; +} diff --git a/widget/windows/nsFilePicker.h b/widget/windows/nsFilePicker.h new file mode 100644 index 0000000000..1938b8bcb6 --- /dev/null +++ b/widget/windows/nsFilePicker.h @@ -0,0 +1,140 @@ +/* -*- 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 nsFilePicker_h__ +#define nsFilePicker_h__ + +#include <windows.h> + +#include "mozilla/MozPromise.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/GetFilesHelper.h" +#include "nsIContentAnalysis.h" +#include "nsIFile.h" +#include "nsISimpleEnumerator.h" +#include "nsCOMArray.h" +#include "nsBaseFilePicker.h" +#include "nsString.h" +#include "nsdefs.h" +#include <commdlg.h> +#include <shobjidl.h> +#undef LogSeverity // SetupAPI.h #defines this as DWORD + +class nsILoadContext; + +namespace mozilla::widget::filedialog { +class Command; +class Results; +enum class FileDialogType : uint8_t; +} // namespace mozilla::widget::filedialog + +class nsBaseWinFilePicker : public nsBaseFilePicker { + public: + NS_IMETHOD GetDefaultString(nsAString& aDefaultString) override; + NS_IMETHOD SetDefaultString(const nsAString& aDefaultString) override; + NS_IMETHOD GetDefaultExtension(nsAString& aDefaultExtension) override; + NS_IMETHOD SetDefaultExtension(const nsAString& aDefaultExtension) override; + + protected: + nsString mDefaultFilePath; + nsString mDefaultFilename; + nsString mDefaultExtension; +}; + +/** + * Native Windows FileSelector wrapper + */ + +class nsFilePicker final : public nsBaseWinFilePicker { + virtual ~nsFilePicker() = default; + + template <typename T> + using Maybe = mozilla::Maybe<T>; + template <typename T> + using Result = mozilla::Result<T, HRESULT>; + template <typename Res> + using FPPromise = RefPtr<mozilla::MozPromise<Maybe<Res>, HRESULT, true>>; + + using Command = mozilla::widget::filedialog::Command; + using Results = mozilla::widget::filedialog::Results; + using FileDialogType = mozilla::widget::filedialog::FileDialogType; + + public: + nsFilePicker(); + + NS_IMETHOD Init(mozIDOMWindowProxy* aParent, const nsAString& aTitle, + nsIFilePicker::Mode aMode, + mozilla::dom::BrowsingContext* aBrowsingContext) override; + + NS_DECL_ISUPPORTS + + // nsIFilePicker (less what's in nsBaseFilePicker and nsBaseWinFilePicker) + NS_IMETHOD GetFilterIndex(int32_t* aFilterIndex) override; + NS_IMETHOD SetFilterIndex(int32_t aFilterIndex) override; + NS_IMETHOD GetFile(nsIFile** aFile) override; + NS_IMETHOD GetFileURL(nsIURI** aFileURL) override; + NS_IMETHOD GetFiles(nsISimpleEnumerator** aFiles) override; + NS_IMETHOD AppendFilter(const nsAString& aTitle, + const nsAString& aFilter) override; + + protected: + /* method from nsBaseFilePicker */ + virtual void InitNative(nsIWidget* aParent, const nsAString& aTitle) override; + nsresult Show(nsIFilePicker::ResultCode* aReturnVal) override; + void GetFilterListArray(nsString& aFilterList); + + NS_IMETHOD Open(nsIFilePickerShownCallback* aCallback) override; + + private: + RefPtr<mozilla::MozPromise<bool, HRESULT, true>> ShowFolderPicker( + const nsString& aInitialDir); + RefPtr<mozilla::MozPromise<bool, HRESULT, true>> ShowFilePicker( + const nsString& aInitialDir); + + // Show the dialog out-of-process. + static FPPromise<Results> ShowFilePickerRemote( + HWND aParent, FileDialogType type, nsTArray<Command> const& commands); + static FPPromise<nsString> ShowFolderPickerRemote( + HWND aParent, nsTArray<Command> const& commands); + + // Show the dialog in-process. + static FPPromise<Results> ShowFilePickerLocal( + HWND aParent, FileDialogType type, nsTArray<Command> const& commands); + static FPPromise<nsString> ShowFolderPickerLocal( + HWND aParent, nsTArray<Command> const& commands); + + void ClearFiles(); + using ContentAnalysisResponse = mozilla::MozPromise<bool, nsresult, true>; + RefPtr<ContentAnalysisResponse> CheckContentAnalysisService(); + + protected: + void RememberLastUsedDirectory(); + bool IsPrivacyModeEnabled(); + bool IsDefaultPathLink(); + bool IsDefaultPathHtml(); + + nsCOMPtr<nsILoadContext> mLoadContext; + nsCOMPtr<nsIWidget> mParentWidget; + nsString mTitle; + nsCString mFile; + int32_t mSelectedType; + nsCOMArray<nsIFile> mFiles; + nsString mUnicodeFile; + + struct FreeDeleter { + void operator()(void* aPtr) { ::free(aPtr); } + }; + static mozilla::UniquePtr<char16_t[], FreeDeleter> sLastUsedUnicodeDirectory; + + struct Filter { + nsString title; + nsString filter; + }; + AutoTArray<Filter, 1> mFilterList; +}; + +#endif // nsFilePicker_h__ diff --git a/widget/windows/nsLookAndFeel.cpp b/widget/windows/nsLookAndFeel.cpp new file mode 100644 index 0000000000..01b126cd42 --- /dev/null +++ b/widget/windows/nsLookAndFeel.cpp @@ -0,0 +1,916 @@ +/* -*- 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 "nsLookAndFeel.h" +#include <stdint.h> +#include <windows.h> +#include <shellapi.h> +#include "nsStyleConsts.h" +#include "nsUXThemeData.h" +#include "nsUXThemeConstants.h" +#include "nsWindowsHelpers.h" +#include "WinUtils.h" +#include "WindowsUIUtils.h" +#include "mozilla/FontPropertyTypes.h" +#include "mozilla/Telemetry.h" +#include "mozilla/widget/WinRegistry.h" + +using namespace mozilla; +using namespace mozilla::widget; + +static Maybe<nscolor> GetColorFromTheme(nsUXThemeClass cls, int32_t aPart, + int32_t aState, int32_t aPropId) { + COLORREF color; + HRESULT hr = GetThemeColor(nsUXThemeData::GetTheme(cls), aPart, aState, + aPropId, &color); + if (hr == S_OK) { + return Some(COLOREF_2_NSRGB(color)); + } + return Nothing(); +} + +static int32_t GetSystemParam(long flag, int32_t def) { + DWORD value; + return ::SystemParametersInfo(flag, 0, &value, 0) ? value : def; +} + +static bool SystemWantsDarkTheme() { + if (nsUXThemeData::IsHighContrastOn()) { + return LookAndFeel::IsDarkColor( + LookAndFeel::Color(StyleSystemColor::Window, ColorScheme::Light, + LookAndFeel::UseStandins::No)); + } + + WinRegistry::Key key( + HKEY_CURRENT_USER, + u"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"_ns, + WinRegistry::KeyMode::QueryValue); + if (NS_WARN_IF(!key)) { + return false; + } + uint32_t light = key.GetValueAsDword(u"AppsUseLightTheme"_ns).valueOr(1); + return !light; +} + +uint32_t nsLookAndFeel::SystemColorFilter() { + if (NS_WARN_IF(!mColorFilterWatcher)) { + return 0; + } + + const auto& key = mColorFilterWatcher->GetKey(); + if (!key.GetValueAsDword(u"Active"_ns).valueOr(0)) { + return 0; + } + return key.GetValueAsDword(u"FilterType"_ns).valueOr(0); +} + +nsLookAndFeel::nsLookAndFeel() { + mozilla::Telemetry::Accumulate(mozilla::Telemetry::TOUCH_ENABLED_DEVICE, + WinUtils::IsTouchDeviceSupportPresent()); +} + +nsLookAndFeel::~nsLookAndFeel() = default; + +void nsLookAndFeel::NativeInit() { EnsureInit(); } + +/* virtual */ +void nsLookAndFeel::RefreshImpl() { + mInitialized = false; // Fetch system colors next time they're used. + nsXPLookAndFeel::RefreshImpl(); +} + +static bool UseNonNativeMenuColors(ColorScheme aScheme) { + return !LookAndFeel::GetInt(LookAndFeel::IntID::UseAccessibilityTheme) || + aScheme == ColorScheme::Dark; +} + +nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme, + nscolor& aColor) { + EnsureInit(); + + auto IsHighlightColor = [&] { + switch (aID) { + case ColorID::MozMenuhover: + return !UseNonNativeMenuColors(aScheme); + case ColorID::Highlight: + case ColorID::Selecteditem: + // We prefer the generic dark selection color if we don't have an + // explicit one. + return aScheme != ColorScheme::Dark || mDarkHighlight; + case ColorID::IMESelectedRawTextBackground: + case ColorID::IMESelectedConvertedTextBackground: + return true; + default: + return false; + } + }; + + auto IsHighlightTextColor = [&] { + switch (aID) { + case ColorID::MozMenubarhovertext: + if (UseNonNativeMenuColors(aScheme)) { + return false; + } + [[fallthrough]]; + case ColorID::MozMenuhovertext: + if (UseNonNativeMenuColors(aScheme)) { + return false; + } + return !mColorMenuHoverText; + case ColorID::Highlighttext: + case ColorID::Selecteditemtext: + // We prefer the generic dark selection color if we don't have an + // explicit one. + return aScheme != ColorScheme::Dark || mDarkHighlightText; + case ColorID::IMESelectedRawTextForeground: + case ColorID::IMESelectedConvertedTextForeground: + return true; + default: + return false; + } + }; + + if (IsHighlightColor()) { + if (aScheme == ColorScheme::Dark && mDarkHighlight) { + aColor = *mDarkHighlight; + } else { + aColor = GetColorForSysColorIndex(COLOR_HIGHLIGHT); + } + return NS_OK; + } + + if (IsHighlightTextColor()) { + if (aScheme == ColorScheme::Dark && mDarkHighlightText) { + aColor = *mDarkHighlightText; + } else { + aColor = GetColorForSysColorIndex(COLOR_HIGHLIGHTTEXT); + } + return NS_OK; + } + + // Titlebar colors are color-scheme aware. + switch (aID) { + case ColorID::Activecaption: + aColor = mTitlebarColors.Get(aScheme, true).mBg; + return NS_OK; + case ColorID::Captiontext: + aColor = mTitlebarColors.Get(aScheme, true).mFg; + return NS_OK; + case ColorID::Activeborder: + aColor = mTitlebarColors.Get(aScheme, true).mBorder; + return NS_OK; + case ColorID::Inactivecaption: + aColor = mTitlebarColors.Get(aScheme, false).mBg; + return NS_OK; + case ColorID::Inactivecaptiontext: + aColor = mTitlebarColors.Get(aScheme, false).mFg; + return NS_OK; + case ColorID::Inactiveborder: + aColor = mTitlebarColors.Get(aScheme, false).mBorder; + return NS_OK; + default: + break; + } + + if (aScheme == ColorScheme::Dark) { + if (auto color = GenericDarkColor(aID)) { + aColor = *color; + return NS_OK; + } + } + + static constexpr auto kNonNativeMenuText = NS_RGB(0x15, 0x14, 0x1a); + nsresult res = NS_OK; + int idx; + switch (aID) { + case ColorID::IMERawInputBackground: + case ColorID::IMEConvertedTextBackground: + aColor = NS_TRANSPARENT; + return NS_OK; + case ColorID::IMERawInputForeground: + case ColorID::IMEConvertedTextForeground: + aColor = NS_SAME_AS_FOREGROUND_COLOR; + return NS_OK; + case ColorID::IMERawInputUnderline: + case ColorID::IMEConvertedTextUnderline: + aColor = NS_SAME_AS_FOREGROUND_COLOR; + return NS_OK; + case ColorID::IMESelectedRawTextUnderline: + case ColorID::IMESelectedConvertedTextUnderline: + aColor = NS_TRANSPARENT; + return NS_OK; + + // New CSS 2 Color definitions + case ColorID::Appworkspace: + idx = COLOR_APPWORKSPACE; + break; + case ColorID::Background: + idx = COLOR_BACKGROUND; + break; + case ColorID::Buttonface: + case ColorID::MozButtonhoverface: + case ColorID::MozButtonactiveface: + case ColorID::MozButtondisabledface: + case ColorID::MozColheader: + case ColorID::MozColheaderhover: + case ColorID::MozColheaderactive: + idx = COLOR_BTNFACE; + break; + case ColorID::Buttonhighlight: + idx = COLOR_BTNHIGHLIGHT; + break; + case ColorID::Buttonshadow: + idx = COLOR_BTNSHADOW; + break; + case ColorID::Buttontext: + case ColorID::MozButtonhovertext: + case ColorID::MozButtonactivetext: + idx = COLOR_BTNTEXT; + break; + case ColorID::MozCellhighlighttext: + aColor = NS_RGB(0, 0, 0); + return NS_OK; + case ColorID::MozCellhighlight: + aColor = NS_RGB(206, 206, 206); + return NS_OK; + case ColorID::Graytext: + idx = COLOR_GRAYTEXT; + break; + case ColorID::MozMenubarhovertext: + if (UseNonNativeMenuColors(aScheme)) { + aColor = kNonNativeMenuText; + return NS_OK; + } + [[fallthrough]]; + case ColorID::MozMenuhovertext: + if (UseNonNativeMenuColors(aScheme)) { + aColor = kNonNativeMenuText; + return NS_OK; + } + if (mColorMenuHoverText) { + aColor = *mColorMenuHoverText; + return NS_OK; + } + idx = COLOR_HIGHLIGHTTEXT; + break; + case ColorID::MozMenuhover: + MOZ_ASSERT(UseNonNativeMenuColors(aScheme)); + aColor = NS_RGB(0xe0, 0xe0, 0xe6); + return NS_OK; + case ColorID::MozMenuhoverdisabled: + if (UseNonNativeMenuColors(aScheme)) { + aColor = NS_RGB(0xf0, 0xf0, 0xf3); + return NS_OK; + } + aColor = NS_TRANSPARENT; + return NS_OK; + case ColorID::Infobackground: + idx = COLOR_INFOBK; + break; + case ColorID::Infotext: + idx = COLOR_INFOTEXT; + break; + case ColorID::Menu: + if (UseNonNativeMenuColors(aScheme)) { + aColor = NS_RGB(0xf9, 0xf9, 0xfb); + return NS_OK; + } + idx = COLOR_MENU; + break; + case ColorID::Menutext: + if (UseNonNativeMenuColors(aScheme)) { + aColor = kNonNativeMenuText; + return NS_OK; + } + idx = COLOR_MENUTEXT; + break; + case ColorID::Scrollbar: + idx = COLOR_SCROLLBAR; + break; + case ColorID::Threeddarkshadow: + idx = COLOR_3DDKSHADOW; + break; + case ColorID::Threedface: + idx = COLOR_3DFACE; + break; + case ColorID::Threedhighlight: + idx = COLOR_3DHIGHLIGHT; + break; + case ColorID::Threedlightshadow: + case ColorID::Buttonborder: + case ColorID::MozDisabledfield: + case ColorID::MozSidebarborder: + idx = COLOR_3DLIGHT; + break; + case ColorID::Threedshadow: + idx = COLOR_3DSHADOW; + break; + case ColorID::Window: + idx = COLOR_WINDOW; + break; + case ColorID::Windowframe: + idx = COLOR_WINDOWFRAME; + break; + case ColorID::Windowtext: + idx = COLOR_WINDOWTEXT; + break; + case ColorID::MozEventreerow: + case ColorID::MozOddtreerow: + case ColorID::Field: + case ColorID::MozSidebar: + case ColorID::MozCombobox: + idx = COLOR_WINDOW; + break; + case ColorID::Fieldtext: + case ColorID::MozSidebartext: + case ColorID::MozComboboxtext: + idx = COLOR_WINDOWTEXT; + break; + case ColorID::MozHeaderbar: + case ColorID::MozHeaderbarinactive: + case ColorID::MozDialog: + idx = COLOR_3DFACE; + break; + case ColorID::Accentcolor: + aColor = mColorAccent; + return NS_OK; + case ColorID::Accentcolortext: + aColor = mColorAccentText; + return NS_OK; + case ColorID::MozHeaderbartext: + case ColorID::MozHeaderbarinactivetext: + case ColorID::MozDialogtext: + case ColorID::MozColheadertext: + case ColorID::MozColheaderhovertext: + case ColorID::MozColheaderactivetext: + idx = COLOR_WINDOWTEXT; + break; + case ColorID::MozNativehyperlinktext: + idx = COLOR_HOTLIGHT; + break; + case ColorID::Marktext: + case ColorID::Mark: + case ColorID::SpellCheckerUnderline: + aColor = GetStandinForNativeColor(aID, aScheme); + return NS_OK; + default: + idx = COLOR_WINDOW; + res = NS_ERROR_FAILURE; + break; + } + + aColor = GetColorForSysColorIndex(idx); + + return res; +} + +nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) { + EnsureInit(); + nsresult res = NS_OK; + + switch (aID) { + case IntID::ScrollButtonLeftMouseButtonAction: + aResult = 0; + break; + case IntID::ScrollButtonMiddleMouseButtonAction: + case IntID::ScrollButtonRightMouseButtonAction: + aResult = 3; + break; + case IntID::CaretBlinkTime: + aResult = static_cast<int32_t>(::GetCaretBlinkTime()); + break; + case IntID::CaretBlinkCount: { + int32_t timeout = GetSystemParam(SPI_GETCARETTIMEOUT, 5000); + auto blinkTime = ::GetCaretBlinkTime(); + if (timeout <= 0 || blinkTime <= 0) { + aResult = -1; + break; + } + // 2 * blinkTime because this integer is a full blink cycle. + aResult = std::ceil(float(timeout) / (2.0f * float(blinkTime))); + break; + } + + case IntID::CaretWidth: + aResult = 1; + break; + case IntID::ShowCaretDuringSelection: + aResult = 0; + break; + case IntID::SelectTextfieldsOnKeyFocus: + // Select textfield content when focused by kbd + // used by EventStateManager::sTextfieldSelectModel + aResult = 1; + break; + case IntID::SubmenuDelay: + // This will default to the Windows' default + // (400ms) on error. + aResult = GetSystemParam(SPI_GETMENUSHOWDELAY, 400); + break; + case IntID::TooltipDelay: + aResult = 500; + break; + case IntID::MenusCanOverlapOSBar: + // we want XUL popups to be able to overlap the task bar. + aResult = 1; + break; + case IntID::DragThresholdX: + // The system metric is the number of pixels at which a drag should + // start. Our look and feel metric is the number of pixels you can + // move before starting a drag, so subtract 1. + aResult = ::GetSystemMetrics(SM_CXDRAG) - 1; + break; + case IntID::DragThresholdY: + aResult = ::GetSystemMetrics(SM_CYDRAG) - 1; + break; + case IntID::UseAccessibilityTheme: + // High contrast is a misnomer under Win32 -- any theme can be used with + // it, e.g. normal contrast with large fonts, low contrast, etc. The high + // contrast flag really means -- use this theme and don't override it. + aResult = nsUXThemeData::IsHighContrastOn(); + break; + case IntID::ScrollArrowStyle: + aResult = eScrollArrowStyle_Single; + break; + case IntID::TreeOpenDelay: + aResult = 1000; + break; + case IntID::TreeCloseDelay: + aResult = 0; + break; + case IntID::TreeLazyScrollDelay: + aResult = 150; + break; + case IntID::TreeScrollDelay: + aResult = 100; + break; + case IntID::TreeScrollLinesMax: + aResult = 3; + break; + case IntID::WindowsAccentColorInTitlebar: { + aResult = mTitlebarColors.mUseAccent; + } break; + case IntID::AlertNotificationOrigin: + aResult = 0; + { + // Get task bar window handle + HWND shellWindow = FindWindowW(L"Shell_TrayWnd", nullptr); + + if (shellWindow != nullptr) { + // Determine position + APPBARDATA appBarData; + appBarData.hWnd = shellWindow; + appBarData.cbSize = sizeof(appBarData); + if (SHAppBarMessage(ABM_GETTASKBARPOS, &appBarData)) { + // Set alert origin as a bit field - see LookAndFeel.h + // 0 represents bottom right, sliding vertically. + switch (appBarData.uEdge) { + case ABE_LEFT: + aResult = NS_ALERT_HORIZONTAL | NS_ALERT_LEFT; + break; + case ABE_RIGHT: + aResult = NS_ALERT_HORIZONTAL; + break; + case ABE_TOP: + aResult = NS_ALERT_TOP; + [[fallthrough]]; + case ABE_BOTTOM: + // If the task bar is right-to-left, + // move the origin to the left + if (::GetWindowLong(shellWindow, GWL_EXSTYLE) & WS_EX_LAYOUTRTL) + aResult |= NS_ALERT_LEFT; + break; + } + } + } + } + break; + case IntID::IMERawInputUnderlineStyle: + case IntID::IMEConvertedTextUnderlineStyle: + aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dashed); + break; + case IntID::IMESelectedRawTextUnderlineStyle: + case IntID::IMESelectedConvertedTextUnderline: + aResult = static_cast<int32_t>(StyleTextDecorationStyle::None); + break; + case IntID::SpellCheckerUnderlineStyle: + aResult = static_cast<int32_t>(StyleTextDecorationStyle::Wavy); + break; + case IntID::ScrollbarButtonAutoRepeatBehavior: + aResult = 0; + break; + case IntID::SwipeAnimationEnabled: + // Forcibly enable the swipe animation on Windows. It doesn't matter on + // platforms where "Drag two fingers to scroll" isn't supported since on + // the platforms we will never generate any swipe gesture events. + aResult = 1; + break; + case IntID::UseOverlayScrollbars: + aResult = WindowsUIUtils::ComputeOverlayScrollbars(); + break; + case IntID::AllowOverlayScrollbarsOverlap: + aResult = 0; + break; + case IntID::ScrollbarDisplayOnMouseMove: + aResult = 1; + break; + case IntID::ScrollbarFadeBeginDelay: + aResult = 2500; + break; + case IntID::ScrollbarFadeDuration: + aResult = 350; + break; + case IntID::ContextMenuOffsetVertical: + case IntID::ContextMenuOffsetHorizontal: + aResult = 2; + break; + case IntID::SystemUsesDarkTheme: + aResult = SystemWantsDarkTheme(); + break; + case IntID::SystemScrollbarSize: + aResult = std::max(WinUtils::GetSystemMetricsForDpi(SM_CXVSCROLL, 96), + WinUtils::GetSystemMetricsForDpi(SM_CXHSCROLL, 96)); + break; + case IntID::PrefersReducedMotion: { + BOOL enable = TRUE; + ::SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &enable, 0); + aResult = !enable; + break; + } + case IntID::PrefersReducedTransparency: { + // Prefers reduced transparency if the option for "Transparency Effects" + // is disabled + aResult = !WindowsUIUtils::ComputeTransparencyEffects(); + break; + } + case IntID::InvertedColors: { + // Color filter values + // 1: Inverted + // 2: Grayscale inverted + aResult = mCurrentColorFilter == 1 || mCurrentColorFilter == 2; + break; + } + case IntID::PrimaryPointerCapabilities: { + aResult = static_cast<int32_t>( + widget::WinUtils::GetPrimaryPointerCapabilities()); + break; + } + case IntID::AllPointerCapabilities: { + aResult = + static_cast<int32_t>(widget::WinUtils::GetAllPointerCapabilities()); + break; + } + case IntID::TouchDeviceSupportPresent: + aResult = !!WinUtils::IsTouchDeviceSupportPresent(); + break; + case IntID::PanelAnimations: + aResult = 1; + break; + case IntID::HideCursorWhileTyping: { + BOOL enable = TRUE; + ::SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &enable, 0); + aResult = enable; + break; + } + default: + aResult = 0; + res = NS_ERROR_FAILURE; + } + return res; +} + +nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) { + nsresult res = NS_OK; + + switch (aID) { + case FloatID::IMEUnderlineRelativeSize: + aResult = 1.0f; + break; + case FloatID::SpellCheckerUnderlineRelativeSize: + aResult = 1.0f; + break; + case FloatID::TextScaleFactor: + aResult = WindowsUIUtils::ComputeTextScaleFactor(); + break; + default: + aResult = -1.0; + res = NS_ERROR_FAILURE; + } + return res; +} + +LookAndFeelFont nsLookAndFeel::GetLookAndFeelFontInternal( + const LOGFONTW& aLogFont, bool aUseShellDlg) { + LookAndFeelFont result{}; + + result.haveFont() = false; + + // Get scaling factor from physical to logical pixels + double pixelScale = + 1.0 / WinUtils::SystemScaleFactor() / LookAndFeel::GetTextScaleFactor(); + + // The lfHeight is in pixels, and it needs to be adjusted for the + // device it will be displayed on. + // Screens and Printers will differ in DPI + // + // So this accounts for the difference in the DeviceContexts + // The pixelScale will typically be 1.0 for the screen + // (though larger for hi-dpi screens where the Windows resolution + // scale factor is 125% or 150% or even more), and could be + // any value when going to a printer, for example pixelScale is + // 6.25 when going to a 600dpi printer. + float pixelHeight = -aLogFont.lfHeight; + if (pixelHeight < 0) { + nsAutoFont hFont(::CreateFontIndirectW(&aLogFont)); + if (!hFont) { + return result; + } + + nsAutoHDC dc(::GetDC(nullptr)); + HGDIOBJ hObject = ::SelectObject(dc, hFont); + TEXTMETRIC tm; + ::GetTextMetrics(dc, &tm); + ::SelectObject(dc, hObject); + + pixelHeight = tm.tmAscent; + } + + pixelHeight *= pixelScale; + + // we have problem on Simplified Chinese system because the system + // report the default font size is 8 points. but if we use 8, the text + // display very ugly. force it to be at 9 points (12 pixels) on that + // system (cp936), but leave other sizes alone. + if (pixelHeight < 12 && ::GetACP() == 936) { + pixelHeight = 12; + } + + result.haveFont() = true; + + if (aUseShellDlg) { + result.name() = u"MS Shell Dlg 2"_ns; + } else { + result.name() = aLogFont.lfFaceName; + } + + result.size() = pixelHeight; + result.italic() = !!aLogFont.lfItalic; + // FIXME: Other weights? + result.weight() = + ((aLogFont.lfWeight == FW_BOLD) ? FontWeight::BOLD : FontWeight::NORMAL) + .ToFloat(); + + return result; +} + +LookAndFeelFont nsLookAndFeel::GetLookAndFeelFont(LookAndFeel::FontID anID) { + LookAndFeelFont result{}; + + result.haveFont() = false; + + // FontID::Icon is handled differently than the others + if (anID == LookAndFeel::FontID::Icon) { + LOGFONTW logFont; + if (::SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(logFont), + (PVOID)&logFont, 0)) { + result = GetLookAndFeelFontInternal(logFont, false); + } + return result; + } + + NONCLIENTMETRICSW ncm; + ncm.cbSize = sizeof(NONCLIENTMETRICSW); + if (!::SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(ncm), + (PVOID)&ncm, 0)) { + return result; + } + + switch (anID) { + case LookAndFeel::FontID::Menu: + case LookAndFeel::FontID::MozPullDownMenu: + result = GetLookAndFeelFontInternal(ncm.lfMenuFont, false); + break; + case LookAndFeel::FontID::Caption: + result = GetLookAndFeelFontInternal(ncm.lfCaptionFont, false); + break; + case LookAndFeel::FontID::SmallCaption: + result = GetLookAndFeelFontInternal(ncm.lfSmCaptionFont, false); + break; + case LookAndFeel::FontID::StatusBar: + result = GetLookAndFeelFontInternal(ncm.lfStatusFont, false); + break; + case LookAndFeel::FontID::MozButton: + case LookAndFeel::FontID::MozField: + case LookAndFeel::FontID::MozList: + // XXX It's not clear to me whether this is exactly the right + // set of LookAndFeel values to map to the dialog font; we may + // want to add or remove cases here after reviewing the visual + // results under various Windows versions. + result = GetLookAndFeelFontInternal(ncm.lfMessageFont, true); + break; + default: + result = GetLookAndFeelFontInternal(ncm.lfMessageFont, false); + break; + } + + return result; +} + +bool nsLookAndFeel::NativeGetFont(LookAndFeel::FontID anID, nsString& aFontName, + gfxFontStyle& aFontStyle) { + LookAndFeelFont font = GetLookAndFeelFont(anID); + return LookAndFeelFontToStyle(font, aFontName, aFontStyle); +} + +/* virtual */ +char16_t nsLookAndFeel::GetPasswordCharacterImpl() { +#define UNICODE_BLACK_CIRCLE_CHAR 0x25cf + return UNICODE_BLACK_CIRCLE_CHAR; +} + +static nscolor GetAccentColorText(const nscolor aAccentColor) { + // We want the color that we return for text that will be drawn over + // a background that has the accent color to have good contrast with + // the accent color. Windows itself uses either white or black text + // depending on how light or dark the accent color is. We do the same + // here based on the luminance of the accent color with a threshhold + // value. This algorithm should match what Windows does. It comes from: + // + // https://docs.microsoft.com/en-us/windows/uwp/style/color + float luminance = (NS_GET_R(aAccentColor) * 2 + NS_GET_G(aAccentColor) * 5 + + NS_GET_B(aAccentColor)) / + 8; + return luminance <= 128 ? NS_RGB(255, 255, 255) : NS_RGB(0, 0, 0); +} + +static Maybe<nscolor> GetAccentColorText(const Maybe<nscolor>& aAccentColor) { + if (!aAccentColor) { + return Nothing(); + } + return Some(GetAccentColorText(*aAccentColor)); +} + +nscolor nsLookAndFeel::GetColorForSysColorIndex(int index) { + MOZ_ASSERT(index >= SYS_COLOR_MIN && index <= SYS_COLOR_MAX); + return mSysColorTable[index - SYS_COLOR_MIN]; +} + +auto nsLookAndFeel::ComputeTitlebarColors() -> TitlebarColors { + TitlebarColors result; + + // Start with the native / non-accent-in-titlebar colors. + result.mActiveLight = {GetColorForSysColorIndex(COLOR_ACTIVECAPTION), + GetColorForSysColorIndex(COLOR_CAPTIONTEXT), + GetColorForSysColorIndex(COLOR_ACTIVEBORDER)}; + + result.mInactiveLight = {GetColorForSysColorIndex(COLOR_INACTIVECAPTION), + GetColorForSysColorIndex(COLOR_INACTIVECAPTIONTEXT), + GetColorForSysColorIndex(COLOR_INACTIVEBORDER)}; + + if (!nsUXThemeData::IsHighContrastOn()) { + // Use our non-native colors. + result.mActiveLight = { + GetStandinForNativeColor(ColorID::Activecaption, ColorScheme::Light), + GetStandinForNativeColor(ColorID::Captiontext, ColorScheme::Light), + GetStandinForNativeColor(ColorID::Activeborder, ColorScheme::Light)}; + result.mInactiveLight = { + GetStandinForNativeColor(ColorID::Inactivecaption, ColorScheme::Light), + GetStandinForNativeColor(ColorID::Inactivecaptiontext, + ColorScheme::Light), + GetStandinForNativeColor(ColorID::Inactiveborder, ColorScheme::Light)}; + } + + // Our dark colors are always non-native. + result.mActiveDark = {*GenericDarkColor(ColorID::Activecaption), + *GenericDarkColor(ColorID::Captiontext), + *GenericDarkColor(ColorID::Activeborder)}; + result.mInactiveDark = {*GenericDarkColor(ColorID::Inactivecaption), + *GenericDarkColor(ColorID::Inactivecaptiontext), + *GenericDarkColor(ColorID::Inactiveborder)}; + + // TODO(bug 1825241): Somehow get notified when this changes? Hopefully the + // sys color notification is enough. + WinRegistry::Key dwmKey(HKEY_CURRENT_USER, + u"SOFTWARE\\Microsoft\\Windows\\DWM"_ns, + WinRegistry::KeyMode::QueryValue); + if (NS_WARN_IF(!dwmKey)) { + return result; + } + + // The order of the color components in the DWORD stored in the registry + // happens to be the same order as we store the components in nscolor + // so we can just assign directly here. + result.mAccent = dwmKey.GetValueAsDword(u"AccentColor"_ns); + result.mAccentText = GetAccentColorText(result.mAccent); + + if (!result.mAccent) { + return result; + } + + result.mAccentInactive = dwmKey.GetValueAsDword(u"AccentColorInactive"_ns); + result.mAccentInactiveText = GetAccentColorText(result.mAccentInactive); + + // The ColorPrevalence value is set to 1 when the "Show color on title bar" + // setting in the Color section of Window's Personalization settings is + // turned on. + result.mUseAccent = + dwmKey.GetValueAsDword(u"ColorPrevalence"_ns).valueOr(0) == 1; + if (!result.mUseAccent) { + return result; + } + + // TODO(emilio): Consider reading ColorizationColorBalance to compute a + // more correct border color, see [1]. Though for opaque accent colors this + // isn't needed. + // + // [1]: + // https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:ui/color/win/accent_color_observer.cc;l=42;drc=9d4eb7ed25296abba8fd525a6bdd0fdbf4bcdd9f + result.mActiveDark.mBorder = result.mActiveLight.mBorder = *result.mAccent; + result.mInactiveDark.mBorder = result.mInactiveLight.mBorder = + result.mAccentInactive.valueOr(NS_RGB(57, 57, 57)); + result.mActiveLight.mBg = result.mActiveDark.mBg = *result.mAccent; + result.mActiveLight.mFg = result.mActiveDark.mFg = *result.mAccentText; + if (result.mAccentInactive) { + result.mInactiveLight.mBg = result.mInactiveDark.mBg = + *result.mAccentInactive; + result.mInactiveLight.mFg = result.mInactiveDark.mFg = + *result.mAccentInactiveText; + } else { + // This is hand-picked to .8 to change the accent color a bit but not too + // much. + constexpr uint8_t kBgAlpha = 208; + const auto BlendWithAlpha = [](nscolor aBg, nscolor aFg, + uint8_t aAlpha) -> nscolor { + return NS_ComposeColors( + aBg, NS_RGBA(NS_GET_R(aFg), NS_GET_G(aFg), NS_GET_B(aFg), aAlpha)); + }; + result.mInactiveLight.mBg = + BlendWithAlpha(NS_RGB(255, 255, 255), *result.mAccent, kBgAlpha); + result.mInactiveDark.mBg = + BlendWithAlpha(NS_RGB(0, 0, 0), *result.mAccent, kBgAlpha); + result.mInactiveLight.mFg = result.mInactiveDark.mFg = *result.mAccentText; + } + return result; +} + +void nsLookAndFeel::EnsureInit() { + if (mInitialized) { + return; + } + mInitialized = true; + + mColorMenuHoverText = + ::GetColorFromTheme(eUXMenu, MENU_POPUPITEM, MPI_HOT, TMT_TEXTCOLOR); + + // Fill out the sys color table. + for (int i = SYS_COLOR_MIN; i <= SYS_COLOR_MAX; ++i) { + mSysColorTable[i - SYS_COLOR_MIN] = [&] { + if (auto c = WindowsUIUtils::GetSystemColor(ColorScheme::Light, i)) { + return *c; + } + DWORD color = ::GetSysColor(i); + return COLOREF_2_NSRGB(color); + }(); + } + + mDarkHighlight = + WindowsUIUtils::GetSystemColor(ColorScheme::Dark, COLOR_HIGHLIGHT); + mDarkHighlightText = + WindowsUIUtils::GetSystemColor(ColorScheme::Dark, COLOR_HIGHLIGHTTEXT); + + mTitlebarColors = ComputeTitlebarColors(); + + mColorAccent = [&] { + if (auto accent = WindowsUIUtils::GetAccentColor()) { + return *accent; + } + // Try the titlebar accent as a fallback. + if (mTitlebarColors.mAccent) { + return *mTitlebarColors.mAccent; + } + // Seems to be the default color (hardcoded because of bug 1065998) + return NS_RGB(0, 120, 215); + }(); + mColorAccentText = GetAccentColorText(mColorAccent); + + if (!mColorFilterWatcher) { + WinRegistry::Key key( + HKEY_CURRENT_USER, u"Software\\Microsoft\\ColorFiltering"_ns, + WinRegistry::KeyMode::QueryValue | WinRegistry::KeyMode::Notify); + if (key) { + mColorFilterWatcher = MakeUnique<WinRegistry::KeyWatcher>( + std::move(key), GetCurrentSerialEventTarget(), [this] { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + if (mCurrentColorFilter != SystemColorFilter()) { + LookAndFeel::NotifyChangedAllWindows( + widget::ThemeChangeKind::MediaQueriesOnly); + } + }); + } + } + mCurrentColorFilter = SystemColorFilter(); + + RecordTelemetry(); +} diff --git a/widget/windows/nsLookAndFeel.h b/widget/windows/nsLookAndFeel.h new file mode 100644 index 0000000000..d19aa91329 --- /dev/null +++ b/widget/windows/nsLookAndFeel.h @@ -0,0 +1,124 @@ +/* -*- 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 __nsLookAndFeel +#define __nsLookAndFeel + +#include <windows.h> + +#include "nsXPLookAndFeel.h" +#include "gfxFont.h" + +/* + * Gesture System Metrics + */ +#ifndef SM_DIGITIZER +# define SM_DIGITIZER 94 +# define TABLET_CONFIG_NONE 0x00000000 +# define NID_INTEGRATED_TOUCH 0x00000001 +# define NID_EXTERNAL_TOUCH 0x00000002 +# define NID_INTEGRATED_PEN 0x00000004 +# define NID_EXTERNAL_PEN 0x00000008 +# define NID_MULTI_INPUT 0x00000040 +# define NID_READY 0x00000080 +#endif + +/* + * Tablet mode detection + */ +#ifndef SM_SYSTEMDOCKED +# define SM_CONVERTIBLESLATEMODE 0x00002003 +# define SM_SYSTEMDOCKED 0x00002004 +#endif + +/* + * Color constant inclusive bounds for GetSysColor + */ +#define SYS_COLOR_MIN 0 +#define SYS_COLOR_MAX 30 +#define SYS_COLOR_COUNT (SYS_COLOR_MAX - SYS_COLOR_MIN + 1) + +namespace mozilla::widget::WinRegistry { +class KeyWatcher; +} + +class nsLookAndFeel final : public nsXPLookAndFeel { + public: + nsLookAndFeel(); + virtual ~nsLookAndFeel(); + + void NativeInit() final; + void RefreshImpl() override; + nsresult NativeGetInt(IntID, int32_t& aResult) override; + nsresult NativeGetFloat(FloatID, float& aResult) override; + nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aResult) override; + bool NativeGetFont(FontID aID, nsString& aFontName, + gfxFontStyle& aFontStyle) override; + char16_t GetPasswordCharacterImpl() override; + + private: + struct TitlebarColors { + // NOTE: These are the DWM accent colors, which might not match the + // UISettings/UWP accent color in some cases, see bug 1796730. + mozilla::Maybe<nscolor> mAccent; + mozilla::Maybe<nscolor> mAccentText; + mozilla::Maybe<nscolor> mAccentInactive; + mozilla::Maybe<nscolor> mAccentInactiveText; + + bool mUseAccent = false; + + struct Set { + nscolor mBg = 0; + nscolor mFg = 0; + nscolor mBorder = 0; + }; + + Set mActiveLight; + Set mActiveDark; + + Set mInactiveLight; + Set mInactiveDark; + + const Set& Get(mozilla::ColorScheme aScheme, bool aActive) const { + if (aScheme == mozilla::ColorScheme::Dark) { + return aActive ? mActiveDark : mInactiveDark; + } + return aActive ? mActiveLight : mInactiveLight; + } + }; + + TitlebarColors ComputeTitlebarColors(); + + nscolor GetColorForSysColorIndex(int index); + + LookAndFeelFont GetLookAndFeelFontInternal(const LOGFONTW& aLogFont, + bool aUseShellDlg); + + uint32_t SystemColorFilter(); + + LookAndFeelFont GetLookAndFeelFont(LookAndFeel::FontID anID); + + // Cached colors and flags indicating success in their retrieval. + mozilla::Maybe<nscolor> mColorMenuHoverText; + + mozilla::Maybe<nscolor> mDarkHighlight; + mozilla::Maybe<nscolor> mDarkHighlightText; + + TitlebarColors mTitlebarColors; + + nscolor mColorAccent = 0; + nscolor mColorAccentText = 0; + + nscolor mSysColorTable[SYS_COLOR_COUNT]; + + mozilla::UniquePtr<mozilla::widget::WinRegistry::KeyWatcher> + mColorFilterWatcher; + uint32_t mCurrentColorFilter = 0; + + bool mInitialized = false; + void EnsureInit(); +}; + +#endif diff --git a/widget/windows/nsNativeDragSource.cpp b/widget/windows/nsNativeDragSource.cpp new file mode 100644 index 0000000000..ca5459e9df --- /dev/null +++ b/widget/windows/nsNativeDragSource.cpp @@ -0,0 +1,98 @@ +/* -*- 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 "nsNativeDragSource.h" +#include <stdio.h> +#include "nsISupportsImpl.h" +#include "nsString.h" +#include "nsToolkit.h" +#include "nsWidgetsCID.h" +#include "nsIDragService.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/DataTransfer.h" + +/* + * class nsNativeDragSource + */ +nsNativeDragSource::nsNativeDragSource( + mozilla::dom::DataTransfer* aDataTransfer) + : m_cRef(0), m_hCursor(nullptr), mUserCancelled(false) { + mDataTransfer = aDataTransfer; +} + +nsNativeDragSource::~nsNativeDragSource() {} + +STDMETHODIMP +nsNativeDragSource::QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + + if (IID_IUnknown == riid || IID_IDropSource == riid) *ppv = this; + + if (nullptr != *ppv) { + ((LPUNKNOWN)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +STDMETHODIMP_(ULONG) +nsNativeDragSource::AddRef(void) { + ++m_cRef; + NS_LOG_ADDREF(this, m_cRef, "nsNativeDragSource", sizeof(*this)); + return m_cRef; +} + +STDMETHODIMP_(ULONG) +nsNativeDragSource::Release(void) { + --m_cRef; + NS_LOG_RELEASE(this, m_cRef, "nsNativeDragSource"); + if (0 != m_cRef) return m_cRef; + + delete this; + return 0; +} + +STDMETHODIMP +nsNativeDragSource::QueryContinueDrag(BOOL fEsc, DWORD grfKeyState) { + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + if (dragService) { + DWORD pos = ::GetMessagePos(); + dragService->DragMoved(GET_X_LPARAM(pos), GET_Y_LPARAM(pos)); + } + + if (fEsc) { + mUserCancelled = true; + return DRAGDROP_S_CANCEL; + } + + if (!(grfKeyState & MK_LBUTTON) || (grfKeyState & MK_RBUTTON)) + return DRAGDROP_S_DROP; + + return S_OK; +} + +STDMETHODIMP +nsNativeDragSource::GiveFeedback(DWORD dwEffect) { + // For drags involving tabs, we do some custom work with cursors. + if (mDataTransfer) { + nsAutoString cursor; + mDataTransfer->GetMozCursor(cursor); + if (cursor.EqualsLiteral("default")) { + m_hCursor = ::LoadCursor(0, IDC_ARROW); + } else { + m_hCursor = nullptr; + } + } + + if (m_hCursor) { + ::SetCursor(m_hCursor); + return S_OK; + } + + // Let the system choose which cursor to apply. + return DRAGDROP_S_USEDEFAULTCURSORS; +} diff --git a/widget/windows/nsNativeDragSource.h b/widget/windows/nsNativeDragSource.h new file mode 100644 index 0000000000..a8e37b90a8 --- /dev/null +++ b/widget/windows/nsNativeDragSource.h @@ -0,0 +1,68 @@ +/* -*- 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 _nsNativeDragSource_h_ +#define _nsNativeDragSource_h_ + +#include "nscore.h" +#include <ole2.h> +#include <oleidl.h> +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { +namespace dom { +class DataTransfer; +} // namespace dom +} // namespace mozilla + +// class nsIDragSource; + +/* + * nsNativeDragSource implements the IDropSource interface and gets + * most of its behavior from the associated adapter (m_dragDrop). + */ +class nsNativeDragSource final : public IDropSource { + public: + // construct an nsNativeDragSource referencing adapter + // nsNativeDragSource(nsIDragSource * adapter); + explicit nsNativeDragSource(mozilla::dom::DataTransfer* aDataTransfer); + ~nsNativeDragSource(); + + // IUnknown methods - see iunknown.h for documentation + + STDMETHODIMP QueryInterface(REFIID, void**); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IDropSource methods - see idropsrc.h for documentation + + // Return DRAGDROP_S_USEDEFAULTCURSORS if this object lets OLE provide + // default cursors, otherwise return NOERROR. This method gets called in + // response to changes that the target makes to dEffect (DragEnter, + // DragOver). + STDMETHODIMP GiveFeedback(DWORD dEffect); + + // This method gets called if there is any change in the mouse or key + // state. Return DRAGDROP_S_CANCEL to stop the drag, DRAGDROP_S_DROP + // to execute the drop, otherwise NOERROR. + STDMETHODIMP QueryContinueDrag(BOOL fESC, DWORD grfKeyState); + + bool UserCancelled() { return mUserCancelled; } + + protected: + // Reference count + ULONG m_cRef; + + // Data object, hold information about cursor state + RefPtr<mozilla::dom::DataTransfer> mDataTransfer; + + // Custom drag cursor + HCURSOR m_hCursor; + + // true if the user cancelled the drag by pressing escape + bool mUserCancelled; +}; + +#endif // _nsNativeDragSource_h_ diff --git a/widget/windows/nsNativeDragTarget.cpp b/widget/windows/nsNativeDragTarget.cpp new file mode 100644 index 0000000000..b615a3e4bc --- /dev/null +++ b/widget/windows/nsNativeDragTarget.cpp @@ -0,0 +1,472 @@ +/* -*- 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 <stdio.h> +#include "nsIDragService.h" +#include "nsWidgetsCID.h" +#include "nsNativeDragTarget.h" +#include "nsDragService.h" +#include "nsINode.h" +#include "nsCOMPtr.h" + +#include "nsIWidget.h" +#include "nsWindow.h" +#include "nsClipboard.h" +#include "KeyboardLayout.h" + +#include "mozilla/MouseEvents.h" + +using namespace mozilla; +using namespace mozilla::widget; + +// This is cached for Leave notification +static POINTL gDragLastPoint; + +bool nsNativeDragTarget::gDragImageChanged = false; + +/* + * class nsNativeDragTarget + */ +nsNativeDragTarget::nsNativeDragTarget(nsIWidget* aWidget) + : m_cRef(0), + mEffectsAllowed(DROPEFFECT_MOVE | DROPEFFECT_COPY | DROPEFFECT_LINK), + mEffectsPreferred(DROPEFFECT_NONE), + mTookOwnRef(false), + mWidget(aWidget), + mDropTargetHelper(nullptr) { + mHWnd = (HWND)mWidget->GetNativeData(NS_NATIVE_WINDOW); + + mDragService = do_GetService("@mozilla.org/widget/dragservice;1"); +} + +nsNativeDragTarget::~nsNativeDragTarget() { + if (mDropTargetHelper) { + mDropTargetHelper->Release(); + mDropTargetHelper = nullptr; + } +} + +// IUnknown methods - see iunknown.h for documentation +STDMETHODIMP +nsNativeDragTarget::QueryInterface(REFIID riid, void** ppv) { + *ppv = nullptr; + + if (IID_IUnknown == riid || IID_IDropTarget == riid) *ppv = this; + + if (nullptr != *ppv) { + ((LPUNKNOWN)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +STDMETHODIMP_(ULONG) +nsNativeDragTarget::AddRef(void) { + ++m_cRef; + NS_LOG_ADDREF(this, m_cRef, "nsNativeDragTarget", sizeof(*this)); + return m_cRef; +} + +STDMETHODIMP_(ULONG) nsNativeDragTarget::Release(void) { + --m_cRef; + NS_LOG_RELEASE(this, m_cRef, "nsNativeDragTarget"); + if (0 != m_cRef) return m_cRef; + + delete this; + return 0; +} + +void nsNativeDragTarget::GetGeckoDragAction(DWORD grfKeyState, + LPDWORD pdwEffect, + uint32_t* aGeckoAction) { + // If a window is disabled or a modal window is on top of it + // (which implies it is disabled), then we should not allow dropping. + if (!mWidget->IsEnabled()) { + *pdwEffect = DROPEFFECT_NONE; + *aGeckoAction = nsIDragService::DRAGDROP_ACTION_NONE; + return; + } + + // If the user explicitly uses a modifier key, they want the associated action + // Shift + Control -> LINK, Shift -> MOVE, Ctrl -> COPY + DWORD desiredEffect = DROPEFFECT_NONE; + if ((grfKeyState & MK_CONTROL) && (grfKeyState & MK_SHIFT)) { + desiredEffect = DROPEFFECT_LINK; + } else if (grfKeyState & MK_SHIFT) { + desiredEffect = DROPEFFECT_MOVE; + } else if (grfKeyState & MK_CONTROL) { + desiredEffect = DROPEFFECT_COPY; + } + + // Determine the desired effect from what is allowed and preferred. + if (!(desiredEffect &= mEffectsAllowed)) { + // No modifier key effect is set which is also allowed, check + // the preference of the data. + desiredEffect = mEffectsPreferred & mEffectsAllowed; + if (!desiredEffect) { + // No preference is set, so just fall back to the allowed effect itself + desiredEffect = mEffectsAllowed; + } + } + + // Otherwise we should specify the first available effect + // from MOVE, COPY, or LINK. + if (desiredEffect & DROPEFFECT_MOVE) { + *pdwEffect = DROPEFFECT_MOVE; + *aGeckoAction = nsIDragService::DRAGDROP_ACTION_MOVE; + } else if (desiredEffect & DROPEFFECT_COPY) { + *pdwEffect = DROPEFFECT_COPY; + *aGeckoAction = nsIDragService::DRAGDROP_ACTION_COPY; + } else if (desiredEffect & DROPEFFECT_LINK) { + *pdwEffect = DROPEFFECT_LINK; + *aGeckoAction = nsIDragService::DRAGDROP_ACTION_LINK; + } else { + *pdwEffect = DROPEFFECT_NONE; + *aGeckoAction = nsIDragService::DRAGDROP_ACTION_NONE; + } +} + +inline bool IsKeyDown(char key) { return GetKeyState(key) < 0; } + +void nsNativeDragTarget::DispatchDragDropEvent(EventMessage aEventMessage, + const POINTL& aPT) { + WidgetDragEvent event(true, aEventMessage, mWidget); + + nsWindow* win = static_cast<nsWindow*>(mWidget); + win->InitEvent(event); + POINT cpos; + + cpos.x = aPT.x; + cpos.y = aPT.y; + + if (mHWnd != nullptr) { + ::ScreenToClient(mHWnd, &cpos); + event.mRefPoint = LayoutDeviceIntPoint(cpos.x, cpos.y); + } else { + event.mRefPoint = LayoutDeviceIntPoint(0, 0); + } + + ModifierKeyState modifierKeyState; + modifierKeyState.InitInputEvent(event); + + event.mInputSource = + static_cast<nsBaseDragService*>(mDragService.get())->GetInputSource(); + + mWidget->DispatchInputEvent(&event); +} + +void nsNativeDragTarget::ProcessDrag(EventMessage aEventMessage, + DWORD grfKeyState, POINTL ptl, + DWORD* pdwEffect) { + // Before dispatching the event make sure we have the correct drop action set + uint32_t geckoAction; + GetGeckoDragAction(grfKeyState, pdwEffect, &geckoAction); + + // Set the current action into the Gecko specific type + nsCOMPtr<nsIDragSession> currSession; + mDragService->GetCurrentSession(getter_AddRefs(currSession)); + if (!currSession) { + return; + } + + currSession->SetDragAction(geckoAction); + + // Dispatch the event into Gecko + DispatchDragDropEvent(aEventMessage, ptl); + + // If TakeChildProcessDragAction returns something other than + // DRAGDROP_ACTION_UNINITIALIZED, it means that the last event was sent + // to the child process and this event is also being sent to the child + // process. In this case, use the last event's action instead. + nsDragService* dragService = static_cast<nsDragService*>(mDragService.get()); + currSession->GetDragAction(&geckoAction); + + int32_t childDragAction = dragService->TakeChildProcessDragAction(); + if (childDragAction != nsIDragService::DRAGDROP_ACTION_UNINITIALIZED) { + geckoAction = childDragAction; + } + + if (nsIDragService::DRAGDROP_ACTION_LINK & geckoAction) { + *pdwEffect = DROPEFFECT_LINK; + } else if (nsIDragService::DRAGDROP_ACTION_COPY & geckoAction) { + *pdwEffect = DROPEFFECT_COPY; + } else if (nsIDragService::DRAGDROP_ACTION_MOVE & geckoAction) { + *pdwEffect = DROPEFFECT_MOVE; + } else { + *pdwEffect = DROPEFFECT_NONE; + } + + if (aEventMessage != eDrop) { + // Get the cached drag effect from the drag service, the data member should + // have been set by whoever handled the WidgetGUIEvent or nsIDOMEvent on + // drags. + bool canDrop; + currSession->GetCanDrop(&canDrop); + if (!canDrop) { + *pdwEffect = DROPEFFECT_NONE; + } + } + + // Clear the cached value + currSession->SetCanDrop(false); +} + +// IDropTarget methods +STDMETHODIMP +nsNativeDragTarget::DragEnter(LPDATAOBJECT pIDataSource, DWORD grfKeyState, + POINTL ptl, DWORD* pdwEffect) { + if (!mDragService) { + return E_FAIL; + } + + mEffectsAllowed = *pdwEffect; + AddLinkSupportIfCanBeGenerated(pIDataSource); + + // Drag and drop image helper + if (GetDropTargetHelper()) { + // We get a lot of crashes (often uncaught by our handler) later on during + // DragOver calls, see bug 1465513. It looks like this might be because + // we're not cleaning up previous drags fully and now released resources get + // used. Calling IDropTargetHelper::DragLeave before DragEnter seems to fix + // this for at least one reproduction of this crash. + GetDropTargetHelper()->DragLeave(); + POINT pt = {ptl.x, ptl.y}; + GetDropTargetHelper()->DragEnter(mHWnd, pIDataSource, &pt, *pdwEffect); + } + + // save a ref to this, in case the window is destroyed underneath us + NS_ASSERTION(!mTookOwnRef, "own ref already taken!"); + this->AddRef(); + mTookOwnRef = true; + + // tell the drag service about this drag (it may have come from an + // outside app). + mDragService->StartDragSession(); + + void* tempOutData = nullptr; + uint32_t tempDataLen = 0; + nsresult loadResult = nsClipboard::GetNativeDataOffClipboard( + pIDataSource, 0, ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT), + nullptr, &tempOutData, &tempDataLen); + if (NS_SUCCEEDED(loadResult) && tempOutData) { + mEffectsPreferred = *((DWORD*)tempOutData); + free(tempOutData); + } else { + // We have no preference if we can't obtain it + mEffectsPreferred = DROPEFFECT_NONE; + } + + // Set the native data object into drag service + // + // This cast is ok because in the constructor we created a + // the actual implementation we wanted, so we know this is + // a nsDragService. It should be a private interface, though. + nsDragService* winDragService = + static_cast<nsDragService*>(mDragService.get()); + winDragService->SetIDataObject(pIDataSource); + + // Now process the native drag state and then dispatch the event + ProcessDrag(eDragEnter, grfKeyState, ptl, pdwEffect); + + return S_OK; +} + +void nsNativeDragTarget::AddLinkSupportIfCanBeGenerated( + LPDATAOBJECT aIDataSource) { + // If we don't have a link effect, but we can generate one, fix the + // drop effect to include it. + if (!(mEffectsAllowed & DROPEFFECT_LINK) && aIDataSource) { + if (S_OK == ::OleQueryLinkFromData(aIDataSource)) { + mEffectsAllowed |= DROPEFFECT_LINK; + } + } +} + +STDMETHODIMP +nsNativeDragTarget::DragOver(DWORD grfKeyState, POINTL ptl, LPDWORD pdwEffect) { + if (!mDragService) { + return E_FAIL; + } + + bool dragImageChanged = gDragImageChanged; + gDragImageChanged = false; + + // If a LINK effect could be generated previously from a DragEnter(), + // then we should include it as an allowed effect. + mEffectsAllowed = (*pdwEffect) | (mEffectsAllowed & DROPEFFECT_LINK); + + nsCOMPtr<nsIDragSession> currentDragSession; + mDragService->GetCurrentSession(getter_AddRefs(currentDragSession)); + if (!currentDragSession) { + return S_OK; // Drag was canceled. + } + + // without the AddRef() |this| can get destroyed in an event handler + this->AddRef(); + + // Drag and drop image helper + if (GetDropTargetHelper()) { + if (dragImageChanged) { + // See comment in nsNativeDragTarget::DragEnter. + GetDropTargetHelper()->DragLeave(); + // The drop helper only updates the image during DragEnter, so emulate + // a DragEnter if the image was changed. + POINT pt = {ptl.x, ptl.y}; + nsDragService* dragService = + static_cast<nsDragService*>(mDragService.get()); + GetDropTargetHelper()->DragEnter(mHWnd, dragService->GetDataObject(), &pt, + *pdwEffect); + } + POINT pt = {ptl.x, ptl.y}; + GetDropTargetHelper()->DragOver(&pt, *pdwEffect); + } + + ModifierKeyState modifierKeyState; + nsCOMPtr<nsIDragService> dragService = mDragService; + dragService->FireDragEventAtSource(eDrag, modifierKeyState.GetModifiers()); + // Now process the native drag state and then dispatch the event + ProcessDrag(eDragOver, grfKeyState, ptl, pdwEffect); + + this->Release(); + + return S_OK; +} + +STDMETHODIMP +nsNativeDragTarget::DragLeave() { + if (!mDragService) { + return E_FAIL; + } + + // Drag and drop image helper + if (GetDropTargetHelper()) { + GetDropTargetHelper()->DragLeave(); + } + + // dispatch the event into Gecko + DispatchDragDropEvent(eDragExit, gDragLastPoint); + + nsCOMPtr<nsIDragSession> currentDragSession; + mDragService->GetCurrentSession(getter_AddRefs(currentDragSession)); + + if (currentDragSession) { + nsCOMPtr<nsINode> sourceNode; + currentDragSession->GetSourceNode(getter_AddRefs(sourceNode)); + + if (!sourceNode) { + // We're leaving a window while doing a drag that was + // initiated in a different app. End the drag session, since + // we're done with it for now (until the user drags back into + // mozilla). + ModifierKeyState modifierKeyState; + nsCOMPtr<nsIDragService> dragService = mDragService; + dragService->EndDragSession(false, modifierKeyState.GetModifiers()); + } + } + + // release the ref that was taken in DragEnter + NS_ASSERTION(mTookOwnRef, "want to release own ref, but not taken!"); + if (mTookOwnRef) { + this->Release(); + mTookOwnRef = false; + } + + return S_OK; +} + +void nsNativeDragTarget::DragCancel() { + // Cancel the drag session if we did DragEnter. + if (mTookOwnRef) { + if (GetDropTargetHelper()) { + GetDropTargetHelper()->DragLeave(); + } + if (mDragService) { + ModifierKeyState modifierKeyState; + nsCOMPtr<nsIDragService> dragService = mDragService; + dragService->EndDragSession(false, modifierKeyState.GetModifiers()); + } + this->Release(); // matching the AddRef in DragEnter + mTookOwnRef = false; + } +} + +STDMETHODIMP +nsNativeDragTarget::Drop(LPDATAOBJECT pData, DWORD grfKeyState, POINTL aPT, + LPDWORD pdwEffect) { + if (!mDragService) { + return E_FAIL; + } + + mEffectsAllowed = *pdwEffect; + AddLinkSupportIfCanBeGenerated(pData); + + // Drag and drop image helper + if (GetDropTargetHelper()) { + POINT pt = {aPT.x, aPT.y}; + GetDropTargetHelper()->Drop(pData, &pt, *pdwEffect); + } + + // Set the native data object into the drag service + // + // This cast is ok because in the constructor we created a + // the actual implementation we wanted, so we know this is + // a nsDragService (but it should still be a private interface) + nsDragService* winDragService = + static_cast<nsDragService*>(mDragService.get()); + winDragService->SetIDataObject(pData); + + // NOTE: ProcessDrag spins the event loop which may destroy arbitrary objects. + // We use strong refs to prevent it from destroying these: + RefPtr<nsNativeDragTarget> kungFuDeathGrip = this; + nsCOMPtr<nsIDragService> serv = mDragService; + + // Now process the native drag state and then dispatch the event + ProcessDrag(eDrop, grfKeyState, aPT, pdwEffect); + + nsCOMPtr<nsIDragSession> currentDragSession; + serv->GetCurrentSession(getter_AddRefs(currentDragSession)); + if (!currentDragSession) { + return S_OK; // DragCancel() was called. + } + + // Let the win drag service know whether this session experienced + // a drop event within the application. Drop will not oocur if the + // drop landed outside the app. (used in tab tear off, bug 455884) + winDragService->SetDroppedLocal(); + + // tell the drag service we're done with the session + // Use GetMessagePos to get the position of the mouse at the last message + // seen by the event loop. (Bug 489729) + DWORD pos = ::GetMessagePos(); + POINT cpos; + cpos.x = GET_X_LPARAM(pos); + cpos.y = GET_Y_LPARAM(pos); + winDragService->SetDragEndPoint(nsIntPoint(cpos.x, cpos.y)); + ModifierKeyState modifierKeyState; + serv->EndDragSession(true, modifierKeyState.GetModifiers()); + + // release the ref that was taken in DragEnter + NS_ASSERTION(mTookOwnRef, "want to release own ref, but not taken!"); + if (mTookOwnRef) { + this->Release(); + mTookOwnRef = false; + } + + return S_OK; +} + +/** + * By lazy loading mDropTargetHelper we save 50-70ms of startup time + * which is ~5% of startup time. + */ +IDropTargetHelper* nsNativeDragTarget::GetDropTargetHelper() { + if (!mDropTargetHelper) { + CoCreateInstance(CLSID_DragDropHelper, nullptr, CLSCTX_INPROC_SERVER, + IID_IDropTargetHelper, (LPVOID*)&mDropTargetHelper); + } + + return mDropTargetHelper; +} diff --git a/widget/windows/nsNativeDragTarget.h b/widget/windows/nsNativeDragTarget.h new file mode 100644 index 0000000000..82edc7aae7 --- /dev/null +++ b/widget/windows/nsNativeDragTarget.h @@ -0,0 +1,103 @@ +/* -*- 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 _nsNativeDragTarget_h_ +#define _nsNativeDragTarget_h_ + +#include "nsCOMPtr.h" +#include <ole2.h> +#include <shlobj.h> + +#ifndef IDropTargetHelper +# include <shobjidl.h> // Vista drag image interfaces +# undef LogSeverity // SetupAPI.h #defines this as DWORD +#endif + +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" + +class nsIDragService; +class nsIWidget; + +/* + * nsNativeDragTarget implements the IDropTarget interface and gets most of its + * behavior from the associated adapter (m_dragDrop). + */ + +class nsNativeDragTarget final : public IDropTarget { + public: + explicit nsNativeDragTarget(nsIWidget* aWidget); + ~nsNativeDragTarget(); + + // IUnknown members - see iunknown.h for documentation + STDMETHODIMP QueryInterface(REFIID, void**); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IDataTarget members + + // Set pEffect based on whether this object can support a drop based on + // the data available from pSource, the key and mouse states specified + // in grfKeyState, and the coordinates specified by point. This is + // called by OLE when a drag enters this object's window (as registered + // by Initialize). + STDMETHODIMP DragEnter(LPDATAOBJECT pSource, DWORD grfKeyState, POINTL point, + DWORD* pEffect); + + // Similar to DragEnter except it is called frequently while the drag + // is over this object's window. + MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP DragOver(DWORD grfKeyState, + POINTL point, + DWORD* pEffect); + + // Release the drag-drop source and put internal state back to the point + // before the call to DragEnter. This is called when the drag leaves + // without a drop occurring. + MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP DragLeave(); + + // If point is within our region of interest and pSource's data supports + // one of our formats, get the data and set pEffect according to + // grfKeyState (DROPEFFECT_MOVE if the control key was not pressed, + // DROPEFFECT_COPY if the control key was pressed). Otherwise return + // E_FAIL. + MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP Drop(LPDATAOBJECT pSource, + DWORD grfKeyState, POINTL point, + DWORD* pEffect); + /** + * Cancel the current drag session, if any. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY void DragCancel(); + + static void DragImageChanged() { gDragImageChanged = true; } + + protected: + void GetGeckoDragAction(DWORD grfKeyState, LPDWORD pdwEffect, + uint32_t* aGeckoAction); + void ProcessDrag(mozilla::EventMessage aEventMessage, DWORD grfKeyState, + POINTL pt, DWORD* pdwEffect); + void DispatchDragDropEvent(mozilla::EventMessage aEventMessage, + const POINTL& aPT); + void AddLinkSupportIfCanBeGenerated(LPDATAOBJECT aIDataSource); + + // Native Stuff + ULONG m_cRef; // reference count + HWND mHWnd; + DWORD mEffectsAllowed; + DWORD mEffectsPreferred; + bool mTookOwnRef; + + // Gecko Stuff + nsIWidget* mWidget; + nsCOMPtr<nsIDragService> mDragService; + // Drag target helper + IDropTargetHelper* GetDropTargetHelper(); + + private: + // Drag target helper + IDropTargetHelper* mDropTargetHelper; + + static bool gDragImageChanged; +}; + +#endif // _nsNativeDragTarget_h_ diff --git a/widget/windows/nsNativeThemeWin.cpp b/widget/windows/nsNativeThemeWin.cpp new file mode 100644 index 0000000000..04883a833f --- /dev/null +++ b/widget/windows/nsNativeThemeWin.cpp @@ -0,0 +1,1975 @@ +/* -*- Mode: C++; tab-width: 40; 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 "nsNativeThemeWin.h" + +#include <algorithm> +#include <malloc.h> + +#include "gfxContext.h" +#include "gfxPlatform.h" +#include "gfxWindowsNativeDrawing.h" +#include "gfxWindowsPlatform.h" +#include "gfxWindowsSurface.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/gfx/Types.h" // for Color::FromABGR +#include "mozilla/Logging.h" +#include "mozilla/RelativeLuminanceUtils.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/dom/XULButtonElement.h" +#include "nsColor.h" +#include "nsComboboxControlFrame.h" +#include "nsDeviceContext.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "nsIFrame.h" +#include "nsLayoutUtils.h" +#include "nsLookAndFeel.h" +#include "nsNameSpaceManager.h" +#include "Theme.h" +#include "nsPresContext.h" +#include "nsRect.h" +#include "nsSize.h" +#include "nsStyleConsts.h" +#include "nsTransform2D.h" +#include "nsWindow.h" +#include "prinrval.h" +#include "WinUtils.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::widget; + +using ElementState = dom::ElementState; + +extern mozilla::LazyLogModule gWindowsLog; + +namespace mozilla::widget { + +nsNativeThemeWin::nsNativeThemeWin() + : Theme(ScrollbarStyle()), + mProgressDeterminateTimeStamp(TimeStamp::Now()), + mProgressIndeterminateTimeStamp(TimeStamp::Now()), + mBorderCacheValid(), + mMinimumWidgetSizeCacheValid(), + mGutterSizeCacheValid(false) {} + +nsNativeThemeWin::~nsNativeThemeWin() { nsUXThemeData::Invalidate(); } + +bool nsNativeThemeWin::IsWidgetAlwaysNonNative(nsIFrame* aFrame, + StyleAppearance aAppearance) { + return Theme::IsWidgetAlwaysNonNative(aFrame, aAppearance) || + aAppearance == StyleAppearance::Checkbox || + aAppearance == StyleAppearance::Radio || + aAppearance == StyleAppearance::MozMenulistArrowButton || + aAppearance == StyleAppearance::SpinnerUpbutton || + aAppearance == StyleAppearance::SpinnerDownbutton; +} + +auto nsNativeThemeWin::IsWidgetNonNative(nsIFrame* aFrame, + StyleAppearance aAppearance) + -> NonNative { + if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { + return NonNative::Always; + } + + // We only know how to draw light widgets, so we defer to the non-native + // theme when appropriate. + if (Theme::ThemeSupportsWidget(aFrame->PresContext(), aFrame, aAppearance) && + LookAndFeel::ColorSchemeForFrame(aFrame) == + LookAndFeel::ColorScheme::Dark) { + return NonNative::BecauseColorMismatch; + } + return NonNative::No; +} + +static MARGINS GetCheckboxMargins(HANDLE theme, HDC hdc) { + MARGINS checkboxContent = {0}; + GetThemeMargins(theme, hdc, MENU_POPUPCHECK, MCB_NORMAL, TMT_CONTENTMARGINS, + nullptr, &checkboxContent); + return checkboxContent; +} + +static SIZE GetCheckboxBGSize(HANDLE theme, HDC hdc) { + SIZE checkboxSize; + GetThemePartSize(theme, hdc, MENU_POPUPCHECK, MC_CHECKMARKNORMAL, nullptr, + TS_TRUE, &checkboxSize); + + MARGINS checkboxMargins = GetCheckboxMargins(theme, hdc); + + int leftMargin = checkboxMargins.cxLeftWidth; + int rightMargin = checkboxMargins.cxRightWidth; + int topMargin = checkboxMargins.cyTopHeight; + int bottomMargin = checkboxMargins.cyBottomHeight; + + int width = leftMargin + checkboxSize.cx + rightMargin; + int height = topMargin + checkboxSize.cy + bottomMargin; + SIZE ret; + ret.cx = width; + ret.cy = height; + return ret; +} + +static SIZE GetCheckboxBGBounds(HANDLE theme, HDC hdc) { + MARGINS checkboxBGSizing = {0}; + MARGINS checkboxBGContent = {0}; + GetThemeMargins(theme, hdc, MENU_POPUPCHECKBACKGROUND, MCB_NORMAL, + TMT_SIZINGMARGINS, nullptr, &checkboxBGSizing); + GetThemeMargins(theme, hdc, MENU_POPUPCHECKBACKGROUND, MCB_NORMAL, + TMT_CONTENTMARGINS, nullptr, &checkboxBGContent); + +#define posdx(d) ((d) > 0 ? d : 0) + + int dx = + posdx(checkboxBGContent.cxRightWidth - checkboxBGSizing.cxRightWidth) + + posdx(checkboxBGContent.cxLeftWidth - checkboxBGSizing.cxLeftWidth); + int dy = + posdx(checkboxBGContent.cyTopHeight - checkboxBGSizing.cyTopHeight) + + posdx(checkboxBGContent.cyBottomHeight - checkboxBGSizing.cyBottomHeight); + +#undef posdx + + SIZE ret(GetCheckboxBGSize(theme, hdc)); + ret.cx += dx; + ret.cy += dy; + return ret; +} + +static SIZE GetGutterSize(HANDLE theme, HDC hdc) { + SIZE gutterSize; + GetThemePartSize(theme, hdc, MENU_POPUPGUTTER, 0, nullptr, TS_TRUE, + &gutterSize); + + SIZE checkboxBGSize(GetCheckboxBGBounds(theme, hdc)); + + SIZE itemSize; + GetThemePartSize(theme, hdc, MENU_POPUPITEM, MPI_NORMAL, nullptr, TS_TRUE, + &itemSize); + + // Figure out how big the menuitem's icon will be (if present) at current DPI + // Needs the system scale for consistency with Windows Theme API. + double scaleFactor = WinUtils::SystemScaleFactor(); + int iconDevicePixels = NSToIntRound(16 * scaleFactor); + SIZE iconSize = {iconDevicePixels, iconDevicePixels}; + // Not really sure what margins should be used here, but this seems to work in + // practice... + MARGINS margins = {0}; + GetThemeMargins(theme, hdc, MENU_POPUPCHECKBACKGROUND, MCB_NORMAL, + TMT_CONTENTMARGINS, nullptr, &margins); + iconSize.cx += margins.cxLeftWidth + margins.cxRightWidth; + iconSize.cy += margins.cyTopHeight + margins.cyBottomHeight; + + int width = std::max( + itemSize.cx, std::max(iconSize.cx, checkboxBGSize.cx) + gutterSize.cx); + int height = std::max(itemSize.cy, std::max(iconSize.cy, checkboxBGSize.cy)); + + SIZE ret; + ret.cx = width; + ret.cy = height; + return ret; +} + +SIZE nsNativeThemeWin::GetCachedGutterSize(HANDLE theme) { + if (mGutterSizeCacheValid) { + return mGutterSizeCache; + } + + mGutterSizeCache = GetGutterSize(theme, nullptr); + mGutterSizeCacheValid = true; + + return mGutterSizeCache; +} + +/* + * Notes on progress track and meter part constants: + * xp and up: + * PP_BAR(_VERT) - base progress track + * PP_TRANSPARENTBAR(_VERT) - transparent progress track. this only works if + * the underlying surface supports alpha. otherwise + * theme lib's DrawThemeBackground falls back on + * opaque PP_BAR. we currently don't use this. + * PP_CHUNK(_VERT) - xp progress meter. this does not draw an xp style + * progress w/chunks, it draws fill using the chunk + * graphic. + * vista and up: + * PP_FILL(_VERT) - progress meter. these have four states/colors. + * PP_PULSEOVERLAY(_VERT) - white reflection - an overlay, not sure what this + * is used for. + * PP_MOVEOVERLAY(_VERT) - green pulse - the pulse effect overlay on + * determined progress bars. we also use this for + * indeterminate chunk. + * + * Notes on state constants: + * PBBS_NORMAL - green progress + * PBBVS_PARTIAL/PBFVS_ERROR - red error progress + * PBFS_PAUSED - yellow paused progress + * + * There is no common controls style indeterminate part on vista and up. + */ + +/* + * Progress bar related constants. These values are found by experimenting and + * comparing against native widgets used by the system. They are very unlikely + * exact but try to not be too wrong. + */ +// The amount of time we animate progress meters parts across the frame. +static const double kProgressDeterminateTimeSpan = 3.0; +static const double kProgressIndeterminateTimeSpan = 5.0; +// The width of the overlay used to animate the horizontal progress bar (Vista +// and later). +static const int32_t kProgressHorizontalOverlaySize = 120; +// The height of the overlay used to animate the vertical progress bar (Vista +// and later). +static const int32_t kProgressVerticalOverlaySize = 45; +// The height of the overlay used for the vertical indeterminate progress bar +// (Vista and later). +static const int32_t kProgressVerticalIndeterminateOverlaySize = 60; +// The width of the overlay used to animate the indeterminate progress bar +// (Windows Classic). +static const int32_t kProgressClassicOverlaySize = 40; + +/* + * GetProgressOverlayStyle - returns the proper overlay part for themed + * progress bars based on os and orientation. + */ +static int32_t GetProgressOverlayStyle(bool aIsVertical) { + return aIsVertical ? PP_MOVEOVERLAYVERT : PP_MOVEOVERLAY; +} + +/* + * GetProgressOverlaySize - returns the minimum width or height for themed + * progress bar overlays. This includes the width of indeterminate chunks + * and vista pulse overlays. + */ +static int32_t GetProgressOverlaySize(bool aIsVertical, bool aIsIndeterminate) { + if (aIsVertical) { + return aIsIndeterminate ? kProgressVerticalIndeterminateOverlaySize + : kProgressVerticalOverlaySize; + } + return kProgressHorizontalOverlaySize; +} + +/* + * IsProgressMeterFilled - Determines if a progress meter is at 100% fill based + * on a comparison of the current value and maximum. + */ +static bool IsProgressMeterFilled(nsIFrame* aFrame) { + NS_ENSURE_TRUE(aFrame, false); + nsIFrame* parentFrame = aFrame->GetParent(); + NS_ENSURE_TRUE(parentFrame, false); + return nsNativeTheme::GetProgressValue(parentFrame) == + nsNativeTheme::GetProgressMaxValue(parentFrame); +} + +/* + * CalculateProgressOverlayRect - returns the padded overlay animation rect + * used in rendering progress bars. Resulting rects are used in rendering + * vista+ pulse overlays and indeterminate progress meters. Graphics should + * be rendered at the origin. + */ +RECT nsNativeThemeWin::CalculateProgressOverlayRect(nsIFrame* aFrame, + RECT* aWidgetRect, + bool aIsVertical, + bool aIsIndeterminate, + bool aIsClassic) { + NS_ASSERTION(aFrame, "bad frame pointer"); + NS_ASSERTION(aWidgetRect, "bad rect pointer"); + + int32_t frameSize = aIsVertical ? aWidgetRect->bottom - aWidgetRect->top + : aWidgetRect->right - aWidgetRect->left; + + // Recycle a set of progress pulse timers - these timers control the position + // of all progress overlays and indeterminate chunks that get rendered. + double span = aIsIndeterminate ? kProgressIndeterminateTimeSpan + : kProgressDeterminateTimeSpan; + TimeDuration period; + if (!aIsIndeterminate) { + if (TimeStamp::Now() > + (mProgressDeterminateTimeStamp + TimeDuration::FromSeconds(span))) { + mProgressDeterminateTimeStamp = TimeStamp::Now(); + } + period = TimeStamp::Now() - mProgressDeterminateTimeStamp; + } else { + if (TimeStamp::Now() > + (mProgressIndeterminateTimeStamp + TimeDuration::FromSeconds(span))) { + mProgressIndeterminateTimeStamp = TimeStamp::Now(); + } + period = TimeStamp::Now() - mProgressIndeterminateTimeStamp; + } + + double percent = period / TimeDuration::FromSeconds(span); + + if (!aIsVertical && IsFrameRTL(aFrame)) percent = 1 - percent; + + RECT overlayRect = *aWidgetRect; + int32_t overlaySize; + if (!aIsClassic) { + overlaySize = GetProgressOverlaySize(aIsVertical, aIsIndeterminate); + } else { + overlaySize = kProgressClassicOverlaySize; + } + + // Calculate a bounds that is larger than the meters frame such that the + // overlay starts and ends completely off the edge of the frame: + // [overlay][frame][overlay] + // This also yields a nice delay on rotation. Use overlaySize as the minimum + // size for [overlay] based on the graphics dims. If [frame] is larger, use + // the frame size instead. + int trackWidth = frameSize > overlaySize ? frameSize : overlaySize; + if (!aIsVertical) { + int xPos = aWidgetRect->left - trackWidth; + xPos += (int)ceil(((double)(trackWidth * 2) * percent)); + overlayRect.left = xPos; + overlayRect.right = xPos + overlaySize; + } else { + int yPos = aWidgetRect->bottom + trackWidth; + yPos -= (int)ceil(((double)(trackWidth * 2) * percent)); + overlayRect.bottom = yPos; + overlayRect.top = yPos - overlaySize; + } + return overlayRect; +} + +/* + * DrawProgressMeter - render an appropriate progress meter based on progress + * meter style, orientation, and os. Note, this does not render the underlying + * progress track. + * + * @param aFrame the widget frame + * @param aAppearance type of widget + * @param aTheme progress theme handle + * @param aHdc hdc returned by gfxWindowsNativeDrawing + * @param aPart the PP_X progress part + * @param aState the theme state + * @param aWidgetRect bounding rect for the widget + * @param aClipRect dirty rect that needs drawing. + * @param aAppUnits app units per device pixel + */ +void nsNativeThemeWin::DrawThemedProgressMeter( + nsIFrame* aFrame, StyleAppearance aAppearance, HANDLE aTheme, HDC aHdc, + int aPart, int aState, RECT* aWidgetRect, RECT* aClipRect) { + if (!aFrame || !aTheme || !aHdc) return; + + NS_ASSERTION(aWidgetRect, "bad rect pointer"); + NS_ASSERTION(aClipRect, "bad clip rect pointer"); + + RECT adjWidgetRect, adjClipRect; + adjWidgetRect = *aWidgetRect; + adjClipRect = *aClipRect; + + nsIFrame* parentFrame = aFrame->GetParent(); + if (!parentFrame) { + // We have no parent to work with, just bail. + NS_WARNING("No parent frame for progress rendering. Can't paint."); + return; + } + + ElementState elementState = GetContentState(parentFrame, aAppearance); + bool vertical = IsVerticalProgress(parentFrame); + bool indeterminate = elementState.HasState(ElementState::INDETERMINATE); + bool animate = indeterminate; + + // Vista and up progress meter is fill style, rendered here. We render + // the pulse overlay in the follow up section below. + DrawThemeBackground(aTheme, aHdc, aPart, aState, &adjWidgetRect, + &adjClipRect); + if (!IsProgressMeterFilled(aFrame)) { + animate = true; + } + + if (animate) { + // Indeterminate rendering + int32_t overlayPart = GetProgressOverlayStyle(vertical); + RECT overlayRect = CalculateProgressOverlayRect( + aFrame, &adjWidgetRect, vertical, indeterminate, false); + DrawThemeBackground(aTheme, aHdc, overlayPart, aState, &overlayRect, + &adjClipRect); + + if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 60)) { + NS_WARNING("unable to animate progress widget!"); + } + } +} + +LayoutDeviceIntMargin nsNativeThemeWin::GetCachedWidgetBorder( + HTHEME aTheme, nsUXThemeClass aThemeClass, StyleAppearance aAppearance, + int32_t aPart, int32_t aState) { + int32_t cacheIndex = aThemeClass * THEME_PART_DISTINCT_VALUE_COUNT + aPart; + int32_t cacheBitIndex = cacheIndex / 8; + uint8_t cacheBit = 1u << (cacheIndex % 8); + + if (mBorderCacheValid[cacheBitIndex] & cacheBit) { + return mBorderCache[cacheIndex]; + } + + // Get our info. + RECT outerRect; // Create a fake outer rect. + outerRect.top = outerRect.left = 100; + outerRect.right = outerRect.bottom = 200; + RECT contentRect(outerRect); + HRESULT res = GetThemeBackgroundContentRect(aTheme, nullptr, aPart, aState, + &outerRect, &contentRect); + + if (FAILED(res)) { + return LayoutDeviceIntMargin(); + } + + // Now compute the delta in each direction and place it in our + // nsIntMargin struct. + LayoutDeviceIntMargin result; + result.top = contentRect.top - outerRect.top; + result.bottom = outerRect.bottom - contentRect.bottom; + result.left = contentRect.left - outerRect.left; + result.right = outerRect.right - contentRect.right; + + mBorderCacheValid[cacheBitIndex] |= cacheBit; + mBorderCache[cacheIndex] = result; + + return result; +} + +nsresult nsNativeThemeWin::GetCachedMinimumWidgetSize( + nsIFrame* aFrame, HANDLE aTheme, nsUXThemeClass aThemeClass, + StyleAppearance aAppearance, int32_t aPart, int32_t aState, + THEMESIZE aSizeReq, mozilla::LayoutDeviceIntSize* aResult) { + int32_t cachePart = aPart; + + if (aAppearance == StyleAppearance::Button && aSizeReq == TS_MIN) { + // In practice, StyleAppearance::Button is the only widget type which has an + // aSizeReq that varies for us, and it can only be TS_MIN or TS_TRUE. Just + // stuff that extra bit into the aPart part of the cache, since BP_Count is + // well below THEME_PART_DISTINCT_VALUE_COUNT anyway. + cachePart = BP_Count; + } + + MOZ_ASSERT(aPart < THEME_PART_DISTINCT_VALUE_COUNT); + int32_t cacheIndex = + aThemeClass * THEME_PART_DISTINCT_VALUE_COUNT + cachePart; + int32_t cacheBitIndex = cacheIndex / 8; + uint8_t cacheBit = 1u << (cacheIndex % 8); + + if (mMinimumWidgetSizeCacheValid[cacheBitIndex] & cacheBit) { + *aResult = mMinimumWidgetSizeCache[cacheIndex]; + return NS_OK; + } + + HDC hdc = ::GetDC(NULL); + if (!hdc) { + return NS_ERROR_FAILURE; + } + + SIZE sz; + GetThemePartSize(aTheme, hdc, aPart, aState, nullptr, aSizeReq, &sz); + aResult->width = sz.cx; + aResult->height = sz.cy; + + ::ReleaseDC(nullptr, hdc); + + mMinimumWidgetSizeCacheValid[cacheBitIndex] |= cacheBit; + mMinimumWidgetSizeCache[cacheIndex] = *aResult; + + return NS_OK; +} + +mozilla::Maybe<nsUXThemeClass> nsNativeThemeWin::GetThemeClass( + StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::Button: + return Some(eUXButton); + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + return Some(eUXEdit); + case StyleAppearance::Toolbox: + return Some(eUXRebar); + case StyleAppearance::Toolbar: + case StyleAppearance::Toolbarbutton: + case StyleAppearance::Separator: + return Some(eUXToolbar); + case StyleAppearance::ProgressBar: + case StyleAppearance::Progresschunk: + return Some(eUXProgress); + case StyleAppearance::Tab: + case StyleAppearance::Tabpanel: + case StyleAppearance::Tabpanels: + return Some(eUXTab); + case StyleAppearance::Range: + case StyleAppearance::RangeThumb: + return Some(eUXTrackbar); + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + return Some(eUXCombobox); + case StyleAppearance::Treeheadercell: + return Some(eUXHeader); + case StyleAppearance::Listbox: + case StyleAppearance::Treeview: + case StyleAppearance::Treetwistyopen: + case StyleAppearance::Treeitem: + return Some(eUXListview); + default: + return Nothing(); + } +} + +HANDLE +nsNativeThemeWin::GetTheme(StyleAppearance aAppearance) { + mozilla::Maybe<nsUXThemeClass> themeClass = GetThemeClass(aAppearance); + if (themeClass.isNothing()) { + return nullptr; + } + return nsUXThemeData::GetTheme(themeClass.value()); +} + +int32_t nsNativeThemeWin::StandardGetState(nsIFrame* aFrame, + StyleAppearance aAppearance, + bool wantFocused) { + ElementState elementState = GetContentState(aFrame, aAppearance); + if (elementState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE)) { + return TS_ACTIVE; + } + if (elementState.HasState(ElementState::HOVER)) { + return TS_HOVER; + } + if (wantFocused) { + if (elementState.HasState(ElementState::FOCUSRING)) { + return TS_FOCUSED; + } + // On Windows, focused buttons are always drawn as such by the native + // theme, that's why we check ElementState::FOCUS instead of + // ElementState::FOCUSRING. + if (aAppearance == StyleAppearance::Button && + elementState.HasState(ElementState::FOCUS)) { + return TS_FOCUSED; + } + } + + return TS_NORMAL; +} + +bool nsNativeThemeWin::IsMenuActive(nsIFrame* aFrame, + StyleAppearance aAppearance) { + nsIContent* content = aFrame->GetContent(); + if (content->IsXULElement() && + content->NodeInfo()->Equals(nsGkAtoms::richlistitem)) + return CheckBooleanAttr(aFrame, nsGkAtoms::selected); + + return CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); +} + +/** + * aPart is filled in with the UXTheme part code. On return, values > 0 + * are the actual UXTheme part code; -1 means the widget will be drawn by + * us; 0 means that we should use part code 0, which isn't a real part code + * but elicits some kind of default behaviour from UXTheme when drawing + * (but isThemeBackgroundPartiallyTransparent may not work). + */ +nsresult nsNativeThemeWin::GetThemePartAndState(nsIFrame* aFrame, + StyleAppearance aAppearance, + int32_t& aPart, + int32_t& aState) { + switch (aAppearance) { + case StyleAppearance::Button: { + aPart = BP_BUTTON; + if (!aFrame) { + aState = TS_NORMAL; + return NS_OK; + } + + ElementState elementState = GetContentState(aFrame, aAppearance); + if (elementState.HasState(ElementState::DISABLED)) { + aState = TS_DISABLED; + return NS_OK; + } + if (IsOpenButton(aFrame) || IsCheckedButton(aFrame)) { + aState = TS_ACTIVE; + return NS_OK; + } + + aState = StandardGetState(aFrame, aAppearance, true); + + // Check for default dialog buttons. These buttons should always look + // focused. + if (aState == TS_NORMAL && IsDefaultButton(aFrame)) aState = TS_FOCUSED; + return NS_OK; + } + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: { + ElementState elementState = GetContentState(aFrame, aAppearance); + + /* Note: the NOSCROLL type has a rounded corner in each corner. The more + * specific HSCROLL, VSCROLL, HVSCROLL types have side and/or top/bottom + * edges rendered as straight horizontal lines with sharp corners to + * accommodate a scrollbar. However, the scrollbar gets rendered on top + * of this for us, so we don't care, and can just use NOSCROLL here. + */ + aPart = TFP_EDITBORDER_NOSCROLL; + + if (!aFrame) { + aState = TFS_EDITBORDER_NORMAL; + } else if (elementState.HasState(ElementState::DISABLED)) { + aState = TFS_EDITBORDER_DISABLED; + } else if (IsReadOnly(aFrame)) { + /* no special read-only state */ + aState = TFS_EDITBORDER_NORMAL; + } else if (elementState.HasAtLeastOneOfStates(ElementState::ACTIVE | + ElementState::FOCUSRING)) { + aState = TFS_EDITBORDER_FOCUSED; + } else if (elementState.HasState(ElementState::HOVER)) { + aState = TFS_EDITBORDER_HOVER; + } else { + aState = TFS_EDITBORDER_NORMAL; + } + + return NS_OK; + } + case StyleAppearance::ProgressBar: { + bool vertical = IsVerticalProgress(aFrame); + aPart = vertical ? PP_BARVERT : PP_BAR; + aState = PBBS_NORMAL; + return NS_OK; + } + case StyleAppearance::Progresschunk: { + nsIFrame* parentFrame = aFrame->GetParent(); + if (IsVerticalProgress(parentFrame)) { + aPart = PP_FILLVERT; + } else { + aPart = PP_FILL; + } + + aState = PBBVS_NORMAL; + return NS_OK; + } + case StyleAppearance::Toolbarbutton: { + aPart = BP_BUTTON; + if (!aFrame) { + aState = TS_NORMAL; + return NS_OK; + } + + ElementState elementState = GetContentState(aFrame, aAppearance); + if (elementState.HasState(ElementState::DISABLED)) { + aState = TS_DISABLED; + return NS_OK; + } + if (IsOpenButton(aFrame)) { + aState = TS_ACTIVE; + return NS_OK; + } + + if (elementState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE)) + aState = TS_ACTIVE; + else if (elementState.HasState(ElementState::HOVER)) { + if (IsCheckedButton(aFrame)) + aState = TB_HOVER_CHECKED; + else + aState = TS_HOVER; + } else { + if (IsCheckedButton(aFrame)) + aState = TB_CHECKED; + else + aState = TS_NORMAL; + } + + return NS_OK; + } + case StyleAppearance::Separator: { + aPart = TP_SEPARATOR; + aState = TS_NORMAL; + return NS_OK; + } + case StyleAppearance::Range: { + if (IsRangeHorizontal(aFrame)) { + aPart = TKP_TRACK; + aState = TRS_NORMAL; + } else { + aPart = TKP_TRACKVERT; + aState = TRVS_NORMAL; + } + return NS_OK; + } + case StyleAppearance::RangeThumb: { + if (IsRangeHorizontal(aFrame)) { + aPart = TKP_THUMBBOTTOM; + } else { + aPart = IsFrameRTL(aFrame) ? TKP_THUMBLEFT : TKP_THUMBRIGHT; + } + ElementState elementState = GetContentState(aFrame, aAppearance); + if (!aFrame) { + aState = TS_NORMAL; + } else if (elementState.HasState(ElementState::DISABLED)) { + aState = TKP_DISABLED; + } else { + if (elementState.HasState( + ElementState::ACTIVE)) // Hover is not also a requirement for + // the thumb, since the drag is not + // canceled when you move outside the + // thumb. + aState = TS_ACTIVE; + else if (elementState.HasState(ElementState::FOCUSRING)) + aState = TKP_FOCUSED; + else if (elementState.HasState(ElementState::HOVER)) + aState = TS_HOVER; + else + aState = TS_NORMAL; + } + return NS_OK; + } + case StyleAppearance::Toolbox: { + aState = 0; + aPart = RP_BACKGROUND; + return NS_OK; + } + case StyleAppearance::Toolbar: { + // Use -1 to indicate we don't wish to have the theme background drawn + // for this item. We will pass any nessessary information via aState, + // and will render the item using separate code. + aPart = -1; + aState = 0; + if (aFrame) { + nsIContent* content = aFrame->GetContent(); + nsIContent* parent = content->GetParent(); + // XXXzeniko hiding the first toolbar will result in an unwanted margin + if (parent && parent->GetFirstChild() == content) { + aState = 1; + } + } + return NS_OK; + } + case StyleAppearance::Treeview: + case StyleAppearance::Listbox: { + aPart = TREEVIEW_BODY; + aState = TS_NORMAL; + return NS_OK; + } + case StyleAppearance::Tabpanels: { + aPart = TABP_PANELS; + aState = TS_NORMAL; + return NS_OK; + } + case StyleAppearance::Tabpanel: { + aPart = TABP_PANEL; + aState = TS_NORMAL; + return NS_OK; + } + case StyleAppearance::Tab: { + aPart = TABP_TAB; + if (!aFrame) { + aState = TS_NORMAL; + return NS_OK; + } + + ElementState elementState = GetContentState(aFrame, aAppearance); + if (elementState.HasState(ElementState::DISABLED)) { + aState = TS_DISABLED; + return NS_OK; + } + + if (IsSelectedTab(aFrame)) { + aPart = TABP_TAB_SELECTED; + aState = TS_ACTIVE; // The selected tab is always "pressed". + } else + aState = StandardGetState(aFrame, aAppearance, true); + + return NS_OK; + } + case StyleAppearance::Treeheadercell: { + aPart = 1; + if (!aFrame) { + aState = TS_NORMAL; + return NS_OK; + } + + aState = StandardGetState(aFrame, aAppearance, true); + + return NS_OK; + } + case StyleAppearance::MenulistButton: + case StyleAppearance::Menulist: { + nsIContent* content = aFrame->GetContent(); + bool useDropBorder = content && content->IsHTMLElement(); + ElementState elementState = GetContentState(aFrame, aAppearance); + + /* On Vista/Win7, we use CBP_DROPBORDER instead of DROPFRAME for HTML + * content or for editable menulists; this gives us the thin outline, + * instead of the gradient-filled background */ + if (useDropBorder) + aPart = CBP_DROPBORDER; + else + aPart = CBP_DROPFRAME; + + if (elementState.HasState(ElementState::DISABLED)) { + aState = TS_DISABLED; + } else if (IsReadOnly(aFrame)) { + aState = TS_NORMAL; + } else if (IsOpenButton(aFrame)) { + aState = TS_ACTIVE; + } else if (useDropBorder && + elementState.HasState(ElementState::FOCUSRING)) { + aState = TS_ACTIVE; + } else if (elementState.HasAllStates(ElementState::HOVER | + ElementState::ACTIVE)) { + aState = TS_ACTIVE; + } else if (elementState.HasState(ElementState::HOVER)) { + aState = TS_HOVER; + } else { + aState = TS_NORMAL; + } + + return NS_OK; + } + default: + aPart = 0; + aState = 0; + return NS_ERROR_FAILURE; + } +} + +static bool AssumeThemePartAndStateAreTransparent(int32_t aPart, + int32_t aState) { + if (!nsUXThemeData::IsHighContrastOn() && aPart == MENU_POPUPITEM && + aState == MBI_NORMAL) { + return true; + } + return false; +} + +// When running with per-monitor DPI (on Win8.1+), and rendering on a display +// with a different DPI setting from the system's default scaling, we need to +// apply scaling to native-themed elements as the Windows theme APIs assume +// the system default resolution. +static inline double GetThemeDpiScaleFactor(nsPresContext* aPresContext) { + if (WinUtils::IsPerMonitorDPIAware() || + StaticPrefs::layout_css_devPixelsPerPx() > 0.0) { + nsCOMPtr<nsIWidget> rootWidget = aPresContext->GetRootWidget(); + if (rootWidget) { + double systemScale = WinUtils::SystemScaleFactor(); + return rootWidget->GetDefaultScale().scale / systemScale; + } + } + return 1.0; +} + +static inline double GetThemeDpiScaleFactor(nsIFrame* aFrame) { + return GetThemeDpiScaleFactor(aFrame->PresContext()); +} + +NS_IMETHODIMP +nsNativeThemeWin::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + const nsRect& aRect, + const nsRect& aDirtyRect, + DrawOverflow aDrawOverflow) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::DrawWidgetBackground(aContext, aFrame, aAppearance, aRect, + aDirtyRect, aDrawOverflow); + } + + HANDLE theme = GetTheme(aAppearance); + if (!theme) + return ClassicDrawWidgetBackground(aContext, aFrame, aAppearance, aRect, + aDirtyRect); + + // ^^ without the right sdk, assume xp theming and fall through. + int32_t part, state; + nsresult rv = GetThemePartAndState(aFrame, aAppearance, part, state); + if (NS_FAILED(rv)) return rv; + + if (AssumeThemePartAndStateAreTransparent(part, state)) { + return NS_OK; + } + + gfxContextMatrixAutoSaveRestore save(aContext); + + double themeScale = GetThemeDpiScaleFactor(aFrame); + if (themeScale != 1.0) { + aContext->SetMatrix( + aContext->CurrentMatrix().PreScale(themeScale, themeScale)); + } + + gfxFloat p2a = gfxFloat(aFrame->PresContext()->AppUnitsPerDevPixel()); + RECT widgetRect; + RECT clipRect; + gfxRect tr(aRect.X(), aRect.Y(), aRect.Width(), aRect.Height()), + dr(aDirtyRect.X(), aDirtyRect.Y(), aDirtyRect.Width(), + aDirtyRect.Height()); + + tr.Scale(1.0 / (p2a * themeScale)); + dr.Scale(1.0 / (p2a * themeScale)); + + gfxWindowsNativeDrawing nativeDrawing( + aContext, dr, GetWidgetNativeDrawingFlags(aAppearance)); + +RENDER_AGAIN: + + HDC hdc = nativeDrawing.BeginNativeDrawing(); + if (!hdc) return NS_ERROR_FAILURE; + + nativeDrawing.TransformToNativeRect(tr, widgetRect); + nativeDrawing.TransformToNativeRect(dr, clipRect); + +#if 0 + { + MOZ_LOG(gWindowsLog, LogLevel::Error, + (stderr, "xform: %f %f %f %f [%f %f]\n", m._11, m._21, m._12, m._22, + m._31, m._32)); + MOZ_LOG(gWindowsLog, LogLevel::Error, + (stderr, "tr: [%d %d %d %d]\ndr: [%d %d %d %d]\noff: [%f %f]\n", + tr.x, tr.y, tr.width, tr.height, dr.x, dr.y, dr.width, dr.height, + offset.x, offset.y)); + } +#endif + + if (aAppearance == StyleAppearance::Tab) { + // For left edge and right edge tabs, we need to adjust the widget + // rects and clip rects so that the edges don't get drawn. + bool isLeft = IsLeftToSelectedTab(aFrame); + bool isRight = !isLeft && IsRightToSelectedTab(aFrame); + + if (isLeft || isRight) { + // HACK ALERT: There appears to be no way to really obtain this value, so + // we're forced to just use the default value for Luna (which also happens + // to be correct for all the other skins I've tried). + int32_t edgeSize = 2; + + // Armed with the size of the edge, we now need to either shift to the + // left or to the right. The clip rect won't include this extra area, so + // we know that we're effectively shifting the edge out of view (such that + // it won't be painted). + if (isLeft) + // The right edge should not be drawn. Extend our rect by the edge + // size. + widgetRect.right += edgeSize; + else + // The left edge should not be drawn. Move the widget rect's left coord + // back. + widgetRect.left -= edgeSize; + } + } + + // widgetRect is the bounding box for a widget, yet the scale track is only + // a small portion of this size, so the edges of the scale need to be + // adjusted to the real size of the track. + if (aAppearance == StyleAppearance::Range) { + RECT contentRect; + GetThemeBackgroundContentRect(theme, hdc, part, state, &widgetRect, + &contentRect); + + SIZE siz; + GetThemePartSize(theme, hdc, part, state, &widgetRect, TS_TRUE, &siz); + + // When rounding is necessary, we round the position of the track + // away from the chevron of the thumb to make it look better. + if (IsRangeHorizontal(aFrame)) { + contentRect.top += (contentRect.bottom - contentRect.top - siz.cy) / 2; + contentRect.bottom = contentRect.top + siz.cy; + } else { + if (!IsFrameRTL(aFrame)) { + contentRect.left += (contentRect.right - contentRect.left - siz.cx) / 2; + contentRect.right = contentRect.left + siz.cx; + } else { + contentRect.right -= + (contentRect.right - contentRect.left - siz.cx) / 2; + contentRect.left = contentRect.right - siz.cx; + } + } + + DrawThemeBackground(theme, hdc, part, state, &contentRect, &clipRect); + } else if (aAppearance == StyleAppearance::NumberInput || + aAppearance == StyleAppearance::Textfield || + aAppearance == StyleAppearance::Textarea) { + DrawThemeBackground(theme, hdc, part, state, &widgetRect, &clipRect); + + if (state == TFS_EDITBORDER_DISABLED) { + InflateRect(&widgetRect, -1, -1); + ::FillRect(hdc, &widgetRect, reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1)); + } + } else if (aAppearance == StyleAppearance::ProgressBar) { + // DrawThemeBackground renders each corner with a solid white pixel. + // Restore these pixels to the underlying color. Tracks are rendered + // using alpha recovery, so this makes the corners transparent. + COLORREF color; + color = GetPixel(hdc, widgetRect.left, widgetRect.top); + DrawThemeBackground(theme, hdc, part, state, &widgetRect, &clipRect); + SetPixel(hdc, widgetRect.left, widgetRect.top, color); + SetPixel(hdc, widgetRect.right - 1, widgetRect.top, color); + SetPixel(hdc, widgetRect.right - 1, widgetRect.bottom - 1, color); + SetPixel(hdc, widgetRect.left, widgetRect.bottom - 1, color); + } else if (aAppearance == StyleAppearance::Progresschunk) { + DrawThemedProgressMeter(aFrame, aAppearance, theme, hdc, part, state, + &widgetRect, &clipRect); + } + // If part is negative, the element wishes us to not render a themed + // background, instead opting to be drawn specially below. + else if (part >= 0) { + DrawThemeBackground(theme, hdc, part, state, &widgetRect, &clipRect); + } + + // Draw focus rectangles for range elements + // XXX it'd be nice to draw these outside of the frame + if (aAppearance == StyleAppearance::Range) { + ElementState contentState = GetContentState(aFrame, aAppearance); + + if (contentState.HasState(ElementState::FOCUSRING)) { + POINT vpOrg; + HPEN hPen = nullptr; + + uint8_t id = SaveDC(hdc); + + ::SelectClipRgn(hdc, nullptr); + ::GetViewportOrgEx(hdc, &vpOrg); + ::SetBrushOrgEx(hdc, vpOrg.x + widgetRect.left, vpOrg.y + widgetRect.top, + nullptr); + ::SetTextColor(hdc, 0); + ::DrawFocusRect(hdc, &widgetRect); + ::RestoreDC(hdc, id); + if (hPen) { + ::DeleteObject(hPen); + } + } + } else if (aAppearance == StyleAppearance::Toolbar && state == 0) { + // Draw toolbar separator lines above all toolbars except the first one. + // The lines are part of the Rebar theme, which is loaded for + // StyleAppearance::Toolbox. + theme = GetTheme(StyleAppearance::Toolbox); + if (!theme) return NS_ERROR_FAILURE; + + widgetRect.bottom = widgetRect.top + TB_SEPARATOR_HEIGHT; + DrawThemeEdge(theme, hdc, RP_BAND, 0, &widgetRect, EDGE_ETCHED, BF_TOP, + nullptr); + } + + nativeDrawing.EndNativeDrawing(); + + if (nativeDrawing.ShouldRenderAgain()) goto RENDER_AGAIN; + + nativeDrawing.PaintToContext(); + + return NS_OK; +} + +bool nsNativeThemeWin::CreateWebRenderCommandsForWidget( + wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources, + const layers::StackingContextHelper& aSc, + layers::RenderRootStateManager* aManager, nsIFrame* aFrame, + StyleAppearance aAppearance, const nsRect& aRect) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::CreateWebRenderCommandsForWidget( + aBuilder, aResources, aSc, aManager, aFrame, aAppearance, aRect); + } + return false; +} + +static void ScaleForFrameDPI(LayoutDeviceIntMargin* aMargin, nsIFrame* aFrame) { + double themeScale = GetThemeDpiScaleFactor(aFrame); + if (themeScale != 1.0) { + aMargin->top = NSToIntRound(aMargin->top * themeScale); + aMargin->left = NSToIntRound(aMargin->left * themeScale); + aMargin->bottom = NSToIntRound(aMargin->bottom * themeScale); + aMargin->right = NSToIntRound(aMargin->right * themeScale); + } +} + +static void ScaleForFrameDPI(LayoutDeviceIntSize* aSize, nsIFrame* aFrame) { + double themeScale = GetThemeDpiScaleFactor(aFrame); + if (themeScale != 1.0) { + aSize->width = NSToIntRound(aSize->width * themeScale); + aSize->height = NSToIntRound(aSize->height * themeScale); + } +} + +LayoutDeviceIntMargin nsNativeThemeWin::GetWidgetBorder( + nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) { + if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { + return Theme::GetWidgetBorder(aContext, aFrame, aAppearance); + } + + LayoutDeviceIntMargin result; + mozilla::Maybe<nsUXThemeClass> themeClass = GetThemeClass(aAppearance); + HTHEME theme = NULL; + if (!themeClass.isNothing()) { + theme = nsUXThemeData::GetTheme(themeClass.value()); + } + if (!theme) { + result = ClassicGetWidgetBorder(aContext, aFrame, aAppearance); + ScaleForFrameDPI(&result, aFrame); + return result; + } + + if (!WidgetIsContainer(aAppearance) || + aAppearance == StyleAppearance::Toolbox || + aAppearance == StyleAppearance::Tabpanel) + return result; // Don't worry about it. + + int32_t part, state; + nsresult rv = GetThemePartAndState(aFrame, aAppearance, part, state); + if (NS_FAILED(rv)) return result; + + if (aAppearance == StyleAppearance::Toolbar) { + // make space for the separator line above all toolbars but the first + if (state == 0) result.top = TB_SEPARATOR_HEIGHT; + return result; + } + + result = GetCachedWidgetBorder(theme, themeClass.value(), aAppearance, part, + state); + + // Remove the edges for tabs that are before or after the selected tab, + if (aAppearance == StyleAppearance::Tab) { + if (IsLeftToSelectedTab(aFrame)) + // Remove the right edge, since we won't be drawing it. + result.right = 0; + else if (IsRightToSelectedTab(aFrame)) + // Remove the left edge, since we won't be drawing it. + result.left = 0; + } + + if (aFrame && (aAppearance == StyleAppearance::NumberInput || + aAppearance == StyleAppearance::Textfield || + aAppearance == StyleAppearance::Textarea)) { + nsIContent* content = aFrame->GetContent(); + if (content && content->IsHTMLElement()) { + // We need to pad textfields by 1 pixel, since the caret will draw + // flush against the edge by default if we don't. + result.top.value++; + result.left.value++; + result.bottom.value++; + result.right.value++; + } + } + + ScaleForFrameDPI(&result, aFrame); + return result; +} + +bool nsNativeThemeWin::GetWidgetPadding(nsDeviceContext* aContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntMargin* aResult) { + if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { + return Theme::GetWidgetPadding(aContext, aFrame, aAppearance, aResult); + } + + bool ok = true; + HANDLE theme = GetTheme(aAppearance); + if (!theme) { + ok = ClassicGetWidgetPadding(aContext, aFrame, aAppearance, aResult); + ScaleForFrameDPI(aResult, aFrame); + return ok; + } + + /* textfields need extra pixels on all sides, otherwise they wrap their + * content too tightly. The actual border is drawn 1px inside the specified + * rectangle, so Gecko will end up making the contents look too small. + * Instead, we add 2px padding for the contents and fix this. (Used to be 1px + * added, see bug 430212) + */ + if (aAppearance == StyleAppearance::NumberInput || + aAppearance == StyleAppearance::Textfield || + aAppearance == StyleAppearance::Textarea) { + aResult->top = aResult->bottom = 2; + aResult->left = aResult->right = 2; + ScaleForFrameDPI(aResult, aFrame); + return ok; + } else if (IsHTMLContent(aFrame) && + (aAppearance == StyleAppearance::Menulist || + aAppearance == StyleAppearance::MenulistButton)) { + /* For content menulist controls, we need an extra pixel so that we have + * room to draw our focus rectangle stuff. Otherwise, the focus rect might + * overlap the control's border. + */ + aResult->top = aResult->bottom = 1; + aResult->left = aResult->right = 1; + ScaleForFrameDPI(aResult, aFrame); + return ok; + } + + int32_t right, left, top, bottom; + right = left = top = bottom = 0; + switch (aAppearance) { + case StyleAppearance::Button: + if (aFrame->GetContent()->IsXULElement()) { + top = 2; + bottom = 3; + } + left = right = 5; + break; + default: + return false; + } + + if (IsFrameRTL(aFrame)) { + aResult->right = left; + aResult->left = right; + } else { + aResult->right = right; + aResult->left = left; + } + aResult->top = top; + aResult->bottom = bottom; + + ScaleForFrameDPI(aResult, aFrame); + return ok; +} + +bool nsNativeThemeWin::GetWidgetOverflow(nsDeviceContext* aContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + nsRect* aOverflowRect) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::GetWidgetOverflow(aContext, aFrame, aAppearance, + aOverflowRect); + } + + /* This is disabled for now, because it causes invalidation problems -- + * see bug 420381. The effect of not updating the overflow area is that + * for dropdown buttons in content areas, there is a 1px border on 3 sides + * where, if invalidated, the dropdown control probably won't be repainted. + * This is fairly minor, as by default there is nothing in that area, and + * a border only shows up if the widget is being hovered. + * + * TODO(jwatt): Figure out what do to about + * StyleAppearance::MozMenulistArrowButton too. + */ +#if 0 + /* We explicitly draw dropdown buttons in HTML content 1px bigger up, right, + * and bottom so that they overlap the dropdown's border like they're + * supposed to. + */ + if (aAppearance == StyleAppearance::MenulistButton && + IsHTMLContent(aFrame) && + !IsWidgetStyled(aFrame->GetParent()->PresContext(), + aFrame->GetParent(), + StyleAppearance::Menulist)) + { + int32_t p2a = aContext->AppUnitsPerDevPixel(); + /* Note: no overflow on the left */ + nsMargin m(p2a, p2a, p2a, 0); + aOverflowRect->Inflate (m); + return true; + } +#endif + + return false; +} + +LayoutDeviceIntSize nsNativeThemeWin::GetMinimumWidgetSize( + nsPresContext* aPresContext, nsIFrame* aFrame, + StyleAppearance aAppearance) { + if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { + return Theme::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance); + } + + mozilla::Maybe<nsUXThemeClass> themeClass = GetThemeClass(aAppearance); + HTHEME theme = NULL; + if (!themeClass.isNothing()) { + theme = nsUXThemeData::GetTheme(themeClass.value()); + } + if (!theme) { + auto result = ClassicGetMinimumWidgetSize(aFrame, aAppearance); + ScaleForFrameDPI(&result, aFrame); + return result; + } + + switch (aAppearance) { + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Toolbox: + case StyleAppearance::Toolbar: + case StyleAppearance::Progresschunk: + case StyleAppearance::Tabpanels: + case StyleAppearance::Tabpanel: + case StyleAppearance::Listbox: + case StyleAppearance::Treeview: + return {}; // Don't worry about it. + default: + break; + } + + // Call GetSystemMetrics to determine size for WinXP scrollbars + // (GetThemeSysSize API returns the optimal size for the theme, but + // Windows appears to always use metrics when drawing standard scrollbars) + THEMESIZE sizeReq = TS_TRUE; // Best-fit size + switch (aAppearance) { + case StyleAppearance::ProgressBar: + // Best-fit size for progress meters is too large for most + // themes. We want these widgets to be able to really shrink + // down, so use the min-size request value (of 0). + sizeReq = TS_MIN; + break; + + case StyleAppearance::RangeThumb: { + LayoutDeviceIntSize result(12, 20); + if (!IsRangeHorizontal(aFrame)) { + std::swap(result.width, result.height); + } + ScaleForFrameDPI(&result, aFrame); + return result; + } + + case StyleAppearance::Separator: { + // that's 2px left margin, 2px right margin and 2px separator + // (the margin is drawn as part of the separator, though) + LayoutDeviceIntSize result(6, 0); + ScaleForFrameDPI(&result, aFrame); + return result; + } + + case StyleAppearance::Button: + // We should let HTML buttons shrink to their min size. + // FIXME bug 403934: We should probably really separate + // GetPreferredWidgetSize from GetMinimumWidgetSize, so callers can + // use the one they want. + if (aFrame->GetContent()->IsHTMLElement()) { + sizeReq = TS_MIN; + } + break; + + default: + break; + } + + int32_t part, state; + nsresult rv = GetThemePartAndState(aFrame, aAppearance, part, state); + if (NS_FAILED(rv)) { + return {}; + } + + LayoutDeviceIntSize result; + rv = GetCachedMinimumWidgetSize(aFrame, theme, themeClass.value(), + aAppearance, part, state, sizeReq, &result); + ScaleForFrameDPI(&result, aFrame); + return result; +} + +NS_IMETHODIMP +nsNativeThemeWin::WidgetStateChanged(nsIFrame* aFrame, + StyleAppearance aAppearance, + nsAtom* aAttribute, bool* aShouldRepaint, + const nsAttrValue* aOldValue) { + // Some widget types just never change state. + if (aAppearance == StyleAppearance::Toolbox || + aAppearance == StyleAppearance::Toolbar || + aAppearance == StyleAppearance::Progresschunk || + aAppearance == StyleAppearance::ProgressBar || + aAppearance == StyleAppearance::Tabpanels || + aAppearance == StyleAppearance::Tabpanel || + aAppearance == StyleAppearance::Separator) { + *aShouldRepaint = false; + return NS_OK; + } + + // We need to repaint the dropdown arrow in vista HTML combobox controls when + // the control is closed to get rid of the hover effect. + if ((aAppearance == StyleAppearance::Menulist || + aAppearance == StyleAppearance::MenulistButton) && + nsNativeTheme::IsHTMLContent(aFrame)) { + *aShouldRepaint = true; + return NS_OK; + } + + // XXXdwh Not sure what can really be done here. Can at least guess for + // specific widgets that they're highly unlikely to have certain states. + // For example, a toolbar doesn't care about any states. + if (!aAttribute) { + // Hover/focus/active changed. Always repaint. + *aShouldRepaint = true; + } else { + // Check the attribute to see if it's relevant. + // disabled, checked, dlgtype, default, etc. + *aShouldRepaint = false; + if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked || + aAttribute == nsGkAtoms::selected || + aAttribute == nsGkAtoms::visuallyselected || + aAttribute == nsGkAtoms::readonly || aAttribute == nsGkAtoms::open || + aAttribute == nsGkAtoms::menuactive || aAttribute == nsGkAtoms::focused) + *aShouldRepaint = true; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsNativeThemeWin::ThemeChanged() { + nsUXThemeData::Invalidate(); + memset(mBorderCacheValid, 0, sizeof(mBorderCacheValid)); + memset(mMinimumWidgetSizeCacheValid, 0, sizeof(mMinimumWidgetSizeCacheValid)); + mGutterSizeCacheValid = false; + return NS_OK; +} + +bool nsNativeThemeWin::ThemeSupportsWidget(nsPresContext* aPresContext, + nsIFrame* aFrame, + StyleAppearance aAppearance) { + // XXXdwh We can go even further and call the API to ask if support exists for + // specific widgets. + + if (IsWidgetAlwaysNonNative(aFrame, aAppearance)) { + return Theme::ThemeSupportsWidget(aPresContext, aFrame, aAppearance); + } + + HANDLE theme = GetTheme(aAppearance); + if (theme || ClassicThemeSupportsWidget(aFrame, aAppearance)) + // turn off theming for some HTML widgets styled by the page + return !IsWidgetStyled(aPresContext, aFrame, aAppearance); + + return false; +} + +bool nsNativeThemeWin::ThemeDrawsFocusForWidget(nsIFrame* aFrame, + StyleAppearance aAppearance) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::ThemeDrawsFocusForWidget(aFrame, aAppearance); + } + switch (aAppearance) { + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::Textarea: + case StyleAppearance::Textfield: + case StyleAppearance::NumberInput: + return true; + default: + return false; + } +} + +bool nsNativeThemeWin::ThemeNeedsComboboxDropmarker() { return true; } + +nsITheme::Transparency nsNativeThemeWin::GetWidgetTransparency( + nsIFrame* aFrame, StyleAppearance aAppearance) { + if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) { + return Theme::GetWidgetTransparency(aFrame, aAppearance); + } + + switch (aAppearance) { + case StyleAppearance::ProgressBar: + case StyleAppearance::Progresschunk: + case StyleAppearance::Range: + return eTransparent; + default: + break; + } + + HANDLE theme = GetTheme(aAppearance); + // For the classic theme we don't really have a way of knowing + if (!theme) { + return eUnknownTransparency; + } + + int32_t part, state; + nsresult rv = GetThemePartAndState(aFrame, aAppearance, part, state); + // Fail conservatively + NS_ENSURE_SUCCESS(rv, eUnknownTransparency); + + if (part <= 0) { + // Not a real part code, so IsThemeBackgroundPartiallyTransparent may + // not work, so don't call it. + return eUnknownTransparency; + } + + if (IsThemeBackgroundPartiallyTransparent(theme, part, state)) + return eTransparent; + return eOpaque; +} + +/* Windows 9x/NT/2000/Classic XP Theme Support */ + +bool nsNativeThemeWin::ClassicThemeSupportsWidget(nsIFrame* aFrame, + StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::Button: + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::Range: + case StyleAppearance::RangeThumb: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::Listbox: + case StyleAppearance::Treeview: + case StyleAppearance::ProgressBar: + case StyleAppearance::Progresschunk: + case StyleAppearance::Tab: + case StyleAppearance::Tabpanel: + case StyleAppearance::Tabpanels: + return true; + default: + return false; + } +} + +LayoutDeviceIntMargin nsNativeThemeWin::ClassicGetWidgetBorder( + nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) { + LayoutDeviceIntMargin result; + switch (aAppearance) { + case StyleAppearance::Button: + result.top = result.left = result.bottom = result.right = 2; + break; + case StyleAppearance::Listbox: + case StyleAppearance::Treeview: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::Tab: + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + result.top = result.left = result.bottom = result.right = 2; + break; + case StyleAppearance::ProgressBar: + result.top = result.left = result.bottom = result.right = 1; + break; + default: + result.top = result.bottom = result.left = result.right = 0; + break; + } + return result; +} + +bool nsNativeThemeWin::ClassicGetWidgetPadding(nsDeviceContext* aContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntMargin* aResult) { + switch (aAppearance) { + case StyleAppearance::ProgressBar: + (*aResult).top = (*aResult).left = (*aResult).bottom = (*aResult).right = + 1; + return true; + default: + return false; + } +} + +LayoutDeviceIntSize nsNativeThemeWin::ClassicGetMinimumWidgetSize( + nsIFrame* aFrame, StyleAppearance aAppearance) { + LayoutDeviceIntSize result; + switch (aAppearance) { + case StyleAppearance::RangeThumb: { + if (IsRangeHorizontal(aFrame)) { + result.width = 12; + result.height = 20; + } else { + result.width = 20; + result.height = 12; + } + break; + } + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::Button: + case StyleAppearance::Listbox: + case StyleAppearance::Treeview: + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::Progresschunk: + case StyleAppearance::ProgressBar: + case StyleAppearance::Tab: + case StyleAppearance::Tabpanel: + case StyleAppearance::Tabpanels: + // no minimum widget size + break; + + default: + break; + } + return result; +} + +nsresult nsNativeThemeWin::ClassicGetThemePartAndState( + nsIFrame* aFrame, StyleAppearance aAppearance, int32_t& aPart, + int32_t& aState, bool& aFocused) { + aFocused = false; + switch (aAppearance) { + case StyleAppearance::Button: { + aPart = DFC_BUTTON; + aState = DFCS_BUTTONPUSH; + aFocused = false; + + ElementState contentState = GetContentState(aFrame, aAppearance); + if (contentState.HasState(ElementState::DISABLED)) { + aState |= DFCS_INACTIVE; + } else if (IsOpenButton(aFrame)) { + aState |= DFCS_PUSHED; + } else if (IsCheckedButton(aFrame)) { + aState |= DFCS_CHECKED; + } else { + if (contentState.HasAllStates(ElementState::ACTIVE | + ElementState::HOVER)) { + aState |= DFCS_PUSHED; + // The down state is flat if the button is focusable + if (aFrame->StyleUI()->UserFocus() == StyleUserFocus::Normal) { + if (!aFrame->GetContent()->IsHTMLElement()) aState |= DFCS_FLAT; + + aFocused = true; + } + } + // On Windows, focused buttons are always drawn as such by the native + // theme, that's why we check ElementState::FOCUS instead of + // ElementState::FOCUSRING. + if (contentState.HasState(ElementState::FOCUS) || + (aState == DFCS_BUTTONPUSH && IsDefaultButton(aFrame))) { + aFocused = true; + } + } + + return NS_OK; + } + case StyleAppearance::Listbox: + case StyleAppearance::Treeview: + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::Range: + case StyleAppearance::RangeThumb: + case StyleAppearance::Progresschunk: + case StyleAppearance::ProgressBar: + case StyleAppearance::Tab: + case StyleAppearance::Tabpanel: + case StyleAppearance::Tabpanels: + // these don't use DrawFrameControl + return NS_OK; + default: + return NS_ERROR_FAILURE; + } +} + +// Draw classic Windows tab +// (no system API for this, but DrawEdge can draw all the parts of a tab) +static void DrawTab(HDC hdc, const RECT& R, int32_t aPosition, bool aSelected, + bool aDrawLeft, bool aDrawRight) { + int32_t leftFlag, topFlag, rightFlag, lightFlag, shadeFlag; + RECT topRect, sideRect, bottomRect, lightRect, shadeRect; + int32_t selectedOffset, lOffset, rOffset; + + selectedOffset = aSelected ? 1 : 0; + lOffset = aDrawLeft ? 2 : 0; + rOffset = aDrawRight ? 2 : 0; + + // Get info for tab orientation/position (Left, Top, Right, Bottom) + switch (aPosition) { + case BF_LEFT: + leftFlag = BF_TOP; + topFlag = BF_LEFT; + rightFlag = BF_BOTTOM; + lightFlag = BF_DIAGONAL_ENDTOPRIGHT; + shadeFlag = BF_DIAGONAL_ENDBOTTOMRIGHT; + + ::SetRect(&topRect, R.left, R.top + lOffset, R.right, R.bottom - rOffset); + ::SetRect(&sideRect, R.left + 2, R.top, R.right - 2 + selectedOffset, + R.bottom); + ::SetRect(&bottomRect, R.right - 2, R.top, R.right, R.bottom); + ::SetRect(&lightRect, R.left, R.top, R.left + 3, R.top + 3); + ::SetRect(&shadeRect, R.left + 1, R.bottom - 2, R.left + 2, R.bottom - 1); + break; + case BF_TOP: + leftFlag = BF_LEFT; + topFlag = BF_TOP; + rightFlag = BF_RIGHT; + lightFlag = BF_DIAGONAL_ENDTOPRIGHT; + shadeFlag = BF_DIAGONAL_ENDBOTTOMRIGHT; + + ::SetRect(&topRect, R.left + lOffset, R.top, R.right - rOffset, R.bottom); + ::SetRect(&sideRect, R.left, R.top + 2, R.right, + R.bottom - 1 + selectedOffset); + ::SetRect(&bottomRect, R.left, R.bottom - 1, R.right, R.bottom); + ::SetRect(&lightRect, R.left, R.top, R.left + 3, R.top + 3); + ::SetRect(&shadeRect, R.right - 2, R.top + 1, R.right - 1, R.top + 2); + break; + case BF_RIGHT: + leftFlag = BF_TOP; + topFlag = BF_RIGHT; + rightFlag = BF_BOTTOM; + lightFlag = BF_DIAGONAL_ENDTOPLEFT; + shadeFlag = BF_DIAGONAL_ENDBOTTOMLEFT; + + ::SetRect(&topRect, R.left, R.top + lOffset, R.right, R.bottom - rOffset); + ::SetRect(&sideRect, R.left + 2 - selectedOffset, R.top, R.right - 2, + R.bottom); + ::SetRect(&bottomRect, R.left, R.top, R.left + 2, R.bottom); + ::SetRect(&lightRect, R.right - 3, R.top, R.right - 1, R.top + 2); + ::SetRect(&shadeRect, R.right - 2, R.bottom - 3, R.right, R.bottom - 1); + break; + case BF_BOTTOM: + leftFlag = BF_LEFT; + topFlag = BF_BOTTOM; + rightFlag = BF_RIGHT; + lightFlag = BF_DIAGONAL_ENDTOPLEFT; + shadeFlag = BF_DIAGONAL_ENDBOTTOMLEFT; + + ::SetRect(&topRect, R.left + lOffset, R.top, R.right - rOffset, R.bottom); + ::SetRect(&sideRect, R.left, R.top + 2 - selectedOffset, R.right, + R.bottom - 2); + ::SetRect(&bottomRect, R.left, R.top, R.right, R.top + 2); + ::SetRect(&lightRect, R.left, R.bottom - 3, R.left + 2, R.bottom - 1); + ::SetRect(&shadeRect, R.right - 2, R.bottom - 3, R.right, R.bottom - 1); + break; + default: + MOZ_CRASH(); + } + + // Background + ::FillRect(hdc, &R, (HBRUSH)(COLOR_3DFACE + 1)); + + // Tab "Top" + ::DrawEdge(hdc, &topRect, EDGE_RAISED, BF_SOFT | topFlag); + + // Tab "Bottom" + if (!aSelected) ::DrawEdge(hdc, &bottomRect, EDGE_RAISED, BF_SOFT | topFlag); + + // Tab "Sides" + if (!aDrawLeft) leftFlag = 0; + if (!aDrawRight) rightFlag = 0; + ::DrawEdge(hdc, &sideRect, EDGE_RAISED, BF_SOFT | leftFlag | rightFlag); + + // Tab Diagonal Corners + if (aDrawLeft) ::DrawEdge(hdc, &lightRect, EDGE_RAISED, BF_SOFT | lightFlag); + + if (aDrawRight) ::DrawEdge(hdc, &shadeRect, EDGE_RAISED, BF_SOFT | shadeFlag); +} + +void nsNativeThemeWin::DrawCheckedRect(HDC hdc, const RECT& rc, int32_t fore, + int32_t back, HBRUSH defaultBack) { + static WORD patBits[8] = {0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55}; + + HBITMAP patBmp = ::CreateBitmap(8, 8, 1, 1, patBits); + if (patBmp) { + HBRUSH brush = (HBRUSH)::CreatePatternBrush(patBmp); + if (brush) { + COLORREF oldForeColor = ::SetTextColor(hdc, ::GetSysColor(fore)); + COLORREF oldBackColor = ::SetBkColor(hdc, ::GetSysColor(back)); + POINT vpOrg; + + ::UnrealizeObject(brush); + ::GetViewportOrgEx(hdc, &vpOrg); + ::SetBrushOrgEx(hdc, vpOrg.x + rc.left, vpOrg.y + rc.top, nullptr); + HBRUSH oldBrush = (HBRUSH)::SelectObject(hdc, brush); + ::FillRect(hdc, &rc, brush); + ::SetTextColor(hdc, oldForeColor); + ::SetBkColor(hdc, oldBackColor); + ::SelectObject(hdc, oldBrush); + ::DeleteObject(brush); + } else + ::FillRect(hdc, &rc, defaultBack); + + ::DeleteObject(patBmp); + } +} + +nsresult nsNativeThemeWin::ClassicDrawWidgetBackground( + gfxContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance, + const nsRect& aRect, const nsRect& aDirtyRect) { + int32_t part, state; + bool focused; + nsresult rv; + rv = ClassicGetThemePartAndState(aFrame, aAppearance, part, state, focused); + if (NS_FAILED(rv)) return rv; + + if (AssumeThemePartAndStateAreTransparent(part, state)) { + return NS_OK; + } + + gfxFloat p2a = gfxFloat(aFrame->PresContext()->AppUnitsPerDevPixel()); + RECT widgetRect; + gfxRect tr(aRect.X(), aRect.Y(), aRect.Width(), aRect.Height()), + dr(aDirtyRect.X(), aDirtyRect.Y(), aDirtyRect.Width(), + aDirtyRect.Height()); + + tr.Scale(1.0 / p2a); + dr.Scale(1.0 / p2a); + + gfxWindowsNativeDrawing nativeDrawing( + aContext, dr, GetWidgetNativeDrawingFlags(aAppearance)); + +RENDER_AGAIN: + + HDC hdc = nativeDrawing.BeginNativeDrawing(); + if (!hdc) return NS_ERROR_FAILURE; + + nativeDrawing.TransformToNativeRect(tr, widgetRect); + + rv = NS_OK; + switch (aAppearance) { + // Draw button + case StyleAppearance::Button: { + if (focused) { + // draw dark button focus border first + if (HBRUSH brush = ::GetSysColorBrush(COLOR_3DDKSHADOW)) { + ::FrameRect(hdc, &widgetRect, brush); + } + InflateRect(&widgetRect, -1, -1); + } + // setup DC to make DrawFrameControl draw correctly + int32_t oldTA = ::SetTextAlign(hdc, TA_TOP | TA_LEFT | TA_NOUPDATECP); + ::DrawFrameControl(hdc, &widgetRect, part, state); + ::SetTextAlign(hdc, oldTA); + break; + } + // Draw controls with 2px 3D inset border + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::Listbox: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: { + // Draw inset edge + ::DrawEdge(hdc, &widgetRect, EDGE_SUNKEN, BF_RECT | BF_ADJUST); + + ElementState elementState = GetContentState(aFrame, aAppearance); + + // Fill in background + + if (elementState.HasState(ElementState::DISABLED) || + (aFrame->GetContent()->IsXULElement() && IsReadOnly(aFrame))) + ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_BTNFACE + 1)); + else + ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_WINDOW + 1)); + + break; + } + case StyleAppearance::Treeview: { + // Draw inset edge + ::DrawEdge(hdc, &widgetRect, EDGE_SUNKEN, BF_RECT | BF_ADJUST); + + // Fill in window color background + ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_WINDOW + 1)); + + break; + } + // Draw 3D face background controls + case StyleAppearance::ProgressBar: + // Draw 3D border + ::DrawEdge(hdc, &widgetRect, BDR_SUNKENOUTER, BF_RECT | BF_MIDDLE); + InflateRect(&widgetRect, -1, -1); + [[fallthrough]]; + case StyleAppearance::Tabpanel: { + ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_BTNFACE + 1)); + break; + } + case StyleAppearance::RangeThumb: { + ElementState elementState = GetContentState(aFrame, aAppearance); + + ::DrawEdge(hdc, &widgetRect, EDGE_RAISED, + BF_RECT | BF_SOFT | BF_MIDDLE | BF_ADJUST); + if (elementState.HasState(ElementState::DISABLED)) { + DrawCheckedRect(hdc, widgetRect, COLOR_3DFACE, COLOR_3DHILIGHT, + (HBRUSH)COLOR_3DHILIGHT); + } + + break; + } + // Draw scale track background + case StyleAppearance::Range: { + const int32_t trackWidth = 4; + // When rounding is necessary, we round the position of the track + // away from the chevron of the thumb to make it look better. + if (IsRangeHorizontal(aFrame)) { + widgetRect.top += (widgetRect.bottom - widgetRect.top - trackWidth) / 2; + widgetRect.bottom = widgetRect.top + trackWidth; + } else { + if (!IsFrameRTL(aFrame)) { + widgetRect.left += + (widgetRect.right - widgetRect.left - trackWidth) / 2; + widgetRect.right = widgetRect.left + trackWidth; + } else { + widgetRect.right -= + (widgetRect.right - widgetRect.left - trackWidth) / 2; + widgetRect.left = widgetRect.right - trackWidth; + } + } + + ::DrawEdge(hdc, &widgetRect, EDGE_SUNKEN, BF_RECT | BF_ADJUST); + ::FillRect(hdc, &widgetRect, (HBRUSH)GetStockObject(GRAY_BRUSH)); + + break; + } + case StyleAppearance::Progresschunk: { + nsIFrame* stateFrame = aFrame->GetParent(); + ElementState elementState = GetContentState(stateFrame, aAppearance); + + const bool indeterminate = + elementState.HasState(ElementState::INDETERMINATE); + bool vertical = IsVerticalProgress(stateFrame); + + nsIContent* content = aFrame->GetContent(); + if (!indeterminate || !content) { + ::FillRect(hdc, &widgetRect, (HBRUSH)(COLOR_HIGHLIGHT + 1)); + break; + } + + RECT overlayRect = CalculateProgressOverlayRect( + aFrame, &widgetRect, vertical, indeterminate, true); + + ::FillRect(hdc, &overlayRect, (HBRUSH)(COLOR_HIGHLIGHT + 1)); + + if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) { + NS_WARNING("unable to animate progress widget!"); + } + break; + } + + // Draw Tab + case StyleAppearance::Tab: { + DrawTab(hdc, widgetRect, IsBottomTab(aFrame) ? BF_BOTTOM : BF_TOP, + IsSelectedTab(aFrame), !IsRightToSelectedTab(aFrame), + !IsLeftToSelectedTab(aFrame)); + + break; + } + case StyleAppearance::Tabpanels: + ::DrawEdge(hdc, &widgetRect, EDGE_RAISED, + BF_SOFT | BF_MIDDLE | BF_LEFT | BF_RIGHT | BF_BOTTOM); + + break; + + default: + rv = NS_ERROR_FAILURE; + break; + } + + nativeDrawing.EndNativeDrawing(); + + if (NS_FAILED(rv)) return rv; + + if (nativeDrawing.ShouldRenderAgain()) goto RENDER_AGAIN; + + nativeDrawing.PaintToContext(); + + return rv; +} + +uint32_t nsNativeThemeWin::GetWidgetNativeDrawingFlags( + StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::Button: + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + return gfxWindowsNativeDrawing::CANNOT_DRAW_TO_COLOR_ALPHA | + gfxWindowsNativeDrawing::CAN_AXIS_ALIGNED_SCALE | + gfxWindowsNativeDrawing::CANNOT_COMPLEX_TRANSFORM; + + // need to check these others + default: + return gfxWindowsNativeDrawing::CANNOT_DRAW_TO_COLOR_ALPHA | + gfxWindowsNativeDrawing::CANNOT_AXIS_ALIGNED_SCALE | + gfxWindowsNativeDrawing::CANNOT_COMPLEX_TRANSFORM; + } +} + +} // namespace mozilla::widget + +/////////////////////////////////////////// +// Creation Routine +/////////////////////////////////////////// + +already_AddRefed<Theme> do_CreateNativeThemeDoNotUseDirectly() { + return do_AddRef(new nsNativeThemeWin()); +} diff --git a/widget/windows/nsNativeThemeWin.h b/widget/windows/nsNativeThemeWin.h new file mode 100644 index 0000000000..9937018197 --- /dev/null +++ b/widget/windows/nsNativeThemeWin.h @@ -0,0 +1,166 @@ +/* -*- 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 nsNativeThemeWin_h +#define nsNativeThemeWin_h + +#include <windows.h> + +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" +#include "Theme.h" +#include "nsUXThemeConstants.h" +#include "nsUXThemeData.h" + +namespace mozilla::widget { + +class nsNativeThemeWin : public Theme { + protected: + virtual ~nsNativeThemeWin(); + + public: + // Whether we draw a non-native widget. + // + // We always draw scrollbars as non-native so that all of Firefox has + // consistent scrollbar styles both in chrome and content (plus, the + // non-native scrollbars support scrollbar-width, auto-darkening...). + // + // We draw other widgets as non-native when their color-scheme is dark. In + // that case (`BecauseColorMismatch`) we don't call into the non-native theme + // for sizing information (GetWidgetPadding/Border and GetMinimumWidgetSize), + // to avoid subtle sizing changes. The non-native theme can basically draw at + // any size, so we prefer to have consistent sizing information. + enum class NonNative { No, Always, BecauseColorMismatch }; + static bool IsWidgetAlwaysNonNative(nsIFrame*, StyleAppearance); + NonNative IsWidgetNonNative(nsIFrame*, StyleAppearance); + + // The nsITheme interface. + NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + const nsRect& aRect, const nsRect& aDirtyRect, + DrawOverflow) override; + + bool CreateWebRenderCommandsForWidget(wr::DisplayListBuilder&, + wr::IpcResourceUpdateQueue&, + const layers::StackingContextHelper&, + layers::RenderRootStateManager*, + nsIFrame*, StyleAppearance, + const nsRect&) override; + + [[nodiscard]] LayoutDeviceIntMargin GetWidgetBorder( + nsDeviceContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance) override; + + bool GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntMargin* aResult) override; + + virtual bool GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + nsRect* aOverflowRect) override; + + LayoutDeviceIntSize GetMinimumWidgetSize( + nsPresContext* aPresContext, nsIFrame* aFrame, + StyleAppearance aAppearance) override; + + virtual Transparency GetWidgetTransparency( + nsIFrame* aFrame, StyleAppearance aAppearance) override; + + NS_IMETHOD WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance, + nsAtom* aAttribute, bool* aShouldRepaint, + const nsAttrValue* aOldValue) override; + + NS_IMETHOD ThemeChanged() override; + + bool ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame, + StyleAppearance aAppearance) override; + + bool ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) override; + + bool ThemeWantsButtonInnerFocusRing() override { return true; } + + bool ThemeNeedsComboboxDropmarker() override; + + nsNativeThemeWin(); + + protected: + Maybe<nsUXThemeClass> GetThemeClass(StyleAppearance aAppearance); + HANDLE GetTheme(StyleAppearance aAppearance); + nsresult GetThemePartAndState(nsIFrame* aFrame, StyleAppearance aAppearance, + int32_t& aPart, int32_t& aState); + nsresult ClassicGetThemePartAndState(nsIFrame* aFrame, + StyleAppearance aAppearance, + int32_t& aPart, int32_t& aState, + bool& aFocused); + nsresult ClassicDrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + const nsRect& aRect, + const nsRect& aClipRect); + [[nodiscard]] LayoutDeviceIntMargin ClassicGetWidgetBorder( + nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance); + bool ClassicGetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntMargin* aResult); + LayoutDeviceIntSize ClassicGetMinimumWidgetSize(nsIFrame* aFrame, + StyleAppearance aAppearance); + bool ClassicThemeSupportsWidget(nsIFrame* aFrame, + StyleAppearance aAppearance); + void DrawCheckedRect(HDC hdc, const RECT& rc, int32_t fore, int32_t back, + HBRUSH defaultBack); + uint32_t GetWidgetNativeDrawingFlags(StyleAppearance aAppearance); + int32_t StandardGetState(nsIFrame* aFrame, StyleAppearance aAppearance, + bool wantFocused); + bool IsMenuActive(nsIFrame* aFrame, StyleAppearance aAppearance); + RECT CalculateProgressOverlayRect(nsIFrame* aFrame, RECT* aWidgetRect, + bool aIsVertical, bool aIsIndeterminate, + bool aIsClassic); + void DrawThemedProgressMeter(nsIFrame* aFrame, StyleAppearance aAppearance, + HANDLE aTheme, HDC aHdc, int aPart, int aState, + RECT* aWidgetRect, RECT* aClipRect); + + [[nodiscard]] LayoutDeviceIntMargin GetCachedWidgetBorder( + HANDLE aTheme, nsUXThemeClass aThemeClass, StyleAppearance aAppearance, + int32_t aPart, int32_t aState); + + nsresult GetCachedMinimumWidgetSize(nsIFrame* aFrame, HANDLE aTheme, + nsUXThemeClass aThemeClass, + StyleAppearance aAppearance, + int32_t aPart, int32_t aState, + THEMESIZE aSizeReq, + LayoutDeviceIntSize* aResult); + + SIZE GetCachedGutterSize(HANDLE theme); + + private: + TimeStamp mProgressDeterminateTimeStamp; + TimeStamp mProgressIndeterminateTimeStamp; + + // eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT is about 800 at the time of + // writing this, and nsIntMargin is 16 bytes wide, which makes this cache (1/8 + // + 16) * 800 bytes, or about ~12KB. We could probably reduce this cache to + // 3KB by caching on the aAppearance value instead, but there would be some + // uncacheable values, since we derive some theme parts from other arguments. + uint8_t + mBorderCacheValid[(eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT + 7) / + 8]; + LayoutDeviceIntMargin + mBorderCache[eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT]; + + // See the above not for mBorderCache and friends. However + // LayoutDeviceIntSize is half the size of nsIntMargin, making the + // cache roughly half as large. In total the caches should come to about 18KB. + uint8_t mMinimumWidgetSizeCacheValid + [(eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT + 7) / 8]; + LayoutDeviceIntSize + mMinimumWidgetSizeCache[eUXNumClasses * THEME_PART_DISTINCT_VALUE_COUNT]; + + bool mGutterSizeCacheValid; + SIZE mGutterSizeCache; +}; + +} // namespace mozilla::widget + +#endif diff --git a/widget/windows/nsPrintDialogUtil.cpp b/widget/windows/nsPrintDialogUtil.cpp new file mode 100644 index 0000000000..43f56e9706 --- /dev/null +++ b/widget/windows/nsPrintDialogUtil.cpp @@ -0,0 +1,360 @@ +/* -*- 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/. */ + +/* ------------------------------------------------------------------- +To Build This: + + You need to add this to the the makefile.win in mozilla/dom/base: + + .\$(OBJDIR)\nsFlyOwnPrintDialog.obj \ + + + And this to the makefile.win in mozilla/content/build: + +WIN_LIBS= \ + winspool.lib \ + comctl32.lib \ + comdlg32.lib + +---------------------------------------------------------------------- */ + +#include <windows.h> +#include <tchar.h> + +#include <unknwn.h> +#include <commdlg.h> + +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Span.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsIPrintSettings.h" +#include "nsIPrintSettingsWin.h" +#include "nsIPrinterList.h" +#include "nsServiceManagerUtils.h" + +#include "nsRect.h" + +#include "nsCRT.h" +#include "prenv.h" /* for PR_GetEnv */ + +#include <windows.h> +#include <winspool.h> + +// For Localization + +// For NS_CopyUnicodeToNative +#include "nsNativeCharsetUtils.h" + +// This is for extending the dialog +#include <dlgs.h> + +#include "nsWindowsHelpers.h" +#include "WinUtils.h" + +//----------------------------------------------- +// Global Data +//----------------------------------------------- + +static HWND gParentWnd = nullptr; + +//---------------------------------------------------------------------------------- +// Returns a Global Moveable Memory Handle to a DevMode +// from the Printer by the name of aPrintName +// +// NOTE: +// This function assumes that aPrintName has already been converted from +// unicode +// +static nsReturnRef<nsHGLOBAL> CreateGlobalDevModeAndInit( + const nsString& aPrintName, nsIPrintSettings* aPS) { + nsHPRINTER hPrinter = nullptr; + // const cast kludge for silly Win32 api's + LPWSTR printName = + const_cast<wchar_t*>(static_cast<const wchar_t*>(aPrintName.get())); + BOOL status = ::OpenPrinterW(printName, &hPrinter, nullptr); + if (!status) { + return nsReturnRef<nsHGLOBAL>(); + } + + // Make sure hPrinter is closed on all paths + nsAutoPrinter autoPrinter(hPrinter); + + // Get the buffer size + LONG needed = ::DocumentPropertiesW(gParentWnd, hPrinter, printName, nullptr, + nullptr, 0); + if (needed < 0) { + return nsReturnRef<nsHGLOBAL>(); + } + + // Some drivers do not return the correct size for their DEVMODE, so we + // over-allocate to try and compensate. + // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1664530#c5) + needed *= 2; + nsAutoDevMode newDevMode( + (LPDEVMODEW)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, needed)); + if (!newDevMode) { + return nsReturnRef<nsHGLOBAL>(); + } + + nsHGLOBAL hDevMode = ::GlobalAlloc(GHND, needed); + nsAutoGlobalMem globalDevMode(hDevMode); + if (!hDevMode) { + return nsReturnRef<nsHGLOBAL>(); + } + + LONG ret = ::DocumentPropertiesW(gParentWnd, hPrinter, printName, newDevMode, + nullptr, DM_OUT_BUFFER); + if (ret != IDOK) { + return nsReturnRef<nsHGLOBAL>(); + } + + // Lock memory and copy contents from DEVMODE (current printer) + // to Global Memory DEVMODE + LPDEVMODEW devMode = (DEVMODEW*)::GlobalLock(hDevMode); + if (!devMode) { + return nsReturnRef<nsHGLOBAL>(); + } + + memcpy(devMode, newDevMode.get(), needed); + // Initialize values from the PrintSettings + nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(aPS); + MOZ_ASSERT(psWin); + psWin->CopyToNative(devMode); + + // Sets back the changes we made to the DevMode into the Printer Driver + ret = ::DocumentPropertiesW(gParentWnd, hPrinter, printName, devMode, devMode, + DM_IN_BUFFER | DM_OUT_BUFFER); + if (ret != IDOK) { + ::GlobalUnlock(hDevMode); + return nsReturnRef<nsHGLOBAL>(); + } + + ::GlobalUnlock(hDevMode); + + return globalDevMode.out(); +} + +//------------------------------------------------------------------ +// helper +static void GetDefaultPrinterNameFromGlobalPrinters(nsAString& aPrinterName) { + aPrinterName.Truncate(); + nsCOMPtr<nsIPrinterList> printerList = + do_GetService("@mozilla.org/gfx/printerlist;1"); + if (printerList) { + printerList->GetSystemDefaultPrinterName(aPrinterName); + } +} + +//------------------------------------------------------------------ +// Displays the native Print Dialog +nsresult NativeShowPrintDialog(HWND aHWnd, bool aHaveSelection, + nsIPrintSettings* aPrintSettings) { + // NS_ENSURE_ARG_POINTER(aHWnd); + NS_ENSURE_ARG_POINTER(aPrintSettings); + + // Get the Print Name to be used + nsString printerName; + aPrintSettings->GetPrinterName(printerName); + + // If there is no name then use the default printer + if (printerName.IsEmpty()) { + GetDefaultPrinterNameFromGlobalPrinters(printerName); + } else { + HANDLE hPrinter = nullptr; + if (!::OpenPrinterW(const_cast<wchar_t*>( + static_cast<const wchar_t*>(printerName.get())), + &hPrinter, nullptr)) { + // If the last used printer is not found, we should use default printer. + GetDefaultPrinterNameFromGlobalPrinters(printerName); + } else { + ::ClosePrinter(hPrinter); + } + } + + // Now create a DEVNAMES struct so the the dialog is initialized correctly. + + uint32_t len = printerName.Length(); + nsHGLOBAL hDevNames = + ::GlobalAlloc(GHND, sizeof(wchar_t) * (len + 1) + sizeof(DEVNAMES)); + nsAutoGlobalMem autoDevNames(hDevNames); + if (!hDevNames) { + return NS_ERROR_OUT_OF_MEMORY; + } + + DEVNAMES* pDevNames = (DEVNAMES*)::GlobalLock(hDevNames); + if (!pDevNames) { + return NS_ERROR_FAILURE; + } + pDevNames->wDriverOffset = sizeof(DEVNAMES) / sizeof(wchar_t); + pDevNames->wDeviceOffset = sizeof(DEVNAMES) / sizeof(wchar_t); + pDevNames->wOutputOffset = sizeof(DEVNAMES) / sizeof(wchar_t) + len; + pDevNames->wDefault = 0; + + memcpy(pDevNames + 1, printerName.get(), (len + 1) * sizeof(wchar_t)); + ::GlobalUnlock(hDevNames); + + // Create a Moveable Memory Object that holds a new DevMode + // from the Printer Name + // The PRINTDLG.hDevMode requires that it be a moveable memory object + // NOTE: autoDevMode is automatically freed when any error occurred + nsAutoGlobalMem autoDevMode( + CreateGlobalDevModeAndInit(printerName, aPrintSettings)); + + // Prepare to Display the Print Dialog + // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms646942(v=vs.85) + // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-printdlgexw + PRINTDLGEXW prntdlg; + memset(&prntdlg, 0, sizeof(prntdlg)); + + prntdlg.lStructSize = sizeof(prntdlg); + prntdlg.hwndOwner = aHWnd; + prntdlg.hDevMode = autoDevMode.get(); + prntdlg.hDevNames = hDevNames; + prntdlg.hDC = nullptr; + prntdlg.Flags = PD_ALLPAGES | PD_RETURNIC | PD_USEDEVMODECOPIESANDCOLLATE | + PD_COLLATE | PD_NOCURRENTPAGE; + + // If there is a current selection then enable the "Selection" radio button + if (!aHaveSelection) { + prntdlg.Flags |= PD_NOSELECTION; + } + + // 10 seems like a reasonable max number of ranges to support by default if + // the user doesn't choose a greater thing in the UI. + constexpr size_t kMinSupportedRanges = 10; + + AutoTArray<PRINTPAGERANGE, kMinSupportedRanges> winPageRanges; + // Set up the page ranges. + { + AutoTArray<int32_t, kMinSupportedRanges * 2> pageRanges; + aPrintSettings->GetPageRanges(pageRanges); + // If there is a specified page range then enable the "Custom" radio button + if (!pageRanges.IsEmpty()) { + prntdlg.Flags |= PD_PAGENUMS; + } + + const size_t specifiedRanges = pageRanges.Length() / 2; + const size_t maxRanges = std::max(kMinSupportedRanges, specifiedRanges); + + prntdlg.nMaxPageRanges = maxRanges; + prntdlg.nPageRanges = specifiedRanges; + + winPageRanges.SetCapacity(maxRanges); + for (size_t i = 0; i < pageRanges.Length(); i += 2) { + PRINTPAGERANGE* range = winPageRanges.AppendElement(); + range->nFromPage = pageRanges[i]; + range->nToPage = pageRanges[i + 1]; + } + prntdlg.lpPageRanges = winPageRanges.Elements(); + + prntdlg.nMinPage = 1; + // TODO(emilio): Could probably get the right page number here from the + // new print UI. + prntdlg.nMaxPage = 0xFFFF; + } + + // NOTE(emilio): This can always be 1 because we use the DEVMODE copies + // feature (see PD_USEDEVMODECOPIESANDCOLLATE). + prntdlg.nCopies = 1; + + prntdlg.hInstance = nullptr; + prntdlg.lpPrintTemplateName = nullptr; + + prntdlg.lpCallback = nullptr; + prntdlg.nPropertyPages = 0; + prntdlg.lphPropertyPages = nullptr; + + prntdlg.nStartPage = START_PAGE_GENERAL; + prntdlg.dwResultAction = 0; + + HRESULT result; + { + mozilla::widget::WinUtils::AutoSystemDpiAware dpiAwareness; + mozilla::BackgroundHangMonitor().NotifyWait(); + result = ::PrintDlgExW(&prntdlg); + } + + auto cancelOnExit = mozilla::MakeScopeExit([&] { ::SetFocus(aHWnd); }); + + if (NS_WARN_IF(!SUCCEEDED(result))) { +#ifdef DEBUG + printf_stderr("PrintDlgExW failed with %lx\n", result); +#endif + return NS_ERROR_FAILURE; + } + if (NS_WARN_IF(prntdlg.dwResultAction != PD_RESULT_PRINT)) { + return NS_ERROR_ABORT; + } + // check to make sure we don't have any nullptr pointers + NS_ENSURE_TRUE(prntdlg.hDevMode, NS_ERROR_ABORT); + NS_ENSURE_TRUE(prntdlg.hDevNames, NS_ERROR_ABORT); + // Lock the deviceNames and check for nullptr + DEVNAMES* devnames = (DEVNAMES*)::GlobalLock(prntdlg.hDevNames); + NS_ENSURE_TRUE(devnames, NS_ERROR_ABORT); + + char16_t* device = &(((char16_t*)devnames)[devnames->wDeviceOffset]); + char16_t* driver = &(((char16_t*)devnames)[devnames->wDriverOffset]); + + // Check to see if the "Print To File" control is checked + // then take the name from devNames and set it in the PrintSettings + // + // NOTE: + // As per Microsoft SDK documentation the returned value offset from + // devnames->wOutputOffset is either "FILE:" or nullptr + // if the "Print To File" checkbox is checked it MUST be "FILE:" + // We assert as an extra safety check. + if (prntdlg.Flags & PD_PRINTTOFILE) { + char16ptr_t fileName = &(((wchar_t*)devnames)[devnames->wOutputOffset]); + NS_ASSERTION(wcscmp(fileName, L"FILE:") == 0, "FileName must be `FILE:`"); + aPrintSettings->SetOutputDestination( + nsIPrintSettings::kOutputDestinationFile); + aPrintSettings->SetToFileName(nsDependentString(fileName)); + } else { + // clear "print to file" info + aPrintSettings->SetOutputDestination( + nsIPrintSettings::kOutputDestinationPrinter); + aPrintSettings->SetToFileName(u""_ns); + } + + nsCOMPtr<nsIPrintSettingsWin> psWin(do_QueryInterface(aPrintSettings)); + MOZ_RELEASE_ASSERT(psWin); + + // Setup local Data members + psWin->SetDeviceName(nsDependentString(device)); + psWin->SetDriverName(nsDependentString(driver)); + + // Fill the print options with the info from the dialog + aPrintSettings->SetPrinterName(nsDependentString(device)); + aPrintSettings->SetPrintSelectionOnly(prntdlg.Flags & PD_SELECTION); + + AutoTArray<int32_t, kMinSupportedRanges * 2> pageRanges; + if (prntdlg.Flags & PD_PAGENUMS) { + pageRanges.SetCapacity(prntdlg.nPageRanges * 2); + for (const auto& range : + mozilla::Span(prntdlg.lpPageRanges, prntdlg.nPageRanges)) { + pageRanges.AppendElement(range.nFromPage); + pageRanges.AppendElement(range.nToPage); + } + } + aPrintSettings->SetPageRanges(pageRanges); + + // Unlock DeviceNames + ::GlobalUnlock(prntdlg.hDevNames); + + // Transfer the settings from the native data to the PrintSettings + LPDEVMODEW devMode = (LPDEVMODEW)::GlobalLock(prntdlg.hDevMode); + if (!devMode || !prntdlg.hDC) { + return NS_ERROR_FAILURE; + } + psWin->SetDevMode(devMode); // copies DevMode + psWin->CopyFromNative(prntdlg.hDC, devMode); + ::GlobalUnlock(prntdlg.hDevMode); + ::DeleteDC(prntdlg.hDC); + + cancelOnExit.release(); + return NS_OK; +} diff --git a/widget/windows/nsPrintDialogUtil.h b/widget/windows/nsPrintDialogUtil.h new file mode 100644 index 0000000000..3ec16e1b1d --- /dev/null +++ b/widget/windows/nsPrintDialogUtil.h @@ -0,0 +1,11 @@ +/* -*- 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 nsFlyOwnDialog_h___ +#define nsFlyOwnDialog_h___ + +nsresult NativeShowPrintDialog(HWND aHWnd, bool aHaveSelection, + nsIPrintSettings* aPrintSettings); + +#endif /* nsFlyOwnDialog_h___ */ diff --git a/widget/windows/nsPrintDialogWin.cpp b/widget/windows/nsPrintDialogWin.cpp new file mode 100644 index 0000000000..35ea52b17b --- /dev/null +++ b/widget/windows/nsPrintDialogWin.cpp @@ -0,0 +1,147 @@ +/* -*- 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 "nsPrintDialogWin.h" + +#include "nsArray.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIBaseWindow.h" +#include "nsIBrowserChild.h" +#include "nsIDialogParamBlock.h" +#include "nsIDocShell.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIPrintSettings.h" +#include "nsIWebBrowserChrome.h" +#include "nsIWidget.h" +#include "nsPrintDialogUtil.h" +#include "nsIPrintSettings.h" +#include "nsIWebBrowserChrome.h" +#include "nsServiceManagerUtils.h" +#include "nsPIDOMWindow.h" +#include "nsQueryObject.h" +#include "WidgetUtils.h" +#include "WinUtils.h" + +static const char* kPageSetupDialogURL = + "chrome://global/content/printPageSetup.xhtml"; + +using namespace mozilla; +using namespace mozilla::widget; + +/** + * ParamBlock + */ + +class ParamBlock { + public: + ParamBlock() { mBlock = 0; } + ~ParamBlock() { NS_IF_RELEASE(mBlock); } + nsresult Init() { + return CallCreateInstance(NS_DIALOGPARAMBLOCK_CONTRACTID, &mBlock); + } + nsIDialogParamBlock* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN { + return mBlock; + } + operator nsIDialogParamBlock* const() { return mBlock; } + + private: + nsIDialogParamBlock* mBlock; +}; + +NS_IMPL_ISUPPORTS(nsPrintDialogServiceWin, nsIPrintDialogService) + +nsPrintDialogServiceWin::nsPrintDialogServiceWin() {} + +nsPrintDialogServiceWin::~nsPrintDialogServiceWin() {} + +NS_IMETHODIMP +nsPrintDialogServiceWin::Init() { + nsresult rv; + mWatcher = do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + return rv; +} + +NS_IMETHODIMP +nsPrintDialogServiceWin::ShowPrintDialog(mozIDOMWindowProxy* aParent, + bool aHaveSelection, + nsIPrintSettings* aSettings) { + NS_ENSURE_ARG(aParent); + RefPtr<nsIWidget> parentWidget = + WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(aParent)); + + ScopedRtlShimWindow shim(parentWidget.get()); + NS_ASSERTION(shim.get(), "Couldn't get native window for PRint Dialog!"); + + return NativeShowPrintDialog(shim.get(), aHaveSelection, aSettings); +} + +NS_IMETHODIMP +nsPrintDialogServiceWin::ShowPageSetupDialog(mozIDOMWindowProxy* aParent, + nsIPrintSettings* aNSSettings) { + NS_ENSURE_ARG(aParent); + NS_ENSURE_ARG(aNSSettings); + + ParamBlock block; + nsresult rv = block.Init(); + if (NS_FAILED(rv)) return rv; + + block->SetInt(0, 0); + rv = DoDialog(aParent, block, aNSSettings, kPageSetupDialogURL); + + // if aWebBrowserPrint is not null then we are printing + // so we want to pass back NS_ERROR_ABORT on cancel + if (NS_SUCCEEDED(rv)) { + int32_t status; + block->GetInt(0, &status); + return status == 0 ? NS_ERROR_ABORT : NS_OK; + } + + // We don't call nsPrintSettingsService::MaybeSavePrintSettingsToPrefs here + // since it's called for us in printPageSetup.js. Maybe we should move that + // call here for consistency with the other platforms though? + + return rv; +} + +nsresult nsPrintDialogServiceWin::DoDialog(mozIDOMWindowProxy* aParent, + nsIDialogParamBlock* aParamBlock, + nsIPrintSettings* aPS, + const char* aChromeURL) { + NS_ENSURE_ARG(aParamBlock); + NS_ENSURE_ARG(aPS); + NS_ENSURE_ARG(aChromeURL); + + if (!mWatcher) return NS_ERROR_FAILURE; + + // get a parent, if at all possible + // (though we'd rather this didn't fail, it's OK if it does. so there's + // no failure or null check.) + // retain ownership for method lifetime + nsCOMPtr<mozIDOMWindowProxy> activeParent; + if (!aParent) { + mWatcher->GetActiveWindow(getter_AddRefs(activeParent)); + aParent = activeParent; + } + + // create a nsIMutableArray of the parameters + // being passed to the window + nsCOMPtr<nsIMutableArray> array = nsArray::Create(); + + nsCOMPtr<nsISupports> psSupports(do_QueryInterface(aPS)); + NS_ASSERTION(psSupports, "PrintSettings must be a supports"); + array->AppendElement(psSupports); + + nsCOMPtr<nsISupports> blkSupps(do_QueryInterface(aParamBlock)); + NS_ASSERTION(blkSupps, "IOBlk must be a supports"); + array->AppendElement(blkSupps); + + nsCOMPtr<mozIDOMWindowProxy> dialog; + nsresult rv = mWatcher->OpenWindow( + aParent, nsDependentCString(aChromeURL), "_blank"_ns, + "centerscreen,chrome,modal,titlebar"_ns, array, getter_AddRefs(dialog)); + + return rv; +} diff --git a/widget/windows/nsPrintDialogWin.h b/widget/windows/nsPrintDialogWin.h new file mode 100644 index 0000000000..bb8f212eb5 --- /dev/null +++ b/widget/windows/nsPrintDialogWin.h @@ -0,0 +1,39 @@ +/* -*- 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 nsPrintDialog_h__ +#define nsPrintDialog_h__ + +#include "nsIPrintDialogService.h" + +#include "nsCOMPtr.h" +#include "nsIWindowWatcher.h" + +#include <windef.h> + +class nsIPrintSettings; +class nsIDialogParamBlock; + +class nsPrintDialogServiceWin final : public nsIPrintDialogService { + public: + nsPrintDialogServiceWin(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIPRINTDIALOGSERVICE + + private: + virtual ~nsPrintDialogServiceWin(); + + nsresult DoDialog(mozIDOMWindowProxy* aParent, + nsIDialogParamBlock* aParamBlock, nsIPrintSettings* aPS, + const char* aChromeURL); + + nsCOMPtr<nsIWindowWatcher> mWatcher; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsPrintDialogServiceWin, + NS_IPRINTDIALOGSERVICE_IID) + +#endif diff --git a/widget/windows/nsPrintSettingsServiceWin.cpp b/widget/windows/nsPrintSettingsServiceWin.cpp new file mode 100644 index 0000000000..cbc99441ab --- /dev/null +++ b/widget/windows/nsPrintSettingsServiceWin.cpp @@ -0,0 +1,127 @@ +/* -*- 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 "nsPrintSettingsServiceWin.h" + +#include "nsCOMPtr.h" +#include "nsPrintSettingsWin.h" +#include "nsPrintDialogUtil.h" + +#include "nsGfxCIID.h" +#include "nsIServiceManager.h" +#include "nsWindowsHelpers.h" +#include "ipc/IPCMessageUtils.h" +#include "chrome/common/ipc_channel.h" +#include "mozilla/embedding/PPrintingTypes.h" + +using namespace mozilla::embedding; + +NS_IMETHODIMP +nsPrintSettingsServiceWin::SerializeToPrintData(nsIPrintSettings* aSettings, + PrintData* data) { + nsresult rv = nsPrintSettingsService::SerializeToPrintData(aSettings, data); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(aSettings); + if (!psWin) { + return NS_ERROR_FAILURE; + } + + nsAutoString deviceName; + nsAutoString driverName; + + psWin->GetDeviceName(deviceName); + psWin->GetDriverName(driverName); + + data->deviceName().Assign(deviceName); + data->driverName().Assign(driverName); + + // When creating the print dialog on Windows, we only need to send certain + // print settings information from the parent to the child not vice versa. + if (XRE_IsParentProcess()) { + // A DEVMODE can actually be of arbitrary size. If it turns out that it'll + // make our IPC message larger than the limit, then we'll error out. + LPDEVMODEW devModeRaw; + psWin->GetDevMode(&devModeRaw); // This actually allocates a copy of the + // the nsIPrintSettingsWin DEVMODE, so + // we're now responsible for deallocating + // it. We'll use an nsAutoDevMode helper + // to do this. + if (devModeRaw) { + nsAutoDevMode devMode(devModeRaw); + devModeRaw = nullptr; + + size_t devModeTotalSize = devMode->dmSize + devMode->dmDriverExtra; + size_t msgTotalSize = sizeof(PrintData) + devModeTotalSize; + + if (msgTotalSize > IPC::Channel::kMaximumMessageSize / 2) { + return NS_ERROR_FAILURE; + } + + // Instead of reaching in and manually reading each member, we'll just + // copy the bits over. + const char* devModeData = reinterpret_cast<const char*>(devMode.get()); + nsTArray<uint8_t> arrayBuf; + arrayBuf.AppendElements(devModeData, devModeTotalSize); + data->devModeData() = std::move(arrayBuf); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrintSettingsServiceWin::DeserializeToPrintSettings( + const PrintData& data, nsIPrintSettings* settings) { + nsresult rv = + nsPrintSettingsService::DeserializeToPrintSettings(data, settings); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(settings); + if (!settings) { + return NS_ERROR_FAILURE; + } + + if (XRE_IsContentProcess()) { + psWin->SetDeviceName(data.deviceName()); + psWin->SetDriverName(data.driverName()); + + if (data.devModeData().IsEmpty()) { + psWin->SetDevMode(nullptr); + } else { + // Check minimum length of DEVMODE data. + auto devModeDataLength = data.devModeData().Length(); + if (devModeDataLength < sizeof(DEVMODEW)) { + NS_WARNING("DEVMODE data is too short."); + return NS_ERROR_FAILURE; + } + + DEVMODEW* devMode = reinterpret_cast<DEVMODEW*>( + const_cast<uint8_t*>(data.devModeData().Elements())); + + // Check actual length of DEVMODE data. + if ((devMode->dmSize + devMode->dmDriverExtra) != devModeDataLength) { + NS_WARNING("DEVMODE length is incorrect."); + return NS_ERROR_FAILURE; + } + + psWin->SetDevMode(devMode); // Copies + } + } + + return NS_OK; +} + +nsresult nsPrintSettingsServiceWin::_CreatePrintSettings( + nsIPrintSettings** _retval) { + *_retval = nullptr; + nsPrintSettingsWin* printSettings = + new nsPrintSettingsWin(); // does not initially ref count + NS_ENSURE_TRUE(printSettings, NS_ERROR_OUT_OF_MEMORY); + + NS_ADDREF(*_retval = printSettings); // ref count + + return NS_OK; +} diff --git a/widget/windows/nsPrintSettingsServiceWin.h b/widget/windows/nsPrintSettingsServiceWin.h new file mode 100644 index 0000000000..e11d307a8b --- /dev/null +++ b/widget/windows/nsPrintSettingsServiceWin.h @@ -0,0 +1,29 @@ +/* -*- Mode: IDL; 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 nsPrintSettingsServiceWin_h +#define nsPrintSettingsServiceWin_h + +#include "nsPrintSettingsService.h" + +class nsIPrintSettings; + +class nsPrintSettingsServiceWin final : public nsPrintSettingsService { + public: + nsPrintSettingsServiceWin() {} + + NS_IMETHODIMP SerializeToPrintData( + nsIPrintSettings* aSettings, + mozilla::embedding::PrintData* data) override; + + NS_IMETHODIMP DeserializeToPrintSettings( + const mozilla::embedding::PrintData& data, + nsIPrintSettings* settings) override; + + nsresult _CreatePrintSettings(nsIPrintSettings** _retval) override; +}; + +#endif // nsPrintSettingsServiceWin_h diff --git a/widget/windows/nsPrintSettingsWin.cpp b/widget/windows/nsPrintSettingsWin.cpp new file mode 100644 index 0000000000..788c4ab6da --- /dev/null +++ b/widget/windows/nsPrintSettingsWin.cpp @@ -0,0 +1,477 @@ +/* -*- 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 "nsPrintSettingsWin.h" + +#include "mozilla/ArrayUtils.h" +#include "nsCRT.h" +#include "nsDeviceContextSpecWin.h" +#include "nsPrintSettingsImpl.h" +#include "WinUtils.h" + +using namespace mozilla; + +// Using paper sizes from wingdi.h and the units given there, plus a little +// extra research for the ones it doesn't give. Looks like the list hasn't +// changed since Windows 2000, so should be fairly stable now. +const short kPaperSizeUnits[] = { + nsIPrintSettings::kPaperSizeMillimeters, // Not Used default to mm as + // DEVMODE uses tenths of mm, just + // in case + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTERSMALL + nsIPrintSettings::kPaperSizeInches, // DMPAPER_TABLOID + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LEDGER + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LEGAL + nsIPrintSettings::kPaperSizeInches, // DMPAPER_STATEMENT + nsIPrintSettings::kPaperSizeInches, // DMPAPER_EXECUTIVE + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4SMALL + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A5 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B4 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B5 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_FOLIO + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_QUARTO + nsIPrintSettings::kPaperSizeInches, // DMPAPER_10X14 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_11X17 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_NOTE + nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_9 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_10 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_11 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_12 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_14 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_CSHEET + nsIPrintSettings::kPaperSizeInches, // DMPAPER_DSHEET + nsIPrintSettings::kPaperSizeInches, // DMPAPER_ESHEET + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_DL + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C5 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C3 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C4 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C6 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_C65 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_B4 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_B5 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_B6 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_ITALY + nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_MONARCH + nsIPrintSettings::kPaperSizeInches, // DMPAPER_ENV_PERSONAL + nsIPrintSettings::kPaperSizeInches, // DMPAPER_FANFOLD_US + nsIPrintSettings::kPaperSizeInches, // DMPAPER_FANFOLD_STD_GERMAN + nsIPrintSettings::kPaperSizeInches, // DMPAPER_FANFOLD_LGL_GERMAN + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ISO_B4 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JAPANESE_POSTCARD + nsIPrintSettings::kPaperSizeInches, // DMPAPER_9X11 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_10X11 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_15X11 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_ENV_INVITE + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_RESERVED_48 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_RESERVED_49 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_EXTRA + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LEGAL_EXTRA + nsIPrintSettings::kPaperSizeInches, // DMPAPER_TABLOID_EXTRA + nsIPrintSettings::kPaperSizeInches, // DMPAPER_A4_EXTRA + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_TRANSVERSE + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4_TRANSVERSE + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_EXTRA_TRANSVERSE + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A_PLUS + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B_PLUS + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_PLUS + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4_PLUS + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A5_TRANSVERSE + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B5_TRANSVERSE + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3_EXTRA + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A5_EXTRA + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B5_EXTRA + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A2 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3_TRANSVERSE + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3_EXTRA_TRANSVERSE + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_DBL_JAPANESE_POSTCARD + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A6 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_KAKU2 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_KAKU3 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_CHOU3 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_CHOU4 + nsIPrintSettings::kPaperSizeInches, // DMPAPER_LETTER_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A3_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A4_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A5_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B4_JIS_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B5_JIS_ROTATED + nsIPrintSettings:: + kPaperSizeMillimeters, // DMPAPER_JAPANESE_POSTCARD_ROTATED + nsIPrintSettings:: + kPaperSizeMillimeters, // DMPAPER_DBL_JAPANESE_POSTCARD_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_A6_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_KAKU2_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_KAKU3_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_CHOU3_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_CHOU4_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B6_JIS + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_B6_JIS_ROTATED + nsIPrintSettings::kPaperSizeInches, // DMPAPER_12X11 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_YOU4 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_JENV_YOU4_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P16K + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P32K + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P32KBIG + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_1 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_2 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_3 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_4 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_5 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_6 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_7 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_8 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_9 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_10 + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P16K_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P32K_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_P32KBIG_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_1_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_2_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_3_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_4_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_5_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_6_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_7_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_8_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_9_ROTATED + nsIPrintSettings::kPaperSizeMillimeters, // DMPAPER_PENV_10_ROTATED +}; + +NS_IMPL_ISUPPORTS_INHERITED(nsPrintSettingsWin, nsPrintSettings, + nsIPrintSettingsWin) + +/** --------------------------------------------------- + * See documentation in nsPrintSettingsWin.h + * @update + */ +nsPrintSettingsWin::nsPrintSettingsWin() + : nsPrintSettings(), + mDeviceName(nullptr), + mDriverName(nullptr), + mDevMode(nullptr) {} + +/** --------------------------------------------------- + * See documentation in nsPrintSettingsWin.h + * @update + */ +nsPrintSettingsWin::nsPrintSettingsWin(const nsPrintSettingsWin& aPS) + : mDevMode(nullptr) { + *this = aPS; +} + +/* static */ +void nsPrintSettingsWin::PaperSizeUnitFromDmPaperSize(short aPaperSize, + int16_t& aPaperSizeUnit) { + if (aPaperSize > 0 && aPaperSize < int32_t(ArrayLength(kPaperSizeUnits))) { + aPaperSizeUnit = kPaperSizeUnits[aPaperSize]; + } +} + +void nsPrintSettingsWin::InitWithInitializer( + const PrintSettingsInitializer& aSettings) { + nsPrintSettings::InitWithInitializer(aSettings); + + if (aSettings.mDevmodeWStorage.Length() < sizeof(DEVMODEW)) { + return; + } + + auto* devmode = + reinterpret_cast<const DEVMODEW*>(aSettings.mDevmodeWStorage.Elements()); + if (devmode->dmSize != sizeof(DEVMODEW) || + devmode->dmSize + devmode->dmDriverExtra > + aSettings.mDevmodeWStorage.Length()) { + return; + } + + // SetDevMode copies the DEVMODE. + SetDevMode(const_cast<DEVMODEW*>(devmode)); + + if (mDevMode->dmFields & DM_SCALE) { + // Since we do the scaling, grab the DEVMODE value and reset it back to 100. + double scale = double(mDevMode->dmScale) / 100.0f; + if (mScaling == 1.0 || scale != 1.0) { + SetScaling(scale); + } + mDevMode->dmScale = 100; + } +} + +already_AddRefed<nsIPrintSettings> CreatePlatformPrintSettings( + const PrintSettingsInitializer& aSettings) { + RefPtr<nsPrintSettings> settings = aSettings.mPrintSettings.get(); + if (!settings) { + settings = MakeRefPtr<nsPrintSettingsWin>(); + } + settings->InitWithInitializer(aSettings); + return settings.forget(); +} + +/** --------------------------------------------------- + * See documentation in nsPrintSettingsWin.h + * @update + */ +nsPrintSettingsWin::~nsPrintSettingsWin() { + if (mDevMode) ::HeapFree(::GetProcessHeap(), 0, mDevMode); +} + +NS_IMETHODIMP nsPrintSettingsWin::SetDeviceName(const nsAString& aDeviceName) { + mDeviceName = aDeviceName; + return NS_OK; +} +NS_IMETHODIMP nsPrintSettingsWin::GetDeviceName(nsAString& aDeviceName) { + aDeviceName = mDeviceName; + return NS_OK; +} + +NS_IMETHODIMP nsPrintSettingsWin::SetDriverName(const nsAString& aDriverName) { + mDriverName = aDriverName; + return NS_OK; +} +NS_IMETHODIMP nsPrintSettingsWin::GetDriverName(nsAString& aDriverName) { + aDriverName = mDriverName; + return NS_OK; +} + +void nsPrintSettingsWin::CopyDevMode(DEVMODEW* aInDevMode, + DEVMODEW*& aOutDevMode) { + aOutDevMode = nullptr; + size_t size = aInDevMode->dmSize + aInDevMode->dmDriverExtra; + aOutDevMode = + (LPDEVMODEW)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, size); + if (aOutDevMode) { + memcpy(aOutDevMode, aInDevMode, size); + } +} + +NS_IMETHODIMP nsPrintSettingsWin::GetDevMode(DEVMODEW** aDevMode) { + NS_ENSURE_ARG_POINTER(aDevMode); + + if (mDevMode) { + CopyDevMode(mDevMode, *aDevMode); + } else { + *aDevMode = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP nsPrintSettingsWin::SetDevMode(DEVMODEW* aDevMode) { + if (mDevMode) { + ::HeapFree(::GetProcessHeap(), 0, mDevMode); + mDevMode = nullptr; + } + + if (aDevMode) { + CopyDevMode(aDevMode, mDevMode); + } + return NS_OK; +} + +void nsPrintSettingsWin::InitUnwriteableMargin(HDC aHdc) { + mozilla::gfx::MarginDouble margin = + mozilla::widget::WinUtils::GetUnwriteableMarginsForDeviceInInches(aHdc); + + mUnwriteableMargin.SizeTo(NS_INCHES_TO_INT_TWIPS(margin.top), + NS_INCHES_TO_INT_TWIPS(margin.right), + NS_INCHES_TO_INT_TWIPS(margin.bottom), + NS_INCHES_TO_INT_TWIPS(margin.left)); +} + +void nsPrintSettingsWin::CopyFromNative(HDC aHdc, DEVMODEW* aDevMode) { + MOZ_ASSERT(aHdc); + MOZ_ASSERT(aDevMode); + + mIsInitedFromPrinter = true; + if (aDevMode->dmFields & DM_ORIENTATION) { + const bool areSheetsOfPaperPortraitMode = + (aDevMode->dmOrientation == DMORIENT_PORTRAIT); + + // If our Windows print settings say that we're producing portrait-mode + // sheets of paper, then our page format must also be portrait-mode; unless + // we've got a pages-per-sheet value with orthogonal pages/sheets, in which + // case it's reversed. + const bool arePagesPortraitMode = + (areSheetsOfPaperPortraitMode != HasOrthogonalPagesPerSheet()); + + // Record the orientation of the pages (determined above) in mOrientation: + mOrientation = int32_t(arePagesPortraitMode ? kPortraitOrientation + : kLandscapeOrientation); + } + + if (aDevMode->dmFields & DM_COPIES) { + mNumCopies = aDevMode->dmCopies; + } + + if (aDevMode->dmFields & DM_DUPLEX) { + switch (aDevMode->dmDuplex) { + default: + MOZ_FALLTHROUGH_ASSERT("bad value for dmDuplex field"); + case DMDUP_SIMPLEX: + mDuplex = kDuplexNone; + break; + case DMDUP_VERTICAL: + mDuplex = kDuplexFlipOnLongEdge; + break; + case DMDUP_HORIZONTAL: + mDuplex = kDuplexFlipOnShortEdge; + break; + } + } + + // Since we do the scaling, grab their value and reset back to 100. + if (aDevMode->dmFields & DM_SCALE) { + double scale = double(aDevMode->dmScale) / 100.0f; + if (mScaling == 1.0 || scale != 1.0) { + mScaling = scale; + } + aDevMode->dmScale = 100; + } + + if (aDevMode->dmFields & DM_PAPERSIZE) { + mPaperId.Truncate(0); + mPaperId.AppendInt(aDevMode->dmPaperSize); + // If it is not a paper size we know about, the unit will remain unchanged. + PaperSizeUnitFromDmPaperSize(aDevMode->dmPaperSize, mPaperSizeUnit); + } + + if (aDevMode->dmFields & DM_COLOR) { + mPrintInColor = aDevMode->dmColor == DMCOLOR_COLOR; + } + + InitUnwriteableMargin(aHdc); + + int pixelsPerInchY = ::GetDeviceCaps(aHdc, LOGPIXELSY); + int physicalHeight = ::GetDeviceCaps(aHdc, PHYSICALHEIGHT); + double physicalHeightInch = double(physicalHeight) / pixelsPerInchY; + int pixelsPerInchX = ::GetDeviceCaps(aHdc, LOGPIXELSX); + int physicalWidth = ::GetDeviceCaps(aHdc, PHYSICALWIDTH); + double physicalWidthInch = double(physicalWidth) / pixelsPerInchX; + + // Get the paper size from the device context rather than the DEVMODE, because + // it is always available. + double paperHeightInch = mOrientation == kPortraitOrientation + ? physicalHeightInch + : physicalWidthInch; + mPaperHeight = mPaperSizeUnit == kPaperSizeInches + ? paperHeightInch + : paperHeightInch * MM_PER_INCH_FLOAT; + + double paperWidthInch = mOrientation == kPortraitOrientation + ? physicalWidthInch + : physicalHeightInch; + mPaperWidth = mPaperSizeUnit == kPaperSizeInches + ? paperWidthInch + : paperWidthInch * MM_PER_INCH_FLOAT; + + // Using LOGPIXELSY to match existing code for print scaling calculations. + mResolution = pixelsPerInchY; +} + +void nsPrintSettingsWin::CopyToNative(DEVMODEW* aDevMode) { + MOZ_ASSERT(aDevMode); + + if (!mPaperId.IsEmpty()) { + aDevMode->dmPaperSize = _wtoi((const wchar_t*)mPaperId.BeginReading()); + aDevMode->dmFields |= DM_PAPERSIZE; + } else { + aDevMode->dmPaperSize = 0; + aDevMode->dmFields &= ~DM_PAPERSIZE; + } + + aDevMode->dmFields |= DM_COLOR; + aDevMode->dmColor = mPrintInColor ? DMCOLOR_COLOR : DMCOLOR_MONOCHROME; + + // The length and width in DEVMODE are always in tenths of a millimeter. + double tenthsOfAmmPerSizeUnit = + mPaperSizeUnit == kPaperSizeInches ? MM_PER_INCH_FLOAT * 10.0 : 10.0; + + // Note: small page sizes can be required here for sticker, label and slide + // printers etc. see bug 1271900. + if (mPaperHeight > 0) { + aDevMode->dmPaperLength = std::round(mPaperHeight * tenthsOfAmmPerSizeUnit); + aDevMode->dmFields |= DM_PAPERLENGTH; + } else { + aDevMode->dmPaperLength = 0; + aDevMode->dmFields &= ~DM_PAPERLENGTH; + } + + if (mPaperWidth > 0) { + aDevMode->dmPaperWidth = std::round(mPaperWidth * tenthsOfAmmPerSizeUnit); + aDevMode->dmFields |= DM_PAPERWIDTH; + } else { + aDevMode->dmPaperWidth = 0; + aDevMode->dmFields &= ~DM_PAPERWIDTH; + } + + // Setup Orientation + aDevMode->dmOrientation = GetSheetOrientation() == kPortraitOrientation + ? DMORIENT_PORTRAIT + : DMORIENT_LANDSCAPE; + aDevMode->dmFields |= DM_ORIENTATION; + + // Setup Number of Copies + aDevMode->dmCopies = mNumCopies; + aDevMode->dmFields |= DM_COPIES; + + // Setup Simplex/Duplex mode + switch (mDuplex) { + case kDuplexNone: + aDevMode->dmDuplex = DMDUP_SIMPLEX; + aDevMode->dmFields |= DM_DUPLEX; + break; + case kDuplexFlipOnLongEdge: + aDevMode->dmDuplex = DMDUP_VERTICAL; + aDevMode->dmFields |= DM_DUPLEX; + break; + case kDuplexFlipOnShortEdge: + aDevMode->dmDuplex = DMDUP_HORIZONTAL; + aDevMode->dmFields |= DM_DUPLEX; + break; + default: + MOZ_ASSERT_UNREACHABLE("bad value for duplex option"); + break; + } +} + +//------------------------------------------- +nsresult nsPrintSettingsWin::_Clone(nsIPrintSettings** _retval) { + RefPtr<nsPrintSettingsWin> printSettings = new nsPrintSettingsWin(*this); + printSettings.forget(_retval); + return NS_OK; +} + +//------------------------------------------- +nsPrintSettingsWin& nsPrintSettingsWin::operator=( + const nsPrintSettingsWin& rhs) { + if (this == &rhs) { + return *this; + } + + ((nsPrintSettings&)*this) = rhs; + + // Use free because we used the native malloc to create the memory + if (mDevMode) { + ::HeapFree(::GetProcessHeap(), 0, mDevMode); + } + + mDeviceName = rhs.mDeviceName; + mDriverName = rhs.mDriverName; + + if (rhs.mDevMode) { + CopyDevMode(rhs.mDevMode, mDevMode); + } else { + mDevMode = nullptr; + } + + return *this; +} + +//------------------------------------------- +nsresult nsPrintSettingsWin::_Assign(nsIPrintSettings* aPS) { + nsPrintSettingsWin* psWin = static_cast<nsPrintSettingsWin*>(aPS); + *this = *psWin; + return NS_OK; +} diff --git a/widget/windows/nsPrintSettingsWin.h b/widget/windows/nsPrintSettingsWin.h new file mode 100644 index 0000000000..c127efbeb6 --- /dev/null +++ b/widget/windows/nsPrintSettingsWin.h @@ -0,0 +1,62 @@ +/* -*- Mode: IDL; 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 nsPrintSettingsWin_h__ +#define nsPrintSettingsWin_h__ + +#include "nsPrintSettingsImpl.h" +#include "nsIPrintSettingsWin.h" +#include <windows.h> + +//***************************************************************************** +//*** nsPrintSettingsWin +//***************************************************************************** +class nsPrintSettingsWin : public nsPrintSettings, public nsIPrintSettingsWin { + virtual ~nsPrintSettingsWin(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPRINTSETTINGSWIN + + nsPrintSettingsWin(); + nsPrintSettingsWin(const nsPrintSettingsWin& aPS); + + /** + * @param aPaperSize the Windows dmPaperSize + * @param aPaperSizeUnit will be set to the nsIPrintSettings paper size unit + * associated with aPaperSize or left unchanged if + * aPaperSize is not recognized + */ + static void PaperSizeUnitFromDmPaperSize(short aPaperSize, + int16_t& aPaperSizeUnit); + + void InitWithInitializer(const PrintSettingsInitializer& aSettings) final; + + /** + * Makes a new copy + */ + virtual nsresult _Clone(nsIPrintSettings** _retval); + + /** + * Assigns values + */ + virtual nsresult _Assign(nsIPrintSettings* aPS); + + /** + * Assignment + */ + nsPrintSettingsWin& operator=(const nsPrintSettingsWin& rhs); + + protected: + void CopyDevMode(DEVMODEW* aInDevMode, DEVMODEW*& aOutDevMode); + void InitUnwriteableMargin(HDC aHdc); + + nsString mDeviceName; + nsString mDriverName; + LPDEVMODEW mDevMode; +}; + +#endif /* nsPrintSettingsWin_h__ */ diff --git a/widget/windows/nsPrinterWin.cpp b/widget/windows/nsPrinterWin.cpp new file mode 100644 index 0000000000..75676cd613 --- /dev/null +++ b/widget/windows/nsPrinterWin.cpp @@ -0,0 +1,521 @@ +/* -*- 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 "nsPrinterWin.h" + +#include <algorithm> +#include <windows.h> +#include <winspool.h> + +#include "mozilla/Array.h" +#include "mozilla/dom/Promise.h" +#include "nsPaper.h" +#include "nsPrintSettingsImpl.h" +#include "nsPrintSettingsWin.h" +#include "nsWindowsHelpers.h" +#include "PrintBackgroundTask.h" +#include "WinUtils.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::widget; +using mozilla::PrintSettingsInitializer; +using mozilla::dom::Promise; + +static const double kPointsPerTenthMM = 72.0 / 254.0; +static const double kPointsPerInch = 72.0; + +nsPrinterWin::nsPrinterWin(const CommonPaperInfoArray* aArray, + const nsAString& aName) + : nsPrinterBase(aArray), + mName(aName), + mDefaultDevmodeWStorage("nsPrinterWin::mDefaultDevmodeWStorage") {} + +// static +already_AddRefed<nsPrinterWin> nsPrinterWin::Create( + const CommonPaperInfoArray* aArray, const nsAString& aName) { + return do_AddRef(new nsPrinterWin(aArray, aName)); +} + +template <class T> +static nsTArray<T> GetDeviceCapabilityArray(const LPWSTR aPrinterName, + WORD aCapabilityID, + mozilla::Mutex& aDriverMutex, + int& aCount) { + MOZ_ASSERT(aCount >= 0, "Possibly passed aCount from previous error case."); + + nsTArray<T> caps; + + // We only want to access printer drivers in the parent process. + if (!XRE_IsParentProcess()) { + return caps; + } + + // Both the call to get the size and the call to actually populate the array + // are relatively expensive, so as sometimes the lengths of the arrays that we + // retrieve depend on each other we allow a count to be passed in to save the + // first call. As we allocate double the count anyway this should allay any + // safety worries. + if (!aCount) { + // Passing nullptr as the port here seems to work. Given that we would have + // to OpenPrinter with just the name anyway to get the port that makes + // sense. Also, the printer set-up seems to stop you from having two + // printers with the same name. Note: this (and the call below) are blocking + // calls, which could be slow. + MutexAutoLock autoLock(aDriverMutex); + aCount = ::DeviceCapabilitiesW(aPrinterName, nullptr, aCapabilityID, + nullptr, nullptr); + if (aCount <= 0) { + return caps; + } + } + + // As DeviceCapabilitiesW doesn't take a size, there is a greater risk of the + // buffer being overflowed, so we over-allocate for safety. + caps.SetLength(aCount * 2); + MutexAutoLock autoLock(aDriverMutex); + int count = + ::DeviceCapabilitiesW(aPrinterName, nullptr, aCapabilityID, + reinterpret_cast<LPWSTR>(caps.Elements()), nullptr); + if (count <= 0) { + caps.Clear(); + return caps; + } + + // We know from bug 1673708 that sometimes the final array returned is smaller + // than the required array count. Assert here to see if this is reproduced on + // test servers. + MOZ_ASSERT(count == aCount, "Different array count returned than expected."); + + // Note that TruncateLength will crash if count > caps.Length(). + caps.TruncateLength(count); + return caps; +} + +static void DevmodeToSettingsInitializer( + const nsString& aPrinterName, const DEVMODEW* aDevmode, + mozilla::Mutex& aDriverMutex, + PrintSettingsInitializer& aSettingsInitializer) { + aSettingsInitializer.mPrinter.Assign(aPrinterName); + + HDC dc; + { + MutexAutoLock autoLock(aDriverMutex); + dc = ::CreateICW(nullptr, aPrinterName.get(), nullptr, aDevmode); + } + nsAutoHDC printerDc(dc); + MOZ_ASSERT(printerDc, "CreateICW failed"); + if (!printerDc) { + return; + } + + if (aDevmode->dmFields & DM_PAPERSIZE) { + aSettingsInitializer.mPaperInfo.mId.Truncate(); + aSettingsInitializer.mPaperInfo.mId.AppendInt(aDevmode->dmPaperSize); + // If it is not a paper size we know about, the unit will remain unchanged. + nsPrintSettingsWin::PaperSizeUnitFromDmPaperSize( + aDevmode->dmPaperSize, aSettingsInitializer.mPaperSizeUnit); + } + + int pixelsPerInchY = ::GetDeviceCaps(printerDc, LOGPIXELSY); + int physicalHeight = ::GetDeviceCaps(printerDc, PHYSICALHEIGHT); + double heightInInches = double(physicalHeight) / pixelsPerInchY; + int pixelsPerInchX = ::GetDeviceCaps(printerDc, LOGPIXELSX); + int physicalWidth = ::GetDeviceCaps(printerDc, PHYSICALWIDTH); + double widthInches = double(physicalWidth) / pixelsPerInchX; + if (aDevmode->dmFields & DM_ORIENTATION && + aDevmode->dmOrientation == DMORIENT_LANDSCAPE) { + std::swap(widthInches, heightInInches); + } + aSettingsInitializer.mPaperInfo.mSize.SizeTo(widthInches * kPointsPerInch, + heightInInches * kPointsPerInch); + + gfx::MarginDouble margin = + WinUtils::GetUnwriteableMarginsForDeviceInInches(printerDc); + aSettingsInitializer.mPaperInfo.mUnwriteableMargin = Some(MarginDouble{ + margin.top * kPointsPerInch, margin.right * kPointsPerInch, + margin.bottom * kPointsPerInch, margin.left * kPointsPerInch}); + + // Using Y to match existing code for print scaling calculations. + aSettingsInitializer.mResolution = pixelsPerInchY; + + if (aDevmode->dmFields & DM_COLOR) { + // See comment for PrintSettingsInitializer.mPrintInColor + aSettingsInitializer.mPrintInColor = + aDevmode->dmColor != DMCOLOR_MONOCHROME; + } + + if (aDevmode->dmFields & DM_ORIENTATION) { + aSettingsInitializer.mSheetOrientation = + int32_t(aDevmode->dmOrientation == DMORIENT_PORTRAIT + ? nsPrintSettings::kPortraitOrientation + : nsPrintSettings::kLandscapeOrientation); + } + + if (aDevmode->dmFields & DM_COPIES) { + aSettingsInitializer.mNumCopies = aDevmode->dmCopies; + } + + if (aDevmode->dmFields & DM_DUPLEX) { + switch (aDevmode->dmDuplex) { + default: + MOZ_FALLTHROUGH_ASSERT("bad value for dmDuplex field"); + case DMDUP_SIMPLEX: + aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexNone; + break; + case DMDUP_VERTICAL: + aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexFlipOnLongEdge; + break; + case DMDUP_HORIZONTAL: + aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexFlipOnShortEdge; + break; + } + } +} + +NS_IMETHODIMP +nsPrinterWin::GetName(nsAString& aName) { + aName.Assign(mName); + return NS_OK; +} + +NS_IMETHODIMP +nsPrinterWin::GetSystemName(nsAString& aName) { + aName.Assign(mName); + return NS_OK; +} + +namespace mozilla { +template <> +void ResolveOrReject(Promise& aPromise, nsPrinterWin& aPrinter, + const PrintSettingsInitializer& aResult) { + aPromise.MaybeResolve( + RefPtr<nsIPrintSettings>(CreatePlatformPrintSettings(aResult))); +} +} // namespace mozilla + +NS_IMETHODIMP nsPrinterWin::CopyFromWithValidation( + nsIPrintSettings* aSettingsToCopyFrom, JSContext* aCx, + Promise** aResultPromise) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aResultPromise); + + PrintSettingsInitializer settingsInitializer = + aSettingsToCopyFrom->GetSettingsInitializer(); + return PrintBackgroundTaskPromise( + *this, aCx, aResultPromise, "CopyFromWithValidation"_ns, + &nsPrinterWin::GetValidatedSettings, settingsInitializer); +} + +bool nsPrinterWin::SupportsDuplex() const { + MutexAutoLock autoLock(mDriverMutex); + return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_DUPLEX, nullptr, + nullptr) == 1; +} + +bool nsPrinterWin::SupportsColor() const { + MutexAutoLock autoLock(mDriverMutex); + return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_COLORDEVICE, nullptr, + nullptr) == 1; +} + +bool nsPrinterWin::SupportsMonochrome() const { + if (!SupportsColor()) { + return true; + } + + nsHPRINTER hPrinter = nullptr; + if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) { + return false; + } + nsAutoPrinter autoPrinter(hPrinter); + + nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW(); + if (devmodeWStorage.IsEmpty()) { + return false; + } + + auto* devmode = reinterpret_cast<DEVMODEW*>(devmodeWStorage.Elements()); + + devmode->dmFields |= DM_COLOR; + devmode->dmColor = DMCOLOR_MONOCHROME; + // Try to modify the devmode settings and see if the setting sticks. + // + // This has been the only reliable way to detect it that we've found. + MutexAutoLock autoLock(mDriverMutex); + LONG ret = + ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(), devmode, + devmode, DM_IN_BUFFER | DM_OUT_BUFFER); + if (ret != IDOK) { + return false; + } + return !(devmode->dmFields & DM_COLOR) || + devmode->dmColor == DMCOLOR_MONOCHROME; +} + +bool nsPrinterWin::SupportsCollation() const { + MutexAutoLock autoLock(mDriverMutex); + return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_COLLATE, nullptr, + nullptr) == 1; +} + +nsPrinterBase::PrinterInfo nsPrinterWin::CreatePrinterInfo() const { + return PrinterInfo{PaperList(), DefaultSettings()}; +} + +mozilla::gfx::MarginDouble nsPrinterWin::GetMarginsForPaper( + nsString aPaperId) const { + gfx::MarginDouble margin; + + nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW(); + if (devmodeWStorage.IsEmpty()) { + return margin; + } + + auto* devmode = reinterpret_cast<DEVMODEW*>(devmodeWStorage.Elements()); + + devmode->dmFields = DM_PAPERSIZE; + devmode->dmPaperSize = _wtoi((const wchar_t*)aPaperId.BeginReading()); + HDC dc; + { + MutexAutoLock autoLock(mDriverMutex); + dc = ::CreateICW(nullptr, mName.get(), nullptr, devmode); + } + nsAutoHDC printerDc(dc); + MOZ_ASSERT(printerDc, "CreateICW failed"); + if (!printerDc) { + return margin; + } + margin = WinUtils::GetUnwriteableMarginsForDeviceInInches(printerDc); + margin.top *= kPointsPerInch; + margin.right *= kPointsPerInch; + margin.bottom *= kPointsPerInch; + margin.left *= kPointsPerInch; + + return margin; +} + +nsTArray<uint8_t> nsPrinterWin::CopyDefaultDevmodeW() const { + nsTArray<uint8_t> devmodeStorageW; + + auto devmodeStorageWLock = mDefaultDevmodeWStorage.Lock(); + if (devmodeStorageWLock->IsEmpty()) { + nsHPRINTER hPrinter = nullptr; + // OpenPrinter could fail if, for example, the printer has been removed + // or otherwise become inaccessible since it was selected. + if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) { + return devmodeStorageW; + } + nsAutoPrinter autoPrinter(hPrinter); + // Allocate devmode storage of the correct size. + MutexAutoLock autoLock(mDriverMutex); + LONG bytesNeeded = ::DocumentPropertiesW(nullptr, autoPrinter.get(), + mName.get(), nullptr, nullptr, 0); + // Note that we must cast the sizeof() to a signed type so that comparison + // with the signed, potentially-negative bytesNeeded will work! + MOZ_ASSERT(bytesNeeded >= LONG(sizeof(DEVMODEW)), + "DocumentPropertiesW failed to get valid size"); + if (bytesNeeded < LONG(sizeof(DEVMODEW))) { + return devmodeStorageW; + } + + // Allocate extra space in case of bad drivers that return a too-small + // result from DocumentProperties. + // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1664530#c5) + if (!devmodeStorageWLock->SetLength(bytesNeeded * 2, fallible)) { + return devmodeStorageW; + } + + memset(devmodeStorageWLock->Elements(), 0, devmodeStorageWLock->Length()); + auto* devmode = + reinterpret_cast<DEVMODEW*>(devmodeStorageWLock->Elements()); + LONG ret = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(), + devmode, nullptr, DM_OUT_BUFFER); + MOZ_ASSERT(ret == IDOK, "DocumentPropertiesW failed"); + // Make sure that the lengths in the DEVMODEW make sense. + if (ret != IDOK || devmode->dmSize != sizeof(DEVMODEW) || + devmode->dmSize + devmode->dmDriverExtra > + devmodeStorageWLock->Length()) { + // Clear mDefaultDevmodeWStorage to make sure we try again next time. + devmodeStorageWLock->Clear(); + return devmodeStorageW; + } + } + + devmodeStorageW.Assign(devmodeStorageWLock.ref()); + return devmodeStorageW; +} + +nsTArray<mozilla::PaperInfo> nsPrinterWin::PaperList() const { + // Paper IDs are returned as WORDs. + int requiredArrayCount = 0; + auto paperIds = GetDeviceCapabilityArray<WORD>( + mName.get(), DC_PAPERS, mDriverMutex, requiredArrayCount); + if (!paperIds.Length()) { + return {}; + } + + // Paper names are returned in 64 long character buffers. + auto paperNames = GetDeviceCapabilityArray<Array<wchar_t, 64>>( + mName.get(), DC_PAPERNAMES, mDriverMutex, requiredArrayCount); + // Check that we have the same number of names as IDs. + if (paperNames.Length() != paperIds.Length()) { + return {}; + } + + // Paper sizes are returned as POINT structs with a tenth of a millimeter as + // the unit. + auto paperSizes = GetDeviceCapabilityArray<POINT>( + mName.get(), DC_PAPERSIZE, mDriverMutex, requiredArrayCount); + // Check that we have the same number of sizes as IDs. + if (paperSizes.Length() != paperIds.Length()) { + return {}; + } + + nsTArray<mozilla::PaperInfo> paperList; + paperList.SetCapacity(paperNames.Length()); + for (size_t i = 0; i < paperNames.Length(); ++i) { + // Paper names are null terminated unless they are 64 characters long. + auto firstNull = + std::find(paperNames[i].cbegin(), paperNames[i].cend(), L'\0'); + auto nameLength = firstNull - paperNames[i].cbegin(); + double width = paperSizes[i].x * kPointsPerTenthMM; + double height = paperSizes[i].y * kPointsPerTenthMM; + + // Skip if no name or invalid size. + if (!nameLength || width <= 0 || height <= 0) { + continue; + } + + // Windows paper IDs are 16-bit integers; we stringify them to store in the + // PaperInfo.mId field. + nsString paperIdString; + paperIdString.AppendInt(paperIds[i]); + + // We don't resolve the margins eagerly because they're really expensive (on + // the order of seconds for some drivers). + nsDependentSubstring name(paperNames[i].cbegin(), nameLength); + paperList.AppendElement(mozilla::PaperInfo(paperIdString, nsString(name), + {width, height}, Nothing())); + } + + return paperList; +} + +PrintSettingsInitializer nsPrinterWin::DefaultSettings() const { + nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW(); + if (devmodeWStorage.IsEmpty()) { + return {}; + } + + const auto* devmode = + reinterpret_cast<const DEVMODEW*>(devmodeWStorage.Elements()); + + PrintSettingsInitializer settingsInitializer; + DevmodeToSettingsInitializer(mName, devmode, mDriverMutex, + settingsInitializer); + settingsInitializer.mDevmodeWStorage = std::move(devmodeWStorage); + return settingsInitializer; +} + +PrintSettingsInitializer nsPrinterWin::GetValidatedSettings( + PrintSettingsInitializer aSettingsToValidate) const { + // This function validates the settings by relying on the printer driver + // rejecting any invalid settings and resetting them to valid values. + + // Create a copy of the default DEVMODE for this printer. + nsTArray<uint8_t> devmodeWStorage = CopyDefaultDevmodeW(); + if (devmodeWStorage.IsEmpty()) { + return aSettingsToValidate; + } + + nsHPRINTER hPrinter = nullptr; + if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) { + return aSettingsToValidate; + } + nsAutoPrinter autoPrinter(hPrinter); + + // Copy the settings from aSettingsToValidate into our DEVMODE. + DEVMODEW* devmode = reinterpret_cast<DEVMODEW*>(devmodeWStorage.Elements()); + if (!aSettingsToValidate.mPaperInfo.mId.IsEmpty()) { + devmode->dmPaperSize = _wtoi( + (const wchar_t*)aSettingsToValidate.mPaperInfo.mId.BeginReading()); + devmode->dmFields |= DM_PAPERSIZE; + } else { + devmode->dmPaperSize = 0; + devmode->dmFields &= ~DM_PAPERSIZE; + } + + devmode->dmFields |= DM_COLOR; + devmode->dmColor = + aSettingsToValidate.mPrintInColor ? DMCOLOR_COLOR : DMCOLOR_MONOCHROME; + + // Note: small page sizes can be required here for sticker, label and slide + // printers etc. see bug 1271900. + if (aSettingsToValidate.mPaperInfo.mSize.height > 0) { + devmode->dmPaperLength = std::round( + aSettingsToValidate.mPaperInfo.mSize.height / kPointsPerTenthMM); + devmode->dmFields |= DM_PAPERLENGTH; + } else { + devmode->dmPaperLength = 0; + devmode->dmFields &= ~DM_PAPERLENGTH; + } + + if (aSettingsToValidate.mPaperInfo.mSize.width > 0) { + devmode->dmPaperWidth = std::round( + aSettingsToValidate.mPaperInfo.mSize.width / kPointsPerTenthMM); + devmode->dmFields |= DM_PAPERWIDTH; + } else { + devmode->dmPaperWidth = 0; + devmode->dmFields &= ~DM_PAPERWIDTH; + } + + // Setup Orientation + devmode->dmOrientation = aSettingsToValidate.mSheetOrientation == + nsPrintSettings::kPortraitOrientation + ? DMORIENT_PORTRAIT + : DMORIENT_LANDSCAPE; + devmode->dmFields |= DM_ORIENTATION; + + // Setup Number of Copies + devmode->dmCopies = aSettingsToValidate.mNumCopies; + devmode->dmFields |= DM_COPIES; + + // Setup Simplex/Duplex mode + devmode->dmFields |= DM_DUPLEX; + switch (aSettingsToValidate.mDuplex) { + case nsPrintSettings::kDuplexNone: + devmode->dmDuplex = DMDUP_SIMPLEX; + break; + case nsPrintSettings::kDuplexFlipOnLongEdge: + devmode->dmDuplex = DMDUP_VERTICAL; + break; + case nsPrintSettings::kDuplexFlipOnShortEdge: + devmode->dmDuplex = DMDUP_HORIZONTAL; + break; + default: + MOZ_ASSERT_UNREACHABLE("bad value for duplex option"); + break; + } + + // Apply the settings in the DEVMODE to the printer and retrieve the updated + // DEVMODE back into the same structure. + LONG ret; + { + MutexAutoLock autoLock(mDriverMutex); + ret = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(), + devmode, devmode, DM_IN_BUFFER | DM_OUT_BUFFER); + } + if (ret != IDOK) { + return aSettingsToValidate; + } + + // Copy the settings back from the DEVMODE into aSettingsToValidate and + // return. + DevmodeToSettingsInitializer(mName, devmode, mDriverMutex, + aSettingsToValidate); + aSettingsToValidate.mDevmodeWStorage = std::move(devmodeWStorage); + return aSettingsToValidate; +} diff --git a/widget/windows/nsPrinterWin.h b/widget/windows/nsPrinterWin.h new file mode 100644 index 0000000000..8afcf14d0e --- /dev/null +++ b/widget/windows/nsPrinterWin.h @@ -0,0 +1,53 @@ +/* -*- 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 nsPrinterWin_h_ +#define nsPrinterWin_h_ + +#include "nsPrinterBase.h" +#include "mozilla/DataMutex.h" +#include "nsTArrayForwardDeclare.h" + +class nsPrinterWin final : public nsPrinterBase { + public: + NS_IMETHOD GetName(nsAString& aName) override; + NS_IMETHOD GetSystemName(nsAString& aName) override; + NS_IMETHOD CopyFromWithValidation(nsIPrintSettings*, JSContext*, + Promise**) final; + bool SupportsDuplex() const final; + bool SupportsColor() const final; + bool SupportsMonochrome() const final; + bool SupportsCollation() const final; + PrinterInfo CreatePrinterInfo() const final; + MarginDouble GetMarginsForPaper(nsString aPaperId) const final; + + nsPrinterWin() = delete; + static already_AddRefed<nsPrinterWin> Create( + const mozilla::CommonPaperInfoArray* aPaperInfoArray, + const nsAString& aName); + + private: + nsPrinterWin(const mozilla::CommonPaperInfoArray* aPaperInfoArray, + const nsAString& aName); + ~nsPrinterWin() = default; + + PrintSettingsInitializer GetValidatedSettings( + PrintSettingsInitializer aSettingsToValidate) const; + + nsTArray<uint8_t> CopyDefaultDevmodeW() const; + nsTArray<mozilla::PaperInfo> PaperList() const; + PrintSettingsInitializer DefaultSettings() const; + + const nsString mName; + mutable mozilla::DataMutex<nsTArray<uint8_t>> mDefaultDevmodeWStorage; + // Even though some documentation seems to suggest that you should be able to + // use printer drivers on separate threads if you have separate handles, we + // see threading issues with multiple drivers. This Mutex is used to lock + // around all calls to DeviceCapabilitiesW, DocumentPropertiesW and + // CreateICW/DCW, to hopefully prevent these issues. + mutable mozilla::Mutex mDriverMutex MOZ_UNANNOTATED{"nsPrinterWin::Driver"}; +}; + +#endif // nsPrinterWin_h_ diff --git a/widget/windows/nsSharePicker.cpp b/widget/windows/nsSharePicker.cpp new file mode 100644 index 0000000000..9cdc792718 --- /dev/null +++ b/widget/windows/nsSharePicker.cpp @@ -0,0 +1,81 @@ +/* -*- 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 "nsSharePicker.h" + +#include "nsString.h" +#include "nsThreadUtils.h" +#include "WindowsUIUtils.h" +#include "nsPIDOMWindow.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Unused.h" + +using mozilla::dom::Promise; + +/////////////////////////////////////////////////////////////////////////////// +// nsISharePicker + +NS_IMPL_ISUPPORTS(nsSharePicker, nsISharePicker) + +namespace { +inline NS_ConvertUTF8toUTF16 NS_ConvertUTF8toUTF16_MaybeVoid( + const nsACString& aStr) { + auto str = NS_ConvertUTF8toUTF16(aStr); + str.SetIsVoid(aStr.IsVoid()); + return str; +} +inline nsIGlobalObject* GetGlobalObject() { + return xpc::NativeGlobal(xpc::PrivilegedJunkScope()); +} +} // namespace + +NS_IMETHODIMP +nsSharePicker::Init(mozIDOMWindowProxy* aOpenerWindow) { + if (mInited) { + return NS_ERROR_FAILURE; + } + mOpenerWindow = aOpenerWindow; + mInited = true; + return NS_OK; +} + +NS_IMETHODIMP +nsSharePicker::GetOpenerWindow(mozIDOMWindowProxy** aOpenerWindow) { + *aOpenerWindow = mOpenerWindow; + return NS_OK; +} + +NS_IMETHODIMP +nsSharePicker::Share(const nsACString& aTitle, const nsACString& aText, + nsIURI* aUrl, Promise** aPromise) { + mozilla::ErrorResult result; + RefPtr<Promise> promise = Promise::Create(GetGlobalObject(), result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + nsAutoCString urlString; + if (aUrl) { + nsresult rv = aUrl->GetSpec(urlString); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mozilla::Unused << rv; + } else { + urlString.SetIsVoid(true); + } + + auto mozPromise = + WindowsUIUtils::Share(NS_ConvertUTF8toUTF16_MaybeVoid(aTitle), + NS_ConvertUTF8toUTF16_MaybeVoid(aText), + NS_ConvertUTF8toUTF16_MaybeVoid(urlString)); + mozPromise->Then( + mozilla::GetCurrentSerialEventTarget(), __func__, + [promise]() { promise->MaybeResolveWithUndefined(); }, + [promise]() { promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); }); + + promise.forget(aPromise); + + return NS_OK; +} diff --git a/widget/windows/nsSharePicker.h b/widget/windows/nsSharePicker.h new file mode 100644 index 0000000000..d9ba9d2a6d --- /dev/null +++ b/widget/windows/nsSharePicker.h @@ -0,0 +1,29 @@ +/* -*- 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 nsSharePicker_h__ +#define nsSharePicker_h__ + +#include "nsCOMPtr.h" +#include "nsISharePicker.h" +#include "nsPIDOMWindow.h" +#include "nsThreadUtils.h" + +class nsSharePicker : public nsISharePicker { + virtual ~nsSharePicker() = default; + + public: + nsSharePicker() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSISHAREPICKER + + private: + bool mInited = false; + mozIDOMWindowProxy* mOpenerWindow; +}; + +#endif // nsSharePicker_h__ diff --git a/widget/windows/nsSound.cpp b/widget/windows/nsSound.cpp new file mode 100644 index 0000000000..1fecf09c3a --- /dev/null +++ b/widget/windows/nsSound.cpp @@ -0,0 +1,331 @@ +/* -*- 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 "nscore.h" +#include <stdio.h> +#include "nsString.h" +#include <windows.h> + +// mmsystem.h is needed to build with WIN32_LEAN_AND_MEAN +#include <mmsystem.h> + +#include "HeadlessSound.h" +#include "nsSound.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsContentUtils.h" +#include "nsCRT.h" +#include "nsIObserverService.h" + +#include "mozilla/Logging.h" +#include "prtime.h" + +#include "nsNativeCharsetUtils.h" +#include "nsThreadUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "gfxPlatform.h" + +using mozilla::LogLevel; + +#ifdef DEBUG +static mozilla::LazyLogModule gWin32SoundLog("nsSound"); +#endif + +// Hackaround for bug 1644240 +// When we call PlaySound for the first time in the process, winmm.dll creates +// a new thread and starts a message loop in winmm!mciwindow. After that, +// every call of PlaySound communicates with that thread via Window messages. +// It seems that Warsaw application hooks USER32!GetMessageA, and there is +// a timing window where they free their trampoline region without reverting +// the hook on USER32!GetMessageA, resulting in crash when winmm!mciwindow +// receives a message because it tries to jump to a freed buffer. +// Based on the crash reports, it happened on all versions of Windows x64, and +// the possible condition was wslbdhm64.dll was loaded but wslbscrwh64.dll was +// unloaded. Therefore we suppress playing a sound under such a condition. +static bool ShouldSuppressPlaySound() { +#if defined(_M_AMD64) + if (::GetModuleHandle(L"wslbdhm64.dll") && + !::GetModuleHandle(L"wslbscrwh64.dll")) { + return true; + } +#endif // defined(_M_AMD64) + return false; +} + +class nsSoundPlayer : public mozilla::Runnable { + public: + explicit nsSoundPlayer(const nsAString& aSoundName) + : mozilla::Runnable("nsSoundPlayer"), + mSoundName(aSoundName), + mSoundData(nullptr) {} + + nsSoundPlayer(const uint8_t* aData, size_t aSize) + : mozilla::Runnable("nsSoundPlayer"), mSoundName(u""_ns) { + MOZ_ASSERT(aSize > 0, "Size should not be zero"); + MOZ_ASSERT(aData, "Data shoud not be null"); + + // We will disptach nsSoundPlayer to playerthread, so keep a data copy + mSoundData = new uint8_t[aSize]; + memcpy(mSoundData, aData, aSize); + } + + NS_DECL_NSIRUNNABLE + + protected: + ~nsSoundPlayer(); + + nsString mSoundName; + uint8_t* mSoundData; +}; + +NS_IMETHODIMP +nsSoundPlayer::Run() { + if (ShouldSuppressPlaySound()) { + return NS_OK; + } + + MOZ_ASSERT(!mSoundName.IsEmpty() || mSoundData, + "Sound name or sound data should be specified"); + DWORD flags = SND_NODEFAULT | SND_ASYNC; + + if (mSoundData) { + flags |= SND_MEMORY; + ::PlaySoundW(reinterpret_cast<LPCWSTR>(mSoundData), nullptr, flags); + } else { + flags |= SND_ALIAS; + ::PlaySoundW(mSoundName.get(), nullptr, flags); + } + return NS_OK; +} + +nsSoundPlayer::~nsSoundPlayer() { delete[] mSoundData; } + +mozilla::StaticRefPtr<nsISound> nsSound::sInstance; + +/* static */ +already_AddRefed<nsISound> nsSound::GetInstance() { + if (!sInstance) { + if (gfxPlatform::IsHeadless()) { + sInstance = new mozilla::widget::HeadlessSound(); + } else { + RefPtr<nsSound> sound = new nsSound(); + nsresult rv = sound->CreatePlayerThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + sInstance = sound.forget(); + } + ClearOnShutdown(&sInstance); + } + + RefPtr<nsISound> service = sInstance; + return service.forget(); +} + +#ifndef SND_PURGE +// Not available on Windows CE, and according to MSDN +// doesn't do anything on recent windows either. +# define SND_PURGE 0 +#endif + +NS_IMPL_ISUPPORTS(nsSound, nsISound, nsIStreamLoaderObserver, nsIObserver) + +nsSound::nsSound() : mInited(false) {} + +nsSound::~nsSound() {} + +void nsSound::PurgeLastSound() { + // Halt any currently playing sound. + if (mSoundPlayer) { + if (mPlayerThread) { + mPlayerThread->Dispatch( + NS_NewRunnableFunction("nsSound::PurgeLastSound", + [player = std::move(mSoundPlayer)]() { + // Capture move mSoundPlayer to lambda then + // PlaySoundW(nullptr, nullptr, SND_PURGE) + // will be called before freeing the + // nsSoundPlayer. + if (ShouldSuppressPlaySound()) { + return; + } + ::PlaySoundW(nullptr, nullptr, SND_PURGE); + }), + NS_DISPATCH_NORMAL); + } + } +} + +NS_IMETHODIMP nsSound::Beep() { + ::MessageBeep(0); + + return NS_OK; +} + +NS_IMETHODIMP nsSound::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* context, nsresult aStatus, + uint32_t dataLen, const uint8_t* data) { + MOZ_ASSERT(mPlayerThread, "player thread should not be null "); + // print a load error on bad status + if (NS_FAILED(aStatus)) { +#ifdef DEBUG + if (aLoader) { + nsCOMPtr<nsIRequest> request; + nsCOMPtr<nsIChannel> channel; + aLoader->GetRequest(getter_AddRefs(request)); + if (request) channel = do_QueryInterface(request); + if (channel) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + if (uri) { + nsAutoCString uriSpec; + uri->GetSpec(uriSpec); + MOZ_LOG(gWin32SoundLog, LogLevel::Info, + ("Failed to load %s\n", uriSpec.get())); + } + } + } +#endif + return aStatus; + } + + PurgeLastSound(); + + if (data && dataLen > 0) { + MOZ_ASSERT(!mSoundPlayer, "mSoundPlayer should be null"); + mSoundPlayer = new nsSoundPlayer(data, dataLen); + MOZ_ASSERT(mSoundPlayer, "Could not create player"); + + nsresult rv = mPlayerThread->Dispatch(mSoundPlayer, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsSound::Play(nsIURL* aURL) { + nsresult rv; + +#ifdef DEBUG_SOUND + char* url; + aURL->GetSpec(&url); + MOZ_LOG(gWin32SoundLog, LogLevel::Info, ("%s\n", url)); +#endif + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader( + getter_AddRefs(loader), aURL, + this, // aObserver + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + return rv; +} + +nsresult nsSound::CreatePlayerThread() { + if (mPlayerThread) { + return NS_OK; + } + if (NS_WARN_IF(NS_FAILED(NS_NewNamedThread("PlayEventSound", + getter_AddRefs(mPlayerThread))))) { + return NS_ERROR_FAILURE; + } + + // Add an observer for shutdown event to release the thread at that time + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) { + return NS_ERROR_FAILURE; + } + + observerService->AddObserver(this, "xpcom-shutdown-threads", false); + return NS_OK; +} + +NS_IMETHODIMP +nsSound::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "xpcom-shutdown-threads")) { + PurgeLastSound(); + + if (mPlayerThread) { + mPlayerThread->Shutdown(); + mPlayerThread = nullptr; + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsSound::Init() { + if (mInited) { + return NS_OK; + } + + MOZ_ASSERT(mPlayerThread, "player thread should not be null "); + // This call halts a sound if it was still playing. + // We have to use the sound library for something to make sure + // it is initialized. + // If we wait until the first sound is played, there will + // be a time lag as the library gets loaded. + // This should be done in player thread otherwise it will block main thread + // at the first time loading sound library. + mPlayerThread->Dispatch( + NS_NewRunnableFunction("nsSound::Init", + []() { + if (ShouldSuppressPlaySound()) { + return; + } + ::PlaySoundW(nullptr, nullptr, SND_PURGE); + }), + NS_DISPATCH_NORMAL); + + mInited = true; + + return NS_OK; +} + +NS_IMETHODIMP nsSound::PlayEventSound(uint32_t aEventId) { + MOZ_ASSERT(mPlayerThread, "player thread should not be null "); + PurgeLastSound(); + + const wchar_t* sound = nullptr; + switch (aEventId) { + case EVENT_NEW_MAIL_RECEIVED: + sound = L"MailBeep"; + break; + case EVENT_ALERT_DIALOG_OPEN: + sound = L"SystemExclamation"; + break; + case EVENT_CONFIRM_DIALOG_OPEN: + sound = L"SystemQuestion"; + break; + case EVENT_MENU_EXECUTE: + sound = L"MenuCommand"; + break; + case EVENT_MENU_POPUP: + sound = L"MenuPopup"; + break; + case EVENT_EDITOR_MAX_LEN: + sound = L".Default"; + break; + default: + // Win32 plays no sounds at NS_SYSSOUND_PROMPT_DIALOG and + // NS_SYSSOUND_SELECT_DIALOG. + return NS_OK; + } + NS_ASSERTION(sound, "sound is null"); + MOZ_ASSERT(!mSoundPlayer, "mSoundPlayer should be null"); + mSoundPlayer = new nsSoundPlayer(nsDependentString(sound)); + MOZ_ASSERT(mSoundPlayer, "Could not create player"); + nsresult rv = mPlayerThread->Dispatch(mSoundPlayer, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} diff --git a/widget/windows/nsSound.h b/widget/windows/nsSound.h new file mode 100644 index 0000000000..d600b0873a --- /dev/null +++ b/widget/windows/nsSound.h @@ -0,0 +1,47 @@ +/* -*- 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 __nsSound_h__ +#define __nsSound_h__ + +#include "nsISound.h" +#include "nsIObserver.h" +#include "nsIStreamLoader.h" +#include "nsCOMPtr.h" +#include "mozilla/StaticPtr.h" + +class nsIThread; +class nsIRunnable; + +class nsSound : public nsISound, + public nsIStreamLoaderObserver, + public nsIObserver + +{ + public: + nsSound(); + static already_AddRefed<nsISound> GetInstance(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISOUND + NS_DECL_NSISTREAMLOADEROBSERVER + NS_DECL_NSIOBSERVER + + private: + virtual ~nsSound(); + void PurgeLastSound(); + + private: + nsresult CreatePlayerThread(); + + nsCOMPtr<nsIThread> mPlayerThread; + nsCOMPtr<nsIRunnable> mSoundPlayer; + bool mInited; + + static mozilla::StaticRefPtr<nsISound> sInstance; +}; + +#endif /* __nsSound_h__ */ diff --git a/widget/windows/nsToolkit.cpp b/widget/windows/nsToolkit.cpp new file mode 100644 index 0000000000..6eea9c958f --- /dev/null +++ b/widget/windows/nsToolkit.cpp @@ -0,0 +1,69 @@ +/* -*- 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 "nsToolkit.h" +#include "nsAppShell.h" +#include "nsWindow.h" +#include "nsWidgetsCID.h" +#include "prmon.h" +#include "prtime.h" +#include "nsComponentManagerUtils.h" +#include <objbase.h> +#include "WinUtils.h" + +#include "nsUXThemeData.h" + +// unknwn.h is needed to build with WIN32_LEAN_AND_MEAN +#include <unknwn.h> + +using namespace mozilla::widget; + +nsToolkit* nsToolkit::gToolkit = nullptr; +HINSTANCE nsToolkit::mDllInstance = 0; + +//------------------------------------------------------------------------- +// +// constructor +// +//------------------------------------------------------------------------- +nsToolkit::nsToolkit() { + MOZ_COUNT_CTOR(nsToolkit); + +#if defined(MOZ_STATIC_COMPONENT_LIBS) + nsToolkit::Startup(GetModuleHandle(nullptr)); +#endif +} + +//------------------------------------------------------------------------- +// +// destructor +// +//------------------------------------------------------------------------- +nsToolkit::~nsToolkit() { MOZ_COUNT_DTOR(nsToolkit); } + +void nsToolkit::Startup(HMODULE hModule) { + nsToolkit::mDllInstance = hModule; + WinUtils::Initialize(); +} + +void nsToolkit::Shutdown() { + delete gToolkit; + gToolkit = nullptr; +} + +//------------------------------------------------------------------------- +// +// Return the nsToolkit for the current thread. If a toolkit does not +// yet exist, then one will be created... +// +//------------------------------------------------------------------------- +// static +nsToolkit* nsToolkit::GetToolkit() { + if (!gToolkit) { + gToolkit = new nsToolkit(); + } + + return gToolkit; +} diff --git a/widget/windows/nsToolkit.h b/widget/windows/nsToolkit.h new file mode 100644 index 0000000000..4be7bdb80a --- /dev/null +++ b/widget/windows/nsToolkit.h @@ -0,0 +1,47 @@ +/* -*- 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 nsToolkit_h__ +#define nsToolkit_h__ + +#include "nsdefs.h" + +#include "nsCOMPtr.h" +#include <windows.h> + +// Avoid including windowsx.h to prevent macro pollution +#ifndef GET_X_LPARAM +# define GET_X_LPARAM(pt) (short(LOWORD(pt))) +#endif +#ifndef GET_Y_LPARAM +# define GET_Y_LPARAM(pt) (short(HIWORD(pt))) +#endif + +/** + * Wrapper around the thread running the message pump. + * The toolkit abstraction is necessary because the message pump must + * execute within the same thread that created the widget under Win32. + */ + +class nsToolkit { + public: + nsToolkit(); + + private: + ~nsToolkit(); + + public: + static nsToolkit* GetToolkit(); + + static HINSTANCE mDllInstance; + + static void Startup(HMODULE hModule); + static void Shutdown(); + + protected: + static nsToolkit* gToolkit; +}; + +#endif // TOOLKIT_H diff --git a/widget/windows/nsUXThemeConstants.h b/widget/windows/nsUXThemeConstants.h new file mode 100644 index 0000000000..44ce9ab34e --- /dev/null +++ b/widget/windows/nsUXThemeConstants.h @@ -0,0 +1,256 @@ +/* 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 nsUXThemeConstants_h +#define nsUXThemeConstants_h + +/* + * The following constants are used to determine how a widget is drawn using + * Windows' Theme API. For more information on theme parts and states see + * http://msdn.microsoft.com/en-us/library/bb773210(VS.85).aspx + */ + +#include <vssym32.h> +#include <vsstyle.h> + +#define THEME_COLOR 204 +#define THEME_FONT 210 + +// Generic state constants +#define TS_NORMAL 1 +#define TS_HOVER 2 +#define TS_ACTIVE 3 +#define TS_DISABLED 4 +#define TS_FOCUSED 5 + +// These constants are reversed for the trackbar (scale) thumb +#define TKP_FOCUSED 4 +#define TKP_DISABLED 5 + +// Toolbarbutton constants +#define TB_CHECKED 5 +#define TB_HOVER_CHECKED 6 + +// Button constants +#define BP_BUTTON 1 +#define BP_RADIO 2 +#define BP_CHECKBOX 3 +#define BP_GROUPBOX 4 +#define BP_Count 5 + +// Textfield constants +/* This is the EP_EDITTEXT part */ +#define TFP_TEXTFIELD 1 +#define TFP_EDITBORDER_NOSCROLL 6 +#define TFS_READONLY 6 + +/* These are the state constants for the EDITBORDER parts */ +#define TFS_EDITBORDER_NORMAL 1 +#define TFS_EDITBORDER_HOVER 2 +#define TFS_EDITBORDER_FOCUSED 3 +#define TFS_EDITBORDER_DISABLED 4 + +// Treeview/listbox constants +#define TREEVIEW_BODY 1 + +// Scrollbar constants +#define SP_BUTTON 1 +#define SP_THUMBHOR 2 +#define SP_THUMBVERT 3 +#define SP_TRACKSTARTHOR 4 +#define SP_TRACKENDHOR 5 +#define SP_TRACKSTARTVERT 6 +#define SP_TRACKENDVERT 7 +#define SP_GRIPPERHOR 8 +#define SP_GRIPPERVERT 9 + +// Implicit hover state. +// BASE + 0 = UP, + 1 = DOWN, etc. +#define SP_BUTTON_IMPLICIT_HOVER_BASE 17 + +// Scale constants +#define TKP_TRACK 1 +#define TKP_TRACKVERT 2 +#define TKP_THUMB 3 +#define TKP_THUMBBOTTOM 4 +#define TKP_THUMBTOP 5 +#define TKP_THUMBVERT 6 +#define TKP_THUMBLEFT 7 +#define TKP_THUMBRIGHT 8 + +// Track state contstants +#define TRS_NORMAL 1 + +// Track vertical state constants +#define TRVS_NORMAL 1 + +// Spin constants +#define SPNP_UP 1 +#define SPNP_DOWN 2 + +// Tab constants +#define TABP_TAB 4 +#define TABP_TAB_SELECTED 5 +#define TABP_PANELS 9 +#define TABP_PANEL 10 + +// Tooltip constants +#define TTP_STANDARD 1 + +// Dropdown constants +#define CBP_DROPMARKER 1 +#define CBP_DROPBORDER 4 +/* This is actually the 'READONLY' style */ +#define CBP_DROPFRAME 5 +#define CBP_DROPMARKER_VISTA 6 + +// Menu Constants +#define MENU_BARBACKGROUND 7 +#define MENU_BARITEM 8 +#define MENU_POPUPBACKGROUND 9 +#define MENU_POPUPBORDERS 10 +#define MENU_POPUPCHECK 11 +#define MENU_POPUPCHECKBACKGROUND 12 +#define MENU_POPUPGUTTER 13 +#define MENU_POPUPITEM 14 +#define MENU_POPUPSEPARATOR 15 +#define MENU_POPUPSUBMENU 16 +#define MENU_SYSTEMCLOSE 17 +#define MENU_SYSTEMMAXIMIZE 18 +#define MENU_SYSTEMMINIMIZE 19 +#define MENU_SYSTEMRESTORE 20 + +#define MB_ACTIVE 1 +#define MB_INACTIVE 2 + +#define MS_NORMAL 1 +#define MS_SELECTED 2 +#define MS_DEMOTED 3 + +#define MBI_NORMAL 1 +#define MBI_HOT 2 +#define MBI_PUSHED 3 +#define MBI_DISABLED 4 +#define MBI_DISABLEDHOT 5 +#define MBI_DISABLEDPUSHED 6 + +#define MC_CHECKMARKNORMAL 1 +#define MC_CHECKMARKDISABLED 2 +#define MC_BULLETNORMAL 3 +#define MC_BULLETDISABLED 4 + +#define MCB_DISABLED 1 +#define MCB_NORMAL 2 +#define MCB_BITMAP 3 + +#define MPI_NORMAL 1 +#define MPI_HOT 2 +#define MPI_DISABLED 3 +#define MPI_DISABLEDHOT 4 + +#define MSM_NORMAL 1 +#define MSM_DISABLED 2 + +// Rebar constants +#define RP_BAND 3 +#define RP_BACKGROUND 6 + +// Constants only found in new (98+, 2K+, XP+, etc.) Windows. +#ifdef DFCS_HOT +# undef DFCS_HOT +#endif +#define DFCS_HOT 0x00001000 + +#ifdef COLOR_MENUHILIGHT +# undef COLOR_MENUHILIGHT +#endif +#define COLOR_MENUHILIGHT 29 + +#ifdef SPI_GETFLATMENU +# undef SPI_GETFLATMENU +#endif +#define SPI_GETFLATMENU 0x1022 +#ifndef SPI_GETMENUSHOWDELAY +# define SPI_GETMENUSHOWDELAY 106 +#endif // SPI_GETMENUSHOWDELAY +#ifndef SPI_GETCARETTIMEOUT +# define SPI_GETCARETTIMEOUT 0x2022 +#endif // SPI_GETCARETTIMEOUT +#ifndef WS_EX_LAYOUTRTL +# define WS_EX_LAYOUTRTL 0x00400000L // Right to left mirroring +#endif + +// Our extra constants for passing a little bit more info to the renderer. +#define DFCS_RTL 0x00010000 + +// Toolbar separator dimension which can't be gotten from Windows +#define TB_SEPARATOR_HEIGHT 2 + +namespace mozilla { +namespace widget { +namespace themeconst { + +// Pulled from sdk/include/vsstyle.h +enum { + WP_CAPTION = 1, + WP_SMALLCAPTION = 2, + WP_MINCAPTION = 3, + WP_SMALLMINCAPTION = 4, + WP_MAXCAPTION = 5, + WP_SMALLMAXCAPTION = 6, + WP_FRAMELEFT = 7, + WP_FRAMERIGHT = 8, + WP_FRAMEBOTTOM = 9, + WP_SMALLFRAMELEFT = 10, + WP_SMALLFRAMERIGHT = 11, + WP_SMALLFRAMEBOTTOM = 12, + WP_SYSBUTTON = 13, + WP_MDISYSBUTTON = 14, + WP_MINBUTTON = 15, + WP_MDIMINBUTTON = 16, + WP_MAXBUTTON = 17, + WP_CLOSEBUTTON = 18, + WP_SMALLCLOSEBUTTON = 19, + WP_MDICLOSEBUTTON = 20, + WP_RESTOREBUTTON = 21, + WP_MDIRESTOREBUTTON = 22, + WP_HELPBUTTON = 23, + WP_MDIHELPBUTTON = 24, + WP_HORZSCROLL = 25, + WP_HORZTHUMB = 26, + WP_VERTSCROLL = 27, + WP_VERTTHUMB = 28, + WP_DIALOG = 29, + WP_CAPTIONSIZINGTEMPLATE = 30, + WP_SMALLCAPTIONSIZINGTEMPLATE = 31, + WP_FRAMELEFTSIZINGTEMPLATE = 32, + WP_SMALLFRAMELEFTSIZINGTEMPLATE = 33, + WP_FRAMERIGHTSIZINGTEMPLATE = 34, + WP_SMALLFRAMERIGHTSIZINGTEMPLATE = 35, + WP_FRAMEBOTTOMSIZINGTEMPLATE = 36, + WP_SMALLFRAMEBOTTOMSIZINGTEMPLATE = 37, + WP_FRAME = 38, + WP_Count +}; + +enum { + BS_NORMAL = 1, + BS_HOT = 2, + BS_PUSHED = 3, + BS_DISABLED = 4, + BS_INACTIVE = 5 /* undocumented, inactive caption button */ +}; + +} // namespace themeconst +} // namespace widget +} // namespace mozilla + +// If any theme part ends up having a value higher than WP_Count, this will +// need to change. +#define THEME_PART_DISTINCT_VALUE_COUNT mozilla::widget::themeconst::WP_Count + +#endif diff --git a/widget/windows/nsUXThemeData.cpp b/widget/windows/nsUXThemeData.cpp new file mode 100644 index 0000000000..b3f9e6fce9 --- /dev/null +++ b/widget/windows/nsUXThemeData.cpp @@ -0,0 +1,98 @@ +/* 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 "mozilla/WindowsVersion.h" + +#include "nsUXThemeData.h" +#include "nsDebug.h" +#include "nsToolkit.h" +#include "nsUXThemeConstants.h" +#include "gfxWindowsPlatform.h" + +using namespace mozilla; +using namespace mozilla::widget; + +nsUXThemeData::ThemeHandle nsUXThemeData::sThemes[eUXNumClasses]; + +nsUXThemeData::ThemeHandle::~ThemeHandle() { Close(); } + +void nsUXThemeData::ThemeHandle::OpenOnce(HWND aWindow, LPCWSTR aClassList) { + if (mHandle.isSome()) { + return; + } + + mHandle = Some(OpenThemeData(aWindow, aClassList)); +} + +void nsUXThemeData::ThemeHandle::Close() { + if (mHandle.isNothing()) { + return; + } + + if (HANDLE rawHandle = mHandle.extract()) { + CloseThemeData(rawHandle); + } +} + +nsUXThemeData::ThemeHandle::operator HANDLE() { + return mHandle.valueOr(nullptr); +} + +void nsUXThemeData::Invalidate() { + for (auto& theme : sThemes) { + theme.Close(); + } +} + +HANDLE +nsUXThemeData::GetTheme(nsUXThemeClass cls) { + NS_ASSERTION(cls < eUXNumClasses, "Invalid theme class!"); + sThemes[cls].OpenOnce(nullptr, GetClassName(cls)); + return sThemes[cls]; +} + +const wchar_t* nsUXThemeData::GetClassName(nsUXThemeClass cls) { + switch (cls) { + case eUXButton: + return L"Button"; + case eUXEdit: + return L"Edit"; + case eUXRebar: + return L"Rebar"; + case eUXToolbar: + return L"Toolbar"; + case eUXProgress: + return L"Progress"; + case eUXTab: + return L"Tab"; + case eUXTrackbar: + return L"Trackbar"; + case eUXCombobox: + return L"Combobox"; + case eUXHeader: + return L"Header"; + case eUXListview: + return L"Listview"; + case eUXMenu: + return L"Menu"; + default: + MOZ_ASSERT_UNREACHABLE("unknown uxtheme class"); + return L""; + } +} + +bool nsUXThemeData::sIsHighContrastOn = false; + +// static +void nsUXThemeData::UpdateNativeThemeInfo() { + HIGHCONTRAST highContrastInfo; + highContrastInfo.cbSize = sizeof(HIGHCONTRAST); + sIsHighContrastOn = + SystemParametersInfo(SPI_GETHIGHCONTRAST, 0, &highContrastInfo, 0) && + highContrastInfo.dwFlags & HCF_HIGHCONTRASTON; +} diff --git a/widget/windows/nsUXThemeData.h b/widget/windows/nsUXThemeData.h new file mode 100644 index 0000000000..38be8b4484 --- /dev/null +++ b/widget/windows/nsUXThemeData.h @@ -0,0 +1,69 @@ +/* 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 __UXThemeData_h__ +#define __UXThemeData_h__ +#include <windows.h> +#include <uxtheme.h> + +#include "nscore.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/Maybe.h" +#include "WinUtils.h" + +#include "nsWindowDefs.h" + +enum nsUXThemeClass { + eUXButton = 0, + eUXEdit, + eUXRebar, + eUXToolbar, + eUXProgress, + eUXTab, + eUXTrackbar, + eUXCombobox, + eUXHeader, + eUXListview, + eUXMenu, + eUXNumClasses +}; + +class nsUXThemeData { + // This class makes sure we don't attempt to open a theme if the previous + // loading attempt has failed because OpenThemeData is a heavy task and + // it's less likely that the API returns a different result. + class ThemeHandle final { + mozilla::Maybe<HANDLE> mHandle; + + public: + ThemeHandle() = default; + ~ThemeHandle(); + + // Disallow copy and move + ThemeHandle(const ThemeHandle&) = delete; + ThemeHandle(ThemeHandle&&) = delete; + ThemeHandle& operator=(const ThemeHandle&) = delete; + ThemeHandle& operator=(ThemeHandle&&) = delete; + + operator HANDLE(); + void OpenOnce(HWND aWindow, LPCWSTR aClassList); + void Close(); + }; + + static ThemeHandle sThemes[eUXNumClasses]; + static const wchar_t* GetClassName(nsUXThemeClass); + + public: + static bool sIsHighContrastOn; + + static void Invalidate(); + static HANDLE GetTheme(nsUXThemeClass cls); + static HMODULE GetThemeDLL(); + + static void UpdateNativeThemeInfo(); + static bool IsHighContrastOn() { return sIsHighContrastOn; } +}; +#endif // __UXThemeData_h__ diff --git a/widget/windows/nsUserIdleServiceWin.cpp b/widget/windows/nsUserIdleServiceWin.cpp new file mode 100644 index 0000000000..7cf957dffd --- /dev/null +++ b/widget/windows/nsUserIdleServiceWin.cpp @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* 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 "nsUserIdleServiceWin.h" +#include <windows.h> + +bool nsUserIdleServiceWin::PollIdleTime(uint32_t* aIdleTime) { + LASTINPUTINFO inputInfo; + inputInfo.cbSize = sizeof(inputInfo); + if (!::GetLastInputInfo(&inputInfo)) return false; + + *aIdleTime = + SAFE_COMPARE_EVEN_WITH_WRAPPING(GetTickCount(), inputInfo.dwTime); + + return true; +} diff --git a/widget/windows/nsUserIdleServiceWin.h b/widget/windows/nsUserIdleServiceWin.h new file mode 100644 index 0000000000..f9a47c8df5 --- /dev/null +++ b/widget/windows/nsUserIdleServiceWin.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* 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 nsUserIdleServiceWin_h__ +#define nsUserIdleServiceWin_h__ + +#include "nsUserIdleService.h" +#include "mozilla/AppShutdown.h" + +/* NOTE: Compare of GetTickCount() could overflow. This corrects for + * overflow situations. + ***/ +#ifndef SAFE_COMPARE_EVEN_WITH_WRAPPING +# define SAFE_COMPARE_EVEN_WITH_WRAPPING(A, B) \ + (((int)((long)A - (long)B) & 0xFFFFFFFF)) +#endif + +class nsUserIdleServiceWin : public nsUserIdleService { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(nsUserIdleServiceWin, nsUserIdleService) + + bool PollIdleTime(uint32_t* aIdleTime) override; + + static already_AddRefed<nsUserIdleServiceWin> GetInstance() { + RefPtr<nsUserIdleServiceWin> idleService = + nsUserIdleService::GetInstance().downcast<nsUserIdleServiceWin>(); + if (!idleService) { + // Avoid late instantiation or resurrection during shutdown. + if (mozilla::AppShutdown::IsInOrBeyond( + mozilla::ShutdownPhase::AppShutdownConfirmed)) { + return nullptr; + } + idleService = new nsUserIdleServiceWin(); + } + + return idleService.forget(); + } + + protected: + nsUserIdleServiceWin() {} + virtual ~nsUserIdleServiceWin() {} +}; + +#endif // nsUserIdleServiceWin_h__ diff --git a/widget/windows/nsWidgetFactory.cpp b/widget/windows/nsWidgetFactory.cpp new file mode 100644 index 0000000000..47c7d021ed --- /dev/null +++ b/widget/windows/nsWidgetFactory.cpp @@ -0,0 +1,60 @@ +/* -*- 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 "nsWidgetFactory.h" + +#include "mozilla/Components.h" +#include "nsISupports.h" +#include "nsdefs.h" +#include "nsWidgetsCID.h" +#include "nsAppShell.h" +#include "nsAppShellSingleton.h" +#include "mozilla/WidgetUtils.h" +#include "mozilla/widget/ScreenManager.h" +#include "nsLookAndFeel.h" +#include "WinMouseScrollHandler.h" +#include "KeyboardLayout.h" +#include "nsToolkit.h" + +// Modules that switch out based on the environment +#include "nsXULAppAPI.h" +// Desktop +#include "nsFilePicker.h" // needs to be included before other shobjidl.h includes +#include "nsColorPicker.h" +// Content processes +#include "nsFilePickerProxy.h" + +// Clipboard +#include "nsClipboardHelper.h" +#include "nsClipboard.h" +#include "HeadlessClipboard.h" + +#include "WindowsUIUtils.h" + +using namespace mozilla; +using namespace mozilla::widget; + +NS_IMPL_COMPONENT_FACTORY(nsIClipboard) { + nsCOMPtr<nsIClipboard> inst; + if (gfxPlatform::IsHeadless()) { + inst = new HeadlessClipboard(); + } else { + inst = new nsClipboard(); + } + return inst.forget().downcast<nsISupports>(); +} + +nsresult nsWidgetWindowsModuleCtor() { return nsAppShellInit(); } + +void nsWidgetWindowsModuleDtor() { + // Shutdown all XP level widget classes. + WidgetUtils::Shutdown(); + + KeyboardLayout::Shutdown(); + MouseScrollHandler::Shutdown(); + nsLookAndFeel::Shutdown(); + nsToolkit::Shutdown(); + nsAppShellShutdown(); +} diff --git a/widget/windows/nsWidgetFactory.h b/widget/windows/nsWidgetFactory.h new file mode 100644 index 0000000000..41a39220e3 --- /dev/null +++ b/widget/windows/nsWidgetFactory.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* 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_nsWidgetFactory_h +#define widget_windows_nsWidgetFactory_h + +#include "nscore.h" +#include "nsID.h" + +class nsISupports; + +nsresult nsAppShellConstructor(const nsIID& iid, void** result); + +nsresult nsWidgetWindowsModuleCtor(); +void nsWidgetWindowsModuleDtor(); + +#endif // defined widget_windows_nsWidgetFactory_h diff --git a/widget/windows/nsWinGesture.cpp b/widget/windows/nsWinGesture.cpp new file mode 100644 index 0000000000..8fd00b3ff0 --- /dev/null +++ b/widget/windows/nsWinGesture.cpp @@ -0,0 +1,388 @@ +/* -*- 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/. */ + +/* + * nsWinGesture - Touch input handling for tablet displays. + */ + +#include "nscore.h" +#include "nsWinGesture.h" +#include "nsUXThemeData.h" +#include "mozilla/Logging.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/dom/SimpleGestureEventBinding.h" +#include "mozilla/dom/WheelEventBinding.h" + +#include <cmath> + +using namespace mozilla; +using namespace mozilla::widget; + +extern mozilla::LazyLogModule gWindowsLog; + +static bool gEnableSingleFingerPanEvents = false; + +nsWinGesture::nsWinGesture() + : mPanActive(false), + mFeedbackActive(false), + mXAxisFeedback(false), + mYAxisFeedback(false), + mPanInertiaActive(false) { + (void)InitLibrary(); + mPixelScrollOverflow = 0; +} + +/* Load and shutdown */ + +bool nsWinGesture::InitLibrary() { + // Check to see if we want single finger gesture input. Only do this once + // for the app so we don't have to look it up on every window create. + gEnableSingleFingerPanEvents = + Preferences::GetBool("gestures.enable_single_finger_input", false); + + return true; +} + +#define GCOUNT 5 + +bool nsWinGesture::SetWinGestureSupport( + HWND hWnd, WidgetGestureNotifyEvent::PanDirection aDirection) { + GESTURECONFIG config[GCOUNT]; + + memset(&config, 0, sizeof(config)); + + config[0].dwID = GID_ZOOM; + config[0].dwWant = GC_ZOOM; + config[0].dwBlock = 0; + + config[1].dwID = GID_ROTATE; + config[1].dwWant = GC_ROTATE; + config[1].dwBlock = 0; + + config[2].dwID = GID_PAN; + config[2].dwWant = GC_PAN | GC_PAN_WITH_INERTIA | GC_PAN_WITH_GUTTER; + config[2].dwBlock = GC_PAN_WITH_SINGLE_FINGER_VERTICALLY | + GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY; + + if (gEnableSingleFingerPanEvents) { + if (aDirection == WidgetGestureNotifyEvent::ePanVertical || + aDirection == WidgetGestureNotifyEvent::ePanBoth) { + config[2].dwWant |= GC_PAN_WITH_SINGLE_FINGER_VERTICALLY; + config[2].dwBlock -= GC_PAN_WITH_SINGLE_FINGER_VERTICALLY; + } + + if (aDirection == WidgetGestureNotifyEvent::ePanHorizontal || + aDirection == WidgetGestureNotifyEvent::ePanBoth) { + config[2].dwWant |= GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY; + config[2].dwBlock -= GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY; + } + } + + config[3].dwWant = GC_TWOFINGERTAP; + config[3].dwID = GID_TWOFINGERTAP; + config[3].dwBlock = 0; + + config[4].dwWant = GC_PRESSANDTAP; + config[4].dwID = GID_PRESSANDTAP; + config[4].dwBlock = 0; + + return SetGestureConfig(hWnd, 0, GCOUNT, (PGESTURECONFIG)&config, + sizeof(GESTURECONFIG)); +} + +/* Helpers */ + +bool nsWinGesture::IsPanEvent(LPARAM lParam) { + GESTUREINFO gi; + + ZeroMemory(&gi, sizeof(GESTUREINFO)); + gi.cbSize = sizeof(GESTUREINFO); + + BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi); + if (!result) return false; + + if (gi.dwID == GID_PAN) return true; + + return false; +} + +/* Gesture event processing */ + +bool nsWinGesture::ProcessGestureMessage(HWND hWnd, WPARAM wParam, + LPARAM lParam, + WidgetSimpleGestureEvent& evt) { + GESTUREINFO gi; + + ZeroMemory(&gi, sizeof(GESTUREINFO)); + gi.cbSize = sizeof(GESTUREINFO); + + BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi); + if (!result) return false; + + // The coordinates of this event + nsPointWin coord; + coord = gi.ptsLocation; + coord.ScreenToClient(hWnd); + + evt.mRefPoint = LayoutDeviceIntPoint(coord.x, coord.y); + + // Multiple gesture can occur at the same time so gesture state + // info can't be shared. + switch (gi.dwID) { + case GID_BEGIN: + case GID_END: + // These should always fall through to DefWndProc + return false; + break; + + case GID_ZOOM: { + if (gi.dwFlags & GF_BEGIN) { + // Send a zoom start event + + // The low 32 bits are the distance in pixels. + mZoomIntermediate = (float)gi.ullArguments; + + evt.mMessage = eMagnifyGestureStart; + evt.mDelta = 0.0; + } else if (gi.dwFlags & GF_END) { + // Send a zoom end event, the delta is the change + // in touch points. + evt.mMessage = eMagnifyGesture; + // (positive for a "zoom in") + evt.mDelta = -1.0 * (mZoomIntermediate - (float)gi.ullArguments); + mZoomIntermediate = (float)gi.ullArguments; + } else { + // Send a zoom intermediate event, the delta is the change + // in touch points. + evt.mMessage = eMagnifyGestureUpdate; + // (positive for a "zoom in") + evt.mDelta = -1.0 * (mZoomIntermediate - (float)gi.ullArguments); + mZoomIntermediate = (float)gi.ullArguments; + } + } break; + + case GID_ROTATE: { + // Send a rotate start event + double radians = 0.0; + + // On GF_BEGIN, ullArguments contains the absolute rotation at the + // start of the gesture. In later events it contains the offset from + // the start angle. + if (gi.ullArguments != 0) + radians = GID_ROTATE_ANGLE_FROM_ARGUMENT(gi.ullArguments); + + double degrees = -1 * radians * (180 / M_PI); + + if (gi.dwFlags & GF_BEGIN) { + // At some point we should pass the initial angle in + // along with delta. It's useful. + degrees = mRotateIntermediate = 0.0; + } + + evt.mDirection = 0; + evt.mDelta = degrees - mRotateIntermediate; + mRotateIntermediate = degrees; + + if (evt.mDelta > 0) { + evt.mDirection = + dom::SimpleGestureEvent_Binding::ROTATION_COUNTERCLOCKWISE; + } else if (evt.mDelta < 0) { + evt.mDirection = dom::SimpleGestureEvent_Binding::ROTATION_CLOCKWISE; + } + + if (gi.dwFlags & GF_BEGIN) { + evt.mMessage = eRotateGestureStart; + } else if (gi.dwFlags & GF_END) { + evt.mMessage = eRotateGesture; + } else { + evt.mMessage = eRotateGestureUpdate; + } + } break; + + case GID_TWOFINGERTAP: + // Normally maps to "restore" from whatever you may have recently changed. + // A simple double click. + evt.mMessage = eTapGesture; + evt.mClickCount = 1; + break; + + case GID_PRESSANDTAP: + // Two finger right click. Defaults to right click if it falls through. + evt.mMessage = ePressTapGesture; + evt.mClickCount = 1; + break; + } + + return true; +} + +bool nsWinGesture::ProcessPanMessage(HWND hWnd, WPARAM wParam, LPARAM lParam) { + GESTUREINFO gi; + + ZeroMemory(&gi, sizeof(GESTUREINFO)); + gi.cbSize = sizeof(GESTUREINFO); + + BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi); + if (!result) return false; + + // The coordinates of this event + nsPointWin coord; + coord = mPanRefPoint = gi.ptsLocation; + // We want screen coordinates in our local offsets as client coordinates will + // change when feedback is taking place. Gui events though require client + // coordinates. + mPanRefPoint.ScreenToClient(hWnd); + + switch (gi.dwID) { + case GID_BEGIN: + case GID_END: + // These should always fall through to DefWndProc + return false; + break; + + // Setup pixel scroll events for both axis + case GID_PAN: { + if (gi.dwFlags & GF_BEGIN) { + mPanIntermediate = coord; + mPixelScrollDelta = 0; + mPanActive = true; + mPanInertiaActive = false; + } else { +#ifdef DBG_jimm + int32_t deltaX = mPanIntermediate.x - coord.x; + int32_t deltaY = mPanIntermediate.y - coord.y; + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("coordX=%d coordY=%d deltaX=%d deltaY=%d x:%d y:%d\n", coord.x, + coord.y, deltaX, deltaY, mXAxisFeedback, mYAxisFeedback)); +#endif + + mPixelScrollDelta.x = mPanIntermediate.x - coord.x; + mPixelScrollDelta.y = mPanIntermediate.y - coord.y; + mPanIntermediate = coord; + + if (gi.dwFlags & GF_INERTIA) mPanInertiaActive = true; + + if (gi.dwFlags & GF_END) { + mPanActive = false; + mPanInertiaActive = false; + PanFeedbackFinalize(hWnd, true); + } + } + } break; + } + return true; +} + +inline bool TestTransition(int32_t a, int32_t b) { + // If a is zero, overflow is zero, implying the cursor has moved back to the + // start position. If b is zero, cached overscroll is zero, implying feedback + // just begun. + if (a == 0 || b == 0) return true; + // Test for different signs. + return (a < 0) == (b < 0); +} + +void nsWinGesture::UpdatePanFeedbackX(HWND hWnd, int32_t scrollOverflow, + bool& endFeedback) { + // If scroll overflow was returned indicating we panned past the bounds of + // the scrollable view port, start feeback. + if (scrollOverflow != 0) { + if (!mFeedbackActive) { + BeginPanningFeedback(hWnd); + mFeedbackActive = true; + } + endFeedback = false; + mXAxisFeedback = true; + return; + } + + if (mXAxisFeedback) { + int32_t newOverflow = mPixelScrollOverflow.x - mPixelScrollDelta.x; + + // Detect a reverse transition past the starting drag point. This tells us + // the user has panned all the way back so we can stop providing feedback + // for this axis. + if (!TestTransition(newOverflow, mPixelScrollOverflow.x) || + newOverflow == 0) + return; + + // Cache the total over scroll in pixels. + mPixelScrollOverflow.x = newOverflow; + endFeedback = false; + } +} + +void nsWinGesture::UpdatePanFeedbackY(HWND hWnd, int32_t scrollOverflow, + bool& endFeedback) { + // If scroll overflow was returned indicating we panned past the bounds of + // the scrollable view port, start feeback. + if (scrollOverflow != 0) { + if (!mFeedbackActive) { + BeginPanningFeedback(hWnd); + mFeedbackActive = true; + } + endFeedback = false; + mYAxisFeedback = true; + return; + } + + if (mYAxisFeedback) { + int32_t newOverflow = mPixelScrollOverflow.y - mPixelScrollDelta.y; + + // Detect a reverse transition past the starting drag point. This tells us + // the user has panned all the way back so we can stop providing feedback + // for this axis. + if (!TestTransition(newOverflow, mPixelScrollOverflow.y) || + newOverflow == 0) + return; + + // Cache the total over scroll in pixels. + mPixelScrollOverflow.y = newOverflow; + endFeedback = false; + } +} + +void nsWinGesture::PanFeedbackFinalize(HWND hWnd, bool endFeedback) { + if (!mFeedbackActive) return; + + if (endFeedback) { + mFeedbackActive = false; + mXAxisFeedback = false; + mYAxisFeedback = false; + mPixelScrollOverflow = 0; + EndPanningFeedback(hWnd, TRUE); + return; + } + + UpdatePanningFeedback(hWnd, mPixelScrollOverflow.x, mPixelScrollOverflow.y, + mPanInertiaActive); +} + +bool nsWinGesture::PanDeltaToPixelScroll(WidgetWheelEvent& aWheelEvent) { + aWheelEvent.mDeltaX = aWheelEvent.mDeltaY = aWheelEvent.mDeltaZ = 0.0; + aWheelEvent.mLineOrPageDeltaX = aWheelEvent.mLineOrPageDeltaY = 0; + + aWheelEvent.mRefPoint = LayoutDeviceIntPoint(mPanRefPoint.x, mPanRefPoint.y); + aWheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_PIXEL; + aWheelEvent.mScrollType = WidgetWheelEvent::SCROLL_SYNCHRONOUSLY; + aWheelEvent.mIsNoLineOrPageDelta = true; + + aWheelEvent.mOverflowDeltaX = 0.0; + aWheelEvent.mOverflowDeltaY = 0.0; + + // Don't scroll the view if we are currently at a bounds, or, if we are + // panning back from a max feedback position. This keeps the original drag + // point constant. + if (!mXAxisFeedback) { + aWheelEvent.mDeltaX = mPixelScrollDelta.x; + } + if (!mYAxisFeedback) { + aWheelEvent.mDeltaY = mPixelScrollDelta.y; + } + + return (aWheelEvent.mDeltaX != 0 || aWheelEvent.mDeltaY != 0); +} diff --git a/widget/windows/nsWinGesture.h b/widget/windows/nsWinGesture.h new file mode 100644 index 0000000000..040b460da9 --- /dev/null +++ b/widget/windows/nsWinGesture.h @@ -0,0 +1,91 @@ +/* -*- 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 WinGesture_h__ +#define WinGesture_h__ + +/* + * nsWinGesture - Touch input handling for tablet displays. + */ + +#include "nsdefs.h" +#include <winuser.h> +#include <tpcshrd.h> +#include "nsPoint.h" +#include "mozilla/EventForwards.h" +#include "mozilla/TouchEvents.h" + +// WM_TABLET_QUERYSYSTEMGESTURESTATUS return values +#define TABLET_ROTATE_GESTURE_ENABLE 0x02000000 + +class nsPointWin : public nsIntPoint { + public: + nsPointWin& operator=(const POINTS& aPoint) { + x = aPoint.x; + y = aPoint.y; + return *this; + } + nsPointWin& operator=(const POINT& aPoint) { + x = aPoint.x; + y = aPoint.y; + return *this; + } + nsPointWin& operator=(int val) { + x = y = val; + return *this; + } + void ScreenToClient(HWND hWnd) { + POINT tmp; + tmp.x = x; + tmp.y = y; + ::ScreenToClient(hWnd, &tmp); + *this = tmp; + } +}; + +class nsWinGesture { + public: + nsWinGesture(); + + public: + bool SetWinGestureSupport( + HWND hWnd, mozilla::WidgetGestureNotifyEvent::PanDirection aDirection); + bool ShutdownWinGestureSupport(); + + // Simple gesture process + bool ProcessGestureMessage(HWND hWnd, WPARAM wParam, LPARAM lParam, + mozilla::WidgetSimpleGestureEvent& evt); + + // Pan processing + bool IsPanEvent(LPARAM lParam); + bool ProcessPanMessage(HWND hWnd, WPARAM wParam, LPARAM lParam); + bool PanDeltaToPixelScroll(mozilla::WidgetWheelEvent& aWheelEvent); + void UpdatePanFeedbackX(HWND hWnd, int32_t scrollOverflow, bool& endFeedback); + void UpdatePanFeedbackY(HWND hWnd, int32_t scrollOverflow, bool& endFeedback); + void PanFeedbackFinalize(HWND hWnd, bool endFeedback); + + private: + // Delay load info + bool InitLibrary(); + + // Pan and feedback state + nsPointWin mPanIntermediate; + nsPointWin mPanRefPoint; + nsPointWin mPixelScrollDelta; + bool mPanActive; + bool mFeedbackActive; + bool mXAxisFeedback; + bool mYAxisFeedback; + bool mPanInertiaActive; + nsPointWin mPixelScrollOverflow; + + // Zoom state + double mZoomIntermediate; + + // Rotate state + double mRotateIntermediate; +}; + +#endif /* WinGesture_h__ */ diff --git a/widget/windows/nsWindow.cpp b/widget/windows/nsWindow.cpp new file mode 100644 index 0000000000..1a8646d620 --- /dev/null +++ b/widget/windows/nsWindow.cpp @@ -0,0 +1,9000 @@ +/* -*- 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/. */ + +/* + * nsWindow - Native window management and event handling. + * + * nsWindow is organized into a set of major blocks and + * block subsections. The layout is as follows: + * + * Includes + * Variables + * nsIWidget impl. + * nsIWidget methods and utilities + * nsSwitchToUIThread impl. + * nsSwitchToUIThread methods and utilities + * Moz events + * Event initialization + * Event dispatching + * Native events + * Wndproc(s) + * Event processing + * OnEvent event handlers + * IME management and accessibility + * Transparency + * Popup hook handling + * Misc. utilities + * Child window impl. + * + * Search for "BLOCK:" to find major blocks. + * Search for "SECTION:" to find specific sections. + * + * Blocks should be split out into separate files if they + * become unmanageable. + * + * Notable related sources: + * + * nsWindowDefs.h - Definitions, macros, structs, enums + * and general setup. + * nsWindowDbg.h/.cpp - Debug related code and directives. + * nsWindowGfx.h/.cpp - Graphics and painting. + * + */ + +/************************************************************** + ************************************************************** + ** + ** BLOCK: Includes + ** + ** Include headers. + ** + ************************************************************** + **************************************************************/ + +#include "gfx2DGlue.h" +#include "gfxEnv.h" +#include "gfxPlatform.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Likely.h" +#include "mozilla/PreXULSkeletonUI.h" +#include "mozilla/Logging.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/SwipeTracker.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/TimeStamp.h" + +#include "mozilla/ipc/MessageChannel.h" +#include <algorithm> +#include <limits> + +#include "mozilla/widget/WinMessages.h" +#include "nsWindow.h" +#include "nsWindowTaskbarConcealer.h" +#include "nsAppRunner.h" + +#include <shellapi.h> +#include <windows.h> +#include <wtsapi32.h> +#include <process.h> +#include <commctrl.h> +#include <dbt.h> +#include <unknwn.h> +#include <psapi.h> +#include <rpc.h> +#include <propvarutil.h> +#include <propkey.h> + +#include "mozilla/Logging.h" +#include "prtime.h" +#include "prenv.h" + +#include "mozilla/WidgetTraceEvent.h" +#include "nsContentUtils.h" +#include "nsISupportsPrimitives.h" +#include "nsITheme.h" +#include "nsIObserverService.h" +#include "nsIScreenManager.h" +#include "imgIContainer.h" +#include "nsIFile.h" +#include "nsIRollupListener.h" +#include "nsIClipboard.h" +#include "WinMouseScrollHandler.h" +#include "nsFontMetrics.h" +#include "nsIFontEnumerator.h" +#include "nsFont.h" +#include "nsRect.h" +#include "nsThreadUtils.h" +#include "nsNativeCharsetUtils.h" +#include "nsGkAtoms.h" +#include "nsCRT.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsWidgetsCID.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsString.h" +#include "mozilla/Components.h" +#include "nsNativeThemeWin.h" +#include "nsXULPopupManager.h" +#include "nsWindowsDllInterceptor.h" +#include "nsLayoutUtils.h" +#include "nsView.h" +#include "nsWindowGfx.h" +#include "gfxWindowsPlatform.h" +#include "gfxDWriteFonts.h" +#include "nsPrintfCString.h" +#include "mozilla/Preferences.h" +#include "SystemTimeConverter.h" +#include "WinTaskbar.h" +#include "WidgetUtils.h" +#include "WinWindowOcclusionTracker.h" +#include "nsIWidgetListener.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/Touch.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/GPUProcessManager.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/layers/WebRenderLayerManager.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/TextEvents.h" // For WidgetKeyboardEvent +#include "mozilla/TextEventDispatcherListener.h" +#include "mozilla/widget/nsAutoRollup.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "mozilla/widget/Screen.h" +#include "nsStyleConsts.h" +#include "nsBidiKeyboard.h" +#include "nsStyleConsts.h" +#include "gfxConfig.h" +#include "InProcessWinCompositorWidget.h" +#include "InputDeviceUtils.h" +#include "ScreenHelperWin.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_gfx.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/StaticPrefs_widget.h" +#include "nsNativeAppSupportWin.h" +#include "mozilla/browser/NimbusFeatures.h" + +#include "nsIGfxInfo.h" +#include "nsUXThemeConstants.h" +#include "KeyboardLayout.h" +#include "nsNativeDragTarget.h" +#include <mmsystem.h> // needed for WIN32_LEAN_AND_MEAN +#include <zmouse.h> +#include <richedit.h> + +#if defined(ACCESSIBILITY) + +# ifdef DEBUG +# include "mozilla/a11y/Logging.h" +# endif + +# include "oleidl.h" +# include <winuser.h> +# include "nsAccessibilityService.h" +# include "mozilla/a11y/DocAccessible.h" +# include "mozilla/a11y/LazyInstantiator.h" +# include "mozilla/a11y/Platform.h" +# if !defined(WINABLEAPI) +# include <winable.h> +# endif // !defined(WINABLEAPI) +#endif // defined(ACCESSIBILITY) + +#include "WindowsUIUtils.h" + +#include "nsWindowDefs.h" + +#include "nsCrashOnException.h" + +#include "nsIContent.h" + +#include "mozilla/BackgroundHangMonitor.h" +#include "WinIMEHandler.h" + +#include "npapi.h" + +#include <d3d11.h> + +// ERROR from wingdi.h (below) gets undefined by some code. +// #define ERROR 0 +// #define RGN_ERROR ERROR +#define ERROR 0 + +#if !defined(SM_CONVERTIBLESLATEMODE) +# define SM_CONVERTIBLESLATEMODE 0x2003 +#endif + +#include "mozilla/gfx/DeviceManagerDx.h" +#include "mozilla/layers/APZInputBridge.h" +#include "mozilla/layers/InputAPZContext.h" +#include "mozilla/layers/KnowsCompositor.h" +#include "InputData.h" + +#include "mozilla/TaskController.h" +#include "mozilla/Telemetry.h" +#include "mozilla/webrender/WebRenderAPI.h" +#include "mozilla/layers/IAPZCTreeManager.h" + +#include "DirectManipulationOwner.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::layers; +using namespace mozilla::widget; +using namespace mozilla::plugins; + +/************************************************************** + ************************************************************** + ** + ** BLOCK: Variables + ** + ** nsWindow Class static initializations and global variables. + ** + ************************************************************** + **************************************************************/ + +/************************************************************** + * + * SECTION: nsWindow statics + * + **************************************************************/ +static const wchar_t kUser32LibName[] = L"user32.dll"; + +uint32_t nsWindow::sInstanceCount = 0; +bool nsWindow::sIsOleInitialized = false; +nsIWidget::Cursor nsWindow::sCurrentCursor = {}; +nsWindow* nsWindow::sCurrentWindow = nullptr; +bool nsWindow::sJustGotDeactivate = false; +bool nsWindow::sJustGotActivate = false; +bool nsWindow::sIsInMouseCapture = false; + +// Urgent-message reentrancy depth for the static `WindowProc` callback. +// +// Three unfortunate facts collide: +// +// 𝛼) Some messages must be processed promptly. If not, Windows will leave the +// receiving window in an intermediate, and potentially unusable, state until +// the WindowProc invocation that is handling it returns. +// +// 𝛽) Some messages have indefinitely long processing time. These are mostly +// messages which may cause us to enter a nested modal loop (via +// `SpinEventLoopUntil` or similar). +// +// 𝛾) Sometimes, messages skip the queue entirely. Our `WindowProc` may be +// reentrantly reinvoked from the kernel while we're blocking _on_ the +// kernel, even briefly, during processing of other messages. (Relevant +// search term: `KeUserModeCallback`.) +// +// The nightmare scenario, then, is that during processing of an 𝛼-message, we +// briefly become blocked (e.g., by calling `::SendMessageW()`), and the kernel +// takes that opportunity to use 𝛾 to hand us a 𝛽-message. (Concretely, see +// bug 1842170.) +// +// There is little we can do to prevent the first half of this scenario. 𝛼) and +// 𝛾) are effectively immutable facts of Windows, and we sometimes legitimately +// need to make blocking calls to process 𝛼-messages. (We may not even be aware +// that we're making such calls, if they're undocumented implementation details +// of another API.) +// +// In an ideal world, WindowProc would always return promptly (or at least in +// bounded time), and 𝛽-messages would not _per se_ exist; long-running modal +// states would instead be implemented in async fashion. In practice, that's far +// easier said than done -- replacing existing uses of `SpinEventLoopUntil` _et +// al._ with asynchronous mechanisms is a collection of mostly-unrelated cross- +// cutting architectural tasks, each of potentially unbounded scope. For now, +// and for the foreseeable future, we're stuck with them. +// +// We therefore simply punt. More specifically: if a known 𝛽-message jumps the +// queue to come in while we're in the middle of processing a known 𝛼-message, +// we: +// * properly queue the message for processing later; +// * respond to the 𝛽-message as though we actually had processed it; and +// * just hope that it can wait until we get around to it. +// +// The word "known" requires a bit of justification. There is no canonical set +// of 𝛼-messages, nor is the set of 𝛽-messages fixed (or even demarcable). We +// can't safely assume that all messages are 𝛼-messages, as that could cause +// 𝛽-messages to be arbitrarily and surprisingly delayed whenever any nested +// event loop is active. We also can't assume all messages are 𝛽-messages, +// since one 𝛼-message jumping the queue while processing another 𝛼-message is +// part of normal and required operation for windowed Windows applications. +// +// So we simply add messages to those sets as we identify them. (Or, preferably, +// rework the 𝛽-message's handling to make it no longer 𝛽. But see above.) +// +// --- +// +// The actual value of `sDepth` is the number of active invocations of +// `WindowProc` that are processing known 𝛼-messages. +size_t nsWindow::WndProcUrgentInvocation::sDepth = 0; + +// Hook Data Members for Dropdowns. sProcessHook Tells the +// hook methods whether they should be processing the hook +// messages. +HHOOK nsWindow::sMsgFilterHook = nullptr; +HHOOK nsWindow::sCallProcHook = nullptr; +HHOOK nsWindow::sCallMouseHook = nullptr; +bool nsWindow::sProcessHook = false; +UINT nsWindow::sRollupMsgId = 0; +HWND nsWindow::sRollupMsgWnd = nullptr; +UINT nsWindow::sHookTimerId = 0; + +// Used to prevent dispatching mouse events that do not originate from user +// input. +POINT nsWindow::sLastMouseMovePoint = {0}; + +bool nsWindow::sIsRestoringSession = false; + +bool nsWindow::sTouchInjectInitialized = false; +InjectTouchInputPtr nsWindow::sInjectTouchFuncPtr; + +static SystemTimeConverter<DWORD>& TimeConverter() { + static SystemTimeConverter<DWORD> timeConverterSingleton; + return timeConverterSingleton; +} + +// Global event hook for window cloaking. Never deregistered. +// - `Nothing` if not yet set. +// - `Some(nullptr)` if no attempt should be made to set it. +static mozilla::Maybe<HWINEVENTHOOK> sWinCloakEventHook = Nothing(); +static mozilla::LazyLogModule sCloakingLog("DWMCloaking"); + +namespace mozilla { + +class CurrentWindowsTimeGetter { + public: + explicit CurrentWindowsTimeGetter(HWND aWnd) : mWnd(aWnd) {} + + DWORD GetCurrentTime() const { return ::GetTickCount(); } + + void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) { + DWORD currentTime = GetCurrentTime(); + if (sBackwardsSkewStamp && currentTime == sLastPostTime) { + // There's already one inflight with this timestamp. Don't + // send a duplicate. + return; + } + sBackwardsSkewStamp = Some(aNow); + sLastPostTime = currentTime; + static_assert(sizeof(WPARAM) >= sizeof(DWORD), + "Can't fit a DWORD in a WPARAM"); + ::PostMessage(mWnd, MOZ_WM_SKEWFIX, sLastPostTime, 0); + } + + static bool GetAndClearBackwardsSkewStamp(DWORD aPostTime, + TimeStamp* aOutSkewStamp) { + if (aPostTime != sLastPostTime) { + // The SKEWFIX message is stale; we've sent a new one since then. + // Ignore this one. + return false; + } + MOZ_ASSERT(sBackwardsSkewStamp); + *aOutSkewStamp = sBackwardsSkewStamp.value(); + sBackwardsSkewStamp = Nothing(); + return true; + } + + private: + static Maybe<TimeStamp> sBackwardsSkewStamp; + static DWORD sLastPostTime; + HWND mWnd; +}; + +Maybe<TimeStamp> CurrentWindowsTimeGetter::sBackwardsSkewStamp; +DWORD CurrentWindowsTimeGetter::sLastPostTime = 0; + +} // namespace mozilla + +/************************************************************** + * + * SECTION: globals variables + * + **************************************************************/ + +static const char* sScreenManagerContractID = + "@mozilla.org/gfx/screenmanager;1"; + +extern mozilla::LazyLogModule gWindowsLog; + +static NS_DEFINE_CID(kCClipboardCID, NS_CLIPBOARD_CID); + +// General purpose user32.dll hook object +static WindowsDllInterceptor sUser32Intercept; + +// When the client area is extended out into the default window frame area, +// this is the minimum amount of space along the edge of resizable windows +// we will always display a resize cursor in, regardless of the underlying +// content. +static const int32_t kResizableBorderMinSize = 3; + +// Getting this object from the window server can be expensive. Keep it +// around, also get it off the main thread. (See bug 1640852) +StaticRefPtr<IVirtualDesktopManager> gVirtualDesktopManager; +static bool gInitializedVirtualDesktopManager = false; + +// We should never really try to accelerate windows bigger than this. In some +// cases this might lead to no D3D9 acceleration where we could have had it +// but D3D9 does not reliably report when it supports bigger windows. 8192 +// is as safe as we can get, we know at least D3D10 hardware always supports +// this, other hardware we expect to report correctly in D3D9. +#define MAX_ACCELERATED_DIMENSION 8192 + +// On window open (as well as after), Windows has an unfortunate habit of +// sending rather a lot of WM_NCHITTEST messages. Because we have to do point +// to DOM target conversions for these, we cache responses for a given +// coordinate this many milliseconds: +#define HITTEST_CACHE_LIFETIME_MS 50 + +#if defined(ACCESSIBILITY) + +namespace mozilla { + +/** + * Windows touchscreen code works by setting a global WH_GETMESSAGE hook and + * injecting tiptsf.dll. The touchscreen process then posts registered messages + * to our main thread. The tiptsf hook picks up those registered messages and + * uses them as commands, some of which call into UIA, which then calls into + * MSAA, which then sends WM_GETOBJECT to us. + * + * We can get ahead of this by installing our own thread-local WH_GETMESSAGE + * hook. Since thread-local hooks are called ahead of global hooks, we will + * see these registered messages before tiptsf does. At this point we can then + * raise a flag that blocks a11y before invoking CallNextHookEx which will then + * invoke the global tiptsf hook. Then when we see WM_GETOBJECT, we check the + * flag by calling TIPMessageHandler::IsA11yBlocked(). + * + * For Windows 8, we also hook tiptsf!ProcessCaretEvents, which is an a11y hook + * function that also calls into UIA. + */ +class TIPMessageHandler { + public: + ~TIPMessageHandler() { + if (mHook) { + ::UnhookWindowsHookEx(mHook); + } + } + + static void Initialize() { + if (sInstance) { + return; + } + + sInstance = new TIPMessageHandler(); + ClearOnShutdown(&sInstance); + } + + static bool IsA11yBlocked() { + if (!sInstance) { + return false; + } + + return sInstance->mA11yBlockCount > 0; + } + + private: + TIPMessageHandler() : mHook(nullptr), mA11yBlockCount(0) { + MOZ_ASSERT(NS_IsMainThread()); + + // Registered messages used by tiptsf + mMessages[0] = ::RegisterWindowMessage(L"ImmersiveFocusNotification"); + mMessages[1] = ::RegisterWindowMessage(L"TipCloseMenus"); + mMessages[2] = ::RegisterWindowMessage(L"TabletInputPanelOpening"); + mMessages[3] = ::RegisterWindowMessage(L"IHM Pen or Touch Event noticed"); + mMessages[4] = ::RegisterWindowMessage(L"ProgrammabilityCaretVisibility"); + mMessages[5] = ::RegisterWindowMessage(L"CaretTrackingUpdateIPHidden"); + mMessages[6] = ::RegisterWindowMessage(L"CaretTrackingUpdateIPInfo"); + + mHook = ::SetWindowsHookEx(WH_GETMESSAGE, &TIPHook, nullptr, + ::GetCurrentThreadId()); + MOZ_ASSERT(mHook); + + if (!sSendMessageTimeoutWStub) { + sUser32Intercept.Init("user32.dll"); + DebugOnly<bool> hooked = sSendMessageTimeoutWStub.Set( + sUser32Intercept, "SendMessageTimeoutW", &SendMessageTimeoutWHook); + MOZ_ASSERT(hooked); + } + } + + class MOZ_RAII A11yInstantiationBlocker { + public: + A11yInstantiationBlocker() { + if (!TIPMessageHandler::sInstance) { + return; + } + ++TIPMessageHandler::sInstance->mA11yBlockCount; + } + + ~A11yInstantiationBlocker() { + if (!TIPMessageHandler::sInstance) { + return; + } + MOZ_ASSERT(TIPMessageHandler::sInstance->mA11yBlockCount > 0); + --TIPMessageHandler::sInstance->mA11yBlockCount; + } + }; + + friend class A11yInstantiationBlocker; + + static LRESULT CALLBACK TIPHook(int aCode, WPARAM aWParam, LPARAM aLParam) { + if (aCode < 0 || !sInstance) { + return ::CallNextHookEx(nullptr, aCode, aWParam, aLParam); + } + + MSG* msg = reinterpret_cast<MSG*>(aLParam); + UINT& msgCode = msg->message; + + for (uint32_t i = 0; i < ArrayLength(sInstance->mMessages); ++i) { + if (msgCode == sInstance->mMessages[i]) { + A11yInstantiationBlocker block; + return ::CallNextHookEx(nullptr, aCode, aWParam, aLParam); + } + } + + return ::CallNextHookEx(nullptr, aCode, aWParam, aLParam); + } + + static LRESULT WINAPI SendMessageTimeoutWHook(HWND aHwnd, UINT aMsgCode, + WPARAM aWParam, LPARAM aLParam, + UINT aFlags, UINT aTimeout, + PDWORD_PTR aMsgResult) { + // We don't want to handle this unless the message is a WM_GETOBJECT that we + // want to block, and the aHwnd is a nsWindow that belongs to the current + // (i.e., main) thread. + if (!aMsgResult || aMsgCode != WM_GETOBJECT || + static_cast<LONG>(aLParam) != OBJID_CLIENT || !::NS_IsMainThread() || + !WinUtils::GetNSWindowPtr(aHwnd) || !IsA11yBlocked()) { + return sSendMessageTimeoutWStub(aHwnd, aMsgCode, aWParam, aLParam, aFlags, + aTimeout, aMsgResult); + } + + // In this case we want to fake the result that would happen if we had + // decided not to handle WM_GETOBJECT in our WndProc. We hand the message + // off to DefWindowProc to accomplish this. + *aMsgResult = static_cast<DWORD_PTR>( + ::DefWindowProcW(aHwnd, aMsgCode, aWParam, aLParam)); + + return static_cast<LRESULT>(TRUE); + } + + static WindowsDllInterceptor::FuncHookType<decltype(&SendMessageTimeoutW)> + sSendMessageTimeoutWStub; + static StaticAutoPtr<TIPMessageHandler> sInstance; + + HHOOK mHook; + UINT mMessages[7]; + uint32_t mA11yBlockCount; +}; + +WindowsDllInterceptor::FuncHookType<decltype(&SendMessageTimeoutW)> + TIPMessageHandler::sSendMessageTimeoutWStub; +StaticAutoPtr<TIPMessageHandler> TIPMessageHandler::sInstance; + +} // namespace mozilla + +#endif // defined(ACCESSIBILITY) + +namespace mozilla { + +// This task will get the VirtualDesktopManager from the generic thread pool +// since doing this on the main thread on startup causes performance issues. +// +// See bug 1640852. +// +// This should be fine and should not require any locking, as when the main +// thread will access it, if it races with this function it will either find +// it to be null or to have a valid value. +class InitializeVirtualDesktopManagerTask : public Task { + public: + InitializeVirtualDesktopManagerTask() + : Task(Kind::OffMainThreadOnly, kDefaultPriorityValue) {} + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + bool GetName(nsACString& aName) override { + aName.AssignLiteral("InitializeVirtualDesktopManagerTask"); + return true; + } +#endif + + virtual TaskResult Run() override { + RefPtr<IVirtualDesktopManager> desktopManager; + HRESULT hr = ::CoCreateInstance( + CLSID_VirtualDesktopManager, NULL, CLSCTX_INPROC_SERVER, + __uuidof(IVirtualDesktopManager), getter_AddRefs(desktopManager)); + if (FAILED(hr)) { + return TaskResult::Complete; + } + + gVirtualDesktopManager = desktopManager; + return TaskResult::Complete; + } +}; + +// Ground-truth query: does Windows claim the window is cloaked right now? +static bool IsCloaked(HWND hwnd) { + DWORD cloakedState; + HRESULT hr = ::DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloakedState, + sizeof(cloakedState)); + + if (FAILED(hr)) { + MOZ_LOG(sCloakingLog, LogLevel::Warning, + ("failed (%08lX) to query cloaking state for HWND %p", hr, hwnd)); + return false; + } + + return cloakedState != 0; +} + +} // namespace mozilla + +/************************************************************** + ************************************************************** + ** + ** BLOCK: nsIWidget impl. + ** + ** nsIWidget interface implementation, broken down into + ** sections. + ** + ************************************************************** + **************************************************************/ + +/************************************************************** + * + * SECTION: nsWindow construction and destruction + * + **************************************************************/ + +nsWindow::nsWindow(bool aIsChildWindow) + : nsBaseWidget(BorderStyle::Default), + mBrush(::CreateSolidBrush(NSRGB_2_COLOREF(::GetSysColor(COLOR_BTNFACE)))), + mFrameState(std::in_place, this), + mIsChildWindow(aIsChildWindow), + mLastPaintEndTime(TimeStamp::Now()), + mCachedHitTestTime(TimeStamp::Now()), + mSizeConstraintsScale(GetDefaultScale().scale), + mDesktopId("DesktopIdMutex") { + MOZ_ASSERT(mWindowType == WindowType::Child); + + if (!gInitializedVirtualDesktopManager) { + TaskController::Get()->AddTask( + MakeAndAddRef<InitializeVirtualDesktopManagerTask>()); + gInitializedVirtualDesktopManager = true; + } + + // Global initialization + if (!sInstanceCount) { + // Global app registration id for Win7 and up. See + // WinTaskbar.cpp for details. + // MSIX packages explicitly do not support setting the appid from within + // the app, as it is set in the package manifest instead. + if (!WinUtils::HasPackageIdentity()) { + mozilla::widget::WinTaskbar::RegisterAppUserModelID(); + } + if (!StaticPrefs::ui_key_layout_load_when_first_needed()) { + KeyboardLayout::GetInstance()->OnLayoutChange(::GetKeyboardLayout(0)); + } +#if defined(ACCESSIBILITY) + mozilla::TIPMessageHandler::Initialize(); +#endif // defined(ACCESSIBILITY) + if (SUCCEEDED(::OleInitialize(nullptr))) { + sIsOleInitialized = true; + } + NS_ASSERTION(sIsOleInitialized, "***** OLE is not initialized!\n"); + MouseScrollHandler::Initialize(); + // Init theme data + nsUXThemeData::UpdateNativeThemeInfo(); + RedirectedKeyDownMessageManager::Forget(); + } // !sInstanceCount + + sInstanceCount++; +} + +nsWindow::~nsWindow() { + mInDtor = true; + + // If the widget was released without calling Destroy() then the native window + // still exists, and we need to destroy it. Destroy() will early-return if it + // was already called. In any case it is important to call it before + // destroying mPresentLock (cf. 1156182). + Destroy(); + + // Free app icon resources. This must happen after `OnDestroy` (see bug + // 708033). + if (mIconSmall) ::DestroyIcon(mIconSmall); + + if (mIconBig) ::DestroyIcon(mIconBig); + + sInstanceCount--; + + // Global shutdown + if (sInstanceCount == 0) { + IMEHandler::Terminate(); + sCurrentCursor = {}; + if (sIsOleInitialized) { + ::OleFlushClipboard(); + ::OleUninitialize(); + sIsOleInitialized = false; + } + } + + NS_IF_RELEASE(mNativeDragTarget); +} + +/************************************************************** + * + * SECTION: nsIWidget::Create, nsIWidget::Destroy + * + * Creating and destroying windows for this widget. + * + **************************************************************/ + +// Allow Derived classes to modify the height that is passed +// when the window is created or resized. +int32_t nsWindow::GetHeight(int32_t aProposedHeight) { return aProposedHeight; } + +void nsWindow::SendAnAPZEvent(InputData& aEvent) { + LRESULT popupHandlingResult; + if (DealWithPopups(mWnd, MOZ_WM_DMANIP, 0, 0, &popupHandlingResult)) { + // We need to consume the event after using it to roll up the popup(s). + return; + } + + if (mSwipeTracker && aEvent.mInputType == PANGESTURE_INPUT) { + // Give the swipe tracker a first pass at the event. If a new pan gesture + // has been started since the beginning of the swipe, the swipe tracker + // will know to ignore the event. + nsEventStatus status = + mSwipeTracker->ProcessEvent(aEvent.AsPanGestureInput()); + if (status == nsEventStatus_eConsumeNoDefault) { + return; + } + } + + APZEventResult result; + if (mAPZC) { + result = mAPZC->InputBridge()->ReceiveInputEvent(aEvent); + } + if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) { + return; + } + + MOZ_ASSERT(aEvent.mInputType == PANGESTURE_INPUT || + aEvent.mInputType == PINCHGESTURE_INPUT); + + if (aEvent.mInputType == PANGESTURE_INPUT) { + PanGestureInput& panInput = aEvent.AsPanGestureInput(); + WidgetWheelEvent event = panInput.ToWidgetEvent(this); + if (!mAPZC) { + if (MayStartSwipeForNonAPZ(panInput)) { + return; + } + } else { + event = MayStartSwipeForAPZ(panInput, result); + } + + ProcessUntransformedAPZEvent(&event, result); + + return; + } + + PinchGestureInput& pinchInput = aEvent.AsPinchGestureInput(); + WidgetWheelEvent event = pinchInput.ToWidgetEvent(this); + ProcessUntransformedAPZEvent(&event, result); +} + +void nsWindow::RecreateDirectManipulationIfNeeded() { + DestroyDirectManipulation(); + + if (mWindowType != WindowType::TopLevel && mWindowType != WindowType::Popup) { + return; + } + + if (!(StaticPrefs::apz_allow_zooming() || + StaticPrefs::apz_windows_use_direct_manipulation()) || + StaticPrefs::apz_windows_force_disable_direct_manipulation()) { + return; + } + + mDmOwner = MakeUnique<DirectManipulationOwner>(this); + + LayoutDeviceIntRect bounds(mBounds.X(), mBounds.Y(), mBounds.Width(), + GetHeight(mBounds.Height())); + mDmOwner->Init(bounds); +} + +void nsWindow::ResizeDirectManipulationViewport() { + if (mDmOwner) { + LayoutDeviceIntRect bounds(mBounds.X(), mBounds.Y(), mBounds.Width(), + GetHeight(mBounds.Height())); + mDmOwner->ResizeViewport(bounds); + } +} + +void nsWindow::DestroyDirectManipulation() { + if (mDmOwner) { + mDmOwner->Destroy(); + mDmOwner.reset(); + } +} + +// Create the proper widget +nsresult nsWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + widget::InitData* aInitData) { + // Historical note: there was once some belief and/or intent that nsWindows + // could be created on arbitrary threads, and this may still be reflected in + // some comments. + MOZ_ASSERT(NS_IsMainThread()); + + widget::InitData defaultInitData; + if (!aInitData) aInitData = &defaultInitData; + + nsIWidget* baseParent = + aInitData->mWindowType == WindowType::Dialog || + aInitData->mWindowType == WindowType::TopLevel || + aInitData->mWindowType == WindowType::Invisible + ? nullptr + : aParent; + + mIsTopWidgetWindow = (nullptr == baseParent); + mBounds = aRect; + + // Ensure that the toolkit is created. + nsToolkit::GetToolkit(); + + BaseCreate(baseParent, aInitData); + + HWND parent; + if (aParent) { // has a nsIWidget parent + parent = aParent ? (HWND)aParent->GetNativeData(NS_NATIVE_WINDOW) : nullptr; + mParent = aParent; + } else { // has a nsNative parent + parent = (HWND)aNativeParent; + mParent = + aNativeParent ? WinUtils::GetNSWindowPtr((HWND)aNativeParent) : nullptr; + } + + mIsRTL = aInitData->mRTL; + mOpeningAnimationSuppressed = aInitData->mIsAnimationSuppressed; + mAlwaysOnTop = aInitData->mAlwaysOnTop; + mIsAlert = aInitData->mIsAlert; + mResizable = aInitData->mResizable; + + DWORD style = WindowStyle(); + DWORD extendedStyle = WindowExStyle(); + + if (mWindowType == WindowType::Popup) { + if (!aParent) { + parent = nullptr; + } + } else if (mWindowType == WindowType::Invisible) { + // Make sure CreateWindowEx succeeds at creating a toplevel window + style &= ~0x40000000; // WS_CHILDWINDOW + } else { + // See if the caller wants to explictly set clip children and clip siblings + if (aInitData->mClipChildren) { + style |= WS_CLIPCHILDREN; + } else { + style &= ~WS_CLIPCHILDREN; + } + if (aInitData->mClipSiblings) { + style |= WS_CLIPSIBLINGS; + } + } + + const wchar_t* className = ChooseWindowClass(mWindowType); + + // Take specific actions when creating the first top-level window + static bool sFirstTopLevelWindowCreated = false; + if (aInitData->mWindowType == WindowType::TopLevel && !aParent && + !sFirstTopLevelWindowCreated) { + sFirstTopLevelWindowCreated = true; + mWnd = ConsumePreXULSkeletonUIHandle(); + auto skeletonUIError = GetPreXULSkeletonUIErrorReason(); + if (skeletonUIError) { + nsAutoString errorString( + GetPreXULSkeletonUIErrorString(skeletonUIError.value())); + Telemetry::ScalarSet( + Telemetry::ScalarID::STARTUP_SKELETON_UI_DISABLED_REASON, + errorString); + } + if (mWnd) { + MOZ_ASSERT(style == kPreXULSkeletonUIWindowStyle, + "The skeleton UI window style should match the expected " + "style for the first window created"); + MOZ_ASSERT(extendedStyle == kPreXULSkeletonUIWindowStyleEx, + "The skeleton UI window extended style should match the " + "expected extended style for the first window created"); + MOZ_ASSERT( + ::GetWindowThreadProcessId(mWnd, nullptr) == ::GetCurrentThreadId(), + "The skeleton UI window should be created on the same thread as " + "other windows"); + mIsShowingPreXULSkeletonUI = true; + + // If we successfully consumed the pre-XUL skeleton UI, just update + // our internal state to match what is currently being displayed. + mIsVisible = true; + mIsCloaked = mozilla::IsCloaked(mWnd); + mFrameState->ConsumePreXULSkeletonState(WasPreXULSkeletonUIMaximized()); + + // These match the margins set in browser-tabsintitlebar.js with + // default prefs on Windows. Bug 1673092 tracks lining this up with + // that more correctly instead of hard-coding it. + SetNonClientMargins(LayoutDeviceIntMargin(0, 2, 2, 2)); + + // Reset the WNDPROC for this window and its whole class, as we had + // to use our own WNDPROC when creating the the skeleton UI window. + ::SetWindowLongPtrW(mWnd, GWLP_WNDPROC, + reinterpret_cast<LONG_PTR>( + WinUtils::NonClientDpiScalingDefWindowProcW)); + ::SetClassLongPtrW(mWnd, GCLP_WNDPROC, + reinterpret_cast<LONG_PTR>( + WinUtils::NonClientDpiScalingDefWindowProcW)); + } + } + + if (!mWnd) { + mWnd = + ::CreateWindowExW(extendedStyle, className, L"", style, aRect.X(), + aRect.Y(), aRect.Width(), GetHeight(aRect.Height()), + parent, nullptr, nsToolkit::mDllInstance, nullptr); + } + + if (!mWnd) { + NS_WARNING("nsWindow CreateWindowEx failed."); + return NS_ERROR_FAILURE; + } + + if (!sWinCloakEventHook) { + MOZ_LOG(sCloakingLog, LogLevel::Info, ("Registering cloaking event hook")); + + // C++03 lambda approximation until P2173R1 is available (-std=c++2b) + struct StdcallLambda { + static void CALLBACK OnCloakUncloakHook(HWINEVENTHOOK hWinEventHook, + DWORD event, HWND hwnd, + LONG idObject, LONG idChild, + DWORD idEventThread, + DWORD dwmsEventTime) { + const bool isCloaked = event == EVENT_OBJECT_CLOAKED ? true : false; + nsWindow::OnCloakEvent(hwnd, isCloaked); + } + }; + + const HWINEVENTHOOK hook = ::SetWinEventHook( + EVENT_OBJECT_CLOAKED, EVENT_OBJECT_UNCLOAKED, HMODULE(nullptr), + &StdcallLambda::OnCloakUncloakHook, ::GetCurrentProcessId(), + ::GetCurrentThreadId(), WINEVENT_OUTOFCONTEXT); + sWinCloakEventHook = Some(hook); + + if (!hook) { + const DWORD err = ::GetLastError(); + MOZ_LOG(sCloakingLog, LogLevel::Error, + ("Failed to register cloaking event hook! GLE = %lu (0x%lX)", err, + err)); + } + } + + if (aInitData->mIsPrivate) { + if (NimbusFeatures::GetBool("majorRelease2022"_ns, + "feltPrivacyWindowSeparation"_ns, true) && + // Although permanent Private Browsing mode is indeed Private Browsing, + // we choose to make it look like regular Firefox in terms of the icon + // it uses (which also means we shouldn't use the Private Browsing + // AUMID). + !StaticPrefs::browser_privatebrowsing_autostart()) { + RefPtr<IPropertyStore> pPropStore; + if (!FAILED(SHGetPropertyStoreForWindow(mWnd, IID_IPropertyStore, + getter_AddRefs(pPropStore)))) { + PROPVARIANT pv; + nsAutoString aumid; + // make sure we're using the private browsing AUMID so that taskbar + // grouping works properly + Unused << NS_WARN_IF( + !mozilla::widget::WinTaskbar::GenerateAppUserModelID(aumid, true)); + if (!FAILED(InitPropVariantFromString(aumid.get(), &pv))) { + if (!FAILED(pPropStore->SetValue(PKEY_AppUserModel_ID, pv))) { + pPropStore->Commit(); + } + + PropVariantClear(&pv); + } + } + HICON icon = ::LoadIconW(::GetModuleHandleW(nullptr), + MAKEINTRESOURCEW(IDI_PBMODE)); + SetBigIcon(icon); + SetSmallIcon(icon); + } + } + + mDeviceNotifyHandle = InputDeviceUtils::RegisterNotification(mWnd); + + // If mDefaultScale is set before mWnd has been set, it will have the scale of + // the primary monitor, rather than the monitor that the window is actually + // on. For non-popup windows this gets corrected by the WM_DPICHANGED message + // which resets mDefaultScale, but for popup windows we don't reset + // mDefaultScale on that message. In order to ensure that popup windows + // spawned on a non-primary monitor end up with the correct scale, we reset + // mDefaultScale here so that it gets recomputed using the correct monitor now + // that we have a mWnd. + mDefaultScale = -1.0; + + if (mIsRTL) { + DWORD dwAttribute = TRUE; + DwmSetWindowAttribute(mWnd, DWMWA_NONCLIENT_RTL_LAYOUT, &dwAttribute, + sizeof dwAttribute); + } + + UpdateDarkModeToolbar(); + + if (mOpeningAnimationSuppressed) { + SuppressAnimation(true); + } + + if (mAlwaysOnTop) { + ::SetWindowPos(mWnd, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } + + if (mWindowType != WindowType::Invisible && + MouseScrollHandler::Device::IsFakeScrollableWindowNeeded()) { + // Ugly Thinkpad Driver Hack (Bugs 507222 and 594977) + // + // We create two zero-sized windows as descendants of the top-level window, + // like so: + // + // Top-level window (MozillaWindowClass) + // FAKETRACKPOINTSCROLLCONTAINER (MozillaWindowClass) + // FAKETRACKPOINTSCROLLABLE (MozillaWindowClass) + // + // We need to have the middle window, otherwise the Trackpoint driver + // will fail to deliver scroll messages. WM_MOUSEWHEEL messages are + // sent to the FAKETRACKPOINTSCROLLABLE, which then propagate up the + // window hierarchy until they are handled by nsWindow::WindowProc. + // WM_HSCROLL messages are also sent to the FAKETRACKPOINTSCROLLABLE, + // but these do not propagate automatically, so we have the window + // procedure pretend that they were dispatched to the top-level window + // instead. + // + // The FAKETRACKPOINTSCROLLABLE needs to have the specific window styles it + // is given below so that it catches the Trackpoint driver's heuristics. + HWND scrollContainerWnd = ::CreateWindowW( + className, L"FAKETRACKPOINTSCROLLCONTAINER", WS_CHILD | WS_VISIBLE, 0, + 0, 0, 0, mWnd, nullptr, nsToolkit::mDllInstance, nullptr); + HWND scrollableWnd = ::CreateWindowW( + className, L"FAKETRACKPOINTSCROLLABLE", + WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | 0x30, 0, 0, 0, 0, + scrollContainerWnd, nullptr, nsToolkit::mDllInstance, nullptr); + + // Give the FAKETRACKPOINTSCROLLABLE window a specific ID so that + // WindowProcInternal can distinguish it from the top-level window + // easily. + ::SetWindowLongPtrW(scrollableWnd, GWLP_ID, eFakeTrackPointScrollableID); + + // Make FAKETRACKPOINTSCROLLABLE use nsWindow::WindowProc, and store the + // old window procedure in its "user data". + WNDPROC oldWndProc = (WNDPROC)::SetWindowLongPtrW( + scrollableWnd, GWLP_WNDPROC, (LONG_PTR)nsWindow::WindowProc); + ::SetWindowLongPtrW(scrollableWnd, GWLP_USERDATA, (LONG_PTR)oldWndProc); + } + + // We will start receiving native events after associating with our native + // window. We will also become the output of WinUtils::GetNSWindowPtr for that + // window. + if (!AssociateWithNativeWindow()) { + return NS_ERROR_FAILURE; + } + + // Starting with Windows XP, a process always runs within a terminal services + // session. In order to play nicely with RDP, fast user switching, and the + // lock screen, we should be handling WM_WTSSESSION_CHANGE. We must register + // our HWND in order to receive this message. + DebugOnly<BOOL> wtsRegistered = + ::WTSRegisterSessionNotification(mWnd, NOTIFY_FOR_THIS_SESSION); + NS_ASSERTION(wtsRegistered, "WTSRegisterSessionNotification failed!\n"); + + mDefaultIMC.Init(this); + IMEHandler::InitInputContext(this, mInputContext); + + static bool a11yPrimed = false; + if (!a11yPrimed && mWindowType == WindowType::TopLevel) { + a11yPrimed = true; + if (Preferences::GetInt("accessibility.force_disabled", 0) == -1) { + ::PostMessage(mWnd, MOZ_WM_STARTA11Y, 0, 0); + } + } + + RecreateDirectManipulationIfNeeded(); + + return NS_OK; +} + +void nsWindow::LocalesChanged() { + bool isRTL = intl::LocaleService::GetInstance()->IsAppLocaleRTL(); + if (mIsRTL != isRTL) { + DWORD dwAttribute = isRTL; + DwmSetWindowAttribute(mWnd, DWMWA_NONCLIENT_RTL_LAYOUT, &dwAttribute, + sizeof dwAttribute); + mIsRTL = isRTL; + } +} + +// Close this nsWindow +void nsWindow::Destroy() { + // WM_DESTROY has already fired, avoid calling it twice + if (mOnDestroyCalled) return; + + // Don't destroy windows that have file pickers open, we'll tear these down + // later once the picker is closed. + mDestroyCalled = true; + if (mPickerDisplayCount) return; + + // During the destruction of all of our children, make sure we don't get + // deleted. + nsCOMPtr<nsIWidget> kungFuDeathGrip(this); + + DestroyDirectManipulation(); + + /** + * On windows the LayerManagerOGL destructor wants the widget to be around for + * cleanup. It also would like to have the HWND intact, so we nullptr it here. + */ + DestroyLayerManager(); + + InputDeviceUtils::UnregisterNotification(mDeviceNotifyHandle); + mDeviceNotifyHandle = nullptr; + + // The DestroyWindow function destroys the specified window. The function + // sends WM_DESTROY and WM_NCDESTROY messages to the window to deactivate it + // and remove the keyboard focus from it. The function also destroys the + // window's menu, flushes the thread message queue, destroys timers, removes + // clipboard ownership, and breaks the clipboard viewer chain (if the window + // is at the top of the viewer chain). + // + // If the specified window is a parent or owner window, DestroyWindow + // automatically destroys the associated child or owned windows when it + // destroys the parent or owner window. The function first destroys child or + // owned windows, and then it destroys the parent or owner window. + VERIFY(::DestroyWindow(mWnd)); + + // Our windows can be subclassed which may prevent us receiving WM_DESTROY. If + // OnDestroy() didn't get called, call it now. + if (false == mOnDestroyCalled) { + MSGResult msgResult; + mWindowHook.Notify(mWnd, WM_DESTROY, 0, 0, msgResult); + OnDestroy(); + } +} + +/************************************************************** + * + * SECTION: Window class utilities + * + * Utilities for calculating the proper window class name for + * Create window. + * + **************************************************************/ + +/* static */ +const wchar_t* nsWindow::RegisterWindowClass(const wchar_t* aClassName, + UINT aExtraStyle, LPWSTR aIconID) { + WNDCLASSW wc; + if (::GetClassInfoW(nsToolkit::mDllInstance, aClassName, &wc)) { + // already registered + return aClassName; + } + + wc.style = CS_DBLCLKS | aExtraStyle; + wc.lpfnWndProc = WinUtils::NonClientDpiScalingDefWindowProcW; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hInstance = nsToolkit::mDllInstance; + wc.hIcon = + aIconID ? ::LoadIconW(::GetModuleHandleW(nullptr), aIconID) : nullptr; + wc.hCursor = nullptr; + wc.hbrBackground = nullptr; + wc.lpszMenuName = nullptr; + wc.lpszClassName = aClassName; + + if (!::RegisterClassW(&wc)) { + // For older versions of Win32 (i.e., not XP), the registration may + // fail with aExtraStyle, so we have to re-register without it. + wc.style = CS_DBLCLKS; + ::RegisterClassW(&wc); + } + return aClassName; +} + +static LPWSTR const gStockApplicationIcon = MAKEINTRESOURCEW(32512); + +/* static */ +const wchar_t* nsWindow::ChooseWindowClass(WindowType aWindowType) { + switch (aWindowType) { + case WindowType::Invisible: + return RegisterWindowClass(kClassNameHidden, 0, gStockApplicationIcon); + case WindowType::Dialog: + return RegisterWindowClass(kClassNameDialog, 0, nullptr); + case WindowType::Popup: + return RegisterWindowClass(kClassNameDropShadow, 0, + gStockApplicationIcon); + default: + return RegisterWindowClass(GetMainWindowClass(), 0, + gStockApplicationIcon); + } +} + +/************************************************************** + * + * SECTION: Window styles utilities + * + * Return the proper windows styles and extended styles. + * + **************************************************************/ + +// Return nsWindow styles +DWORD nsWindow::WindowStyle() { + DWORD style; + + switch (mWindowType) { + case WindowType::Child: + style = WS_OVERLAPPED; + break; + + case WindowType::Dialog: + style = WS_OVERLAPPED | WS_BORDER | WS_DLGFRAME | WS_SYSMENU | DS_3DLOOK | + DS_MODALFRAME | WS_CLIPCHILDREN; + if (mBorderStyle != BorderStyle::Default) + style |= WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; + break; + + case WindowType::Popup: + style = WS_POPUP | WS_OVERLAPPED; + break; + + default: + NS_ERROR("unknown border style"); + [[fallthrough]]; + + case WindowType::TopLevel: + case WindowType::Invisible: + style = WS_OVERLAPPED | WS_BORDER | WS_DLGFRAME | WS_SYSMENU | + WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN; + break; + } + + if (mBorderStyle != BorderStyle::Default && + mBorderStyle != BorderStyle::All) { + if (mBorderStyle == BorderStyle::None || + !(mBorderStyle & BorderStyle::Border)) + style &= ~WS_BORDER; + + if (mBorderStyle == BorderStyle::None || + !(mBorderStyle & BorderStyle::Title)) { + style &= ~WS_DLGFRAME; + } + + if (mBorderStyle == BorderStyle::None || + !(mBorderStyle & BorderStyle::Close)) + style &= ~0; + // XXX The close box can only be removed by changing the window class, + // as far as I know --- roc+moz@cs.cmu.edu + + if (mBorderStyle == BorderStyle::None || + !(mBorderStyle & (BorderStyle::Menu | BorderStyle::Close))) + style &= ~WS_SYSMENU; + // Looks like getting rid of the system menu also does away with the + // close box. So, we only get rid of the system menu if you want neither it + // nor the close box. How does the Windows "Dialog" window class get just + // closebox and no sysmenu? Who knows. + + if (mBorderStyle == BorderStyle::None || + !(mBorderStyle & BorderStyle::ResizeH)) + style &= ~WS_THICKFRAME; + + if (mBorderStyle == BorderStyle::None || + !(mBorderStyle & BorderStyle::Minimize)) + style &= ~WS_MINIMIZEBOX; + + if (mBorderStyle == BorderStyle::None || + !(mBorderStyle & BorderStyle::Maximize)) + style &= ~WS_MAXIMIZEBOX; + + if (IsPopupWithTitleBar()) { + style |= WS_CAPTION; + if (mBorderStyle & BorderStyle::Close) { + style |= WS_SYSMENU; + } + } + } + + if (mIsChildWindow) { + style |= WS_CLIPCHILDREN; + if (!(style & WS_POPUP)) { + style |= WS_CHILD; // WS_POPUP and WS_CHILD are mutually exclusive. + } + } + + VERIFY_WINDOW_STYLE(style); + return style; +} + +// Return nsWindow extended styles +DWORD nsWindow::WindowExStyle() { + MOZ_ASSERT_IF(mIsAlert, mWindowType == WindowType::Dialog); + switch (mWindowType) { + case WindowType::Child: + return 0; + case WindowType::Popup: { + DWORD extendedStyle = WS_EX_TOOLWINDOW; + if (mPopupLevel == PopupLevel::Top) { + extendedStyle |= WS_EX_TOPMOST; + } + return extendedStyle; + } + case WindowType::Dialog: { + if (mIsAlert) { + return WS_EX_TOOLWINDOW | WS_EX_TOPMOST; + } + return WS_EX_WINDOWEDGE | WS_EX_DLGMODALFRAME; + } + case WindowType::Sheet: + MOZ_FALLTHROUGH_ASSERT("Sheets are macOS specific"); + case WindowType::TopLevel: + case WindowType::Invisible: + break; + } + return WS_EX_WINDOWEDGE; +} + +/************************************************************** + * + * SECTION: Native window association utilities + * + * Used in Create and Destroy. A nsWindow can associate with its + * underlying native window mWnd. Once a native window is + * associated with a nsWindow, its native events will be handled + * by the static member function nsWindow::WindowProc. Moreover, + * the association will be registered in the WinUtils association + * list, that is, calling WinUtils::GetNSWindowPtr on the native + * window will return the associated nsWindow. This is used in + * nsWindow::WindowProc to correctly dispatch native events to + * the handler methods defined in nsWindow, even though it is a + * static member function. + * + * After dissociation, the native events of the native window will + * no longer be handled by nsWindow::WindowProc, and will thus not + * be dispatched to the nsWindow native event handler methods. + * Moreover, the association will no longer be registered in the + * WinUtils association list, so calling WinUtils::GetNSWindowPtr + * on the native window will return nullptr. + * + **************************************************************/ + +bool nsWindow::AssociateWithNativeWindow() { + if (!mWnd || !IsWindow(mWnd)) { + NS_ERROR("Invalid window handle"); + return false; + } + + // Connect the this pointer to the native window handle. + // This should be done before SetWindowLongPtrW, because nsWindow::WindowProc + // uses WinUtils::GetNSWindowPtr internally. + WinUtils::SetNSWindowPtr(mWnd, this); + + ::SetLastError(ERROR_SUCCESS); + const auto prevWndProc = reinterpret_cast<WNDPROC>(::SetWindowLongPtrW( + mWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(nsWindow::WindowProc))); + if (!prevWndProc && GetLastError() != ERROR_SUCCESS) { + NS_ERROR("Failure in SetWindowLongPtrW"); + WinUtils::SetNSWindowPtr(mWnd, nullptr); + return false; + } + + mPrevWndProc.emplace(prevWndProc); + return true; +} + +void nsWindow::DissociateFromNativeWindow() { + if (!mWnd || !IsWindow(mWnd) || mPrevWndProc.isNothing()) { + return; + } + + DebugOnly<WNDPROC> wndProcBeforeDissociate = + reinterpret_cast<WNDPROC>(::SetWindowLongPtrW( + mWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(*mPrevWndProc))); + NS_ASSERTION(wndProcBeforeDissociate == nsWindow::WindowProc, + "Unstacked an unexpected native window procedure"); + + WinUtils::SetNSWindowPtr(mWnd, nullptr); + mPrevWndProc.reset(); +} + +/************************************************************** + * + * SECTION: nsIWidget::SetParent, nsIWidget::GetParent + * + * Set or clear the parent widgets using window properties, and + * handles calculating native parent handles. + * + **************************************************************/ + +// Get and set parent widgets +void nsWindow::SetParent(nsIWidget* aNewParent) { + nsCOMPtr<nsIWidget> kungFuDeathGrip(this); + nsIWidget* parent = GetParent(); + if (parent) { + parent->RemoveChild(this); + } + + mParent = aNewParent; + + if (aNewParent) { + ReparentNativeWidget(aNewParent); + aNewParent->AddChild(this); + return; + } + if (mWnd) { + // If we have no parent, SetParent should return the desktop. + VERIFY(::SetParent(mWnd, nullptr)); + RecreateDirectManipulationIfNeeded(); + } +} + +void nsWindow::ReparentNativeWidget(nsIWidget* aNewParent) { + MOZ_ASSERT(aNewParent, "null widget"); + + mParent = aNewParent; + if (mWindowType == WindowType::Popup) { + return; + } + HWND newParent = (HWND)aNewParent->GetNativeData(NS_NATIVE_WINDOW); + NS_ASSERTION(newParent, "Parent widget has a null native window handle"); + if (newParent && mWnd) { + ::SetParent(mWnd, newParent); + RecreateDirectManipulationIfNeeded(); + } +} + +nsIWidget* nsWindow::GetParent(void) { + if (mIsTopWidgetWindow) { + return nullptr; + } + if (mInDtor || mOnDestroyCalled) { + return nullptr; + } + return mParent; +} + +static int32_t RoundDown(double aDouble) { + return aDouble > 0 ? static_cast<int32_t>(floor(aDouble)) + : static_cast<int32_t>(ceil(aDouble)); +} + +float nsWindow::GetDPI() { + float dpi = 96.0f; + nsCOMPtr<nsIScreen> screen = GetWidgetScreen(); + if (screen) { + screen->GetDpi(&dpi); + } + return dpi; +} + +double nsWindow::GetDefaultScaleInternal() { + if (mDefaultScale <= 0.0) { + mDefaultScale = WinUtils::LogToPhysFactor(mWnd); + } + return mDefaultScale; +} + +int32_t nsWindow::LogToPhys(double aValue) { + return WinUtils::LogToPhys( + ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTOPRIMARY), aValue); +} + +nsWindow* nsWindow::GetParentWindow(bool aIncludeOwner) { + return static_cast<nsWindow*>(GetParentWindowBase(aIncludeOwner)); +} + +nsWindow* nsWindow::GetParentWindowBase(bool aIncludeOwner) { + if (mIsTopWidgetWindow) { + // Must use a flag instead of mWindowType to tell if the window is the + // owned by the topmost widget, because a child window can be embedded + // inside a HWND which is not associated with a nsIWidget. + return nullptr; + } + + // If this widget has already been destroyed, pretend we have no parent. + // This corresponds to code in Destroy which removes the destroyed + // widget from its parent's child list. + if (mInDtor || mOnDestroyCalled) return nullptr; + + // aIncludeOwner set to true implies walking the parent chain to retrieve the + // root owner. aIncludeOwner set to false implies the search will stop at the + // true parent (default). + nsWindow* widget = nullptr; + if (mWnd) { + HWND parent = nullptr; + if (aIncludeOwner) + parent = ::GetParent(mWnd); + else + parent = ::GetAncestor(mWnd, GA_PARENT); + + if (parent) { + widget = WinUtils::GetNSWindowPtr(parent); + if (widget) { + // If the widget is in the process of being destroyed then + // do NOT return it + if (widget->mInDtor) { + widget = nullptr; + } + } + } + } + + return widget; +} + +/************************************************************** + * + * SECTION: nsIWidget::Show + * + * Hide or show this component. + * + **************************************************************/ + +void nsWindow::Show(bool bState) { + if (bState && mIsShowingPreXULSkeletonUI) { + // The first time we decide to actually show the window is when we decide + // that we've taken over the window from the skeleton UI, and we should + // no longer treat resizes / moves specially. + mIsShowingPreXULSkeletonUI = false; +#if defined(ACCESSIBILITY) + // If our HWND has focus and the a11y engine hasn't started yet, fire a + // focus win event. Windows already did this when the skeleton UI appeared, + // but a11y wouldn't have been able to start at that point even if a client + // responded. Firing this now gives clients the chance to respond with + // WM_GETOBJECT, which will trigger the a11y engine. We don't want to do + // this if the a11y engine has already started because it has probably + // already fired focus on a descendant. + if (::GetFocus() == mWnd && !GetAccService()) { + ::NotifyWinEvent(EVENT_OBJECT_FOCUS, mWnd, OBJID_CLIENT, CHILDID_SELF); + } +#endif // defined(ACCESSIBILITY) + } + + if (mWindowType == WindowType::Popup) { + MOZ_ASSERT(ChooseWindowClass(mWindowType) == kClassNameDropShadow); + // WS_EX_COMPOSITED conflicts with the WS_EX_LAYERED style and causes + // some popup menus to become invisible. + LONG_PTR exStyle = ::GetWindowLongPtrW(mWnd, GWL_EXSTYLE); + if (exStyle & WS_EX_LAYERED) { + ::SetWindowLongPtrW(mWnd, GWL_EXSTYLE, exStyle & ~WS_EX_COMPOSITED); + } + } + + bool syncInvalidate = false; + + bool wasVisible = mIsVisible; + // Set the status now so that anyone asking during ShowWindow or + // SetWindowPos would get the correct answer. + mIsVisible = bState; + + // We may have cached an out of date visible state. This can happen + // when session restore sets the full screen mode. + if (mIsVisible) + mOldStyle |= WS_VISIBLE; + else + mOldStyle &= ~WS_VISIBLE; + + if (mWnd) { + if (bState) { + if (!wasVisible && mWindowType == WindowType::TopLevel) { + // speed up the initial paint after show for + // top level windows: + syncInvalidate = true; + + // Set the cursor before showing the window to avoid the default wait + // cursor. + SetCursor(Cursor{eCursor_standard}); + + switch (mFrameState->GetSizeMode()) { + case nsSizeMode_Fullscreen: + ::ShowWindow(mWnd, SW_SHOW); + break; + case nsSizeMode_Maximized: + ::ShowWindow(mWnd, SW_SHOWMAXIMIZED); + break; + case nsSizeMode_Minimized: + ::ShowWindow(mWnd, SW_SHOWMINIMIZED); + break; + default: + if (CanTakeFocus() && !mAlwaysOnTop) { + ::ShowWindow(mWnd, SW_SHOWNORMAL); + } else { + ::ShowWindow(mWnd, SW_SHOWNOACTIVATE); + // Don't flicker the window if we're restoring session + if (!sIsRestoringSession) { + Unused << GetAttention(2); + } + } + break; + } + } else { + DWORD flags = SWP_NOSIZE | SWP_NOMOVE | SWP_SHOWWINDOW; + if (wasVisible) { + flags |= SWP_NOZORDER; + } + if (mAlwaysOnTop || mIsAlert) { + flags |= SWP_NOACTIVATE; + } + + if (mWindowType == WindowType::Popup) { + // ensure popups are the topmost of the TOPMOST + // layer. Remember not to set the SWP_NOZORDER + // flag as that might allow the taskbar to overlap + // the popup. + flags |= SWP_NOACTIVATE; + HWND owner = ::GetWindow(mWnd, GW_OWNER); + if (owner) { + // PopupLevel::Top popups should be above all else. All other + // types should be placed in front of their owner, without + // changing the owner's z-level relative to other windows. + if (mPopupLevel != PopupLevel::Top) { + ::SetWindowPos(mWnd, owner, 0, 0, 0, 0, flags); + ::SetWindowPos(owner, mWnd, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } else { + ::SetWindowPos(mWnd, HWND_TOP, 0, 0, 0, 0, flags); + } + } else { + ::SetWindowPos(mWnd, HWND_TOPMOST, 0, 0, 0, 0, flags); + } + } else { + if (mWindowType == WindowType::Dialog && !CanTakeFocus()) + flags |= SWP_NOACTIVATE; + + ::SetWindowPos(mWnd, HWND_TOP, 0, 0, 0, 0, flags); + } + } + } else { + // Clear contents to avoid ghosting of old content if we display + // this window again. + if (wasVisible && mTransparencyMode == TransparencyMode::Transparent) { + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->ClearTransparentWindow(); + } + } + if (mWindowType != WindowType::Dialog) { + ::ShowWindow(mWnd, SW_HIDE); + } else { + ::SetWindowPos(mWnd, 0, 0, 0, 0, 0, + SWP_HIDEWINDOW | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | + SWP_NOACTIVATE); + } + } + } + + if (!wasVisible && bState) { + Invalidate(); + if (syncInvalidate && !mInDtor && !mOnDestroyCalled) { + ::UpdateWindow(mWnd); + } + } + + if (mOpeningAnimationSuppressed) { + SuppressAnimation(false); + } +} + +/************************************************************** + * + * SECTION: nsIWidget::IsVisible + * + * Returns the visibility state. + * + **************************************************************/ + +// Return true if the component is visible, false otherwise. +// +// This does not take cloaking into account. +bool nsWindow::IsVisible() const { return mIsVisible; } + +/************************************************************** + * + * SECTION: Touch and APZ-related functions + * + **************************************************************/ + +void nsWindow::RegisterTouchWindow() { + mTouchWindow = true; + ::RegisterTouchWindow(mWnd, TWF_WANTPALM); + ::EnumChildWindows(mWnd, nsWindow::RegisterTouchForDescendants, 0); +} + +BOOL CALLBACK nsWindow::RegisterTouchForDescendants(HWND aWnd, LPARAM aMsg) { + nsWindow* win = WinUtils::GetNSWindowPtr(aWnd); + if (win) { + ::RegisterTouchWindow(aWnd, TWF_WANTPALM); + } + return TRUE; +} + +void nsWindow::LockAspectRatio(bool aShouldLock) { + if (aShouldLock) { + mAspectRatio = (float)mBounds.Width() / (float)mBounds.Height(); + } else { + mAspectRatio = 0.0; + } +} + +/************************************************************** + * + * SECTION: nsIWidget::SetInputRegion + * + * Sets whether the window should ignore mouse events. + * + **************************************************************/ +void nsWindow::SetInputRegion(const InputRegion& aInputRegion) { + mInputRegion = aInputRegion; +} + +/************************************************************** + * + * SECTION: nsIWidget::Move, nsIWidget::Resize, nsIWidget::Size + * + * Repositioning and sizing a window. + * + **************************************************************/ + +void nsWindow::SetSizeConstraints(const SizeConstraints& aConstraints) { + SizeConstraints c = aConstraints; + + if (mWindowType != WindowType::Popup && mResizable) { + c.mMinSize.width = + std::max(int32_t(::GetSystemMetrics(SM_CXMINTRACK)), c.mMinSize.width); + c.mMinSize.height = + std::max(int32_t(::GetSystemMetrics(SM_CYMINTRACK)), c.mMinSize.height); + } + + if (mMaxTextureSize > 0) { + // We can't make ThebesLayers bigger than this anyway.. no point it letting + // a window grow bigger as we won't be able to draw content there in + // general. + c.mMaxSize.width = std::min(c.mMaxSize.width, mMaxTextureSize); + c.mMaxSize.height = std::min(c.mMaxSize.height, mMaxTextureSize); + } + + mSizeConstraintsScale = GetDefaultScale().scale; + + nsBaseWidget::SetSizeConstraints(c); +} + +const SizeConstraints nsWindow::GetSizeConstraints() { + double scale = GetDefaultScale().scale; + if (mSizeConstraintsScale == scale || mSizeConstraintsScale == 0.0) { + return mSizeConstraints; + } + scale /= mSizeConstraintsScale; + SizeConstraints c = mSizeConstraints; + if (c.mMinSize.width != NS_MAXSIZE) { + c.mMinSize.width = NSToIntRound(c.mMinSize.width * scale); + } + if (c.mMinSize.height != NS_MAXSIZE) { + c.mMinSize.height = NSToIntRound(c.mMinSize.height * scale); + } + if (c.mMaxSize.width != NS_MAXSIZE) { + c.mMaxSize.width = NSToIntRound(c.mMaxSize.width * scale); + } + if (c.mMaxSize.height != NS_MAXSIZE) { + c.mMaxSize.height = NSToIntRound(c.mMaxSize.height * scale); + } + return c; +} + +// Move this component +void nsWindow::Move(double aX, double aY) { + if (mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog) { + SetSizeMode(nsSizeMode_Normal); + } + + // for top-level windows only, convert coordinates from desktop pixels + // (the "parent" coordinate space) to the window's device pixel space + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + int32_t x = NSToIntRound(aX * scale); + int32_t y = NSToIntRound(aY * scale); + + // Check to see if window needs to be moved first + // to avoid a costly call to SetWindowPos. This check + // can not be moved to the calling code in nsView, because + // some platforms do not position child windows correctly + + // Only perform this check for non-popup windows, since the positioning can + // in fact change even when the x/y do not. We always need to perform the + // check. See bug #97805 for details. + if (mWindowType != WindowType::Popup && mBounds.IsEqualXY(x, y)) { + // Nothing to do, since it is already positioned correctly. + return; + } + + mBounds.MoveTo(x, y); + + if (mWnd) { +#ifdef DEBUG + // complain if a window is moved offscreen (legal, but potentially + // worrisome) + if (mIsTopWidgetWindow) { // only a problem for top-level windows + // Make sure this window is actually on the screen before we move it + // XXX: Needs multiple monitor support + HDC dc = ::GetDC(mWnd); + if (dc) { + if (::GetDeviceCaps(dc, TECHNOLOGY) == DT_RASDISPLAY) { + RECT workArea; + ::SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0); + // no annoying assertions. just mention the issue. + if (x < 0 || x >= workArea.right || y < 0 || y >= workArea.bottom) { + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("window moved to offscreen position\n")); + } + } + ::ReleaseDC(mWnd, dc); + } + } +#endif + + // Normally, when the skeleton UI is disabled, we resize+move the window + // before showing it in order to ensure that it restores to the correct + // position when the user un-maximizes it. However, when we are using the + // skeleton UI, this results in the skeleton UI window being moved around + // undesirably before being locked back into the maximized position. To + // avoid this, we simply set the placement to restore to via + // SetWindowPlacement. It's a little bit more of a dance, though, since we + // need to convert the workspace coords that SetWindowPlacement uses to the + // screen space coordinates we normally use with SetWindowPos. + if (mIsShowingPreXULSkeletonUI && WasPreXULSkeletonUIMaximized()) { + WINDOWPLACEMENT pl = {sizeof(WINDOWPLACEMENT)}; + VERIFY(::GetWindowPlacement(mWnd, &pl)); + + HMONITOR monitor = ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTONULL); + if (NS_WARN_IF(!monitor)) { + return; + } + MONITORINFO mi = {sizeof(MONITORINFO)}; + VERIFY(::GetMonitorInfo(monitor, &mi)); + + int32_t deltaX = + x + mi.rcWork.left - mi.rcMonitor.left - pl.rcNormalPosition.left; + int32_t deltaY = + y + mi.rcWork.top - mi.rcMonitor.top - pl.rcNormalPosition.top; + pl.rcNormalPosition.left += deltaX; + pl.rcNormalPosition.right += deltaX; + pl.rcNormalPosition.top += deltaY; + pl.rcNormalPosition.bottom += deltaY; + VERIFY(::SetWindowPlacement(mWnd, &pl)); + } else { + UINT flags = SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE; + double oldScale = mDefaultScale; + mResizeState = IN_SIZEMOVE; + VERIFY(::SetWindowPos(mWnd, nullptr, x, y, 0, 0, flags)); + mResizeState = NOT_RESIZING; + if (WinUtils::LogToPhysFactor(mWnd) != oldScale) { + ChangedDPI(); + } + } + + ResizeDirectManipulationViewport(); + } +} + +// Resize this component +void nsWindow::Resize(double aWidth, double aHeight, bool aRepaint) { + // for top-level windows only, convert coordinates from desktop pixels + // (the "parent" coordinate space) to the window's device pixel space + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + int32_t width = NSToIntRound(aWidth * scale); + int32_t height = NSToIntRound(aHeight * scale); + + NS_ASSERTION((width >= 0), "Negative width passed to nsWindow::Resize"); + NS_ASSERTION((height >= 0), "Negative height passed to nsWindow::Resize"); + if (width < 0 || height < 0) { + gfxCriticalNoteOnce << "Negative passed to Resize(" << width << ", " + << height << ") repaint: " << aRepaint; + } + + ConstrainSize(&width, &height); + + // Avoid unnecessary resizing calls + if (mBounds.IsEqualSize(width, height)) { + if (aRepaint) { + Invalidate(); + } + return; + } + + // Set cached value for lightweight and printing + bool wasLocking = mAspectRatio != 0.0; + mBounds.SizeTo(width, height); + if (wasLocking) { + LockAspectRatio(true); // This causes us to refresh the mAspectRatio value + } + + if (mWnd) { + // Refer to the comment above a similar check in nsWindow::Move + if (mIsShowingPreXULSkeletonUI && WasPreXULSkeletonUIMaximized()) { + WINDOWPLACEMENT pl = {sizeof(WINDOWPLACEMENT)}; + VERIFY(::GetWindowPlacement(mWnd, &pl)); + pl.rcNormalPosition.right = pl.rcNormalPosition.left + width; + pl.rcNormalPosition.bottom = pl.rcNormalPosition.top + GetHeight(height); + mResizeState = RESIZING; + VERIFY(::SetWindowPlacement(mWnd, &pl)); + mResizeState = NOT_RESIZING; + } else { + UINT flags = SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE; + + if (!aRepaint) { + flags |= SWP_NOREDRAW; + } + + double oldScale = mDefaultScale; + mResizeState = RESIZING; + VERIFY( + ::SetWindowPos(mWnd, nullptr, 0, 0, width, GetHeight(height), flags)); + + mResizeState = NOT_RESIZING; + if (WinUtils::LogToPhysFactor(mWnd) != oldScale) { + ChangedDPI(); + } + } + + ResizeDirectManipulationViewport(); + } + + if (aRepaint) Invalidate(); +} + +// Resize this component +void nsWindow::Resize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint) { + // for top-level windows only, convert coordinates from desktop pixels + // (the "parent" coordinate space) to the window's device pixel space + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + int32_t x = NSToIntRound(aX * scale); + int32_t y = NSToIntRound(aY * scale); + int32_t width = NSToIntRound(aWidth * scale); + int32_t height = NSToIntRound(aHeight * scale); + + NS_ASSERTION((width >= 0), "Negative width passed to nsWindow::Resize"); + NS_ASSERTION((height >= 0), "Negative height passed to nsWindow::Resize"); + if (width < 0 || height < 0) { + gfxCriticalNoteOnce << "Negative passed to Resize(" << x << " ," << y + << ", " << width << ", " << height + << ") repaint: " << aRepaint; + } + + ConstrainSize(&width, &height); + + // Avoid unnecessary resizing calls + if (mBounds.IsEqualRect(x, y, width, height)) { + if (aRepaint) { + Invalidate(); + } + return; + } + + // Set cached value for lightweight and printing + mBounds.SetRect(x, y, width, height); + + if (mWnd) { + // Refer to the comment above a similar check in nsWindow::Move + if (mIsShowingPreXULSkeletonUI && WasPreXULSkeletonUIMaximized()) { + WINDOWPLACEMENT pl = {sizeof(WINDOWPLACEMENT)}; + VERIFY(::GetWindowPlacement(mWnd, &pl)); + + HMONITOR monitor = ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTONULL); + if (NS_WARN_IF(!monitor)) { + return; + } + MONITORINFO mi = {sizeof(MONITORINFO)}; + VERIFY(::GetMonitorInfo(monitor, &mi)); + + int32_t deltaX = + x + mi.rcWork.left - mi.rcMonitor.left - pl.rcNormalPosition.left; + int32_t deltaY = + y + mi.rcWork.top - mi.rcMonitor.top - pl.rcNormalPosition.top; + pl.rcNormalPosition.left += deltaX; + pl.rcNormalPosition.right = pl.rcNormalPosition.left + width; + pl.rcNormalPosition.top += deltaY; + pl.rcNormalPosition.bottom = pl.rcNormalPosition.top + GetHeight(height); + VERIFY(::SetWindowPlacement(mWnd, &pl)); + } else { + UINT flags = SWP_NOZORDER | SWP_NOACTIVATE; + if (!aRepaint) { + flags |= SWP_NOREDRAW; + } + + double oldScale = mDefaultScale; + mResizeState = RESIZING; + VERIFY( + ::SetWindowPos(mWnd, nullptr, x, y, width, GetHeight(height), flags)); + mResizeState = NOT_RESIZING; + if (WinUtils::LogToPhysFactor(mWnd) != oldScale) { + ChangedDPI(); + } + + if (mTransitionWnd) { + // If we have a fullscreen transition window, we need to make + // it topmost again, otherwise the taskbar may be raised by + // the system unexpectedly when we leave fullscreen state. + ::SetWindowPos(mTransitionWnd, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } + } + + ResizeDirectManipulationViewport(); + } + + if (aRepaint) Invalidate(); +} + +mozilla::Maybe<bool> nsWindow::IsResizingNativeWidget() { + if (mResizeState == RESIZING) { + return Some(true); + } + return Some(false); +} + +/************************************************************** + * + * SECTION: Window Z-order and state. + * + * nsIWidget::PlaceBehind, nsIWidget::SetSizeMode, + * nsIWidget::ConstrainPosition + * + * Z-order, positioning, restore, minimize, and maximize. + * + **************************************************************/ + +// Position the window behind the given window +void nsWindow::PlaceBehind(nsTopLevelWidgetZPlacement aPlacement, + nsIWidget* aWidget, bool aActivate) { + HWND behind = HWND_TOP; + if (aPlacement == eZPlacementBottom) + behind = HWND_BOTTOM; + else if (aPlacement == eZPlacementBelow && aWidget) + behind = (HWND)aWidget->GetNativeData(NS_NATIVE_WINDOW); + UINT flags = SWP_NOMOVE | SWP_NOREPOSITION | SWP_NOSIZE; + if (!aActivate) flags |= SWP_NOACTIVATE; + + if (!CanTakeFocus() && behind == HWND_TOP) { + // Can't place the window to top so place it behind the foreground window + // (as long as it is not topmost) + HWND wndAfter = ::GetForegroundWindow(); + if (!wndAfter) + behind = HWND_BOTTOM; + else if (!(GetWindowLongPtrW(wndAfter, GWL_EXSTYLE) & WS_EX_TOPMOST)) + behind = wndAfter; + flags |= SWP_NOACTIVATE; + } + + ::SetWindowPos(mWnd, behind, 0, 0, 0, 0, flags); +} + +static UINT GetCurrentShowCmd(HWND aWnd) { + WINDOWPLACEMENT pl; + pl.length = sizeof(pl); + ::GetWindowPlacement(aWnd, &pl); + return pl.showCmd; +} + +// Maximize, minimize or restore the window. +void nsWindow::SetSizeMode(nsSizeMode aMode) { + // If we are still displaying a maximized pre-XUL skeleton UI, ignore the + // noise of sizemode changes. Once we have "shown" the window for the first + // time (called nsWindow::Show(true), even though the window is already + // technically displayed), we will again accept sizemode changes. + if (mIsShowingPreXULSkeletonUI && WasPreXULSkeletonUIMaximized()) { + return; + } + + mFrameState->EnsureSizeMode(aMode); +} + +nsSizeMode nsWindow::SizeMode() { return mFrameState->GetSizeMode(); } + +void DoGetWorkspaceID(HWND aWnd, nsAString* aWorkspaceID) { + RefPtr<IVirtualDesktopManager> desktopManager = gVirtualDesktopManager; + if (!desktopManager || !aWnd) { + return; + } + + GUID desktop; + HRESULT hr = desktopManager->GetWindowDesktopId(aWnd, &desktop); + if (FAILED(hr)) { + return; + } + + RPC_WSTR workspaceIDStr = nullptr; + if (UuidToStringW(&desktop, &workspaceIDStr) == RPC_S_OK) { + aWorkspaceID->Assign((wchar_t*)workspaceIDStr); + RpcStringFreeW(&workspaceIDStr); + } +} + +void nsWindow::GetWorkspaceID(nsAString& workspaceID) { + // If we have a value cached, use that, but also make sure it is + // scheduled to be updated. If we don't yet have a value, get + // one synchronously. + auto desktop = mDesktopId.Lock(); + if (desktop->mID.IsEmpty()) { + DoGetWorkspaceID(mWnd, &desktop->mID); + desktop->mUpdateIsQueued = false; + } else { + AsyncUpdateWorkspaceID(*desktop); + } + + workspaceID = desktop->mID; +} + +void nsWindow::AsyncUpdateWorkspaceID(Desktop& aDesktop) { + struct UpdateWorkspaceIdTask : public Task { + explicit UpdateWorkspaceIdTask(nsWindow* aSelf) + : Task(Kind::OffMainThreadOnly, EventQueuePriority::Normal), + mSelf(aSelf) {} + + TaskResult Run() override { + auto desktop = mSelf->mDesktopId.Lock(); + if (desktop->mUpdateIsQueued) { + DoGetWorkspaceID(mSelf->mWnd, &desktop->mID); + desktop->mUpdateIsQueued = false; + } + return TaskResult::Complete; + } + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + bool GetName(nsACString& aName) override { + aName.AssignLiteral("UpdateWorkspaceIdTask"); + return true; + } +#endif + + RefPtr<nsWindow> mSelf; + }; + + if (aDesktop.mUpdateIsQueued) { + return; + } + + aDesktop.mUpdateIsQueued = true; + TaskController::Get()->AddTask(MakeAndAddRef<UpdateWorkspaceIdTask>(this)); +} + +void nsWindow::MoveToWorkspace(const nsAString& workspaceID) { + RefPtr<IVirtualDesktopManager> desktopManager = gVirtualDesktopManager; + if (!desktopManager) { + return; + } + + GUID desktop; + const nsString flat = PromiseFlatString(workspaceID); + RPC_WSTR workspaceIDStr = reinterpret_cast<RPC_WSTR>((wchar_t*)flat.get()); + if (UuidFromStringW(workspaceIDStr, &desktop) == RPC_S_OK) { + if (SUCCEEDED(desktopManager->MoveWindowToDesktop(mWnd, desktop))) { + auto desktop = mDesktopId.Lock(); + desktop->mID = workspaceID; + } + } +} + +void nsWindow::SuppressAnimation(bool aSuppress) { + DWORD dwAttribute = aSuppress ? TRUE : FALSE; + DwmSetWindowAttribute(mWnd, DWMWA_TRANSITIONS_FORCEDISABLED, &dwAttribute, + sizeof dwAttribute); +} + +// Constrain a potential move to fit onscreen +// Position (aX, aY) is specified in Windows screen (logical) pixels, +// except when using per-monitor DPI, in which case it's device pixels. +void nsWindow::ConstrainPosition(DesktopIntPoint& aPoint) { + if (!mIsTopWidgetWindow) // only a problem for top-level windows + return; + + double dpiScale = GetDesktopToDeviceScale().scale; + + // We need to use the window size in the kind of pixels used for window- + // manipulation APIs. + int32_t logWidth = + std::max<int32_t>(NSToIntRound(mBounds.Width() / dpiScale), 1); + int32_t logHeight = + std::max<int32_t>(NSToIntRound(mBounds.Height() / dpiScale), 1); + + /* get our playing field. use the current screen, or failing that + for any reason, use device caps for the default screen. */ + RECT screenRect; + + nsCOMPtr<nsIScreenManager> screenmgr = + do_GetService(sScreenManagerContractID); + if (!screenmgr) { + return; + } + nsCOMPtr<nsIScreen> screen; + int32_t left, top, width, height; + + screenmgr->ScreenForRect(aPoint.x, aPoint.y, logWidth, logHeight, + getter_AddRefs(screen)); + if (mFrameState->GetSizeMode() != nsSizeMode_Fullscreen) { + // For normalized windows, use the desktop work area. + nsresult rv = screen->GetAvailRectDisplayPix(&left, &top, &width, &height); + if (NS_FAILED(rv)) { + return; + } + } else { + // For full screen windows, use the desktop. + nsresult rv = screen->GetRectDisplayPix(&left, &top, &width, &height); + if (NS_FAILED(rv)) { + return; + } + } + screenRect.left = left; + screenRect.right = left + width; + screenRect.top = top; + screenRect.bottom = top + height; + + if (aPoint.x < screenRect.left) + aPoint.x = screenRect.left; + else if (aPoint.x >= screenRect.right - logWidth) + aPoint.x = screenRect.right - logWidth; + + if (aPoint.y < screenRect.top) + aPoint.y = screenRect.top; + else if (aPoint.y >= screenRect.bottom - logHeight) + aPoint.y = screenRect.bottom - logHeight; +} + +/************************************************************** + * + * SECTION: nsIWidget::Enable, nsIWidget::IsEnabled + * + * Enabling and disabling the widget. + * + **************************************************************/ + +// Enable/disable this component +void nsWindow::Enable(bool bState) { + if (mWnd) { + ::EnableWindow(mWnd, bState); + } +} + +// Return the current enable state +bool nsWindow::IsEnabled() const { + return !mWnd || (::IsWindowEnabled(mWnd) && + ::IsWindowEnabled(::GetAncestor(mWnd, GA_ROOT))); +} + +/************************************************************** + * + * SECTION: nsIWidget::SetFocus + * + * Give the focus to this widget. + * + **************************************************************/ + +void nsWindow::SetFocus(Raise aRaise, mozilla::dom::CallerType aCallerType) { + if (mWnd) { +#ifdef WINSTATE_DEBUG_OUTPUT + if (mWnd == WinUtils::GetTopLevelHWND(mWnd)) { + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("*** SetFocus: [ top] raise=%d\n", aRaise == Raise::Yes)); + } else { + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("*** SetFocus: [child] raise=%d\n", aRaise == Raise::Yes)); + } +#endif + // Uniconify, if necessary + HWND toplevelWnd = WinUtils::GetTopLevelHWND(mWnd); + if (aRaise == Raise::Yes && ::IsIconic(toplevelWnd)) { + ::ShowWindow(toplevelWnd, SW_RESTORE); + } + ::SetFocus(mWnd); + } +} + +/************************************************************** + * + * SECTION: Bounds + * + * GetBounds, GetClientBounds, GetScreenBounds, + * GetRestoredBounds, GetClientOffset, SetNonClientMargins + * + * Bound calculations. + * + **************************************************************/ + +// Return the window's full dimensions in screen coordinates. +// If the window has a parent, converts the origin to an offset +// of the parent's screen origin. +LayoutDeviceIntRect nsWindow::GetBounds() { + if (!mWnd) { + return mBounds; + } + + RECT r; + VERIFY(::GetWindowRect(mWnd, &r)); + + LayoutDeviceIntRect rect; + + // assign size + rect.SizeTo(r.right - r.left, r.bottom - r.top); + + // popup window bounds' are in screen coordinates, not relative to parent + // window + if (mWindowType == WindowType::Popup) { + rect.MoveTo(r.left, r.top); + return rect; + } + + // chrome on parent: + // ___ 5,5 (chrome start) + // | ____ 10,10 (client start) + // | | ____ 20,20 (child start) + // | | | + // 20,20 - 5,5 = 15,15 (??) + // minus GetClientOffset: + // 15,15 - 5,5 = 10,10 + // + // no chrome on parent: + // ______ 10,10 (win start) + // | ____ 20,20 (child start) + // | | + // 20,20 - 10,10 = 10,10 + // + // walking the chain: + // ___ 5,5 (chrome start) + // | ___ 10,10 (client start) + // | | ___ 20,20 (child start) + // | | | __ 30,30 (child start) + // | | | | + // 30,30 - 20,20 = 10,10 (offset from second child to first) + // 20,20 - 5,5 = 15,15 + 10,10 = 25,25 (??) + // minus GetClientOffset: + // 25,25 - 5,5 = 20,20 (offset from second child to parent client) + + // convert coordinates if parent exists + HWND parent = ::GetParent(mWnd); + if (parent) { + RECT pr; + VERIFY(::GetWindowRect(parent, &pr)); + r.left -= pr.left; + r.top -= pr.top; + // adjust for chrome + nsWindow* pWidget = static_cast<nsWindow*>(GetParent()); + if (pWidget && pWidget->IsTopLevelWidget()) { + LayoutDeviceIntPoint clientOffset = pWidget->GetClientOffset(); + r.left -= clientOffset.x; + r.top -= clientOffset.y; + } + } + rect.MoveTo(r.left, r.top); + if (mCompositorSession && + !wr::WindowSizeSanityCheck(rect.width, rect.height)) { + gfxCriticalNoteOnce << "Invalid size" << rect << " size mode " + << mFrameState->GetSizeMode(); + } + + return rect; +} + +// Get this component dimension +LayoutDeviceIntRect nsWindow::GetClientBounds() { + if (!mWnd) { + return LayoutDeviceIntRect(0, 0, 0, 0); + } + + RECT r; + if (!::GetClientRect(mWnd, &r)) { + MOZ_ASSERT_UNREACHABLE("unexpected to be called"); + gfxCriticalNoteOnce << "GetClientRect failed " << ::GetLastError(); + return mBounds; + } + + LayoutDeviceIntRect bounds = GetBounds(); + LayoutDeviceIntRect rect; + rect.MoveTo(bounds.TopLeft() + GetClientOffset()); + rect.SizeTo(r.right - r.left, r.bottom - r.top); + return rect; +} + +// Like GetBounds, but don't offset by the parent +LayoutDeviceIntRect nsWindow::GetScreenBounds() { + if (!mWnd) { + return mBounds; + } + + RECT r; + VERIFY(::GetWindowRect(mWnd, &r)); + + return LayoutDeviceIntRect(r.left, r.top, r.right - r.left, r.bottom - r.top); +} + +nsresult nsWindow::GetRestoredBounds(LayoutDeviceIntRect& aRect) { + if (SizeMode() == nsSizeMode_Normal) { + aRect = GetScreenBounds(); + return NS_OK; + } + if (!mWnd) { + return NS_ERROR_FAILURE; + } + + WINDOWPLACEMENT pl = {sizeof(WINDOWPLACEMENT)}; + VERIFY(::GetWindowPlacement(mWnd, &pl)); + const RECT& r = pl.rcNormalPosition; + + HMONITOR monitor = ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTONULL); + if (!monitor) { + return NS_ERROR_FAILURE; + } + MONITORINFO mi = {sizeof(MONITORINFO)}; + VERIFY(::GetMonitorInfo(monitor, &mi)); + + aRect.SetRect(r.left, r.top, r.right - r.left, r.bottom - r.top); + aRect.MoveBy(mi.rcWork.left - mi.rcMonitor.left, + mi.rcWork.top - mi.rcMonitor.top); + return NS_OK; +} + +// Return the x,y offset of the client area from the origin of the window. If +// the window is borderless returns (0,0). +LayoutDeviceIntPoint nsWindow::GetClientOffset() { + if (!mWnd) { + return LayoutDeviceIntPoint(0, 0); + } + + RECT r1; + GetWindowRect(mWnd, &r1); + LayoutDeviceIntPoint pt = WidgetToScreenOffset(); + return LayoutDeviceIntPoint(pt.x - LayoutDeviceIntCoord(r1.left), + pt.y - LayoutDeviceIntCoord(r1.top)); +} + +void nsWindow::ResetLayout() { + // This will trigger a frame changed event, triggering + // nc calc size and a sizemode gecko event. + SetWindowPos(mWnd, 0, 0, 0, 0, 0, + SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE | + SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOZORDER); + + // If hidden, just send the frame changed event for now. + if (!mIsVisible) { + return; + } + + // Send a gecko size event to trigger reflow. + RECT clientRc = {0}; + GetClientRect(mWnd, &clientRc); + OnResize(WinUtils::ToIntRect(clientRc).Size()); + + // Invalidate and update + Invalidate(); +} + +// Internally track the caption status via a window property. Required +// due to our internal handling of WM_NCACTIVATE when custom client +// margins are set. +static const wchar_t kManageWindowInfoProperty[] = L"ManageWindowInfoProperty"; +typedef BOOL(WINAPI* GetWindowInfoPtr)(HWND hwnd, PWINDOWINFO pwi); +static WindowsDllInterceptor::FuncHookType<GetWindowInfoPtr> + sGetWindowInfoPtrStub; + +BOOL WINAPI GetWindowInfoHook(HWND hWnd, PWINDOWINFO pwi) { + if (!sGetWindowInfoPtrStub) { + NS_ASSERTION(FALSE, "Something is horribly wrong in GetWindowInfoHook!"); + return FALSE; + } + int windowStatus = + reinterpret_cast<LONG_PTR>(GetPropW(hWnd, kManageWindowInfoProperty)); + // No property set, return the default data. + if (!windowStatus) return sGetWindowInfoPtrStub(hWnd, pwi); + // Call GetWindowInfo and update dwWindowStatus with our + // internally tracked value. + BOOL result = sGetWindowInfoPtrStub(hWnd, pwi); + if (result && pwi) + pwi->dwWindowStatus = (windowStatus == 1 ? 0 : WS_ACTIVECAPTION); + return result; +} + +void nsWindow::UpdateGetWindowInfoCaptionStatus(bool aActiveCaption) { + if (!mWnd) return; + + sUser32Intercept.Init("user32.dll"); + sGetWindowInfoPtrStub.Set(sUser32Intercept, "GetWindowInfo", + &GetWindowInfoHook); + if (!sGetWindowInfoPtrStub) { + return; + } + + // Update our internally tracked caption status + SetPropW(mWnd, kManageWindowInfoProperty, + reinterpret_cast<HANDLE>(static_cast<INT_PTR>(aActiveCaption) + 1)); +} + +#define DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 19 +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 + +void nsWindow::UpdateDarkModeToolbar() { + PreferenceSheet::EnsureInitialized(); + BOOL dark = PreferenceSheet::ColorSchemeForChrome() == ColorScheme::Dark; + DwmSetWindowAttribute(mWnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, &dark, + sizeof dark); + DwmSetWindowAttribute(mWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, + sizeof dark); +} + +LayoutDeviceIntMargin nsWindow::NormalWindowNonClientOffset() const { + LayoutDeviceIntMargin nonClientOffset; + + // We're dealing with a "normal" window (not maximized, minimized, or + // fullscreen), so process `mNonClientMargins` and set `mNonClientOffset` + // accordingly. + // + // Setting `mNonClientOffset` to 0 has the effect of leaving the default + // frame intact. Setting it to a value greater than 0 reduces the frame + // size by that amount. + + if (mNonClientMargins.top > 0) { + nonClientOffset.top = std::min(mCaptionHeight, mNonClientMargins.top); + } else if (mNonClientMargins.top == 0) { + nonClientOffset.top = mCaptionHeight; + } else { + nonClientOffset.top = 0; + } + + if (mNonClientMargins.bottom > 0) { + nonClientOffset.bottom = + std::min(mVertResizeMargin, mNonClientMargins.bottom); + } else if (mNonClientMargins.bottom == 0) { + nonClientOffset.bottom = mVertResizeMargin; + } else { + nonClientOffset.bottom = 0; + } + + if (mNonClientMargins.left > 0) { + nonClientOffset.left = std::min(mHorResizeMargin, mNonClientMargins.left); + } else if (mNonClientMargins.left == 0) { + nonClientOffset.left = mHorResizeMargin; + } else { + nonClientOffset.left = 0; + } + + if (mNonClientMargins.right > 0) { + nonClientOffset.right = std::min(mHorResizeMargin, mNonClientMargins.right); + } else if (mNonClientMargins.right == 0) { + nonClientOffset.right = mHorResizeMargin; + } else { + nonClientOffset.right = 0; + } + return nonClientOffset; +} + +/** + * Called when the window layout changes: full screen mode transitions, + * theme changes, and composition changes. Calculates the new non-client + * margins and fires off a frame changed event, which triggers an nc calc + * size windows event, kicking the changes in. + * + * The offsets calculated here are based on the value of `mNonClientMargins` + * which is specified in the "chromemargins" attribute of the window. For + * each margin, the value specified has the following meaning: + * -1 - leave the default frame in place + * 0 - remove the frame + * >0 - frame size equals min(0, (default frame size - margin value)) + * + * This function calculates and populates `mNonClientOffset`. + * In our processing of `WM_NCCALCSIZE`, the frame size will be calculated + * as (default frame size - offset). For example, if the left frame should + * be 1 pixel narrower than the default frame size, `mNonClientOffset.left` + * will equal 1. + * + * For maximized, fullscreen, and minimized windows, the values stored in + * `mNonClientMargins` are ignored, and special processing takes place. + * + * For non-glass windows, we only allow frames to be their default size + * or removed entirely. + */ +bool nsWindow::UpdateNonClientMargins(bool aReflowWindow) { + if (!mCustomNonClient) { + return false; + } + + const nsSizeMode sizeMode = mFrameState->GetSizeMode(); + + bool hasCaption = + bool(mBorderStyle & (BorderStyle::All | BorderStyle::Title | + BorderStyle::Menu | BorderStyle::Default)); + + float dpi = GetDPI(); + + // mCaptionHeight is the default size of the NC area at + // the top of the window. If the window has a caption, + // the size is calculated as the sum of: + // SM_CYFRAME - The thickness of the sizing border + // around a resizable window + // SM_CXPADDEDBORDER - The amount of border padding + // for captioned windows + // SM_CYCAPTION - The height of the caption area + // + // If the window does not have a caption, mCaptionHeight will be equal to + // `WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi)` + mCaptionHeight = + WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi) + + (hasCaption ? WinUtils::GetSystemMetricsForDpi(SM_CYCAPTION, dpi) + + WinUtils::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) + : 0); + if (!mUseResizeMarginOverrides) { + // mHorResizeMargin is the size of the default NC areas on the + // left and right sides of our window. It is calculated as + // the sum of: + // SM_CXFRAME - The thickness of the sizing border + // SM_CXPADDEDBORDER - The amount of border padding + // for captioned windows + // + // If the window does not have a caption, mHorResizeMargin will be equal to + // `WinUtils::GetSystemMetricsForDpi(SM_CXFRAME, dpi)` + mHorResizeMargin = + WinUtils::GetSystemMetricsForDpi(SM_CXFRAME, dpi) + + (hasCaption ? WinUtils::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) + : 0); + + // mVertResizeMargin is the size of the default NC area at the + // bottom of the window. It is calculated as the sum of: + // SM_CYFRAME - The thickness of the sizing border + // SM_CXPADDEDBORDER - The amount of border padding + // for captioned windows. + // + // If the window does not have a caption, mVertResizeMargin will be equal to + // `WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi)` + mVertResizeMargin = + WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi) + + (hasCaption ? WinUtils::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) + : 0); + } + + if (sizeMode == nsSizeMode_Minimized) { + // Use default frame size for minimized windows + mNonClientOffset.top = 0; + mNonClientOffset.left = 0; + mNonClientOffset.right = 0; + mNonClientOffset.bottom = 0; + } else if (sizeMode == nsSizeMode_Fullscreen) { + // Remove the default frame from the top of our fullscreen window. This + // makes the whole caption part of our client area, allowing us to draw + // in the whole caption area. Additionally remove the default frame from + // the left, right, and bottom. + mNonClientOffset.top = mCaptionHeight; + mNonClientOffset.bottom = mVertResizeMargin; + mNonClientOffset.left = mHorResizeMargin; + mNonClientOffset.right = mHorResizeMargin; + } else if (sizeMode == nsSizeMode_Maximized) { + // We make the entire frame part of the client area. We leave the default + // frame sizes for left, right and bottom since Windows will automagically + // position the edges "offscreen" for maximized windows. + int verticalResize = + WinUtils::GetSystemMetricsForDpi(SM_CYFRAME, dpi) + + (hasCaption ? WinUtils::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) + : 0); + + mNonClientOffset.top = mCaptionHeight - verticalResize; + mNonClientOffset.bottom = 0; + mNonClientOffset.left = 0; + mNonClientOffset.right = 0; + + mozilla::Maybe<UINT> maybeEdge = GetHiddenTaskbarEdge(); + if (maybeEdge) { + auto edge = maybeEdge.value(); + if (ABE_LEFT == edge) { + mNonClientOffset.left -= kHiddenTaskbarSize; + } else if (ABE_RIGHT == edge) { + mNonClientOffset.right -= kHiddenTaskbarSize; + } else if (ABE_BOTTOM == edge || ABE_TOP == edge) { + mNonClientOffset.bottom -= kHiddenTaskbarSize; + } + + // When we are drawing the non-client region, we need + // to clear the portion of the NC region that is exposed by the + // hidden taskbar. As above, we clear the bottom of the NC region + // when the taskbar is at the top of the screen. + UINT clearEdge = (edge == ABE_TOP) ? ABE_BOTTOM : edge; + mClearNCEdge = Some(clearEdge); + } + } else { + mNonClientOffset = NormalWindowNonClientOffset(); + } + + if (aReflowWindow) { + // Force a reflow of content based on the new client + // dimensions. + ResetLayout(); + } + + return true; +} + +nsresult nsWindow::SetNonClientMargins(const LayoutDeviceIntMargin& margins) { + if (!mIsTopWidgetWindow || mBorderStyle == BorderStyle::None) + return NS_ERROR_INVALID_ARG; + + if (mHideChrome) { + mFutureMarginsOnceChromeShows = margins; + mFutureMarginsToUse = true; + return NS_OK; + } + mFutureMarginsToUse = false; + + // Request for a reset + if (margins.top == -1 && margins.left == -1 && margins.right == -1 && + margins.bottom == -1) { + mCustomNonClient = false; + mNonClientMargins = margins; + // Force a reflow of content based on the new client + // dimensions. + ResetLayout(); + + int windowStatus = + reinterpret_cast<LONG_PTR>(GetPropW(mWnd, kManageWindowInfoProperty)); + if (windowStatus) { + ::SendMessageW(mWnd, WM_NCACTIVATE, 1 != windowStatus, 0); + } + + return NS_OK; + } + + if (margins.top < -1 || margins.bottom < -1 || margins.left < -1 || + margins.right < -1) + return NS_ERROR_INVALID_ARG; + + mNonClientMargins = margins; + mCustomNonClient = true; + if (!UpdateNonClientMargins()) { + NS_WARNING("UpdateNonClientMargins failed!"); + return NS_OK; + } + + return NS_OK; +} + +void nsWindow::SetResizeMargin(mozilla::LayoutDeviceIntCoord aResizeMargin) { + mUseResizeMarginOverrides = true; + mHorResizeMargin = aResizeMargin; + mVertResizeMargin = aResizeMargin; + UpdateNonClientMargins(); +} + +void nsWindow::InvalidateNonClientRegion() { + // +-+-----------------------+-+ + // | | app non-client chrome | | + // | +-----------------------+ | + // | | app client chrome | | } + // | +-----------------------+ | } + // | | app content | | } area we don't want to invalidate + // | +-----------------------+ | } + // | | app client chrome | | } + // | +-----------------------+ | + // +---------------------------+ < + // ^ ^ windows non-client chrome + // client area = app * + RECT rect; + GetWindowRect(mWnd, &rect); + MapWindowPoints(nullptr, mWnd, (LPPOINT)&rect, 2); + HRGN winRgn = CreateRectRgnIndirect(&rect); + + // Subtract app client chrome and app content leaving + // windows non-client chrome and app non-client chrome + // in winRgn. + GetWindowRect(mWnd, &rect); + rect.top += mCaptionHeight; + rect.right -= mHorResizeMargin; + rect.bottom -= mVertResizeMargin; + rect.left += mHorResizeMargin; + MapWindowPoints(nullptr, mWnd, (LPPOINT)&rect, 2); + HRGN clientRgn = CreateRectRgnIndirect(&rect); + CombineRgn(winRgn, winRgn, clientRgn, RGN_DIFF); + DeleteObject(clientRgn); + + // triggers ncpaint and paint events for the two areas + RedrawWindow(mWnd, nullptr, winRgn, RDW_FRAME | RDW_INVALIDATE); + DeleteObject(winRgn); +} + +/************************************************************** + * + * SECTION: nsIWidget::SetBackgroundColor + * + * Sets the window background paint color. + * + **************************************************************/ + +void nsWindow::SetBackgroundColor(const nscolor& aColor) { + if (mBrush) ::DeleteObject(mBrush); + + mBrush = ::CreateSolidBrush(NSRGB_2_COLOREF(aColor)); + if (mWnd != nullptr) { + ::SetClassLongPtrW(mWnd, GCLP_HBRBACKGROUND, (LONG_PTR)mBrush); + } +} + +/************************************************************** + * + * SECTION: nsIWidget::SetCursor + * + * SetCursor and related utilities for manging cursor state. + * + **************************************************************/ + +// Set this component cursor +static HCURSOR CursorFor(nsCursor aCursor) { + switch (aCursor) { + case eCursor_select: + return ::LoadCursor(nullptr, IDC_IBEAM); + case eCursor_wait: + return ::LoadCursor(nullptr, IDC_WAIT); + case eCursor_hyperlink: + return ::LoadCursor(nullptr, IDC_HAND); + case eCursor_standard: + case eCursor_context_menu: // XXX See bug 258960. + return ::LoadCursor(nullptr, IDC_ARROW); + + case eCursor_n_resize: + case eCursor_s_resize: + return ::LoadCursor(nullptr, IDC_SIZENS); + + case eCursor_w_resize: + case eCursor_e_resize: + return ::LoadCursor(nullptr, IDC_SIZEWE); + + case eCursor_nw_resize: + case eCursor_se_resize: + return ::LoadCursor(nullptr, IDC_SIZENWSE); + + case eCursor_ne_resize: + case eCursor_sw_resize: + return ::LoadCursor(nullptr, IDC_SIZENESW); + + case eCursor_crosshair: + return ::LoadCursor(nullptr, IDC_CROSS); + + case eCursor_move: + return ::LoadCursor(nullptr, IDC_SIZEALL); + + case eCursor_help: + return ::LoadCursor(nullptr, IDC_HELP); + + case eCursor_copy: // CSS3 + return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_COPY)); + + case eCursor_alias: + return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_ALIAS)); + + case eCursor_cell: + return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_CELL)); + case eCursor_grab: + return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_GRAB)); + + case eCursor_grabbing: + return ::LoadCursor(nsToolkit::mDllInstance, + MAKEINTRESOURCE(IDC_GRABBING)); + + case eCursor_spinning: + return ::LoadCursor(nullptr, IDC_APPSTARTING); + + case eCursor_zoom_in: + return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_ZOOMIN)); + + case eCursor_zoom_out: + return ::LoadCursor(nsToolkit::mDllInstance, + MAKEINTRESOURCE(IDC_ZOOMOUT)); + + case eCursor_not_allowed: + case eCursor_no_drop: + return ::LoadCursor(nullptr, IDC_NO); + + case eCursor_col_resize: + return ::LoadCursor(nsToolkit::mDllInstance, + MAKEINTRESOURCE(IDC_COLRESIZE)); + + case eCursor_row_resize: + return ::LoadCursor(nsToolkit::mDllInstance, + MAKEINTRESOURCE(IDC_ROWRESIZE)); + + case eCursor_vertical_text: + return ::LoadCursor(nsToolkit::mDllInstance, + MAKEINTRESOURCE(IDC_VERTICALTEXT)); + + case eCursor_all_scroll: + // XXX not 100% appropriate perhaps + return ::LoadCursor(nullptr, IDC_SIZEALL); + + case eCursor_nesw_resize: + return ::LoadCursor(nullptr, IDC_SIZENESW); + + case eCursor_nwse_resize: + return ::LoadCursor(nullptr, IDC_SIZENWSE); + + case eCursor_ns_resize: + return ::LoadCursor(nullptr, IDC_SIZENS); + + case eCursor_ew_resize: + return ::LoadCursor(nullptr, IDC_SIZEWE); + + case eCursor_none: + return ::LoadCursor(nsToolkit::mDllInstance, MAKEINTRESOURCE(IDC_NONE)); + + default: + NS_ERROR("Invalid cursor type"); + return nullptr; + } +} + +static HCURSOR CursorForImage(const nsIWidget::Cursor& aCursor, + CSSToLayoutDeviceScale aScale) { + if (!aCursor.IsCustom()) { + return nullptr; + } + + nsIntSize size = nsIWidget::CustomCursorSize(aCursor); + + // Reject cursors greater than 128 pixels in either direction, to prevent + // spoofing. + // XXX ideally we should rescale. Also, we could modify the API to + // allow trusted content to set larger cursors. + if (size.width > 128 || size.height > 128) { + return nullptr; + } + + LayoutDeviceIntSize layoutSize = + RoundedToInt(CSSIntSize(size.width, size.height) * aScale); + LayoutDeviceIntPoint hotspot = + RoundedToInt(CSSIntPoint(aCursor.mHotspotX, aCursor.mHotspotY) * aScale); + HCURSOR cursor; + nsresult rv = nsWindowGfx::CreateIcon(aCursor.mContainer, true, hotspot, + layoutSize, &cursor); + if (NS_FAILED(rv)) { + return nullptr; + } + + return cursor; +} + +void nsWindow::SetCursor(const Cursor& aCursor) { + static HCURSOR sCurrentHCursor = nullptr; + static bool sCurrentHCursorIsCustom = false; + + mCursor = aCursor; + + if (sCurrentCursor == aCursor && sCurrentHCursor && !mUpdateCursor) { + // Cursors in windows are global, so even if our mUpdateCursor flag is + // false we always need to make sure the Windows cursor is up-to-date, + // since stuff like native drag and drop / resizers code can mutate it + // outside of this method. + ::SetCursor(sCurrentHCursor); + return; + } + + mUpdateCursor = false; + + if (sCurrentHCursorIsCustom) { + ::DestroyIcon(sCurrentHCursor); + } + sCurrentHCursor = nullptr; + sCurrentHCursorIsCustom = false; + sCurrentCursor = aCursor; + + HCURSOR cursor = nullptr; + if (mCustomCursorAllowed) { + cursor = CursorForImage(aCursor, GetDefaultScale()); + } + bool custom = false; + if (cursor) { + custom = true; + } else { + cursor = CursorFor(aCursor.mDefaultCursor); + } + + if (!cursor) { + return; + } + + sCurrentHCursor = cursor; + sCurrentHCursorIsCustom = custom; + ::SetCursor(cursor); +} + +/************************************************************** + * + * SECTION: nsIWidget::Get/SetTransparencyMode + * + * Manage the transparency mode of the window containing this + * widget. Only works for popup and dialog windows when the + * Desktop Window Manager compositor is not enabled. + * + **************************************************************/ + +TransparencyMode nsWindow::GetTransparencyMode() { + return GetTopLevelWindow(true)->GetWindowTranslucencyInner(); +} + +void nsWindow::SetTransparencyMode(TransparencyMode aMode) { + nsWindow* window = GetTopLevelWindow(true); + MOZ_ASSERT(window); + + if (!window || window->DestroyCalled()) { + return; + } + + window->SetWindowTranslucencyInner(aMode); +} + +/************************************************************** + * + * SECTION: nsIWidget::UpdateWindowDraggingRegion + * + * For setting the draggable titlebar region from CSS + * with -moz-window-dragging: drag. + * + **************************************************************/ + +void nsWindow::UpdateWindowDraggingRegion( + const LayoutDeviceIntRegion& aRegion) { + if (mDraggableRegion != aRegion) { + mDraggableRegion = aRegion; + } +} + +/************************************************************** + * + * SECTION: nsIWidget::HideWindowChrome + * + * Show or hide window chrome. + * + **************************************************************/ + +void nsWindow::HideWindowChrome(bool aShouldHide) { + HWND hwnd = WinUtils::GetTopLevelHWND(mWnd, true); + if (!WinUtils::GetNSWindowPtr(hwnd)) { + NS_WARNING("Trying to hide window decorations in an embedded context"); + return; + } + + if (mHideChrome == aShouldHide) return; + + DWORD_PTR style, exStyle; + mHideChrome = aShouldHide; + if (aShouldHide) { + DWORD_PTR tempStyle = ::GetWindowLongPtrW(hwnd, GWL_STYLE); + DWORD_PTR tempExStyle = ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + + style = tempStyle & ~(WS_CAPTION | WS_THICKFRAME); + exStyle = tempExStyle & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | + WS_EX_CLIENTEDGE | WS_EX_STATICEDGE); + + mOldStyle = tempStyle; + mOldExStyle = tempExStyle; + } else { + if (!mOldStyle || !mOldExStyle) { + mOldStyle = ::GetWindowLongPtrW(hwnd, GWL_STYLE); + mOldExStyle = ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + } + + style = mOldStyle; + exStyle = mOldExStyle; + if (mFutureMarginsToUse) { + SetNonClientMargins(mFutureMarginsOnceChromeShows); + } + } + + VERIFY_WINDOW_STYLE(style); + ::SetWindowLongPtrW(hwnd, GWL_STYLE, style); + ::SetWindowLongPtrW(hwnd, GWL_EXSTYLE, exStyle); +} + +/************************************************************** + * + * SECTION: nsWindow::Invalidate + * + * Invalidate an area of the client for painting. + * + **************************************************************/ + +// Invalidate this component visible area +void nsWindow::Invalidate(bool aEraseBackground, bool aUpdateNCArea, + bool aIncludeChildren) { + if (!mWnd) { + return; + } + +#ifdef WIDGET_DEBUG_OUTPUT + debug_DumpInvalidate(stdout, this, nullptr, "noname", (int32_t)mWnd); +#endif // WIDGET_DEBUG_OUTPUT + + DWORD flags = RDW_INVALIDATE; + if (aEraseBackground) { + flags |= RDW_ERASE; + } + if (aUpdateNCArea) { + flags |= RDW_FRAME; + } + if (aIncludeChildren) { + flags |= RDW_ALLCHILDREN; + } + + VERIFY(::RedrawWindow(mWnd, nullptr, nullptr, flags)); +} + +// Invalidate this component visible area +void nsWindow::Invalidate(const LayoutDeviceIntRect& aRect) { + if (mWnd) { +#ifdef WIDGET_DEBUG_OUTPUT + debug_DumpInvalidate(stdout, this, &aRect, "noname", (int32_t)mWnd); +#endif // WIDGET_DEBUG_OUTPUT + + RECT rect; + + rect.left = aRect.X(); + rect.top = aRect.Y(); + rect.right = aRect.XMost(); + rect.bottom = aRect.YMost(); + + VERIFY(::InvalidateRect(mWnd, &rect, FALSE)); + } +} + +static LRESULT CALLBACK FullscreenTransitionWindowProc(HWND hWnd, UINT uMsg, + WPARAM wParam, + LPARAM lParam) { + switch (uMsg) { + case WM_FULLSCREEN_TRANSITION_BEFORE: + case WM_FULLSCREEN_TRANSITION_AFTER: { + DWORD duration = (DWORD)lParam; + DWORD flags = AW_BLEND; + if (uMsg == WM_FULLSCREEN_TRANSITION_AFTER) { + flags |= AW_HIDE; + } + ::AnimateWindow(hWnd, duration, flags); + // The message sender should have added ref for us. + NS_DispatchToMainThread( + already_AddRefed<nsIRunnable>((nsIRunnable*)wParam)); + break; + } + case WM_DESTROY: + ::PostQuitMessage(0); + break; + default: + return ::DefWindowProcW(hWnd, uMsg, wParam, lParam); + } + return 0; +} + +struct FullscreenTransitionInitData { + LayoutDeviceIntRect mBounds; + HANDLE mSemaphore; + HANDLE mThread; + HWND mWnd; + + FullscreenTransitionInitData() + : mSemaphore(nullptr), mThread(nullptr), mWnd(nullptr) {} + + ~FullscreenTransitionInitData() { + if (mSemaphore) { + ::CloseHandle(mSemaphore); + } + if (mThread) { + ::CloseHandle(mThread); + } + } +}; + +static DWORD WINAPI FullscreenTransitionThreadProc(LPVOID lpParam) { + // Initialize window class + static bool sInitialized = false; + if (!sInitialized) { + WNDCLASSW wc = {}; + wc.lpfnWndProc = ::FullscreenTransitionWindowProc; + wc.hInstance = nsToolkit::mDllInstance; + wc.hbrBackground = ::CreateSolidBrush(RGB(0, 0, 0)); + wc.lpszClassName = kClassNameTransition; + ::RegisterClassW(&wc); + sInitialized = true; + } + + auto data = static_cast<FullscreenTransitionInitData*>(lpParam); + HWND wnd = ::CreateWindowW(kClassNameTransition, L"", 0, 0, 0, 0, 0, nullptr, + nullptr, nsToolkit::mDllInstance, nullptr); + if (!wnd) { + ::ReleaseSemaphore(data->mSemaphore, 1, nullptr); + return 0; + } + + // Since AnimateWindow blocks the thread of the transition window, + // we need to hide the cursor for that window, otherwise the system + // would show the busy pointer to the user. + ::ShowCursor(false); + ::SetWindowLongW(wnd, GWL_STYLE, 0); + ::SetWindowLongW( + wnd, GWL_EXSTYLE, + WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE); + ::SetWindowPos(wnd, HWND_TOPMOST, data->mBounds.X(), data->mBounds.Y(), + data->mBounds.Width(), data->mBounds.Height(), 0); + data->mWnd = wnd; + ::ReleaseSemaphore(data->mSemaphore, 1, nullptr); + // The initialization data may no longer be valid + // after we release the semaphore. + data = nullptr; + + MSG msg; + while (::GetMessageW(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + ::ShowCursor(true); + ::DestroyWindow(wnd); + return 0; +} + +class FullscreenTransitionData final : public nsISupports { + public: + NS_DECL_ISUPPORTS + + explicit FullscreenTransitionData(HWND aWnd) : mWnd(aWnd) { + MOZ_ASSERT(NS_IsMainThread(), + "FullscreenTransitionData " + "should be constructed in the main thread"); + } + + const HWND mWnd; + + private: + ~FullscreenTransitionData() { + MOZ_ASSERT(NS_IsMainThread(), + "FullscreenTransitionData " + "should be deconstructed in the main thread"); + ::PostMessageW(mWnd, WM_DESTROY, 0, 0); + } +}; + +NS_IMPL_ISUPPORTS0(FullscreenTransitionData) + +/* virtual */ +bool nsWindow::PrepareForFullscreenTransition(nsISupports** aData) { + FullscreenTransitionInitData initData; + nsCOMPtr<nsIScreen> screen = GetWidgetScreen(); + const DesktopIntRect rect = screen->GetRectDisplayPix(); + MOZ_ASSERT(BoundsUseDesktopPixels(), + "Should only be called on top-level window"); + initData.mBounds = + LayoutDeviceIntRect::Round(rect * GetDesktopToDeviceScale()); + + // Create a semaphore for synchronizing the window handle which will + // be created by the transition thread and used by the main thread for + // posting the transition messages. + initData.mSemaphore = ::CreateSemaphore(nullptr, 0, 1, nullptr); + if (initData.mSemaphore) { + initData.mThread = ::CreateThread( + nullptr, 0, FullscreenTransitionThreadProc, &initData, 0, nullptr); + if (initData.mThread) { + ::WaitForSingleObject(initData.mSemaphore, INFINITE); + } + } + if (!initData.mWnd) { + return false; + } + + mTransitionWnd = initData.mWnd; + + auto data = new FullscreenTransitionData(initData.mWnd); + *aData = data; + NS_ADDREF(data); + return true; +} + +/* virtual */ +void nsWindow::PerformFullscreenTransition(FullscreenTransitionStage aStage, + uint16_t aDuration, + nsISupports* aData, + nsIRunnable* aCallback) { + auto data = static_cast<FullscreenTransitionData*>(aData); + nsCOMPtr<nsIRunnable> callback = aCallback; + UINT msg = aStage == eBeforeFullscreenToggle ? WM_FULLSCREEN_TRANSITION_BEFORE + : WM_FULLSCREEN_TRANSITION_AFTER; + WPARAM wparam = (WPARAM)callback.forget().take(); + ::PostMessage(data->mWnd, msg, wparam, (LPARAM)aDuration); +} + +/* virtual */ +void nsWindow::CleanupFullscreenTransition() { + MOZ_ASSERT(NS_IsMainThread(), + "CleanupFullscreenTransition " + "should only run on the main thread"); + + mTransitionWnd = nullptr; +} + +void nsWindow::TryDwmResizeHack() { + // The "DWM resize hack", aka the "fullscreen resize hack", is a workaround + // for DWM's occasional and not-entirely-predictable failure to update its + // internal state when the client area of a window changes without changing + // the window size. The effect of this is that DWM will clip the content of + // the window to its former client area. + // + // It is not known under what circumstances the bug will trigger. Windows 11 + // is known to be required, but many Windows 11 machines do not exhibit the + // issue. Even machines that _do_ exhibit it will sometimes not do so when + // apparently-irrelevant changes are made to the configuration. (See bug + // 1763981.) + // + // The bug is triggered by Firefox when a maximized window (which has window + // decorations) becomes fullscreen (which doesn't). To work around this, if we + // think it may occur, we "flicker-resize" the relevant window -- that is, we + // reduce its height by 1px, then restore it. This causes DWM to acquire the + // new client-area metrics. + // + // Note that, in particular, this bug will not occur when using a separate + // compositor window, as our compositor windows never have any nonclient area. + // + // This is admittedly a sledgehammer where a screwdriver should suffice. + + // --------------------------------------------------------------------------- + + // Regardless of preferences or heuristics, only apply the hack if this is the + // first time we've entered fullscreen across the entire Firefox session. + // (Subsequent transitions to fullscreen, even with different windows, don't + // appear to induce the bug.) + { + // (main thread only; `atomic` not needed) + static bool sIsFirstFullscreenEntry = true; + bool isFirstFullscreenEntry = sIsFirstFullscreenEntry; + sIsFirstFullscreenEntry = false; + if (MOZ_LIKELY(!isFirstFullscreenEntry)) { + return; + } + MOZ_LOG(gWindowsLog, LogLevel::Verbose, + ("%s: first fullscreen entry", __PRETTY_FUNCTION__)); + } + + // Check whether to try to apply the DWM resize hack, based on the override + // pref and/or some internal heuristics. + { + const auto hackApplicationHeuristics = [&]() -> bool { + // The bug has only been seen under Windows 11. (At time of writing, this + // is the latest version of Windows.) + if (!IsWin11OrLater()) { + return false; + } + + KnowsCompositor const* const kc = mWindowRenderer->AsKnowsCompositor(); + // This should never happen... + MOZ_ASSERT(kc); + // ... so if it does, we are in uncharted territory: don't apply the hack. + if (!kc) { + return false; + } + + // The bug doesn't occur when we're using a separate compositor window + // (since the compositor window always comprises exactly its client area, + // with no non-client border). + if (kc->GetUseCompositorWnd()) { + return false; + } + + // Otherwise, apply the hack. + return true; + }; + + // Figure out whether or not we should perform the hack, and -- arguably + // more importantly -- log that decision. + bool const shouldApplyHack = [&]() { + enum Reason : bool { Pref, Heuristics }; + auto const msg = [&](bool decision, Reason reason) -> bool { + MOZ_LOG(gWindowsLog, LogLevel::Verbose, + ("%s %s per %s", decision ? "applying" : "skipping", + "DWM resize hack", reason == Pref ? "pref" : "heuristics")); + return decision; + }; + switch (StaticPrefs::widget_windows_apply_dwm_resize_hack()) { + case 0: + return msg(false, Pref); + case 1: + return msg(true, Pref); + default: // treat all other values as `auto` + return msg(hackApplicationHeuristics(), Heuristics); + } + }(); + + if (!shouldApplyHack) { + return; + } + } + + // The DWM bug is believed to involve a race condition: some users have + // reported that setting a custom theme or adding unused command-line + // parameters sometimes causes the bug to vanish. + // + // Out of an abundance of caution, we therefore apply the hack in a later + // event, rather than inline. + NS_DispatchToMainThread(NS_NewRunnableFunction( + "nsWindow::TryFullscreenResizeHack", [self = RefPtr(this)]() { + HWND const hwnd = self->GetWindowHandle(); + + if (self->mFrameState->GetSizeMode() != nsSizeMode_Fullscreen) { + MOZ_LOG(gWindowsLog, mozilla::LogLevel::Info, + ("DWM resize hack: window no longer fullscreen; aborting")); + return; + } + + RECT origRect; + if (!::GetWindowRect(hwnd, &origRect)) { + MOZ_LOG(gWindowsLog, mozilla::LogLevel::Error, + ("DWM resize hack: could not get window size?!")); + return; + } + LONG const x = origRect.left; + LONG const y = origRect.top; + LONG const width = origRect.right - origRect.left; + LONG const height = origRect.bottom - origRect.top; + + MOZ_DIAGNOSTIC_ASSERT(!self->mIsPerformingDwmFlushHack); + auto const onExit = + MakeScopeExit([&, oldVal = self->mIsPerformingDwmFlushHack]() { + self->mIsPerformingDwmFlushHack = oldVal; + }); + self->mIsPerformingDwmFlushHack = true; + + MOZ_LOG(gWindowsLog, LogLevel::Debug, + ("beginning DWM resize hack for HWND %08" PRIXPTR, + uintptr_t(hwnd))); + ::MoveWindow(hwnd, x, y, width, height - 1, FALSE); + ::MoveWindow(hwnd, x, y, width, height, TRUE); + MOZ_LOG(gWindowsLog, LogLevel::Debug, + ("concluded DWM resize hack for HWND %08" PRIXPTR, + uintptr_t(hwnd))); + })); +} + +void nsWindow::OnFullscreenChanged(nsSizeMode aOldSizeMode, bool aFullScreen) { + MOZ_ASSERT((aOldSizeMode != nsSizeMode_Fullscreen) == aFullScreen); + + // HACK: Potentially flicker-resize the window, to force DWM to get the right + // client-area information. + if (aFullScreen) { + TryDwmResizeHack(); + } + + // Hide chrome and reposition window. Note this will also cache dimensions for + // restoration, so it should only be called once per fullscreen request. + // + // Don't do this when minimized, since our bounds make no sense then, nor when + // coming back from that state. + const bool toOrFromMinimized = + mFrameState->GetSizeMode() == nsSizeMode_Minimized || + aOldSizeMode == nsSizeMode_Minimized; + if (!toOrFromMinimized) { + InfallibleMakeFullScreen(aFullScreen); + } + + // Possibly notify the taskbar that we have changed our fullscreen mode. + TaskbarConcealer::OnFullscreenChanged(this, aFullScreen); +} + +nsresult nsWindow::MakeFullScreen(bool aFullScreen) { + mFrameState->EnsureFullscreenMode(aFullScreen); + return NS_OK; +} + +/************************************************************** + * + * SECTION: Native data storage + * + * nsIWidget::GetNativeData + * nsIWidget::FreeNativeData + * + * Set or clear native data based on a constant. + * + **************************************************************/ + +// Return some native data according to aDataType +void* nsWindow::GetNativeData(uint32_t aDataType) { + switch (aDataType) { + case NS_NATIVE_WIDGET: + case NS_NATIVE_WINDOW: + case NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID: + return (void*)mWnd; + case NS_NATIVE_GRAPHIC: + MOZ_ASSERT_UNREACHABLE("Not supported on Windows:"); + return nullptr; + case NS_RAW_NATIVE_IME_CONTEXT: { + void* pseudoIMEContext = GetPseudoIMEContext(); + if (pseudoIMEContext) { + return pseudoIMEContext; + } + [[fallthrough]]; + } + case NS_NATIVE_TSF_THREAD_MGR: + case NS_NATIVE_TSF_CATEGORY_MGR: + case NS_NATIVE_TSF_DISPLAY_ATTR_MGR: + return IMEHandler::GetNativeData(this, aDataType); + + default: + break; + } + + return nullptr; +} + +// Free some native data according to aDataType +void nsWindow::FreeNativeData(void* data, uint32_t aDataType) { + switch (aDataType) { + case NS_NATIVE_GRAPHIC: + case NS_NATIVE_WIDGET: + case NS_NATIVE_WINDOW: + break; + default: + break; + } +} + +/************************************************************** + * + * SECTION: nsIWidget::SetTitle + * + * Set the main windows title text. + * + **************************************************************/ + +nsresult nsWindow::SetTitle(const nsAString& aTitle) { + const nsString& strTitle = PromiseFlatString(aTitle); + AutoRestore<bool> sendingText(mSendingSetText); + mSendingSetText = true; + ::SendMessageW(mWnd, WM_SETTEXT, (WPARAM)0, (LPARAM)(LPCWSTR)strTitle.get()); + return NS_OK; +} + +/************************************************************** + * + * SECTION: nsIWidget::SetIcon + * + * Set the main windows icon. + * + **************************************************************/ + +void nsWindow::SetBigIcon(HICON aIcon) { + HICON icon = + (HICON)::SendMessageW(mWnd, WM_SETICON, (WPARAM)ICON_BIG, (LPARAM)aIcon); + if (icon) { + ::DestroyIcon(icon); + } + + mIconBig = aIcon; +} + +void nsWindow::SetSmallIcon(HICON aIcon) { + HICON icon = (HICON)::SendMessageW(mWnd, WM_SETICON, (WPARAM)ICON_SMALL, + (LPARAM)aIcon); + if (icon) { + ::DestroyIcon(icon); + } + + mIconSmall = aIcon; +} + +void nsWindow::SetIcon(const nsAString& aIconSpec) { + // Assume the given string is a local identifier for an icon file. + + nsCOMPtr<nsIFile> iconFile; + ResolveIconName(aIconSpec, u".ico"_ns, getter_AddRefs(iconFile)); + if (!iconFile) return; + + nsAutoString iconPath; + iconFile->GetPath(iconPath); + + // XXX this should use MZLU (see bug 239279) + + ::SetLastError(0); + + HICON bigIcon = + (HICON)::LoadImageW(nullptr, (LPCWSTR)iconPath.get(), IMAGE_ICON, + ::GetSystemMetrics(SM_CXICON), + ::GetSystemMetrics(SM_CYICON), LR_LOADFROMFILE); + HICON smallIcon = + (HICON)::LoadImageW(nullptr, (LPCWSTR)iconPath.get(), IMAGE_ICON, + ::GetSystemMetrics(SM_CXSMICON), + ::GetSystemMetrics(SM_CYSMICON), LR_LOADFROMFILE); + + if (bigIcon) { + SetBigIcon(bigIcon); + } +#ifdef DEBUG_SetIcon + else { + NS_LossyConvertUTF16toASCII cPath(iconPath); + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("\nIcon load error; icon=%s, rc=0x%08X\n\n", cPath.get(), + ::GetLastError())); + } +#endif + if (smallIcon) { + SetSmallIcon(smallIcon); + } +#ifdef DEBUG_SetIcon + else { + NS_LossyConvertUTF16toASCII cPath(iconPath); + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("\nSmall icon load error; icon=%s, rc=0x%08X\n\n", cPath.get(), + ::GetLastError())); + } +#endif +} + +void nsWindow::SetBigIconNoData() { + HICON bigIcon = + ::LoadIconW(::GetModuleHandleW(nullptr), gStockApplicationIcon); + SetBigIcon(bigIcon); +} + +void nsWindow::SetSmallIconNoData() { + HICON smallIcon = + ::LoadIconW(::GetModuleHandleW(nullptr), gStockApplicationIcon); + SetSmallIcon(smallIcon); +} + +/************************************************************** + * + * SECTION: nsIWidget::WidgetToScreenOffset + * + * Return this widget's origin in screen coordinates. + * + **************************************************************/ + +LayoutDeviceIntPoint nsWindow::WidgetToScreenOffset() { + POINT point; + point.x = 0; + point.y = 0; + ::ClientToScreen(mWnd, &point); + return LayoutDeviceIntPoint(point.x, point.y); +} + +LayoutDeviceIntMargin nsWindow::ClientToWindowMargin() { + if (mWindowType == WindowType::Popup && !IsPopupWithTitleBar()) { + return {}; + } + + if (mCustomNonClient) { + return NonClientSizeMargin(NormalWindowNonClientOffset()); + } + + // Just use a dummy 200x200 at (200, 200) client rect as the rect. + RECT clientRect; + clientRect.left = 200; + clientRect.top = 200; + clientRect.right = 400; + clientRect.bottom = 400; + + auto ToRect = [](const RECT& aRect) -> LayoutDeviceIntRect { + return {aRect.left, aRect.top, aRect.right - aRect.left, + aRect.bottom - aRect.top}; + }; + + RECT windowRect = clientRect; + ::AdjustWindowRectEx(&windowRect, WindowStyle(), false, WindowExStyle()); + + return ToRect(windowRect) - ToRect(clientRect); +} + +/************************************************************** + * + * SECTION: nsIWidget::EnableDragDrop + * + * Enables/Disables drag and drop of files on this widget. + * + **************************************************************/ + +void nsWindow::EnableDragDrop(bool aEnable) { + if (!mWnd) { + // Return early if the window already closed + return; + } + + if (aEnable) { + if (!mNativeDragTarget) { + mNativeDragTarget = new nsNativeDragTarget(this); + mNativeDragTarget->AddRef(); + ::RegisterDragDrop(mWnd, (LPDROPTARGET)mNativeDragTarget); + } + } else { + if (mWnd && mNativeDragTarget) { + ::RevokeDragDrop(mWnd); + mNativeDragTarget->DragCancel(); + NS_RELEASE(mNativeDragTarget); + } + } +} + +/************************************************************** + * + * SECTION: nsIWidget::CaptureMouse + * + * Enables/Disables system mouse capture. + * + **************************************************************/ + +void nsWindow::CaptureMouse(bool aCapture) { + TRACKMOUSEEVENT mTrack; + mTrack.cbSize = sizeof(TRACKMOUSEEVENT); + mTrack.dwHoverTime = 0; + mTrack.hwndTrack = mWnd; + if (aCapture) { + mTrack.dwFlags = TME_CANCEL | TME_LEAVE; + ::SetCapture(mWnd); + } else { + mTrack.dwFlags = TME_LEAVE; + ::ReleaseCapture(); + } + sIsInMouseCapture = aCapture; + TrackMouseEvent(&mTrack); +} + +/************************************************************** + * + * SECTION: nsIWidget::CaptureRollupEvents + * + * Dealing with event rollup on destroy for popups. Enables & + * Disables system capture of any and all events that would + * cause a dropdown to be rolled up. + * + **************************************************************/ + +void nsWindow::CaptureRollupEvents(bool aDoCapture) { + if (aDoCapture) { + if (!sMsgFilterHook && !sCallProcHook && !sCallMouseHook) { + RegisterSpecialDropdownHooks(); + } + sProcessHook = true; + } else { + sProcessHook = false; + UnregisterSpecialDropdownHooks(); + } +} + +/************************************************************** + * + * SECTION: nsIWidget::GetAttention + * + * Bring this window to the user's attention. + * + **************************************************************/ + +// Draw user's attention to this window until it comes to foreground. +nsresult nsWindow::GetAttention(int32_t aCycleCount) { + // Got window? + if (!mWnd) return NS_ERROR_NOT_INITIALIZED; + + HWND flashWnd = WinUtils::GetTopLevelHWND(mWnd, false, false); + HWND fgWnd = ::GetForegroundWindow(); + // Don't flash if the flash count is 0 or if the foreground window is our + // window handle or that of our owned-most window. + if (aCycleCount == 0 || flashWnd == fgWnd || + flashWnd == WinUtils::GetTopLevelHWND(fgWnd, false, false)) { + return NS_OK; + } + + DWORD defaultCycleCount = 0; + ::SystemParametersInfo(SPI_GETFOREGROUNDFLASHCOUNT, 0, &defaultCycleCount, 0); + + FLASHWINFO flashInfo = {sizeof(FLASHWINFO), flashWnd, FLASHW_ALL, + aCycleCount > 0 ? aCycleCount : defaultCycleCount, 0}; + ::FlashWindowEx(&flashInfo); + + return NS_OK; +} + +void nsWindow::StopFlashing() { + HWND flashWnd = mWnd; + while (HWND ownerWnd = ::GetWindow(flashWnd, GW_OWNER)) { + flashWnd = ownerWnd; + } + + FLASHWINFO flashInfo = {sizeof(FLASHWINFO), flashWnd, FLASHW_STOP, 0, 0}; + ::FlashWindowEx(&flashInfo); +} + +/************************************************************** + * + * SECTION: nsIWidget::HasPendingInputEvent + * + * Ask whether there user input events pending. All input events are + * included, including those not targeted at this nsIwidget instance. + * + **************************************************************/ + +bool nsWindow::HasPendingInputEvent() { + // If there is pending input or the user is currently + // moving the window then return true. + // Note: When the user is moving the window WIN32 spins + // a separate event loop and input events are not + // reported to the application. + if (HIWORD(GetQueueStatus(QS_INPUT))) return true; + GUITHREADINFO guiInfo; + guiInfo.cbSize = sizeof(GUITHREADINFO); + if (!GetGUIThreadInfo(GetCurrentThreadId(), &guiInfo)) return false; + return GUI_INMOVESIZE == (guiInfo.flags & GUI_INMOVESIZE); +} + +/************************************************************** + * + * SECTION: nsIWidget::GetWindowRenderer + * + * Get the window renderer associated with this widget. + * + **************************************************************/ + +WindowRenderer* nsWindow::GetWindowRenderer() { + if (mWindowRenderer) { + return mWindowRenderer; + } + + if (!mLocalesChangedObserver) { + mLocalesChangedObserver = new LocalesChangedObserver(this); + } + + // Try OMTC first. + if (!mWindowRenderer && ShouldUseOffMainThreadCompositing()) { + gfxWindowsPlatform::GetPlatform()->UpdateRenderMode(); + CreateCompositor(); + } + + if (!mWindowRenderer) { + MOZ_ASSERT(!mCompositorSession && !mCompositorBridgeChild); + MOZ_ASSERT(!mCompositorWidgetDelegate); + + // Ensure we have a widget proxy even if we're not using the compositor, + // since all our transparent window handling lives there. + WinCompositorWidgetInitData initData( + reinterpret_cast<uintptr_t>(mWnd), + reinterpret_cast<uintptr_t>(static_cast<nsIWidget*>(this)), + mTransparencyMode, mFrameState->GetSizeMode()); + // If we're not using the compositor, the options don't actually matter. + CompositorOptions options(false, false); + mBasicLayersSurface = + new InProcessWinCompositorWidget(initData, options, this); + mCompositorWidgetDelegate = mBasicLayersSurface; + mWindowRenderer = CreateFallbackRenderer(); + } + + NS_ASSERTION(mWindowRenderer, "Couldn't provide a valid window renderer."); + + if (mWindowRenderer) { + // Update the size constraints now that the layer manager has been + // created. + KnowsCompositor* knowsCompositor = mWindowRenderer->AsKnowsCompositor(); + if (knowsCompositor) { + SizeConstraints c = mSizeConstraints; + mMaxTextureSize = knowsCompositor->GetMaxTextureSize(); + c.mMaxSize.width = std::min(c.mMaxSize.width, mMaxTextureSize); + c.mMaxSize.height = std::min(c.mMaxSize.height, mMaxTextureSize); + nsBaseWidget::SetSizeConstraints(c); + } + } + + return mWindowRenderer; +} + +/************************************************************** + * + * SECTION: nsBaseWidget::SetCompositorWidgetDelegate + * + * Called to connect the nsWindow to the delegate providing + * platform compositing API access. + * + **************************************************************/ + +void nsWindow::SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) { + if (delegate) { + mCompositorWidgetDelegate = delegate->AsPlatformSpecificDelegate(); + MOZ_ASSERT(mCompositorWidgetDelegate, + "nsWindow::SetCompositorWidgetDelegate called with a " + "non-PlatformCompositorWidgetDelegate"); + } else { + mCompositorWidgetDelegate = nullptr; + } +} + +/************************************************************** + * + * SECTION: nsIWidget::OnDefaultButtonLoaded + * + * Called after the dialog is loaded and it has a default button. + * + **************************************************************/ + +nsresult nsWindow::OnDefaultButtonLoaded( + const LayoutDeviceIntRect& aButtonRect) { + if (aButtonRect.IsEmpty()) return NS_OK; + + // Don't snap when we are not active. + HWND activeWnd = ::GetActiveWindow(); + if (activeWnd != ::GetForegroundWindow() || + WinUtils::GetTopLevelHWND(mWnd, true) != + WinUtils::GetTopLevelHWND(activeWnd, true)) { + return NS_OK; + } + + bool isAlwaysSnapCursor = + Preferences::GetBool("ui.cursor_snapping.always_enabled", false); + + if (!isAlwaysSnapCursor) { + BOOL snapDefaultButton; + if (!::SystemParametersInfo(SPI_GETSNAPTODEFBUTTON, 0, &snapDefaultButton, + 0) || + !snapDefaultButton) + return NS_OK; + } + + LayoutDeviceIntRect widgetRect = GetScreenBounds(); + LayoutDeviceIntRect buttonRect(aButtonRect + widgetRect.TopLeft()); + + LayoutDeviceIntPoint centerOfButton(buttonRect.X() + buttonRect.Width() / 2, + buttonRect.Y() + buttonRect.Height() / 2); + // The center of the button can be outside of the widget. + // E.g., it could be hidden by scrolling. + if (!widgetRect.Contains(centerOfButton)) { + return NS_OK; + } + + if (!::SetCursorPos(centerOfButton.x, centerOfButton.y)) { + NS_ERROR("SetCursorPos failed"); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +uint32_t nsWindow::GetMaxTouchPoints() const { + return WinUtils::GetMaxTouchPoints(); +} + +void nsWindow::SetWindowClass(const nsAString& xulWinType, + const nsAString& xulWinClass, + const nsAString& xulWinName) { + mIsEarlyBlankWindow = xulWinType.EqualsLiteral("navigator:blank"); +} + +/************************************************************** + ************************************************************** + ** + ** BLOCK: Moz Events + ** + ** Moz GUI event management. + ** + ************************************************************** + **************************************************************/ + +/************************************************************** + * + * SECTION: Mozilla event initialization + * + * Helpers for initializing moz events. + * + **************************************************************/ + +// Event initialization +void nsWindow::InitEvent(WidgetGUIEvent& event, LayoutDeviceIntPoint* aPoint) { + if (nullptr == aPoint) { // use the point from the event + // get the message position in client coordinates + if (mWnd != nullptr) { + DWORD pos = ::GetMessagePos(); + POINT cpos; + + cpos.x = GET_X_LPARAM(pos); + cpos.y = GET_Y_LPARAM(pos); + + ::ScreenToClient(mWnd, &cpos); + event.mRefPoint = LayoutDeviceIntPoint(cpos.x, cpos.y); + } else { + event.mRefPoint = LayoutDeviceIntPoint(0, 0); + } + } else { + // use the point override if provided + event.mRefPoint = *aPoint; + } + + event.AssignEventTime(CurrentMessageWidgetEventTime()); +} + +WidgetEventTime nsWindow::CurrentMessageWidgetEventTime() const { + LONG messageTime = ::GetMessageTime(); + return WidgetEventTime(GetMessageTimeStamp(messageTime)); +} + +/************************************************************** + * + * SECTION: Moz event dispatch helpers + * + * Helpers for dispatching different types of moz events. + * + **************************************************************/ + +// Main event dispatch. Invokes callback and ProcessEvent method on +// Event Listener object. Part of nsIWidget. +nsresult nsWindow::DispatchEvent(WidgetGUIEvent* event, + nsEventStatus& aStatus) { +#ifdef WIDGET_DEBUG_OUTPUT + debug_DumpEvent(stdout, event->mWidget, event, "something", (int32_t)mWnd); +#endif // WIDGET_DEBUG_OUTPUT + + aStatus = nsEventStatus_eIgnore; + + // Top level windows can have a view attached which requires events be sent + // to the underlying base window and the view. Added when we combined the + // base chrome window with the main content child for nc client area (title + // bar) rendering. + if (mAttachedWidgetListener) { + aStatus = mAttachedWidgetListener->HandleEvent(event, mUseAttachedEvents); + } else if (mWidgetListener) { + aStatus = mWidgetListener->HandleEvent(event, mUseAttachedEvents); + } + + // the window can be destroyed during processing of seemingly innocuous events + // like, say, mousedowns due to the magic of scripting. mousedowns will return + // nsEventStatus_eIgnore, which causes problems with the deleted window. + // therefore: + if (mOnDestroyCalled) aStatus = nsEventStatus_eConsumeNoDefault; + return NS_OK; +} + +bool nsWindow::DispatchStandardEvent(EventMessage aMsg) { + WidgetGUIEvent event(true, aMsg, this); + InitEvent(event); + + bool result = DispatchWindowEvent(event); + return result; +} + +bool nsWindow::DispatchKeyboardEvent(WidgetKeyboardEvent* event) { + nsEventStatus status = DispatchInputEvent(event).mContentStatus; + return ConvertStatus(status); +} + +bool nsWindow::DispatchContentCommandEvent(WidgetContentCommandEvent* aEvent) { + nsEventStatus status; + DispatchEvent(aEvent, status); + return ConvertStatus(status); +} + +bool nsWindow::DispatchWheelEvent(WidgetWheelEvent* aEvent) { + nsEventStatus status = + DispatchInputEvent(aEvent->AsInputEvent()).mContentStatus; + return ConvertStatus(status); +} + +// Recursively dispatch synchronous paints for nsIWidget +// descendants with invalidated rectangles. +BOOL CALLBACK nsWindow::DispatchStarvedPaints(HWND aWnd, LPARAM aMsg) { + LONG_PTR proc = ::GetWindowLongPtrW(aWnd, GWLP_WNDPROC); + if (proc == (LONG_PTR)&nsWindow::WindowProc) { + // its one of our windows so check to see if it has a + // invalidated rect. If it does. Dispatch a synchronous + // paint. + if (GetUpdateRect(aWnd, nullptr, FALSE)) VERIFY(::UpdateWindow(aWnd)); + } + return TRUE; +} + +// Check for pending paints and dispatch any pending paint +// messages for any nsIWidget which is a descendant of the +// top-level window that *this* window is embedded within. +// +// Note: We do not dispatch pending paint messages for non +// nsIWidget managed windows. +void nsWindow::DispatchPendingEvents() { + // We need to ensure that reflow events do not get starved. + // At the same time, we don't want to recurse through here + // as that would prevent us from dispatching starved paints. + static int recursionBlocker = 0; + if (recursionBlocker++ == 0) { + NS_ProcessPendingEvents(nullptr, PR_MillisecondsToInterval(100)); + --recursionBlocker; + } + + // Quickly check to see if there are any paint events pending, + // but only dispatch them if it has been long enough since the + // last paint completed. + if (::GetQueueStatus(QS_PAINT) && + ((TimeStamp::Now() - mLastPaintEndTime).ToMilliseconds() >= 50)) { + // Find the top level window. + HWND topWnd = WinUtils::GetTopLevelHWND(mWnd); + + // Dispatch pending paints for topWnd and all its descendant windows. + // Note: EnumChildWindows enumerates all descendant windows not just + // the children (but not the window itself). + nsWindow::DispatchStarvedPaints(topWnd, 0); + ::EnumChildWindows(topWnd, nsWindow::DispatchStarvedPaints, 0); + } +} + +void nsWindow::DispatchCustomEvent(const nsString& eventName) { + if (Document* doc = GetDocument()) { + if (nsPIDOMWindowOuter* win = doc->GetWindow()) { + win->DispatchCustomEvent(eventName, ChromeOnlyDispatch::eYes); + } + } +} + +bool nsWindow::TouchEventShouldStartDrag(EventMessage aEventMessage, + LayoutDeviceIntPoint aEventPoint) { + // Allow users to start dragging by double-tapping. + if (aEventMessage == eMouseDoubleClick) { + return true; + } + + // In chrome UI, allow touchdownstartsdrag attributes + // to cause any touchdown event to trigger a drag. + if (aEventMessage == eMouseDown) { + WidgetMouseEvent hittest(true, eMouseHitTest, this, + WidgetMouseEvent::eReal); + hittest.mRefPoint = aEventPoint; + hittest.mIgnoreRootScrollFrame = true; + hittest.mInputSource = MouseEvent_Binding::MOZ_SOURCE_TOUCH; + DispatchInputEvent(&hittest); + + if (EventTarget* target = hittest.GetDOMEventTarget()) { + if (nsIContent* content = nsIContent::FromEventTarget(target)) { + // Check if the element or any parent element has the + // attribute we're looking for. + for (Element* element = content->GetAsElementOrParentElement(); element; + element = element->GetParentElement()) { + nsAutoString startDrag; + element->GetAttribute(u"touchdownstartsdrag"_ns, startDrag); + if (!startDrag.IsEmpty()) { + return true; + } + } + } + } + } + + return false; +} + +// Deal with all sort of mouse event +bool nsWindow::DispatchMouseEvent(EventMessage aEventMessage, WPARAM wParam, + LPARAM lParam, bool aIsContextMenuKey, + int16_t aButton, uint16_t aInputSource, + WinPointerInfo* aPointerInfo, + bool aIgnoreAPZ) { + ContextMenuPreventer contextMenuPreventer(this); + bool result = false; + + UserActivity(); + + if (!mWidgetListener) { + return result; + } + + LayoutDeviceIntPoint eventPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + LayoutDeviceIntPoint mpScreen = eventPoint + WidgetToScreenOffset(); + + // Suppress mouse moves caused by widget creation. Make sure to do this early + // so that we update sLastMouseMovePoint even for touch-induced mousemove + // events. + if (aEventMessage == eMouseMove) { + if ((sLastMouseMovePoint.x == mpScreen.x.value) && + (sLastMouseMovePoint.y == mpScreen.y.value)) { + return result; + } + sLastMouseMovePoint.x = mpScreen.x; + sLastMouseMovePoint.y = mpScreen.y; + } + + if (!aIgnoreAPZ && WinUtils::GetIsMouseFromTouch(aEventMessage)) { + if (mTouchWindow) { + // If mTouchWindow is true, then we must have APZ enabled and be + // feeding it raw touch events. In that case we only want to + // send touch-generated mouse events to content if they should + // start a touch-based drag-and-drop gesture, such as on + // double-tapping or when tapping elements marked with the + // touchdownstartsdrag attribute in chrome UI. + MOZ_ASSERT(mAPZC); + if (TouchEventShouldStartDrag(aEventMessage, eventPoint)) { + aEventMessage = eMouseTouchDrag; + } else { + return result; + } + } + } + + uint32_t pointerId = + aPointerInfo ? aPointerInfo->pointerId : MOUSE_POINTERID(); + + switch (aEventMessage) { + case eMouseDown: + CaptureMouse(true); + break; + + // eMouseMove and eMouseExitFromWidget are here because we need to make + // sure capture flag isn't left on after a drag where we wouldn't see a + // button up message (see bug 324131). + case eMouseUp: + case eMouseMove: + case eMouseExitFromWidget: + if (!(wParam & (MK_LBUTTON | MK_MBUTTON | MK_RBUTTON)) && + sIsInMouseCapture) + CaptureMouse(false); + break; + + default: + break; + + } // switch + + WidgetMouseEvent event(true, aEventMessage, this, WidgetMouseEvent::eReal, + aIsContextMenuKey ? WidgetMouseEvent::eContextMenuKey + : WidgetMouseEvent::eNormal); + if (aEventMessage == eContextMenu && aIsContextMenuKey) { + LayoutDeviceIntPoint zero(0, 0); + InitEvent(event, &zero); + } else { + InitEvent(event, &eventPoint); + } + + ModifierKeyState modifierKeyState; + modifierKeyState.InitInputEvent(event); + + // eContextMenu with Shift state is special. It won't fire "contextmenu" + // event in the web content for blocking web content to prevent its default. + // However, Shift+F10 is a standard shortcut key on Windows. Therefore, + // this should not block web page to prevent its default. I.e., it should + // behave same as ContextMenu key without Shift key. + // XXX Should we allow to block web page to prevent its default with + // Ctrl+Shift+F10 or Alt+Shift+F10 instead? + if (aEventMessage == eContextMenu && aIsContextMenuKey && event.IsShift() && + NativeKey::LastKeyOrCharMSG().message == WM_SYSKEYDOWN && + NativeKey::LastKeyOrCharMSG().wParam == VK_F10) { + event.mModifiers &= ~MODIFIER_SHIFT; + } + + event.mButton = aButton; + event.mInputSource = aInputSource; + if (aPointerInfo) { + // Mouse events from Windows WM_POINTER*. Fill more information in + // WidgetMouseEvent. + event.AssignPointerHelperData(*aPointerInfo); + event.mPressure = aPointerInfo->mPressure; + event.mButtons = aPointerInfo->mButtons; + } else { + // If we get here the mouse events must be from non-touch sources, so + // convert it to pointer events as well + event.convertToPointer = true; + event.pointerId = pointerId; + } + + // Static variables used to distinguish simple-, double- and triple-clicks. + static POINT sLastMousePoint = {0}; + static LONG sLastMouseDownTime = 0L; + static LONG sLastClickCount = 0L; + static BYTE sLastMouseButton = 0; + + bool insideMovementThreshold = + (DeprecatedAbs(sLastMousePoint.x - eventPoint.x.value) < + (short)::GetSystemMetrics(SM_CXDOUBLECLK)) && + (DeprecatedAbs(sLastMousePoint.y - eventPoint.y.value) < + (short)::GetSystemMetrics(SM_CYDOUBLECLK)); + + BYTE eventButton; + switch (aButton) { + case MouseButton::ePrimary: + eventButton = VK_LBUTTON; + break; + case MouseButton::eMiddle: + eventButton = VK_MBUTTON; + break; + case MouseButton::eSecondary: + eventButton = VK_RBUTTON; + break; + default: + eventButton = 0; + break; + } + + // Doubleclicks are used to set the click count, then changed to mousedowns + // We're going to time double-clicks from mouse *up* to next mouse *down* + LONG curMsgTime = ::GetMessageTime(); + + switch (aEventMessage) { + case eMouseDoubleClick: + event.mMessage = eMouseDown; + event.mButton = aButton; + sLastClickCount = 2; + sLastMouseDownTime = curMsgTime; + break; + case eMouseUp: + // remember when this happened for the next mouse down + sLastMousePoint.x = eventPoint.x; + sLastMousePoint.y = eventPoint.y; + sLastMouseButton = eventButton; + break; + case eMouseDown: + // now look to see if we want to convert this to a double- or triple-click + if (((curMsgTime - sLastMouseDownTime) < (LONG)::GetDoubleClickTime()) && + insideMovementThreshold && eventButton == sLastMouseButton) { + sLastClickCount++; + } else { + // reset the click count, to count *this* click + sLastClickCount = 1; + } + // Set last Click time on MouseDown only + sLastMouseDownTime = curMsgTime; + break; + case eMouseMove: + if (!insideMovementThreshold) { + sLastClickCount = 0; + } + break; + case eMouseExitFromWidget: + event.mExitFrom = + Some(IsTopLevelMouseExit(mWnd) ? WidgetMouseEvent::ePlatformTopLevel + : WidgetMouseEvent::ePlatformChild); + break; + default: + break; + } + event.mClickCount = sLastClickCount; + +#ifdef NS_DEBUG_XX + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("Msg Time: %d Click Count: %d\n", curMsgTime, event.mClickCount)); +#endif + + // call the event callback + if (mWidgetListener) { + if (aEventMessage == eMouseMove) { + LayoutDeviceIntRect rect = GetBounds(); + rect.MoveTo(0, 0); + + if (rect.Contains(event.mRefPoint)) { + if (sCurrentWindow == nullptr || sCurrentWindow != this) { + if ((nullptr != sCurrentWindow) && (!sCurrentWindow->mInDtor)) { + LPARAM pos = sCurrentWindow->lParamToClient(lParamToScreen(lParam)); + sCurrentWindow->DispatchMouseEvent( + eMouseExitFromWidget, wParam, pos, false, MouseButton::ePrimary, + aInputSource, aPointerInfo); + } + sCurrentWindow = this; + if (!mInDtor) { + LPARAM pos = sCurrentWindow->lParamToClient(lParamToScreen(lParam)); + sCurrentWindow->DispatchMouseEvent( + eMouseEnterIntoWidget, wParam, pos, false, + MouseButton::ePrimary, aInputSource, aPointerInfo); + } + } + } + } else if (aEventMessage == eMouseExitFromWidget) { + if (sCurrentWindow == this) { + sCurrentWindow = nullptr; + } + } + + nsIWidget::ContentAndAPZEventStatus eventStatus = + DispatchInputEvent(&event); + contextMenuPreventer.Update(event, eventStatus); + return ConvertStatus(eventStatus.mContentStatus); + } + + return result; +} + +HWND nsWindow::GetTopLevelForFocus(HWND aCurWnd) { + // retrieve the toplevel window or dialogue + HWND toplevelWnd = nullptr; + while (aCurWnd) { + toplevelWnd = aCurWnd; + nsWindow* win = WinUtils::GetNSWindowPtr(aCurWnd); + if (win) { + if (win->mWindowType == WindowType::TopLevel || + win->mWindowType == WindowType::Dialog) { + break; + } + } + + aCurWnd = ::GetParent(aCurWnd); // Parent or owner (if has no parent) + } + return toplevelWnd; +} + +void nsWindow::DispatchFocusToTopLevelWindow(bool aIsActivate) { + if (aIsActivate) { + sJustGotActivate = false; + } + sJustGotDeactivate = false; + mLastKillFocusWindow = nullptr; + + HWND toplevelWnd = GetTopLevelForFocus(mWnd); + + if (toplevelWnd) { + nsWindow* win = WinUtils::GetNSWindowPtr(toplevelWnd); + if (win && win->mWidgetListener) { + if (aIsActivate) { + win->mWidgetListener->WindowActivated(); + } else { + win->mWidgetListener->WindowDeactivated(); + } + } + } +} + +HWND nsWindow::WindowAtMouse() { + DWORD pos = ::GetMessagePos(); + POINT mp; + mp.x = GET_X_LPARAM(pos); + mp.y = GET_Y_LPARAM(pos); + return ::WindowFromPoint(mp); +} + +bool nsWindow::IsTopLevelMouseExit(HWND aWnd) { + HWND mouseWnd = WindowAtMouse(); + + // WinUtils::GetTopLevelHWND() will return a HWND for the window frame + // (which includes the non-client area). If the mouse has moved into + // the non-client area, we should treat it as a top-level exit. + HWND mouseTopLevel = WinUtils::GetTopLevelHWND(mouseWnd); + if (mouseWnd == mouseTopLevel) return true; + + return WinUtils::GetTopLevelHWND(aWnd) != mouseTopLevel; +} + +/************************************************************** + * + * SECTION: IPC + * + * IPC related helpers. + * + **************************************************************/ + +// static +bool nsWindow::IsAsyncResponseEvent(UINT aMsg, LRESULT& aResult) { + switch (aMsg) { + case WM_SETFOCUS: + case WM_KILLFOCUS: + case WM_ENABLE: + case WM_WINDOWPOSCHANGING: + case WM_WINDOWPOSCHANGED: + case WM_PARENTNOTIFY: + case WM_ACTIVATEAPP: + case WM_NCACTIVATE: + case WM_ACTIVATE: + case WM_CHILDACTIVATE: + case WM_IME_SETCONTEXT: + case WM_IME_NOTIFY: + case WM_SHOWWINDOW: + case WM_CANCELMODE: + case WM_MOUSEACTIVATE: + case WM_CONTEXTMENU: + aResult = 0; + return true; + + case WM_SETTINGCHANGE: + case WM_SETCURSOR: + return false; + } + +#ifdef DEBUG + char szBuf[200]; + sprintf(szBuf, + "An unhandled ISMEX_SEND message was received during spin loop! (%X)", + aMsg); + NS_WARNING(szBuf); +#endif + + return false; +} + +void nsWindow::IPCWindowProcHandler(UINT& msg, WPARAM& wParam, LPARAM& lParam) { + MOZ_ASSERT_IF( + msg != WM_GETOBJECT, + !mozilla::ipc::MessageChannel::IsPumpingMessages() || + mozilla::ipc::SuppressedNeuteringRegion::IsNeuteringSuppressed()); + + // Modal UI being displayed in windowless plugins. + if (mozilla::ipc::MessageChannel::IsSpinLoopActive() && + (InSendMessageEx(nullptr) & (ISMEX_REPLIED | ISMEX_SEND)) == ISMEX_SEND) { + LRESULT res; + if (IsAsyncResponseEvent(msg, res)) { + ReplyMessage(res); + } + return; + } + + // Handle certain sync plugin events sent to the parent which + // trigger ipc calls that result in deadlocks. + + DWORD dwResult = 0; + bool handled = false; + + switch (msg) { + // Windowless flash sending WM_ACTIVATE events to the main window + // via calls to ShowWindow. + case WM_ACTIVATE: + if (lParam != 0 && LOWORD(wParam) == WA_ACTIVE && + IsWindow((HWND)lParam)) { + // Check for Adobe Reader X sync activate message from their + // helper window and ignore. Fixes an annoying focus problem. + if ((InSendMessageEx(nullptr) & (ISMEX_REPLIED | ISMEX_SEND)) == + ISMEX_SEND) { + wchar_t szClass[10]; + HWND focusWnd = (HWND)lParam; + if (IsWindowVisible(focusWnd) && + GetClassNameW(focusWnd, szClass, + sizeof(szClass) / sizeof(char16_t)) && + !wcscmp(szClass, L"Edit") && + !WinUtils::IsOurProcessWindow(focusWnd)) { + break; + } + } + handled = true; + } + break; + // Plugins taking or losing focus triggering focus app messages. + case WM_SETFOCUS: + case WM_KILLFOCUS: + // Windowed plugins that pass sys key events to defwndproc generate + // WM_SYSCOMMAND events to the main window. + case WM_SYSCOMMAND: + // Windowed plugins that fire context menu selection events to parent + // windows. + case WM_CONTEXTMENU: + // IME events fired as a result of synchronous focus changes + case WM_IME_SETCONTEXT: + handled = true; + break; + } + + if (handled && + (InSendMessageEx(nullptr) & (ISMEX_REPLIED | ISMEX_SEND)) == ISMEX_SEND) { + ReplyMessage(dwResult); + } +} + +/************************************************************** + ************************************************************** + ** + ** BLOCK: Native events + ** + ** Main Windows message handlers and OnXXX handlers for + ** Windows event handling. + ** + ************************************************************** + **************************************************************/ + +/************************************************************** + * + * SECTION: Wind proc. + * + * The main Windows event procedures and associated + * message processing methods. + * + **************************************************************/ + +static bool DisplaySystemMenu(HWND hWnd, nsSizeMode sizeMode, bool isRtl, + int32_t x, int32_t y) { + HMENU hMenu = GetSystemMenu(hWnd, FALSE); + if (hMenu) { + MENUITEMINFO mii; + mii.cbSize = sizeof(MENUITEMINFO); + mii.fMask = MIIM_STATE; + mii.fType = 0; + + // update the options + mii.fState = MF_ENABLED; + SetMenuItemInfo(hMenu, SC_RESTORE, FALSE, &mii); + SetMenuItemInfo(hMenu, SC_SIZE, FALSE, &mii); + SetMenuItemInfo(hMenu, SC_MOVE, FALSE, &mii); + SetMenuItemInfo(hMenu, SC_MAXIMIZE, FALSE, &mii); + SetMenuItemInfo(hMenu, SC_MINIMIZE, FALSE, &mii); + + mii.fState = MF_GRAYED; + switch (sizeMode) { + case nsSizeMode_Fullscreen: + // intentional fall through + case nsSizeMode_Maximized: + SetMenuItemInfo(hMenu, SC_SIZE, FALSE, &mii); + SetMenuItemInfo(hMenu, SC_MOVE, FALSE, &mii); + SetMenuItemInfo(hMenu, SC_MAXIMIZE, FALSE, &mii); + break; + case nsSizeMode_Minimized: + SetMenuItemInfo(hMenu, SC_MINIMIZE, FALSE, &mii); + break; + case nsSizeMode_Normal: + SetMenuItemInfo(hMenu, SC_RESTORE, FALSE, &mii); + break; + case nsSizeMode_Invalid: + NS_ASSERTION(false, "Did the argument come from invalid IPC?"); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unhnalded nsSizeMode value detected"); + break; + } + LPARAM cmd = TrackPopupMenu( + hMenu, + (TPM_LEFTBUTTON | TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_TOPALIGN | + (isRtl ? TPM_RIGHTALIGN : TPM_LEFTALIGN)), + x, y, 0, hWnd, nullptr); + if (cmd) { + PostMessage(hWnd, WM_SYSCOMMAND, cmd, 0); + return true; + } + } + return false; +} + +// The WndProc procedure for all nsWindows in this toolkit. This merely catches +// SEH exceptions and passes the real work to WindowProcInternal. See bug 587406 +// and http://msdn.microsoft.com/en-us/library/ms633573%28VS.85%29.aspx +LRESULT CALLBACK nsWindow::WindowProc(HWND hWnd, UINT msg, WPARAM wParam, + LPARAM lParam) { + mozilla::ipc::CancelCPOWs(); + + BackgroundHangMonitor().NotifyActivity(); + + return mozilla::CallWindowProcCrashProtected(WindowProcInternal, hWnd, msg, + wParam, lParam); +} + +LRESULT CALLBACK nsWindow::WindowProcInternal(HWND hWnd, UINT msg, + WPARAM wParam, LPARAM lParam) { + if (::GetWindowLongPtrW(hWnd, GWLP_ID) == eFakeTrackPointScrollableID) { + // This message was sent to the FAKETRACKPOINTSCROLLABLE. + if (msg == WM_HSCROLL) { + // Route WM_HSCROLL messages to the main window. + hWnd = ::GetParent(::GetParent(hWnd)); + } else { + // Handle all other messages with its original window procedure. + WNDPROC prevWindowProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_USERDATA); + return ::CallWindowProcW(prevWindowProc, hWnd, msg, wParam, lParam); + } + } + + if (msg == MOZ_WM_TRACE) { + // This is a tracer event for measuring event loop latency. + // See WidgetTraceEvent.cpp for more details. + mozilla::SignalTracerThread(); + return 0; + } + + // Get the window which caused the event and ask it to process the message + nsWindow* targetWindow = WinUtils::GetNSWindowPtr(hWnd); + NS_ASSERTION(targetWindow, "nsWindow* is null!"); + if (!targetWindow) return ::DefWindowProcW(hWnd, msg, wParam, lParam); + + // Hold the window for the life of this method, in case it gets + // destroyed during processing, unless we're in the dtor already. + nsCOMPtr<nsIWidget> kungFuDeathGrip; + if (!targetWindow->mInDtor) kungFuDeathGrip = targetWindow; + + targetWindow->IPCWindowProcHandler(msg, wParam, lParam); + + // Create this here so that we store the last rolled up popup until after + // the event has been processed. + nsAutoRollup autoRollup; + + LRESULT popupHandlingResult; + if (DealWithPopups(hWnd, msg, wParam, lParam, &popupHandlingResult)) + return popupHandlingResult; + + // Call ProcessMessage + LRESULT retValue; + if (targetWindow->ProcessMessage(msg, wParam, lParam, &retValue)) { + return retValue; + } + + LRESULT res = ::CallWindowProcW(targetWindow->GetPrevWindowProc(), hWnd, msg, + wParam, lParam); + + return res; +} + +const char16_t* GetQuitType() { + if (Preferences::GetBool(PREF_WIN_REGISTER_APPLICATION_RESTART, false)) { + DWORD cchCmdLine = 0; + HRESULT rc = ::GetApplicationRestartSettings(::GetCurrentProcess(), nullptr, + &cchCmdLine, nullptr); + if (rc == S_OK) { + return u"os-restart"; + } + } + return nullptr; +} + +bool nsWindow::ExternalHandlerProcessMessage(UINT aMessage, WPARAM& aWParam, + LPARAM& aLParam, + MSGResult& aResult) { + if (mWindowHook.Notify(mWnd, aMessage, aWParam, aLParam, aResult)) { + return true; + } + + if (IMEHandler::ProcessMessage(this, aMessage, aWParam, aLParam, aResult)) { + return true; + } + + if (MouseScrollHandler::ProcessMessage(this, aMessage, aWParam, aLParam, + aResult)) { + return true; + } + + return false; +} + +// The main windows message processing method. Wraps ProcessMessageInternal so +// we can log aRetValue. +bool nsWindow::ProcessMessage(UINT msg, WPARAM& wParam, LPARAM& lParam, + LRESULT* aRetValue) { + // For some events we might change the parameter values, so log + // before and after we process them. + NativeEventLogger eventLogger("nsWindow", mWnd, msg, wParam, lParam); + bool result = ProcessMessageInternal(msg, wParam, lParam, aRetValue); + eventLogger.SetResult(*aRetValue, result); + + return result; +} + +// The main windows message processing method. Called by ProcessMessage. +bool nsWindow::ProcessMessageInternal(UINT msg, WPARAM& wParam, LPARAM& lParam, + LRESULT* aRetValue) { + MSGResult msgResult(aRetValue); + if (ExternalHandlerProcessMessage(msg, wParam, lParam, msgResult)) { + return (msgResult.mConsumed || !mWnd); + } + + bool result = false; // call the default nsWindow proc + *aRetValue = 0; + + // The DWM resize hack (see bug 1763981) causes us to process a number of + // messages, notably including some WM_WINDOWPOSCHANG{ING,ED} messages which + // would ordinarily result in a whole lot of internal state being updated. + // + // Since we're supposed to end in the same state we started in (and since the + // content shouldn't know about any of this nonsense), just discard any + // messages synchronously dispatched from within the hack. + if (MOZ_UNLIKELY(mIsPerformingDwmFlushHack)) { + return true; + } + + // Glass hit testing w/custom transparent margins. + // + // FIXME(emilio): is this needed? We deal with titlebar buttons non-natively + // now. + LRESULT dwmHitResult; + if (mCustomNonClient && + DwmDefWindowProc(mWnd, msg, wParam, lParam, &dwmHitResult)) { + *aRetValue = dwmHitResult; + return true; + } + + // The preference whether to use a different keyboard layout for each + // window is cached, and updating it will not take effect until the + // next restart. We read the preference here and not upon WM_ACTIVATE to make + // sure that this behavior is consistent. Otherwise, if the user changed the + // preference before having ever lowered the window, the preference would take + // effect immediately. + static const bool sSwitchKeyboardLayout = + Preferences::GetBool("intl.keyboard.per_window_layout", false); + AppShutdownReason shutdownReason = AppShutdownReason::Unknown; + + // (Large blocks of code should be broken out into OnEvent handlers.) + switch (msg) { + // WM_QUERYENDSESSION must be handled by all windows. + // Otherwise Windows thinks the window can just be killed at will. + case WM_QUERYENDSESSION: { + // Ask around if it's ok to quit. + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + nsCOMPtr<nsISupportsPRBool> cancelQuitWrapper = + do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID); + cancelQuitWrapper->SetData(false); + + const char16_t* quitType = GetQuitType(); + obsServ->NotifyObservers(cancelQuitWrapper, "quit-application-requested", + quitType); + + bool shouldCancelQuit; + cancelQuitWrapper->GetData(&shouldCancelQuit); + *aRetValue = !shouldCancelQuit; + result = true; + } break; + + case MOZ_WM_STARTA11Y: +#if defined(ACCESSIBILITY) + Unused << GetAccessible(); + result = true; +#else + result = false; +#endif + break; + + case WM_ENDSESSION: { + // For WM_ENDSESSION, wParam indicates whether we need to shutdown + // (TRUE) or not (FALSE). + if (!wParam) { + result = true; + break; + } + // According to WM_ENDSESSION lParam documentation: + // 0 -> OS shutdown or restart (no way to distinguish) + // ENDSESSION_LOGOFF -> User is logging off + // ENDSESSION_CLOSEAPP -> Application must shutdown + // ENDSESSION_CRITICAL -> Application is forced to shutdown + // The difference of the last two is not very clear. + if (lParam == 0) { + shutdownReason = AppShutdownReason::OSShutdown; + } else if (lParam & ENDSESSION_LOGOFF) { + shutdownReason = AppShutdownReason::OSSessionEnd; + } else if (lParam & (ENDSESSION_CLOSEAPP | ENDSESSION_CRITICAL)) { + shutdownReason = AppShutdownReason::OSForceClose; + } else { + MOZ_DIAGNOSTIC_ASSERT(false, + "Received WM_ENDSESSION with unknown flags."); + shutdownReason = AppShutdownReason::OSForceClose; + } + } + [[fallthrough]]; + case MOZ_WM_APP_QUIT: { + if (shutdownReason == AppShutdownReason::Unknown) { + // TODO: We do not expect that these days anybody sends us + // MOZ_WM_APP_QUIT, see bug 1827807. + shutdownReason = AppShutdownReason::WinUnexpectedMozQuit; + } + // Let's fake a shutdown sequence without actually closing windows etc. + // to avoid Windows killing us in the middle. A proper shutdown would + // require having a chance to pump some messages. Unfortunately + // Windows won't let us do that. Bug 212316. + nsCOMPtr<nsIObserverService> obsServ = + mozilla::services::GetObserverService(); + const char16_t* syncShutdown = u"syncShutdown"; + const char16_t* quitType = GetQuitType(); + + AppShutdown::Init(AppShutdownMode::Normal, 0, shutdownReason); + + obsServ->NotifyObservers(nullptr, "quit-application-granted", + syncShutdown); + obsServ->NotifyObservers(nullptr, "quit-application-forced", nullptr); + + AppShutdown::OnShutdownConfirmed(); + + AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownConfirmed, + quitType); + AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownNetTeardown, + nullptr); + AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTeardown, + nullptr); + AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdown, nullptr); + AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownQM, nullptr); + AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTelemetry, + nullptr); + + AppShutdown::DoImmediateExit(); + MOZ_ASSERT_UNREACHABLE("Our process was supposed to exit."); + } break; + + case WM_SYSCOLORCHANGE: + // No need to invalidate layout for system color changes, but we need to + // invalidate style. + NotifyThemeChanged(widget::ThemeChangeKind::Style); + break; + + case WM_THEMECHANGED: { + // Update non-client margin offsets + UpdateNonClientMargins(); + nsUXThemeData::UpdateNativeThemeInfo(); + + // We assume pretty much everything could've changed here. + NotifyThemeChanged(widget::ThemeChangeKind::StyleAndLayout); + + UpdateDarkModeToolbar(); + + // Invalidate the window so that the repaint will + // pick up the new theme. + Invalidate(true, true, true); + } break; + + case WM_WTSSESSION_CHANGE: { + switch (wParam) { + case WTS_CONSOLE_CONNECT: + case WTS_REMOTE_CONNECT: + case WTS_SESSION_UNLOCK: + // When a session becomes visible, we should invalidate. + Invalidate(true, true, true); + break; + default: + break; + } + } break; + + case WM_FONTCHANGE: { + // We only handle this message for the hidden window, + // as we only need to update the (global) font list once + // for any given change, not once per window! + if (mWindowType != WindowType::Invisible) { + break; + } + + // update the global font list + gfxPlatform::GetPlatform()->UpdateFontList(); + } break; + + case WM_SETTINGCHANGE: { + if (wParam == SPI_SETCLIENTAREAANIMATION || + wParam == SPI_SETKEYBOARDDELAY || wParam == SPI_SETMOUSEVANISH) { + // These need to update LookAndFeel cached values. + // They affect reduced motion settings / caret blink count / show + // pointer while typing, so no need to invalidate style / layout. + NotifyThemeChanged(widget::ThemeChangeKind::MediaQueriesOnly); + break; + } + if (wParam == SPI_SETFONTSMOOTHING || + wParam == SPI_SETFONTSMOOTHINGTYPE) { + gfxDWriteFont::UpdateSystemTextVars(); + break; + } + if (wParam == SPI_SETWORKAREA) { + // NB: We also refresh screens on WM_DISPLAYCHANGE but the rcWork + // values are sometimes wrong at that point. This message then + // arrives soon afterward, when we can get the right rcWork values. + ScreenHelperWin::RefreshScreens(); + break; + } + if (auto lParamString = reinterpret_cast<const wchar_t*>(lParam)) { + if (!wcscmp(lParamString, L"ImmersiveColorSet")) { + // This affects system colors (-moz-win-accentcolor), so gotta pass + // the style flag. + NotifyThemeChanged(widget::ThemeChangeKind::Style); + break; + } + + // UserInteractionMode, ConvertibleSlateMode, SystemDockMode may cause + // @media(pointer) queries to change, which layout needs to know about + // + // (WM_SETTINGCHANGE will be sent to all top-level windows, so we + // only respond to the hidden top-level window to avoid hammering + // layout with a bunch of NotifyThemeChanged() calls) + // + if (mWindowType == WindowType::Invisible) { + if (!wcscmp(lParamString, L"UserInteractionMode") || + !wcscmp(lParamString, L"ConvertibleSlateMode") || + !wcscmp(lParamString, L"SystemDockMode")) { + NotifyThemeChanged(widget::ThemeChangeKind::MediaQueriesOnly); + WindowsUIUtils::UpdateInTabletMode(); + } + } + } + } break; + + case WM_DEVICECHANGE: { + if (wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE) { + DEV_BROADCAST_HDR* hdr = reinterpret_cast<DEV_BROADCAST_HDR*>(lParam); + // Check dbch_devicetype explicitly since we will get other device types + // (e.g. DBT_DEVTYP_VOLUME) for some reasons even if we specify + // DBT_DEVTYP_DEVICEINTERFACE in the filter for + // RegisterDeviceNotification. + if (hdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + // This can only change media queries (any-hover/any-pointer). + NotifyThemeChanged(widget::ThemeChangeKind::MediaQueriesOnly); + } + } + } break; + + case WM_NCCALCSIZE: { + // NOTE: the following block is mirrored in PreXULSkeletonUI.cpp, and + // will need to be kept in sync. + if (mCustomNonClient) { + // If `wParam` is `FALSE`, `lParam` points to a `RECT` that contains + // the proposed window rectangle for our window. During our + // processing of the `WM_NCCALCSIZE` message, we are expected to + // modify the `RECT` that `lParam` points to, so that its value upon + // our return is the new client area. We must return 0 if `wParam` + // is `FALSE`. + // + // If `wParam` is `TRUE`, `lParam` points to a `NCCALCSIZE_PARAMS` + // struct. This struct contains an array of 3 `RECT`s, the first of + // which has the exact same meaning as the `RECT` that is pointed to + // by `lParam` when `wParam` is `FALSE`. The remaining `RECT`s, in + // conjunction with our return value, can + // be used to specify portions of the source and destination window + // rectangles that are valid and should be preserved. We opt not to + // implement an elaborate client-area preservation technique, and + // simply return 0, which means "preserve the entire old client area + // and align it with the upper-left corner of our new client area". + RECT* clientRect = + wParam ? &(reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam))->rgrc[0] + : (reinterpret_cast<RECT*>(lParam)); + auto margin = NonClientSizeMargin(); + clientRect->top += margin.top; + clientRect->left += margin.left; + clientRect->right -= margin.right; + clientRect->bottom -= margin.bottom; + // Make client rect's width and height more than 0 to + // avoid problems of webrender and angle. + clientRect->right = std::max(clientRect->right, clientRect->left + 1); + clientRect->bottom = std::max(clientRect->bottom, clientRect->top + 1); + + result = true; + *aRetValue = 0; + } + break; + } + + case WM_NCHITTEST: { + if (mInputRegion.mFullyTransparent) { + // Treat this window as transparent. + *aRetValue = HTTRANSPARENT; + result = true; + break; + } + + if (mInputRegion.mMargin) { + const LayoutDeviceIntPoint screenPoint(GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam)); + LayoutDeviceIntRect screenRect = GetScreenBounds(); + screenRect.Deflate(mInputRegion.mMargin); + if (!screenRect.Contains(screenPoint)) { + *aRetValue = HTTRANSPARENT; + result = true; + break; + } + } + + /* + * If an nc client area margin has been moved, we are responsible + * for calculating where the resize margins are and returning the + * appropriate set of hit test constants. DwmDefWindowProc (above) + * will handle hit testing on it's command buttons if we are on a + * composited desktop. + */ + + if (!mCustomNonClient) { + break; + } + + *aRetValue = + ClientMarginHitTestPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + result = true; + break; + } + + case WM_SETTEXT: + /* + * WM_SETTEXT paints the titlebar area. Avoid this if we have a + * custom titlebar we paint ourselves, or if we're the ones + * sending the message with an updated title + */ + + if (mSendingSetText || !mCustomNonClient || mNonClientMargins.top == -1) + break; + + { + // From msdn, the way around this is to disable the visible state + // temporarily. We need the text to be set but we don't want the + // redraw to occur. However, we need to make sure that we don't + // do this at the same time that a Present is happening. + // + // To do this we take mPresentLock in nsWindow::PreRender and + // if that lock is taken we wait before doing WM_SETTEXT + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->EnterPresentLock(); + } + DWORD style = GetWindowLong(mWnd, GWL_STYLE); + SetWindowLong(mWnd, GWL_STYLE, style & ~WS_VISIBLE); + *aRetValue = + CallWindowProcW(GetPrevWindowProc(), mWnd, msg, wParam, lParam); + SetWindowLong(mWnd, GWL_STYLE, style); + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->LeavePresentLock(); + } + + return true; + } + + case WM_NCACTIVATE: { + /* + * WM_NCACTIVATE paints nc areas. Avoid this and re-route painting + * through WM_NCPAINT via InvalidateNonClientRegion. + */ + UpdateGetWindowInfoCaptionStatus(FALSE != wParam); + + if (!mCustomNonClient) { + break; + } + + // There is a case that rendered result is not kept. Bug 1237617 + if (wParam == TRUE && !gfxEnv::MOZ_DISABLE_FORCE_PRESENT()) { + NS_DispatchToMainThread(NewRunnableMethod( + "nsWindow::ForcePresent", this, &nsWindow::ForcePresent)); + } + + // let the dwm handle nc painting on glass + // Never allow native painting if we are on fullscreen + if (mFrameState->GetSizeMode() != nsSizeMode_Fullscreen) break; + + if (wParam == TRUE) { + // going active + *aRetValue = FALSE; // ignored + result = true; + // invalidate to trigger a paint + InvalidateNonClientRegion(); + break; + } else { + // going inactive + *aRetValue = TRUE; // go ahead and deactive + result = true; + // invalidate to trigger a paint + InvalidateNonClientRegion(); + break; + } + } + + case WM_NCPAINT: { + /* + * ClearType changes often don't send a WM_SETTINGCHANGE message. But they + * do seem to always send a WM_NCPAINT message, so let's update on that. + */ + gfxDWriteFont::UpdateSystemTextVars(); + } break; + + case WM_POWERBROADCAST: + switch (wParam) { + case PBT_APMSUSPEND: + PostSleepWakeNotification(true); + break; + case PBT_APMRESUMEAUTOMATIC: + case PBT_APMRESUMECRITICAL: + case PBT_APMRESUMESUSPEND: + PostSleepWakeNotification(false); + break; + } + break; + + case WM_CLOSE: // close request + if (mWidgetListener) mWidgetListener->RequestWindowClose(this); + result = true; // abort window closure + break; + + case WM_DESTROY: + // clean up. + DestroyLayerManager(); + OnDestroy(); + result = true; + break; + + case WM_PAINT: + *aRetValue = (int)OnPaint(0); + result = true; + break; + + case WM_HOTKEY: + result = OnHotKey(wParam, lParam); + break; + + case WM_SYSCHAR: + case WM_CHAR: { + MSG nativeMsg = WinUtils::InitMSG(msg, wParam, lParam, mWnd); + result = ProcessCharMessage(nativeMsg, nullptr); + DispatchPendingEvents(); + } break; + + case WM_SYSKEYUP: + case WM_KEYUP: { + MSG nativeMsg = WinUtils::InitMSG(msg, wParam, lParam, mWnd); + nativeMsg.time = ::GetMessageTime(); + result = ProcessKeyUpMessage(nativeMsg, nullptr); + DispatchPendingEvents(); + } break; + + case WM_SYSKEYDOWN: + case WM_KEYDOWN: { + MSG nativeMsg = WinUtils::InitMSG(msg, wParam, lParam, mWnd); + result = ProcessKeyDownMessage(nativeMsg, nullptr); + DispatchPendingEvents(); + } break; + + // Say we've dealt with erasing the background. (This is actually handled in + // WM_PAINT, where necessary.) + case WM_ERASEBKGND: { + *aRetValue = 1; + result = true; + } break; + + case WM_MOUSEMOVE: { + LPARAM lParamScreen = lParamToScreen(lParam); + mSimulatedClientArea = IsSimulatedClientArea(GET_X_LPARAM(lParamScreen), + GET_Y_LPARAM(lParamScreen)); + + if (!mMousePresent && !sIsInMouseCapture) { + // First MOUSEMOVE over the client area. Ask for MOUSELEAVE + TRACKMOUSEEVENT mTrack; + mTrack.cbSize = sizeof(TRACKMOUSEEVENT); + mTrack.dwFlags = TME_LEAVE; + mTrack.dwHoverTime = 0; + mTrack.hwndTrack = mWnd; + TrackMouseEvent(&mTrack); + } + mMousePresent = true; + + // Suppress dispatch of pending events + // when mouse moves are generated by widget + // creation instead of user input. + POINT mp; + mp.x = GET_X_LPARAM(lParamScreen); + mp.y = GET_Y_LPARAM(lParamScreen); + bool userMovedMouse = false; + if ((sLastMouseMovePoint.x != mp.x) || (sLastMouseMovePoint.y != mp.y)) { + userMovedMouse = true; + } + + if (userMovedMouse) { + result = DispatchMouseEvent( + eMouseMove, wParam, lParam, false, MouseButton::ePrimary, + MOUSE_INPUT_SOURCE(), + mPointerEvents.GetCachedPointerInfo(msg, wParam)); + DispatchPendingEvents(); + } + } break; + + case WM_NCMOUSEMOVE: { + LPARAM lParamClient = lParamToClient(lParam); + if (IsSimulatedClientArea(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))) { + if (!sIsInMouseCapture) { + TRACKMOUSEEVENT mTrack; + mTrack.cbSize = sizeof(TRACKMOUSEEVENT); + mTrack.dwFlags = TME_LEAVE | TME_NONCLIENT; + mTrack.dwHoverTime = 0; + mTrack.hwndTrack = mWnd; + TrackMouseEvent(&mTrack); + } + // If we noticed the mouse moving in our draggable region, forward the + // message as a normal WM_MOUSEMOVE. + SendMessage(mWnd, WM_MOUSEMOVE, 0, lParamClient); + } else { + // We've transitioned from a draggable area to somewhere else within + // the non-client area - perhaps one of the edges of the window for + // resizing. + mSimulatedClientArea = false; + } + + if (mMousePresent && !sIsInMouseCapture && !mSimulatedClientArea) { + SendMessage(mWnd, WM_MOUSELEAVE, 0, 0); + } + } break; + + case WM_LBUTTONDOWN: { + result = + DispatchMouseEvent(eMouseDown, wParam, lParam, false, + MouseButton::ePrimary, MOUSE_INPUT_SOURCE(), + mPointerEvents.GetCachedPointerInfo(msg, wParam)); + DispatchPendingEvents(); + } break; + + case WM_LBUTTONUP: { + result = + DispatchMouseEvent(eMouseUp, wParam, lParam, false, + MouseButton::ePrimary, MOUSE_INPUT_SOURCE(), + mPointerEvents.GetCachedPointerInfo(msg, wParam)); + DispatchPendingEvents(); + } break; + + case WM_NCMOUSELEAVE: { + mSimulatedClientArea = false; + + if (EventIsInsideWindow(this)) { + // If we're handling WM_NCMOUSELEAVE and the mouse is still over the + // window, then by process of elimination, the mouse has moved from the + // non-client to client area, so no need to fall-through to the + // WM_MOUSELEAVE handler. We also need to re-register for the + // WM_MOUSELEAVE message, since according to the documentation at [1], + // all tracking requested via TrackMouseEvent is cleared once + // WM_NCMOUSELEAVE or WM_MOUSELEAVE fires. + // [1]: + // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-trackmouseevent + TRACKMOUSEEVENT mTrack; + mTrack.cbSize = sizeof(TRACKMOUSEEVENT); + mTrack.dwFlags = TME_LEAVE; + mTrack.dwHoverTime = 0; + mTrack.hwndTrack = mWnd; + TrackMouseEvent(&mTrack); + break; + } + // We've transitioned from non-client to outside of the window, so + // fall-through to the WM_MOUSELEAVE handler. + [[fallthrough]]; + } + case WM_MOUSELEAVE: { + if (!mMousePresent) break; + if (mSimulatedClientArea) break; + mMousePresent = false; + + // Check if the mouse is over the fullscreen transition window, if so + // clear sLastMouseMovePoint. This way the WM_MOUSEMOVE we get after the + // transition window disappears will not be ignored, even if the mouse + // hasn't moved. + if (mTransitionWnd && WindowAtMouse() == mTransitionWnd) { + sLastMouseMovePoint = {0}; + } + + // We need to check mouse button states and put them in for + // wParam. + WPARAM mouseState = (GetKeyState(VK_LBUTTON) ? MK_LBUTTON : 0) | + (GetKeyState(VK_MBUTTON) ? MK_MBUTTON : 0) | + (GetKeyState(VK_RBUTTON) ? MK_RBUTTON : 0); + // Synthesize an event position because we don't get one from + // WM_MOUSELEAVE. + LPARAM pos = lParamToClient(::GetMessagePos()); + DispatchMouseEvent(eMouseExitFromWidget, mouseState, pos, false, + MouseButton::ePrimary, MOUSE_INPUT_SOURCE()); + } break; + + case WM_CONTEXTMENU: { + // If the context menu is brought up by a touch long-press, then + // the APZ code is responsible for dealing with this, so we don't + // need to do anything. + if (mTouchWindow && + MOUSE_INPUT_SOURCE() == MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + MOZ_ASSERT(mAPZC); // since mTouchWindow is true, APZ must be enabled + result = true; + break; + } + + // If this WM_CONTEXTMENU is triggered by a mouse's secondary button up + // event in overscroll gutter, we shouldn't open context menu. + if (MOUSE_INPUT_SOURCE() == MouseEvent_Binding::MOZ_SOURCE_MOUSE && + mNeedsToPreventContextMenu) { + result = true; + break; + } + + // if the context menu is brought up from the keyboard, |lParam| + // will be -1. + LPARAM pos; + bool contextMenukey = false; + if (lParam == -1) { + contextMenukey = true; + pos = lParamToClient(GetMessagePos()); + } else { + pos = lParamToClient(lParam); + } + + result = DispatchMouseEvent( + eContextMenu, wParam, pos, contextMenukey, + contextMenukey ? MouseButton::ePrimary : MouseButton::eSecondary, + MOUSE_INPUT_SOURCE()); + if (lParam != -1 && !result && mCustomNonClient && + mDraggableRegion.Contains(GET_X_LPARAM(pos), GET_Y_LPARAM(pos))) { + // Blank area hit, throw up the system menu. + DisplaySystemMenu(mWnd, mFrameState->GetSizeMode(), mIsRTL, + GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + result = true; + } + } break; + + case WM_POINTERLEAVE: + case WM_POINTERDOWN: + case WM_POINTERUP: + case WM_POINTERUPDATE: + result = OnPointerEvents(msg, wParam, lParam); + if (result) { + DispatchPendingEvents(); + } + break; + + case DM_POINTERHITTEST: + if (mDmOwner) { + UINT contactId = GET_POINTERID_WPARAM(wParam); + POINTER_INPUT_TYPE pointerType; + if (mPointerEvents.GetPointerType(contactId, &pointerType) && + pointerType == PT_TOUCHPAD) { + mDmOwner->SetContact(contactId); + } + } + break; + + case WM_LBUTTONDBLCLK: + result = DispatchMouseEvent(eMouseDoubleClick, wParam, lParam, false, + MouseButton::ePrimary, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_MBUTTONDOWN: + result = DispatchMouseEvent(eMouseDown, wParam, lParam, false, + MouseButton::eMiddle, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_MBUTTONUP: + result = DispatchMouseEvent(eMouseUp, wParam, lParam, false, + MouseButton::eMiddle, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_MBUTTONDBLCLK: + result = DispatchMouseEvent(eMouseDoubleClick, wParam, lParam, false, + MouseButton::eMiddle, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_NCMBUTTONDOWN: + result = DispatchMouseEvent(eMouseDown, 0, lParamToClient(lParam), false, + MouseButton::eMiddle, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_NCMBUTTONUP: + result = DispatchMouseEvent(eMouseUp, 0, lParamToClient(lParam), false, + MouseButton::eMiddle, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_NCMBUTTONDBLCLK: + result = + DispatchMouseEvent(eMouseDoubleClick, 0, lParamToClient(lParam), + false, MouseButton::eMiddle, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_RBUTTONDOWN: + result = + DispatchMouseEvent(eMouseDown, wParam, lParam, false, + MouseButton::eSecondary, MOUSE_INPUT_SOURCE(), + mPointerEvents.GetCachedPointerInfo(msg, wParam)); + DispatchPendingEvents(); + break; + + case WM_RBUTTONUP: + result = + DispatchMouseEvent(eMouseUp, wParam, lParam, false, + MouseButton::eSecondary, MOUSE_INPUT_SOURCE(), + mPointerEvents.GetCachedPointerInfo(msg, wParam)); + DispatchPendingEvents(); + break; + + case WM_RBUTTONDBLCLK: + result = + DispatchMouseEvent(eMouseDoubleClick, wParam, lParam, false, + MouseButton::eSecondary, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_NCRBUTTONDOWN: + result = + DispatchMouseEvent(eMouseDown, 0, lParamToClient(lParam), false, + MouseButton::eSecondary, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_NCRBUTTONUP: + result = + DispatchMouseEvent(eMouseUp, 0, lParamToClient(lParam), false, + MouseButton::eSecondary, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_NCRBUTTONDBLCLK: + result = DispatchMouseEvent(eMouseDoubleClick, 0, lParamToClient(lParam), + false, MouseButton::eSecondary, + MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + // Windows doesn't provide to customize the behavior of 4th nor 5th button + // of mouse. If 5-button mouse works with standard mouse deriver of + // Windows, users cannot disable 4th button (browser back) nor 5th button + // (browser forward). We should allow to do it with our prefs since we can + // prevent Windows to generate WM_APPCOMMAND message if WM_XBUTTONUP + // messages are not sent to DefWindowProc. + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + case WM_NCXBUTTONDOWN: + case WM_NCXBUTTONUP: + *aRetValue = TRUE; + switch (GET_XBUTTON_WPARAM(wParam)) { + case XBUTTON1: + result = !Preferences::GetBool("mousebutton.4th.enabled", true); + break; + case XBUTTON2: + result = !Preferences::GetBool("mousebutton.5th.enabled", true); + break; + default: + break; + } + break; + + case WM_SIZING: { + if (mAspectRatio > 0) { + LPRECT rect = (LPRECT)lParam; + int32_t newWidth, newHeight; + + // The following conditions and switch statement borrow heavily from the + // Chromium source code from + // https://chromium.googlesource.com/chromium/src/+/456d6e533cfb4531995e0ef52c279d4b5aa8a352/ui/views/window/window_resize_utils.cc#45 + if (wParam == WMSZ_LEFT || wParam == WMSZ_RIGHT || + wParam == WMSZ_TOPLEFT || wParam == WMSZ_BOTTOMLEFT) { + newWidth = rect->right - rect->left; + newHeight = newWidth / mAspectRatio; + if (newHeight < mSizeConstraints.mMinSize.height) { + newHeight = mSizeConstraints.mMinSize.height; + newWidth = newHeight * mAspectRatio; + } else if (newHeight > mSizeConstraints.mMaxSize.height) { + newHeight = mSizeConstraints.mMaxSize.height; + newWidth = newHeight * mAspectRatio; + } + } else { + newHeight = rect->bottom - rect->top; + newWidth = newHeight * mAspectRatio; + if (newWidth < mSizeConstraints.mMinSize.width) { + newWidth = mSizeConstraints.mMinSize.width; + newHeight = newWidth / mAspectRatio; + } else if (newWidth > mSizeConstraints.mMaxSize.width) { + newWidth = mSizeConstraints.mMaxSize.width; + newHeight = newWidth / mAspectRatio; + } + } + + switch (wParam) { + case WMSZ_RIGHT: + case WMSZ_BOTTOM: + rect->right = newWidth + rect->left; + rect->bottom = rect->top + newHeight; + break; + case WMSZ_TOP: + rect->right = newWidth + rect->left; + rect->top = rect->bottom - newHeight; + break; + case WMSZ_LEFT: + case WMSZ_TOPLEFT: + rect->left = rect->right - newWidth; + rect->top = rect->bottom - newHeight; + break; + case WMSZ_TOPRIGHT: + rect->right = rect->left + newWidth; + rect->top = rect->bottom - newHeight; + break; + case WMSZ_BOTTOMLEFT: + rect->left = rect->right - newWidth; + rect->bottom = rect->top + newHeight; + break; + case WMSZ_BOTTOMRIGHT: + rect->right = rect->left + newWidth; + rect->bottom = rect->top + newHeight; + break; + } + } + + // When we get WM_ENTERSIZEMOVE we don't know yet if we're in a live + // resize or move event. Instead we wait for first VM_SIZING message + // within a ENTERSIZEMOVE to consider this a live resize event. + if (mResizeState == IN_SIZEMOVE) { + mResizeState = RESIZING; + NotifyLiveResizeStarted(); + } + break; + } + + case WM_MOVING: + FinishLiveResizing(MOVING); + if (WinUtils::IsPerMonitorDPIAware()) { + // Sometimes, we appear to miss a WM_DPICHANGED message while moving + // a window around. Therefore, call ChangedDPI and ResetLayout here + // if it appears that the window's scaling is not what we expect. + // This causes the prescontext and appshell window management code to + // check the appUnitsPerDevPixel value and current widget size, and + // refresh them if necessary. If nothing has changed, these calls will + // return without actually triggering any extra reflow or painting. + if (WinUtils::LogToPhysFactor(mWnd) != mDefaultScale) { + ChangedDPI(); + ResetLayout(); + if (mWidgetListener) { + mWidgetListener->UIResolutionChanged(); + } + } + } + break; + + case WM_ENTERSIZEMOVE: { + if (mResizeState == NOT_RESIZING) { + mResizeState = IN_SIZEMOVE; + } + break; + } + + case WM_EXITSIZEMOVE: { + FinishLiveResizing(NOT_RESIZING); + + if (!sIsInMouseCapture) { + NotifySizeMoveDone(); + } + + // Windows spins a separate hidden event loop when moving a window so we + // don't hear mouse events during this time and WM_EXITSIZEMOVE is fired + // when the hidden event loop exits. We set mDraggingWindowWithMouse to + // true in WM_NCLBUTTONDOWN when we started moving the window with the + // mouse so we know that if mDraggingWindowWithMouse is true, we can send + // a mouse up event. + if (mDraggingWindowWithMouse) { + mDraggingWindowWithMouse = false; + result = DispatchMouseEvent( + eMouseUp, wParam, lParam, false, MouseButton::ePrimary, + MOUSE_INPUT_SOURCE(), + mPointerEvents.GetCachedPointerInfo(msg, wParam)); + } + + break; + } + + case WM_DISPLAYCHANGE: { + ScreenHelperWin::RefreshScreens(); + if (mWidgetListener) { + mWidgetListener->UIResolutionChanged(); + } + break; + } + + case WM_NCLBUTTONDBLCLK: + DispatchMouseEvent(eMouseDoubleClick, 0, lParamToClient(lParam), false, + MouseButton::ePrimary, MOUSE_INPUT_SOURCE()); + result = DispatchMouseEvent(eMouseUp, 0, lParamToClient(lParam), false, + MouseButton::ePrimary, MOUSE_INPUT_SOURCE()); + DispatchPendingEvents(); + break; + + case WM_NCLBUTTONDOWN: { + // Dispatch a custom event when this happens in the draggable region, so + // that non-popup-based panels can react to it. This doesn't send an + // actual mousedown event because that would break dragging or interfere + // with other mousedown handling in the caption area. + if (ClientMarginHitTestPoint(GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam)) == HTCAPTION) { + DispatchCustomEvent(u"draggableregionleftmousedown"_ns); + mDraggingWindowWithMouse = true; + } + + if (IsWindowButton(wParam) && mCustomNonClient) { + DispatchMouseEvent(eMouseDown, wParamFromGlobalMouseState(), + lParamToClient(lParam), false, MouseButton::ePrimary, + MOUSE_INPUT_SOURCE(), nullptr, true); + DispatchPendingEvents(); + result = true; + } + break; + } + + case WM_APPCOMMAND: { + MSG nativeMsg = WinUtils::InitMSG(msg, wParam, lParam, mWnd); + result = HandleAppCommandMsg(nativeMsg, aRetValue); + break; + } + + // The WM_ACTIVATE event is fired when a window is raised or lowered, + // and the loword of wParam specifies which. But we don't want to tell + // the focus system about this until the WM_SETFOCUS or WM_KILLFOCUS + // events are fired. Instead, set either the sJustGotActivate or + // gJustGotDeactivate flags and activate/deactivate once the focus + // events arrive. + case WM_ACTIVATE: { + int32_t fActive = LOWORD(wParam); + if (mWidgetListener) { + if (WA_INACTIVE == fActive) { + // when minimizing a window, the deactivation and focus events will + // be fired in the reverse order. Instead, just deactivate right away. + // This can also happen when a modal system dialog is opened, so check + // if the last window to receive the WM_KILLFOCUS message was this one + // or a child of this one. + if (HIWORD(wParam) || + (mLastKillFocusWindow && + (GetTopLevelForFocus(mLastKillFocusWindow) == mWnd))) { + DispatchFocusToTopLevelWindow(false); + } else { + sJustGotDeactivate = true; + } + if (mIsTopWidgetWindow) { + mLastKeyboardLayout = KeyboardLayout::GetLayout(); + } + } else { + StopFlashing(); + + sJustGotActivate = true; + WidgetMouseEvent event(true, eMouseActivate, this, + WidgetMouseEvent::eReal); + InitEvent(event); + ModifierKeyState modifierKeyState; + modifierKeyState.InitInputEvent(event); + DispatchInputEvent(&event); + if (sSwitchKeyboardLayout && mLastKeyboardLayout) + ActivateKeyboardLayout(mLastKeyboardLayout, 0); + +#ifdef ACCESSIBILITY + a11y::LazyInstantiator::ResetUiaDetectionCache(); +#endif + } + } + } break; + + case WM_ACTIVATEAPP: { + // Bug 1851991: Sometimes this can be called before gfxPlatform::Init + // when a window is created very early. In that case we just forego + // setting this and accept the GPU process might briefly run at a lower + // priority. + if (GPUProcessManager::Get()) { + GPUProcessManager::Get()->SetAppInForeground(wParam); + } + } break; + + case WM_MOUSEACTIVATE: + // A popup with a parent owner should not be activated when clicked but + // should still allow the mouse event to be fired, so the return value + // is set to MA_NOACTIVATE. But if the owner isn't the frontmost window, + // just use default processing so that the window is activated. + if (IsPopup() && IsOwnerForegroundWindow()) { + *aRetValue = MA_NOACTIVATE; + result = true; + } + break; + + case WM_WINDOWPOSCHANGING: { + LPWINDOWPOS info = (LPWINDOWPOS)lParam; + OnWindowPosChanging(info); + result = true; + } break; + + // Workaround for race condition in explorer.exe. + case MOZ_WM_FULLSCREEN_STATE_UPDATE: { + TaskbarConcealer::OnAsyncStateUpdateRequest(mWnd); + result = true; + } break; + + case WM_GETMINMAXINFO: { + MINMAXINFO* mmi = (MINMAXINFO*)lParam; + // Set the constraints. The minimum size should also be constrained to the + // default window maximum size so that it fits on screen. + mmi->ptMinTrackSize.x = + std::min((int32_t)mmi->ptMaxTrackSize.x, + std::max((int32_t)mmi->ptMinTrackSize.x, + mSizeConstraints.mMinSize.width)); + mmi->ptMinTrackSize.y = + std::min((int32_t)mmi->ptMaxTrackSize.y, + std::max((int32_t)mmi->ptMinTrackSize.y, + mSizeConstraints.mMinSize.height)); + mmi->ptMaxTrackSize.x = std::min((int32_t)mmi->ptMaxTrackSize.x, + mSizeConstraints.mMaxSize.width); + mmi->ptMaxTrackSize.y = std::min((int32_t)mmi->ptMaxTrackSize.y, + mSizeConstraints.mMaxSize.height); + } break; + + case WM_SETFOCUS: { + WndProcUrgentInvocation::Marker _marker; + + // If previous focused window isn't ours, it must have received the + // redirected message. So, we should forget it. + if (!WinUtils::IsOurProcessWindow(HWND(wParam))) { + RedirectedKeyDownMessageManager::Forget(); + } + if (sJustGotActivate) { + DispatchFocusToTopLevelWindow(true); + } + TaskbarConcealer::OnFocusAcquired(this); + } break; + + case WM_KILLFOCUS: + if (sJustGotDeactivate) { + DispatchFocusToTopLevelWindow(false); + } else { + mLastKillFocusWindow = mWnd; + } + break; + + case WM_WINDOWPOSCHANGED: { + WINDOWPOS* wp = (LPWINDOWPOS)lParam; + OnWindowPosChanged(wp); + TaskbarConcealer::OnWindowPosChanged(this); + result = true; + } break; + + case WM_INPUTLANGCHANGEREQUEST: + *aRetValue = TRUE; + result = false; + break; + + case WM_INPUTLANGCHANGE: + KeyboardLayout::GetInstance()->OnLayoutChange( + reinterpret_cast<HKL>(lParam)); + nsBidiKeyboard::OnLayoutChange(); + result = false; // always pass to child window + break; + + case WM_DESTROYCLIPBOARD: { + nsIClipboard* clipboard; + nsresult rv = CallGetService(kCClipboardCID, &clipboard); + if (NS_SUCCEEDED(rv)) { + clipboard->EmptyClipboard(nsIClipboard::kGlobalClipboard); + NS_RELEASE(clipboard); + } + } break; + +#ifdef ACCESSIBILITY + case WM_GETOBJECT: { + *aRetValue = 0; + // Do explicit casting to make it working on 64bit systems (see bug 649236 + // for details). + int32_t objId = static_cast<DWORD>(lParam); + if (objId == OBJID_CLIENT) { // oleacc.dll will be loaded dynamically + RefPtr<IAccessible> root( + a11y::LazyInstantiator::GetRootAccessible(mWnd)); + if (root) { + *aRetValue = LresultFromObject(IID_IAccessible, wParam, root); + a11y::LazyInstantiator::EnableBlindAggregation(mWnd); + result = true; + } + } + } break; +#endif + + case WM_SYSCOMMAND: { + WPARAM const filteredWParam = (wParam & 0xFFF0); + + // SC_CLOSE may trigger a synchronous confirmation prompt. If we're in the + // middle of something important, put off responding to it. + if (filteredWParam == SC_CLOSE && WndProcUrgentInvocation::IsActive()) { + ::PostMessageW(mWnd, msg, wParam, lParam); + result = true; + break; + } + + if (mFrameState->GetSizeMode() == nsSizeMode_Fullscreen && + filteredWParam == SC_RESTORE && + GetCurrentShowCmd(mWnd) != SW_SHOWMINIMIZED) { + mFrameState->EnsureFullscreenMode(false); + result = true; + } + + // Handle the system menu manually when we're in full screen mode + // so we can set the appropriate options. + if (filteredWParam == SC_KEYMENU && lParam == VK_SPACE && + mFrameState->GetSizeMode() == nsSizeMode_Fullscreen) { + DisplaySystemMenu(mWnd, mFrameState->GetSizeMode(), mIsRTL, + MOZ_SYSCONTEXT_X_POS, MOZ_SYSCONTEXT_Y_POS); + result = true; + } + } break; + + case WM_DPICHANGED: { + LPRECT rect = (LPRECT)lParam; + OnDPIChanged(rect->left, rect->top, rect->right - rect->left, + rect->bottom - rect->top); + break; + } + + /* Gesture support events */ + case WM_TABLET_QUERYSYSTEMGESTURESTATUS: + // According to MS samples, this must be handled to enable + // rotational support in multi-touch drivers. + result = true; + *aRetValue = TABLET_ROTATE_GESTURE_ENABLE; + break; + + case WM_TOUCH: + result = OnTouch(wParam, lParam); + if (result) { + *aRetValue = 0; + } + break; + + case WM_GESTURE: + result = OnGesture(wParam, lParam); + break; + + case WM_GESTURENOTIFY: { + if (mWindowType != WindowType::Invisible) { + // A GestureNotify event is dispatched to decide which single-finger + // panning direction should be active (including none) and if pan + // feedback should be displayed. Java and plugin windows can make their + // own calls. + + GESTURENOTIFYSTRUCT* gestureinfo = (GESTURENOTIFYSTRUCT*)lParam; + nsPointWin touchPoint; + touchPoint = gestureinfo->ptsLocation; + touchPoint.ScreenToClient(mWnd); + WidgetGestureNotifyEvent gestureNotifyEvent(true, eGestureNotify, this); + gestureNotifyEvent.mRefPoint = + LayoutDeviceIntPoint::FromUnknownPoint(touchPoint); + nsEventStatus status; + DispatchEvent(&gestureNotifyEvent, status); + mDisplayPanFeedback = gestureNotifyEvent.mDisplayPanFeedback; + if (!mTouchWindow) + mGesture.SetWinGestureSupport(mWnd, gestureNotifyEvent.mPanDirection); + } + result = false; // should always bubble to DefWindowProc + } break; + + case WM_CLEAR: { + WidgetContentCommandEvent command(true, eContentCommandDelete, this); + DispatchWindowEvent(command); + result = true; + } break; + + case WM_CUT: { + WidgetContentCommandEvent command(true, eContentCommandCut, this); + DispatchWindowEvent(command); + result = true; + } break; + + case WM_COPY: { + WidgetContentCommandEvent command(true, eContentCommandCopy, this); + DispatchWindowEvent(command); + result = true; + } break; + + case WM_PASTE: { + WidgetContentCommandEvent command(true, eContentCommandPaste, this); + DispatchWindowEvent(command); + result = true; + } break; + + case EM_UNDO: { + WidgetContentCommandEvent command(true, eContentCommandUndo, this); + DispatchWindowEvent(command); + *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled); + result = true; + } break; + + case EM_REDO: { + WidgetContentCommandEvent command(true, eContentCommandRedo, this); + DispatchWindowEvent(command); + *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled); + result = true; + } break; + + case EM_CANPASTE: { + // Support EM_CANPASTE message only when wParam isn't specified or + // is plain text format. + if (wParam == 0 || wParam == CF_TEXT || wParam == CF_UNICODETEXT) { + WidgetContentCommandEvent command(true, eContentCommandPaste, this, + true); + DispatchWindowEvent(command); + *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled); + result = true; + } + } break; + + case EM_CANUNDO: { + WidgetContentCommandEvent command(true, eContentCommandUndo, this, true); + DispatchWindowEvent(command); + *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled); + result = true; + } break; + + case EM_CANREDO: { + WidgetContentCommandEvent command(true, eContentCommandRedo, this, true); + DispatchWindowEvent(command); + *aRetValue = (LRESULT)(command.mSucceeded && command.mIsEnabled); + result = true; + } break; + + case MOZ_WM_SKEWFIX: { + TimeStamp skewStamp; + if (CurrentWindowsTimeGetter::GetAndClearBackwardsSkewStamp(wParam, + &skewStamp)) { + TimeConverter().CompensateForBackwardsSkew(::GetMessageTime(), + skewStamp); + } + } break; + + default: { + if (msg == nsAppShell::GetTaskbarButtonCreatedMessage()) { + SetHasTaskbarIconBeenCreated(); + } + } break; + } + + //*aRetValue = result; + if (mWnd) { + return result; + } else { + // Events which caused mWnd destruction and aren't consumed + // will crash during the Windows default processing. + return true; + } +} + +void nsWindow::FinishLiveResizing(ResizeState aNewState) { + if (mResizeState == RESIZING) { + NotifyLiveResizeStopped(); + } + mResizeState = aNewState; + ForcePresent(); +} + +/************************************************************** + * + * SECTION: Event processing helpers + * + * Special processing for certain event types and + * synthesized events. + * + **************************************************************/ + +LayoutDeviceIntMargin nsWindow::NonClientSizeMargin( + const LayoutDeviceIntMargin& aNonClientOffset) const { + return LayoutDeviceIntMargin(mCaptionHeight - aNonClientOffset.top, + mHorResizeMargin - aNonClientOffset.right, + mVertResizeMargin - aNonClientOffset.bottom, + mHorResizeMargin - aNonClientOffset.left); +} + +int32_t nsWindow::ClientMarginHitTestPoint(int32_t aX, int32_t aY) { + const nsSizeMode sizeMode = mFrameState->GetSizeMode(); + if (sizeMode == nsSizeMode_Minimized || sizeMode == nsSizeMode_Fullscreen) { + return HTCLIENT; + } + + // Calculations are done in screen coords + const LayoutDeviceIntRect winRect = GetScreenBounds(); + const LayoutDeviceIntPoint point(aX, aY); + + // hit return constants: + // HTBORDER - non-resizable border + // HTBOTTOM, HTLEFT, HTRIGHT, HTTOP - resizable border + // HTBOTTOMLEFT, HTBOTTOMRIGHT - resizable corner + // HTTOPLEFT, HTTOPRIGHT - resizable corner + // HTCAPTION - general title bar area + // HTCLIENT - area considered the client + // HTCLOSE - hovering over the close button + // HTMAXBUTTON - maximize button + // HTMINBUTTON - minimize button + + int32_t testResult = HTCLIENT; + const bool isResizable = + sizeMode != nsSizeMode_Maximized && + (mBorderStyle & + (BorderStyle::All | BorderStyle::ResizeH | BorderStyle::Default)); + + LayoutDeviceIntMargin nonClientSizeMargin = NonClientSizeMargin(); + + // Ensure being accessible to borders of window. Even if contents are in + // this area, the area must behave as border. + nonClientSizeMargin.EnsureAtLeast( + LayoutDeviceIntMargin(kResizableBorderMinSize, kResizableBorderMinSize, + kResizableBorderMinSize, kResizableBorderMinSize)); + + LayoutDeviceIntRect clientRect = winRect; + clientRect.Deflate(nonClientSizeMargin); + + const bool allowContentOverride = + sizeMode == nsSizeMode_Maximized || clientRect.Contains(point); + + // The border size. If there is no content under mouse cursor, the border + // size should be larger than the values in system settings. Otherwise, + // contents under the mouse cursor should be able to override the behavior. + // E.g., user must expect that Firefox button always opens the popup menu + // even when the user clicks on the above edge of it. + LayoutDeviceIntMargin borderSize = nonClientSizeMargin; + borderSize.EnsureAtLeast( + LayoutDeviceIntMargin(mVertResizeMargin, mHorResizeMargin, + mVertResizeMargin, mHorResizeMargin)); + + bool top = false; + bool bottom = false; + bool left = false; + bool right = false; + + if (point.y >= winRect.y && point.y < winRect.y + borderSize.top) { + top = true; + } else if (point.y <= winRect.YMost() && + point.y > winRect.YMost() - borderSize.bottom) { + bottom = true; + } + + // (the 2x case here doubles the resize area for corners) + int multiplier = (top || bottom) ? 2 : 1; + if (point.x >= winRect.x && + point.x < winRect.x + (multiplier * borderSize.left)) { + left = true; + } else if (point.x <= winRect.XMost() && + point.x > winRect.XMost() - (multiplier * borderSize.right)) { + right = true; + } + + bool inResizeRegion = false; + if (isResizable) { + if (top) { + testResult = HTTOP; + if (left) { + testResult = HTTOPLEFT; + } else if (right) { + testResult = HTTOPRIGHT; + } + } else if (bottom) { + testResult = HTBOTTOM; + if (left) { + testResult = HTBOTTOMLEFT; + } else if (right) { + testResult = HTBOTTOMRIGHT; + } + } else { + if (left) { + testResult = HTLEFT; + } + if (right) { + testResult = HTRIGHT; + } + } + inResizeRegion = (testResult != HTCLIENT); + } else { + if (top) { + testResult = HTCAPTION; + } else if (bottom || left || right) { + testResult = HTBORDER; + } + } + + if (!sIsInMouseCapture && allowContentOverride) { + { + POINT pt = {aX, aY}; + ::ScreenToClient(mWnd, &pt); + + if (pt.x == mCachedHitTestPoint.x.value && + pt.y == mCachedHitTestPoint.y.value && + TimeStamp::Now() - mCachedHitTestTime < + TimeDuration::FromMilliseconds(HITTEST_CACHE_LIFETIME_MS)) { + return mCachedHitTestResult; + } + + mCachedHitTestPoint = {pt.x, pt.y}; + mCachedHitTestTime = TimeStamp::Now(); + } + + auto pt = mCachedHitTestPoint; + + if (mWindowBtnRect[WindowButtonType::Minimize].Contains(pt)) { + testResult = HTMINBUTTON; + } else if (mWindowBtnRect[WindowButtonType::Maximize].Contains(pt)) { + testResult = HTMAXBUTTON; + } else if (mWindowBtnRect[WindowButtonType::Close].Contains(pt)) { + testResult = HTCLOSE; + } else if (!inResizeRegion) { + // If we're in the resize region, avoid overriding that with either a + // drag or a client result; resize takes priority over either (but not + // over the window controls, which is why we check this after those). + if (mDraggableRegion.Contains(pt)) { + testResult = HTCAPTION; + } else { + testResult = HTCLIENT; + } + } + + mCachedHitTestResult = testResult; + } + + return testResult; +} + +bool nsWindow::IsSimulatedClientArea(int32_t screenX, int32_t screenY) { + int32_t testResult = ClientMarginHitTestPoint(screenX, screenY); + return testResult == HTCAPTION || IsWindowButton(testResult); +} + +bool nsWindow::IsWindowButton(int32_t hitTestResult) { + return hitTestResult == HTMINBUTTON || hitTestResult == HTMAXBUTTON || + hitTestResult == HTCLOSE; +} + +TimeStamp nsWindow::GetMessageTimeStamp(LONG aEventTime) const { + CurrentWindowsTimeGetter getCurrentTime(mWnd); + return TimeConverter().GetTimeStampFromSystemTime(aEventTime, getCurrentTime); +} + +void nsWindow::PostSleepWakeNotification(const bool aIsSleepMode) { + // Retain the previous mode that was notified to observers + static bool sWasSleepMode = false; + + // Only notify observers if mode changed + if (aIsSleepMode == sWasSleepMode) return; + + sWasSleepMode = aIsSleepMode; + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) + observerService->NotifyObservers(nullptr, + aIsSleepMode + ? NS_WIDGET_SLEEP_OBSERVER_TOPIC + : NS_WIDGET_WAKE_OBSERVER_TOPIC, + nullptr); +} + +LRESULT nsWindow::ProcessCharMessage(const MSG& aMsg, bool* aEventDispatched) { + if (IMEHandler::IsComposingOn(this)) { + IMEHandler::NotifyIME(this, REQUEST_TO_COMMIT_COMPOSITION); + } + // These must be checked here too as a lone WM_CHAR could be received + // if a child window didn't handle it (for example Alt+Space in a content + // window) + ModifierKeyState modKeyState; + NativeKey nativeKey(this, aMsg, modKeyState); + return static_cast<LRESULT>(nativeKey.HandleCharMessage(aEventDispatched)); +} + +LRESULT nsWindow::ProcessKeyUpMessage(const MSG& aMsg, bool* aEventDispatched) { + ModifierKeyState modKeyState; + NativeKey nativeKey(this, aMsg, modKeyState); + bool result = nativeKey.HandleKeyUpMessage(aEventDispatched); + if (aMsg.wParam == VK_F10) { + // Bug 1382199: Windows default behavior will trigger the System menu bar + // when F10 is released. Among other things, this causes the System menu bar + // to appear when a web page overrides the contextmenu event. We *never* + // want this default behavior, so eat this key (never pass it to Windows). + return true; + } + return result; +} + +LRESULT nsWindow::ProcessKeyDownMessage(const MSG& aMsg, + bool* aEventDispatched) { + // If this method doesn't call NativeKey::HandleKeyDownMessage(), this method + // must clean up the redirected message information itself. For more + // information, see above comment of + // RedirectedKeyDownMessageManager::AutoFlusher class definition in + // KeyboardLayout.h. + RedirectedKeyDownMessageManager::AutoFlusher redirectedMsgFlusher(this, aMsg); + + ModifierKeyState modKeyState; + + NativeKey nativeKey(this, aMsg, modKeyState); + LRESULT result = + static_cast<LRESULT>(nativeKey.HandleKeyDownMessage(aEventDispatched)); + // HandleKeyDownMessage cleaned up the redirected message information + // itself, so, we should do nothing. + redirectedMsgFlusher.Cancel(); + + if (aMsg.wParam == VK_MENU || + (aMsg.wParam == VK_F10 && !modKeyState.IsShift())) { + // We need to let Windows handle this keypress, + // by returning false, if there's a native menu + // bar somewhere in our containing window hierarchy. + // Otherwise we handle the keypress and don't pass + // it on to Windows, by returning true. + bool hasNativeMenu = false; + HWND hWnd = mWnd; + while (hWnd) { + if (::GetMenu(hWnd)) { + hasNativeMenu = true; + break; + } + hWnd = ::GetParent(hWnd); + } + result = !hasNativeMenu; + } + + return result; +} + +nsresult nsWindow::SynthesizeNativeKeyEvent( + int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode, + uint32_t aModifierFlags, const nsAString& aCharacters, + const nsAString& aUnmodifiedCharacters, nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "keyevent"); + + KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance(); + return keyboardLayout->SynthesizeNativeKeyEvent( + this, aNativeKeyboardLayout, aNativeKeyCode, aModifierFlags, aCharacters, + aUnmodifiedCharacters); +} + +nsresult nsWindow::SynthesizeNativeMouseEvent( + LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage, + MouseButton aButton, nsIWidget::Modifiers aModifierFlags, + nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "mouseevent"); + + INPUT input; + memset(&input, 0, sizeof(input)); + + // TODO (bug 1693240): + // Now, we synthesize native mouse events asynchronously since we want to + // synthesize the event on the front window at the point. However, Windows + // does not provide a way to set modifier only while a mouse message is + // being handled, and MOUSEEVENTF_MOVE may be coalesced by Windows. So, we + // need a trick for handling it. + + switch (aNativeMessage) { + case NativeMouseMessage::Move: + input.mi.dwFlags = MOUSEEVENTF_MOVE; + // Reset sLastMouseMovePoint so that even if we're moving the mouse + // to the position it's already at, we still dispatch a mousemove + // event, because the callers of this function expect that. + sLastMouseMovePoint = {0}; + break; + case NativeMouseMessage::ButtonDown: + case NativeMouseMessage::ButtonUp: { + const bool isDown = aNativeMessage == NativeMouseMessage::ButtonDown; + switch (aButton) { + case MouseButton::ePrimary: + input.mi.dwFlags = isDown ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP; + break; + case MouseButton::eMiddle: + input.mi.dwFlags = + isDown ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP; + break; + case MouseButton::eSecondary: + input.mi.dwFlags = + isDown ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP; + break; + case MouseButton::eX1: + input.mi.dwFlags = isDown ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP; + input.mi.mouseData = XBUTTON1; + break; + case MouseButton::eX2: + input.mi.dwFlags = isDown ? MOUSEEVENTF_XDOWN : MOUSEEVENTF_XUP; + input.mi.mouseData = XBUTTON2; + break; + default: + return NS_ERROR_INVALID_ARG; + } + break; + } + case NativeMouseMessage::EnterWindow: + case NativeMouseMessage::LeaveWindow: + MOZ_ASSERT_UNREACHABLE("Non supported mouse event on Windows"); + return NS_ERROR_INVALID_ARG; + } + + input.type = INPUT_MOUSE; + ::SetCursorPos(aPoint.x, aPoint.y); + ::SendInput(1, &input, sizeof(INPUT)); + + return NS_OK; +} + +nsresult nsWindow::SynthesizeNativeMouseScrollEvent( + LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX, + double aDeltaY, double aDeltaZ, uint32_t aModifierFlags, + uint32_t aAdditionalFlags, nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "mousescrollevent"); + return MouseScrollHandler::SynthesizeNativeMouseScrollEvent( + this, aPoint, aNativeMessage, + (aNativeMessage == WM_MOUSEWHEEL || aNativeMessage == WM_VSCROLL) + ? static_cast<int32_t>(aDeltaY) + : static_cast<int32_t>(aDeltaX), + aModifierFlags, aAdditionalFlags); +} + +nsresult nsWindow::SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase, + LayoutDeviceIntPoint aPoint, + double aDeltaX, double aDeltaY, + int32_t aModifierFlags, + nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "touchpadpanevent"); + DirectManipulationOwner::SynthesizeNativeTouchpadPan( + this, aEventPhase, aPoint, aDeltaX, aDeltaY, aModifierFlags); + return NS_OK; +} + +static void MaybeLogPosChanged(HWND aWnd, WINDOWPOS* wp) { +#ifdef WINSTATE_DEBUG_OUTPUT + if (aWnd == WinUtils::GetTopLevelHWND(aWnd)) { + MOZ_LOG(gWindowsLog, LogLevel::Info, ("*** OnWindowPosChanged: [ top] ")); + } else { + MOZ_LOG(gWindowsLog, LogLevel::Info, ("*** OnWindowPosChanged: [child] ")); + } + MOZ_LOG(gWindowsLog, LogLevel::Info, ("WINDOWPOS flags:")); + if (wp->flags & SWP_FRAMECHANGED) { + MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_FRAMECHANGED ")); + } + if (wp->flags & SWP_SHOWWINDOW) { + MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_SHOWWINDOW ")); + } + if (wp->flags & SWP_NOSIZE) { + MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_NOSIZE ")); + } + if (wp->flags & SWP_HIDEWINDOW) { + MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_HIDEWINDOW ")); + } + if (wp->flags & SWP_NOZORDER) { + MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_NOZORDER ")); + } + if (wp->flags & SWP_NOACTIVATE) { + MOZ_LOG(gWindowsLog, LogLevel::Info, ("SWP_NOACTIVATE ")); + } + MOZ_LOG(gWindowsLog, LogLevel::Info, ("\n")); +#endif +} + +/************************************************************** + * + * SECTION: OnXXX message handlers + * + * For message handlers that need to be broken out or + * implemented in specific platform code. + * + **************************************************************/ + +void nsWindow::OnWindowPosChanged(WINDOWPOS* wp) { + if (!wp) { + return; + } + + MaybeLogPosChanged(mWnd, wp); + + // Handle window size mode changes + if (wp->flags & SWP_FRAMECHANGED) { + // Bug 566135 - Windows theme code calls show window on SW_SHOWMINIMIZED + // windows when fullscreen games disable desktop composition. If we're + // minimized and not being activated, ignore the event and let windows + // handle it. + if (mFrameState->GetSizeMode() == nsSizeMode_Minimized && + (wp->flags & SWP_NOACTIVATE)) { + return; + } + + mFrameState->OnFrameChanged(); + + if (mFrameState->GetSizeMode() == nsSizeMode_Minimized) { + // Skip window size change events below on minimization. + return; + } + } + + // Notify visibility change when window is activated. + if (!(wp->flags & SWP_NOACTIVATE) && NeedsToTrackWindowOcclusionState()) { + WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged( + this, mFrameState->GetSizeMode() != nsSizeMode_Minimized); + } + + // Handle window position changes + if (!(wp->flags & SWP_NOMOVE)) { + mBounds.MoveTo(wp->x, wp->y); + NotifyWindowMoved(wp->x, wp->y); + } + + // Handle window size changes + if (!(wp->flags & SWP_NOSIZE)) { + RECT r; + int32_t newWidth, newHeight; + + ::GetWindowRect(mWnd, &r); + + newWidth = r.right - r.left; + newHeight = r.bottom - r.top; + + if (newWidth > mLastSize.width) { + RECT drect; + + // getting wider + drect.left = wp->x + mLastSize.width; + drect.top = wp->y; + drect.right = drect.left + (newWidth - mLastSize.width); + drect.bottom = drect.top + newHeight; + + ::RedrawWindow(mWnd, &drect, nullptr, + RDW_INVALIDATE | RDW_NOERASE | RDW_NOINTERNALPAINT | + RDW_ERASENOW | RDW_ALLCHILDREN); + } + if (newHeight > mLastSize.height) { + RECT drect; + + // getting taller + drect.left = wp->x; + drect.top = wp->y + mLastSize.height; + drect.right = drect.left + newWidth; + drect.bottom = drect.top + (newHeight - mLastSize.height); + + ::RedrawWindow(mWnd, &drect, nullptr, + RDW_INVALIDATE | RDW_NOERASE | RDW_NOINTERNALPAINT | + RDW_ERASENOW | RDW_ALLCHILDREN); + } + + mBounds.SizeTo(newWidth, newHeight); + mLastSize.width = newWidth; + mLastSize.height = newHeight; + +#ifdef WINSTATE_DEBUG_OUTPUT + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("*** Resize window: %d x %d x %d x %d\n", wp->x, wp->y, newWidth, + newHeight)); +#endif + + if (mAspectRatio > 0) { + // It's possible (via Windows Aero Snap) that the size of the window + // has changed such that it violates the aspect ratio constraint. If so, + // queue up an event to enforce the aspect ratio constraint and repaint. + // When resized with Windows Aero Snap, we are in the NOT_RESIZING state. + float newAspectRatio = (float)newWidth / newHeight; + if (mResizeState == NOT_RESIZING && mAspectRatio != newAspectRatio) { + // Hold a reference to self alive and pass it into the lambda to make + // sure this nsIWidget stays alive long enough to run this function. + nsCOMPtr<nsIWidget> self(this); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "EnforceAspectRatio", [self, this, newWidth]() -> void { + if (mWnd) { + Resize(newWidth, newWidth / mAspectRatio, true); + } + })); + } + } + + // If a maximized window is resized, recalculate the non-client margins. + if (mFrameState->GetSizeMode() == nsSizeMode_Maximized) { + if (UpdateNonClientMargins(true)) { + // gecko resize event already sent by UpdateNonClientMargins. + return; + } + } + } + + // Notify the widget listener for size change of client area for gecko + // events. This needs to be done when either window size is changed, + // or window frame is changed. They may not happen together. + // However, we don't invoke that for popup when window frame changes, + // because popups may trigger frame change before size change via + // {Set,Clear}ThemeRegion they invoke in Resize. That would make the + // code below call OnResize with a wrong client size first, which can + // lead to flickerling for some popups. + if (!(wp->flags & SWP_NOSIZE) || + ((wp->flags & SWP_FRAMECHANGED) && !IsPopup())) { + RECT r; + LayoutDeviceIntSize clientSize; + if (::GetClientRect(mWnd, &r)) { + clientSize = WinUtils::ToIntRect(r).Size(); + } else { + clientSize = mBounds.Size(); + } + // Send a gecko resize event + OnResize(clientSize); + } +} + +void nsWindow::OnWindowPosChanging(WINDOWPOS* info) { + // Update non-client margins if the frame size is changing, and let the + // browser know we are changing size modes, so alternative css can kick in. + // If we're going into fullscreen mode, ignore this, since it'll reset + // margins to normal mode. + if (info->flags & SWP_FRAMECHANGED && !(info->flags & SWP_NOSIZE)) { + mFrameState->OnFrameChanging(); + } + + // Force fullscreen. This works around a bug in Windows 10 1809 where + // using fullscreen when a window is "snapped" causes a spurious resize + // smaller than the full screen, see bug 1482920. + if (mFrameState->GetSizeMode() == nsSizeMode_Fullscreen && + !(info->flags & SWP_NOMOVE) && !(info->flags & SWP_NOSIZE)) { + nsCOMPtr<nsIScreenManager> screenmgr = + do_GetService(sScreenManagerContractID); + if (screenmgr) { + LayoutDeviceIntRect bounds(info->x, info->y, info->cx, info->cy); + DesktopIntRect deskBounds = + RoundedToInt(bounds / GetDesktopToDeviceScale()); + nsCOMPtr<nsIScreen> screen; + screenmgr->ScreenForRect(deskBounds.X(), deskBounds.Y(), + deskBounds.Width(), deskBounds.Height(), + getter_AddRefs(screen)); + + if (screen) { + auto rect = screen->GetRect(); + info->x = rect.x; + info->y = rect.y; + info->cx = rect.width; + info->cy = rect.height; + } + } + } + + // enforce local z-order rules + if (!(info->flags & SWP_NOZORDER)) { + HWND hwndAfter = info->hwndInsertAfter; + + nsWindow* aboveWindow = 0; + nsWindowZ placement; + + if (hwndAfter == HWND_BOTTOM) + placement = nsWindowZBottom; + else if (hwndAfter == HWND_TOP || hwndAfter == HWND_TOPMOST || + hwndAfter == HWND_NOTOPMOST) + placement = nsWindowZTop; + else { + placement = nsWindowZRelative; + aboveWindow = WinUtils::GetNSWindowPtr(hwndAfter); + } + + if (mWidgetListener) { + nsCOMPtr<nsIWidget> actualBelow = nullptr; + if (mWidgetListener->ZLevelChanged(false, &placement, aboveWindow, + getter_AddRefs(actualBelow))) { + if (placement == nsWindowZBottom) + info->hwndInsertAfter = HWND_BOTTOM; + else if (placement == nsWindowZTop) + info->hwndInsertAfter = HWND_TOP; + else { + info->hwndInsertAfter = + (HWND)actualBelow->GetNativeData(NS_NATIVE_WINDOW); + } + } + } + } + // prevent rude external programs from making hidden window visible + if (mWindowType == WindowType::Invisible) info->flags &= ~SWP_SHOWWINDOW; + + // When waking from sleep or switching out of tablet mode, Windows 10 + // Version 1809 will reopen popup windows that should be hidden. Detect + // this case and refuse to show the window. + static bool sDWMUnhidesPopups = IsWin10Sep2018UpdateOrLater(); + if (sDWMUnhidesPopups && (info->flags & SWP_SHOWWINDOW) && + mWindowType == WindowType::Popup && mWidgetListener && + mWidgetListener->ShouldNotBeVisible()) { + info->flags &= ~SWP_SHOWWINDOW; + } +} + +void nsWindow::UserActivity() { + // Check if we have the idle service, if not we try to get it. + if (!mIdleService) { + mIdleService = do_GetService("@mozilla.org/widget/useridleservice;1"); + } + + // Check that we now have the idle service. + if (mIdleService) { + mIdleService->ResetIdleTimeOut(0); + } +} + +// Helper function for TouchDeviceNeedsPanGestureConversion(PTOUCHINPUT, +// uint32_t). +static bool TouchDeviceNeedsPanGestureConversion(HANDLE aSource) { + std::string deviceName; + UINT dataSize = 0; + // The first call just queries how long the name string will be. + GetRawInputDeviceInfoA(aSource, RIDI_DEVICENAME, nullptr, &dataSize); + if (!dataSize || dataSize > 0x10000) { + return false; + } + deviceName.resize(dataSize); + // The second call actually populates the string. + UINT result = GetRawInputDeviceInfoA(aSource, RIDI_DEVICENAME, &deviceName[0], + &dataSize); + if (result == UINT_MAX) { + return false; + } + // The affected device name is "\\?\VIRTUAL_DIGITIZER", but each backslash + // needs to be escaped with another one. + std::string expectedDeviceName = "\\\\?\\VIRTUAL_DIGITIZER"; + // For some reason, the dataSize returned by the first call is double the + // actual length of the device name (as if it were returning the size of a + // wide-character string in bytes) even though we are using the narrow + // version of the API. For the comparison against the expected device name + // to pass, we truncate the buffer to be no longer tha the expected device + // name. + if (deviceName.substr(0, expectedDeviceName.length()) != expectedDeviceName) { + return false; + } + + RID_DEVICE_INFO deviceInfo; + deviceInfo.cbSize = sizeof(deviceInfo); + dataSize = sizeof(deviceInfo); + result = + GetRawInputDeviceInfoA(aSource, RIDI_DEVICEINFO, &deviceInfo, &dataSize); + if (result == UINT_MAX) { + return false; + } + // The device identifiers that we check for here come from bug 1355162 + // comment 1 (see also bug 1511901 comment 35). + return deviceInfo.dwType == RIM_TYPEHID && deviceInfo.hid.dwVendorId == 0 && + deviceInfo.hid.dwProductId == 0 && + deviceInfo.hid.dwVersionNumber == 1 && + deviceInfo.hid.usUsagePage == 13 && deviceInfo.hid.usUsage == 4; +} + +// Determine if the touch device that originated |aOSEvent| needs to have +// touch events representing a two-finger gesture converted to pan +// gesture events. +// We only do this for touch devices with a specific name and identifiers. +static bool TouchDeviceNeedsPanGestureConversion(PTOUCHINPUT aOSEvent, + uint32_t aTouchCount) { + if (!StaticPrefs::apz_windows_check_for_pan_gesture_conversion()) { + return false; + } + if (aTouchCount == 0) { + return false; + } + HANDLE source = aOSEvent[0].hSource; + + // Cache the result of this computation for each touch device. + // Touch devices are identified by the HANDLE stored in the hSource + // field of TOUCHINPUT. + static std::map<HANDLE, bool> sResultCache; + auto [iter, inserted] = sResultCache.emplace(source, false); + if (inserted) { + iter->second = TouchDeviceNeedsPanGestureConversion(source); + } + return iter->second; +} + +Maybe<PanGestureInput> nsWindow::ConvertTouchToPanGesture( + const MultiTouchInput& aTouchInput, PTOUCHINPUT aOSEvent) { + // Checks if the touch device that originated the touch event is one + // for which we want to convert the touch events to pang gesture events. + bool shouldConvert = TouchDeviceNeedsPanGestureConversion( + aOSEvent, aTouchInput.mTouches.Length()); + if (!shouldConvert) { + return Nothing(); + } + + // Only two-finger gestures need conversion. + if (aTouchInput.mTouches.Length() != 2) { + return Nothing(); + } + + PanGestureInput::PanGestureType eventType = PanGestureInput::PANGESTURE_PAN; + if (aTouchInput.mType == MultiTouchInput::MULTITOUCH_START) { + eventType = PanGestureInput::PANGESTURE_START; + } else if (aTouchInput.mType == MultiTouchInput::MULTITOUCH_END) { + eventType = PanGestureInput::PANGESTURE_END; + } else if (aTouchInput.mType == MultiTouchInput::MULTITOUCH_CANCEL) { + eventType = PanGestureInput::PANGESTURE_CANCELLED; + } + + // Use the midpoint of the two touches as the start point of the pan gesture. + ScreenPoint focusPoint = (aTouchInput.mTouches[0].mScreenPoint + + aTouchInput.mTouches[1].mScreenPoint) / + 2; + // To compute the displacement of the pan gesture, we keep track of the + // location of the previous event. + ScreenPoint displacement = (eventType == PanGestureInput::PANGESTURE_START) + ? ScreenPoint(0, 0) + : (focusPoint - mLastPanGestureFocus); + mLastPanGestureFocus = focusPoint; + + // We need to negate the displacement because for a touch event, moving the + // fingers down results in scrolling up, but for a touchpad gesture, we want + // moving the fingers down to result in scrolling down. + PanGestureInput result(eventType, aTouchInput.mTimeStamp, focusPoint, + -displacement, aTouchInput.modifiers); + result.mSimulateMomentum = true; + + return Some(result); +} + +// Dispatch an event that originated as an OS touch event. +// Usually, we want to dispatch it as a touch event, but some touchpads +// produce touch events for two-finger scrolling, which need to be converted +// to pan gesture events for correct behaviour. +void nsWindow::DispatchTouchOrPanGestureInput(MultiTouchInput& aTouchInput, + PTOUCHINPUT aOSEvent) { + if (Maybe<PanGestureInput> panInput = + ConvertTouchToPanGesture(aTouchInput, aOSEvent)) { + DispatchPanGestureInput(*panInput); + return; + } + + DispatchTouchInput(aTouchInput); +} + +bool nsWindow::OnTouch(WPARAM wParam, LPARAM lParam) { + uint32_t cInputs = LOWORD(wParam); + PTOUCHINPUT pInputs = new TOUCHINPUT[cInputs]; + + if (GetTouchInputInfo((HTOUCHINPUT)lParam, cInputs, pInputs, + sizeof(TOUCHINPUT))) { + MultiTouchInput touchInput, touchEndInput; + + // Walk across the touch point array processing each contact point. + for (uint32_t i = 0; i < cInputs; i++) { + bool addToEvent = false, addToEndEvent = false; + + // N.B.: According with MS documentation + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317334(v=vs.85).aspx + // TOUCHEVENTF_DOWN cannot be combined with TOUCHEVENTF_MOVE or + // TOUCHEVENTF_UP. Possibly, it means that TOUCHEVENTF_MOVE and + // TOUCHEVENTF_UP can be combined together. + + if (pInputs[i].dwFlags & (TOUCHEVENTF_DOWN | TOUCHEVENTF_MOVE)) { + if (touchInput.mTimeStamp.IsNull()) { + // Initialize a touch event to send. + touchInput.mType = MultiTouchInput::MULTITOUCH_MOVE; + touchInput.mTimeStamp = GetMessageTimeStamp(::GetMessageTime()); + ModifierKeyState modifierKeyState; + touchInput.modifiers = modifierKeyState.GetModifiers(); + } + // Pres shell expects this event to be a eTouchStart + // if any new contact points have been added since the last event sent. + if (pInputs[i].dwFlags & TOUCHEVENTF_DOWN) { + touchInput.mType = MultiTouchInput::MULTITOUCH_START; + } + addToEvent = true; + } + if (pInputs[i].dwFlags & TOUCHEVENTF_UP) { + // Pres shell expects removed contacts points to be delivered in a + // separate eTouchEnd event containing only the contact points that were + // removed. + if (touchEndInput.mTimeStamp.IsNull()) { + // Initialize a touch event to send. + touchEndInput.mType = MultiTouchInput::MULTITOUCH_END; + touchEndInput.mTimeStamp = GetMessageTimeStamp(::GetMessageTime()); + ModifierKeyState modifierKeyState; + touchEndInput.modifiers = modifierKeyState.GetModifiers(); + } + addToEndEvent = true; + } + if (!addToEvent && !addToEndEvent) { + // Filter out spurious Windows events we don't understand, like palm + // contact. + continue; + } + + // Setup the touch point we'll append to the touch event array. + nsPointWin touchPoint; + touchPoint.x = TOUCH_COORD_TO_PIXEL(pInputs[i].x); + touchPoint.y = TOUCH_COORD_TO_PIXEL(pInputs[i].y); + touchPoint.ScreenToClient(mWnd); + + // Initialize the touch data. + SingleTouchData touchData( + pInputs[i].dwID, // aIdentifier + ScreenIntPoint::FromUnknownPoint(touchPoint), // aScreenPoint + // The contact area info cannot be trusted even when + // TOUCHINPUTMASKF_CONTACTAREA is set when the input source is pen, + // which somehow violates the API docs. (bug 1710509) Ultimately the + // dwFlags check will become redundant since we want to migrate to + // WM_POINTER for pens. (bug 1707075) + (pInputs[i].dwMask & TOUCHINPUTMASKF_CONTACTAREA) && + !(pInputs[i].dwFlags & TOUCHEVENTF_PEN) + ? ScreenSize(TOUCH_COORD_TO_PIXEL(pInputs[i].cxContact) / 2, + TOUCH_COORD_TO_PIXEL(pInputs[i].cyContact) / 2) + : ScreenSize(1, 1), // aRadius + 0.0f, // aRotationAngle + 0.0f); // aForce + + // Append touch data to the appropriate event. + if (addToEvent) { + touchInput.mTouches.AppendElement(touchData); + } + if (addToEndEvent) { + touchEndInput.mTouches.AppendElement(touchData); + } + } + + // Dispatch touch start and touch move event if we have one. + if (!touchInput.mTimeStamp.IsNull()) { + DispatchTouchOrPanGestureInput(touchInput, pInputs); + } + // Dispatch touch end event if we have one. + if (!touchEndInput.mTimeStamp.IsNull()) { + DispatchTouchOrPanGestureInput(touchEndInput, pInputs); + } + } + + delete[] pInputs; + CloseTouchInputHandle((HTOUCHINPUT)lParam); + return true; +} + +// Gesture event processing. Handles WM_GESTURE events. +bool nsWindow::OnGesture(WPARAM wParam, LPARAM lParam) { + // Treatment for pan events which translate into scroll events: + if (mGesture.IsPanEvent(lParam)) { + if (!mGesture.ProcessPanMessage(mWnd, wParam, lParam)) + return false; // ignore + + nsEventStatus status; + + WidgetWheelEvent wheelEvent(true, eWheel, this); + + ModifierKeyState modifierKeyState; + modifierKeyState.InitInputEvent(wheelEvent); + + wheelEvent.mButton = 0; + wheelEvent.mTimeStamp = GetMessageTimeStamp(::GetMessageTime()); + wheelEvent.mInputSource = MouseEvent_Binding::MOZ_SOURCE_TOUCH; + + bool endFeedback = true; + + if (mGesture.PanDeltaToPixelScroll(wheelEvent)) { + DispatchEvent(&wheelEvent, status); + } + + if (mDisplayPanFeedback) { + mGesture.UpdatePanFeedbackX( + mWnd, DeprecatedAbs(RoundDown(wheelEvent.mOverflowDeltaX)), + endFeedback); + mGesture.UpdatePanFeedbackY( + mWnd, DeprecatedAbs(RoundDown(wheelEvent.mOverflowDeltaY)), + endFeedback); + mGesture.PanFeedbackFinalize(mWnd, endFeedback); + } + + CloseGestureInfoHandle((HGESTUREINFO)lParam); + + return true; + } + + // Other gestures translate into simple gesture events: + WidgetSimpleGestureEvent event(true, eVoidEvent, this); + if (!mGesture.ProcessGestureMessage(mWnd, wParam, lParam, event)) { + return false; // fall through to DefWndProc + } + + // Polish up and send off the new event + ModifierKeyState modifierKeyState; + modifierKeyState.InitInputEvent(event); + event.mButton = 0; + event.mTimeStamp = GetMessageTimeStamp(::GetMessageTime()); + event.mInputSource = MouseEvent_Binding::MOZ_SOURCE_TOUCH; + + nsEventStatus status; + DispatchEvent(&event, status); + if (status == nsEventStatus_eIgnore) { + return false; // Ignored, fall through + } + + // Only close this if we process and return true. + CloseGestureInfoHandle((HGESTUREINFO)lParam); + + return true; // Handled +} + +// WM_DESTROY event handler +void nsWindow::OnDestroy() { + mOnDestroyCalled = true; + + // If this is a toplevel window, notify the taskbar concealer to clean up any + // relevant state. + if (!mParent) { + TaskbarConcealer::OnWindowDestroyed(mWnd); + } + + // Make sure we don't get destroyed in the process of tearing down. + nsCOMPtr<nsIWidget> kungFuDeathGrip(this); + + // Dispatch the destroy notification. + if (!mInDtor) NotifyWindowDestroyed(); + + // Prevent the widget from sending additional events. + mWidgetListener = nullptr; + mAttachedWidgetListener = nullptr; + + DestroyDirectManipulation(); + + if (mWnd == mLastKillFocusWindow) { + mLastKillFocusWindow = nullptr; + } + // Unregister notifications from terminal services + ::WTSUnRegisterSessionNotification(mWnd); + + // We will stop receiving native events after dissociating from our native + // window. We will also disappear from the output of WinUtils::GetNSWindowPtr + // for that window. + DissociateFromNativeWindow(); + + // Once mWidgetListener is cleared and the subclass is reset, sCurrentWindow + // can be cleared. (It's used in tracking windows for mouse events.) + if (sCurrentWindow == this) sCurrentWindow = nullptr; + + // Disconnects us from our parent, will call our GetParent(). + nsBaseWidget::Destroy(); + + // Release references to children, device context, toolkit, and app shell. + nsBaseWidget::OnDestroy(); + + // Clear our native parent handle. + // XXX Windows will take care of this in the proper order, and + // SetParent(nullptr)'s remove child on the parent already took place in + // nsBaseWidget's Destroy call above. + // SetParent(nullptr); + mParent = nullptr; + + // We have to destroy the native drag target before we null out our window + // pointer. + EnableDragDrop(false); + + // If we're going away and for some reason we're still the rollup widget, + // rollup and turn off capture. + nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); + nsCOMPtr<nsIWidget> rollupWidget; + if (rollupListener) { + rollupWidget = rollupListener->GetRollupWidget(); + } + if (this == rollupWidget) { + rollupListener->Rollup({}); + CaptureRollupEvents(false); + } + + IMEHandler::OnDestroyWindow(this); + + // Free GDI window class objects + if (mBrush) { + VERIFY(::DeleteObject(mBrush)); + mBrush = nullptr; + } + + // Destroy any custom cursor resources. + if (mCursor.IsCustom()) { + SetCursor(Cursor{eCursor_standard}); + } + + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->OnDestroyWindow(); + } + mBasicLayersSurface = nullptr; + + // Finalize panning feedback to possibly restore window displacement + mGesture.PanFeedbackFinalize(mWnd, true); + + // Clear the main HWND. + mWnd = nullptr; +} + +// Send a resize message to the listener +bool nsWindow::OnResize(const LayoutDeviceIntSize& aSize) { + if (mCompositorWidgetDelegate && + !mCompositorWidgetDelegate->OnWindowResize(aSize)) { + return false; + } + + bool result = false; + if (mWidgetListener) { + result = mWidgetListener->WindowResized(this, aSize.width, aSize.height); + } + + // If there is an attached view, inform it as well as the normal widget + // listener. + if (mAttachedWidgetListener) { + return mAttachedWidgetListener->WindowResized(this, aSize.width, + aSize.height); + } + + return result; +} + +void nsWindow::OnSizeModeChange() { + const nsSizeMode mode = mFrameState->GetSizeMode(); + + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("nsWindow::OnSizeModeChange() sizeMode %d", mode)); + + if (NeedsToTrackWindowOcclusionState()) { + WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged( + this, mode != nsSizeMode_Minimized); + + wr::DebugFlags flags{0}; + flags._0 = gfx::gfxVars::WebRenderDebugFlags(); + bool debugEnabled = bool(flags & wr::DebugFlags::WINDOW_VISIBILITY_DBG); + if (debugEnabled && mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyVisibilityUpdated(mode, + mIsFullyOccluded); + } + } + + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->OnWindowModeChange(mode); + } + + if (mWidgetListener) { + mWidgetListener->SizeModeChanged(mode); + } +} + +bool nsWindow::OnHotKey(WPARAM wParam, LPARAM lParam) { return true; } + +bool nsWindow::IsPopup() { return mWindowType == WindowType::Popup; } + +bool nsWindow::ShouldUseOffMainThreadCompositing() { + if (mWindowType == WindowType::Popup && mPopupType == PopupType::Tooltip) { + return false; + } + + // Content rendering of popup is always done by child window. + // See nsDocumentViewer::ShouldAttachToTopLevel(). + if (mWindowType == WindowType::Popup && !mIsChildWindow) { + MOZ_ASSERT(!mParent); + return false; + } + + return nsBaseWidget::ShouldUseOffMainThreadCompositing(); +} + +void nsWindow::WindowUsesOMTC() { + ULONG_PTR style = ::GetClassLongPtr(mWnd, GCL_STYLE); + if (!style) { + NS_WARNING("Could not get window class style"); + return; + } + style |= CS_HREDRAW | CS_VREDRAW; + DebugOnly<ULONG_PTR> result = ::SetClassLongPtr(mWnd, GCL_STYLE, style); + NS_WARNING_ASSERTION(result, "Could not reset window class style"); +} + +void nsWindow::OnDPIChanged(int32_t x, int32_t y, int32_t width, + int32_t height) { + // Don't try to handle WM_DPICHANGED for popup windows (see bug 1239353); + // they remain tied to their original parent's resolution. + if (mWindowType == WindowType::Popup) { + return; + } + if (StaticPrefs::layout_css_devPixelsPerPx() > 0.0) { + return; + } + mDefaultScale = -1.0; // force recomputation of scale factor + + if (mResizeState != RESIZING && + mFrameState->GetSizeMode() == nsSizeMode_Normal) { + // Limit the position (if not in the middle of a drag-move) & size, + // if it would overflow the destination screen + nsCOMPtr<nsIScreenManager> sm = do_GetService(sScreenManagerContractID); + if (sm) { + nsCOMPtr<nsIScreen> screen; + sm->ScreenForRect(x, y, width, height, getter_AddRefs(screen)); + if (screen) { + int32_t availLeft, availTop, availWidth, availHeight; + screen->GetAvailRect(&availLeft, &availTop, &availWidth, &availHeight); + if (mResizeState != MOVING) { + x = std::max(x, availLeft); + y = std::max(y, availTop); + } + width = std::min(width, availWidth); + height = std::min(height, availHeight); + } + } + + Resize(x, y, width, height, true); + } + UpdateNonClientMargins(); + ChangedDPI(); + ResetLayout(); +} + +// Callback to generate OnCloakChanged pseudo-events. +/* static */ +void nsWindow::OnCloakEvent(HWND aWnd, bool aCloaked) { + MOZ_ASSERT(NS_IsMainThread()); + + const char* const kEventName = aCloaked ? "CLOAKED" : "UNCLOAKED"; + nsWindow* pWin = WinUtils::GetNSWindowPtr(aWnd); + if (!pWin) { + MOZ_LOG( + sCloakingLog, LogLevel::Debug, + ("Received %s event for HWND %p (not an nsWindow)", kEventName, aWnd)); + return; + } + + const char* const kWasCloakedStr = pWin->mIsCloaked ? "cloaked" : "uncloaked"; + if (mozilla::IsCloaked(aWnd) == pWin->mIsCloaked) { + MOZ_LOG(sCloakingLog, LogLevel::Debug, + ("Received redundant %s event for %s HWND %p; discarding", + kEventName, kWasCloakedStr, aWnd)); + return; + } + + MOZ_LOG( + sCloakingLog, LogLevel::Info, + ("Received %s event for %s HWND %p", kEventName, kWasCloakedStr, aWnd)); + + // Cloaking events like the one we've just received are sent asynchronously. + // Rather than process them one-by-one, we jump the gun a bit and perform + // updates on all newly cloaked/uncloaked nsWindows at once. This also lets us + // batch operations that consider more than one window's state. + struct Item { + nsWindow* win; + bool nowCloaked; + }; + nsTArray<Item> changedWindows; + + mozilla::EnumerateThreadWindows([&](HWND hwnd) { + nsWindow* pWin = WinUtils::GetNSWindowPtr(hwnd); + if (!pWin) { + return; + } + + const bool isCloaked = mozilla::IsCloaked(hwnd); + if (isCloaked != pWin->mIsCloaked) { + changedWindows.AppendElement(Item{pWin, isCloaked}); + } + }); + + if (changedWindows.IsEmpty()) { + return; + } + + for (const Item& item : changedWindows) { + item.win->OnCloakChanged(item.nowCloaked); + } + + nsWindow::TaskbarConcealer::OnCloakChanged(); +} + +void nsWindow::OnCloakChanged(bool aCloaked) { + MOZ_LOG(sCloakingLog, LogLevel::Info, + ("Calling OnCloakChanged(): HWND %p, aCloaked %s", mWnd, + aCloaked ? "true" : "false")); + mIsCloaked = aCloaked; +} + +/************************************************************** + ************************************************************** + ** + ** BLOCK: IME management and accessibility + ** + ** Handles managing IME input and accessibility. + ** + ************************************************************** + **************************************************************/ + +void nsWindow::SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) { + InputContext newInputContext = aContext; + IMEHandler::SetInputContext(this, newInputContext, aAction); + mInputContext = newInputContext; +} + +InputContext nsWindow::GetInputContext() { + mInputContext.mIMEState.mOpen = IMEState::CLOSED; + if (WinUtils::IsIMEEnabled(mInputContext) && IMEHandler::GetOpenState(this)) { + mInputContext.mIMEState.mOpen = IMEState::OPEN; + } else { + mInputContext.mIMEState.mOpen = IMEState::CLOSED; + } + return mInputContext; +} + +TextEventDispatcherListener* nsWindow::GetNativeTextEventDispatcherListener() { + return IMEHandler::GetNativeTextEventDispatcherListener(); +} + +#ifdef ACCESSIBILITY +# ifdef DEBUG +# define NS_LOG_WMGETOBJECT(aWnd, aHwnd, aAcc) \ + if (a11y::logging::IsEnabled(a11y::logging::ePlatforms)) { \ + printf( \ + "Get the window:\n {\n HWND: %p, parent HWND: %p, wndobj: " \ + "%p,\n", \ + aHwnd, ::GetParent(aHwnd), aWnd); \ + printf(" acc: %p", aAcc); \ + if (aAcc) { \ + nsAutoString name; \ + aAcc->Name(name); \ + printf(", accname: %s", NS_ConvertUTF16toUTF8(name).get()); \ + } \ + printf("\n }\n"); \ + } + +# else +# define NS_LOG_WMGETOBJECT(aWnd, aHwnd, aAcc) +# endif + +a11y::LocalAccessible* nsWindow::GetAccessible() { + // If the pref was ePlatformIsDisabled, return null here, disabling a11y. + if (a11y::PlatformDisabledState() == a11y::ePlatformIsDisabled) + return nullptr; + + if (mInDtor || mOnDestroyCalled || mWindowType == WindowType::Invisible) { + return nullptr; + } + + // In case of popup window return a popup accessible. + nsView* view = nsView::GetViewFor(this); + if (view) { + nsIFrame* frame = view->GetFrame(); + if (frame && nsLayoutUtils::IsPopup(frame)) { + nsAccessibilityService* accService = GetOrCreateAccService(); + if (accService) { + a11y::DocAccessible* docAcc = + GetAccService()->GetDocAccessible(frame->PresShell()); + if (docAcc) { + NS_LOG_WMGETOBJECT( + this, mWnd, + docAcc->GetAccessibleOrDescendant(frame->GetContent())); + return docAcc->GetAccessibleOrDescendant(frame->GetContent()); + } + } + } + } + + // otherwise root document accessible. + NS_LOG_WMGETOBJECT(this, mWnd, GetRootAccessible()); + return GetRootAccessible(); +} +#endif + +/************************************************************** + ************************************************************** + ** + ** BLOCK: Transparency + ** + ** Window transparency helpers. + ** + ************************************************************** + **************************************************************/ + +void nsWindow::SetWindowTranslucencyInner(TransparencyMode aMode) { + if (aMode == mTransparencyMode) return; + + // stop on dialogs and popups! + HWND hWnd = WinUtils::GetTopLevelHWND(mWnd, true); + nsWindow* parent = WinUtils::GetNSWindowPtr(hWnd); + + if (!parent) { + NS_WARNING("Trying to use transparent chrome in an embedded context"); + return; + } + + if (parent != this) { + NS_WARNING( + "Setting SetWindowTranslucencyInner on a parent this is not us!"); + } + + if (aMode == TransparencyMode::Transparent) { + // If we're switching to the use of a transparent window, hide the chrome + // on our parent. + HideWindowChrome(true); + } else if (mHideChrome && + mTransparencyMode == TransparencyMode::Transparent) { + // if we're switching out of transparent, re-enable our parent's chrome. + HideWindowChrome(false); + } + + LONG_PTR style = ::GetWindowLongPtrW(hWnd, GWL_STYLE), + exStyle = ::GetWindowLongPtr(hWnd, GWL_EXSTYLE); + + if (parent->mIsVisible) { + style |= WS_VISIBLE; + if (parent->mFrameState->GetSizeMode() == nsSizeMode_Maximized) { + style |= WS_MAXIMIZE; + } else if (parent->mFrameState->GetSizeMode() == nsSizeMode_Minimized) { + style |= WS_MINIMIZE; + } + } + + if (aMode == TransparencyMode::Transparent) + exStyle |= WS_EX_LAYERED; + else + exStyle &= ~WS_EX_LAYERED; + + VERIFY_WINDOW_STYLE(style); + ::SetWindowLongPtrW(hWnd, GWL_STYLE, style); + ::SetWindowLongPtrW(hWnd, GWL_EXSTYLE, exStyle); + + mTransparencyMode = aMode; + + if (mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->UpdateTransparency(aMode); + } +} + +/************************************************************** + ************************************************************** + ** + ** BLOCK: Popup rollup hooks + ** + ** Deals with CaptureRollup on popup windows. + ** + ************************************************************** + **************************************************************/ + +// Schedules a timer for a window, so we can rollup after processing the hook +// event +void nsWindow::ScheduleHookTimer(HWND aWnd, UINT aMsgId) { + // In some cases multiple hooks may be scheduled + // so ignore any other requests once one timer is scheduled + if (sHookTimerId == 0) { + // Remember the window handle and the message ID to be used later + sRollupMsgId = aMsgId; + sRollupMsgWnd = aWnd; + // Schedule native timer for doing the rollup after + // this event is done being processed + sHookTimerId = ::SetTimer(nullptr, 0, 0, (TIMERPROC)HookTimerForPopups); + NS_ASSERTION(sHookTimerId, "Timer couldn't be created."); + } +} + +#ifdef POPUP_ROLLUP_DEBUG_OUTPUT +int gLastMsgCode = 0; +extern MSGFEventMsgInfo gMSGFEvents[]; +#endif + +// Process Menu messages, rollup when popup is clicked. +LRESULT CALLBACK nsWindow::MozSpecialMsgFilter(int code, WPARAM wParam, + LPARAM lParam) { +#ifdef POPUP_ROLLUP_DEBUG_OUTPUT + if (sProcessHook) { + MSG* pMsg = (MSG*)lParam; + + int inx = 0; + while (gMSGFEvents[inx].mId != code && gMSGFEvents[inx].mStr != nullptr) { + inx++; + } + if (code != gLastMsgCode) { + if (gMSGFEvents[inx].mId == code) { +# ifdef DEBUG + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("MozSpecialMessageProc - code: 0x%X - %s hw: %p\n", code, + gMSGFEvents[inx].mStr, pMsg->hwnd)); +# endif + } else { +# ifdef DEBUG + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("MozSpecialMessageProc - code: 0x%X - %d hw: %p\n", code, + gMSGFEvents[inx].mId, pMsg->hwnd)); +# endif + } + gLastMsgCode = code; + } + PrintEvent(pMsg->message, FALSE, FALSE); + } +#endif // #ifdef POPUP_ROLLUP_DEBUG_OUTPUT + + if (sProcessHook && code == MSGF_MENU) { + MSG* pMsg = (MSG*)lParam; + ScheduleHookTimer(pMsg->hwnd, pMsg->message); + } + + return ::CallNextHookEx(sMsgFilterHook, code, wParam, lParam); +} + +// Process all mouse messages. Roll up when a click is in a native window +// that doesn't have an nsIWidget. +LRESULT CALLBACK nsWindow::MozSpecialMouseProc(int code, WPARAM wParam, + LPARAM lParam) { + if (sProcessHook) { + switch (WinUtils::GetNativeMessage(wParam)) { + case WM_LBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: { + MOUSEHOOKSTRUCT* ms = (MOUSEHOOKSTRUCT*)lParam; + nsIWidget* mozWin = WinUtils::GetNSWindowPtr(ms->hwnd); + if (!mozWin) { + ScheduleHookTimer(ms->hwnd, (UINT)wParam); + } + break; + } + } + } + return ::CallNextHookEx(sCallMouseHook, code, wParam, lParam); +} + +// Process all messages. Roll up when the window is moving, or +// is resizing or when maximized or mininized. +LRESULT CALLBACK nsWindow::MozSpecialWndProc(int code, WPARAM wParam, + LPARAM lParam) { +#ifdef POPUP_ROLLUP_DEBUG_OUTPUT + if (sProcessHook) { + CWPSTRUCT* cwpt = (CWPSTRUCT*)lParam; + PrintEvent(cwpt->message, FALSE, FALSE); + } +#endif + + if (sProcessHook) { + CWPSTRUCT* cwpt = (CWPSTRUCT*)lParam; + if (cwpt->message == WM_MOVING || cwpt->message == WM_SIZING || + cwpt->message == WM_GETMINMAXINFO) { + ScheduleHookTimer(cwpt->hwnd, (UINT)cwpt->message); + } + } + + return ::CallNextHookEx(sCallProcHook, code, wParam, lParam); +} + +// Register the special "hooks" for dropdown processing. +void nsWindow::RegisterSpecialDropdownHooks() { + NS_ASSERTION(!sMsgFilterHook, "sMsgFilterHook must be NULL!"); + NS_ASSERTION(!sCallProcHook, "sCallProcHook must be NULL!"); + + DISPLAY_NMM_PRT("***************** Installing Msg Hooks ***************\n"); + + // Install msg hook for moving the window and resizing + if (!sMsgFilterHook) { + DISPLAY_NMM_PRT("***** Hooking sMsgFilterHook!\n"); + sMsgFilterHook = SetWindowsHookEx(WH_MSGFILTER, MozSpecialMsgFilter, + nullptr, GetCurrentThreadId()); +#ifdef POPUP_ROLLUP_DEBUG_OUTPUT + if (!sMsgFilterHook) { + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("***** SetWindowsHookEx is NOT installed for WH_MSGFILTER!\n")); + } +#endif + } + + // Install msg hook for menus + if (!sCallProcHook) { + DISPLAY_NMM_PRT("***** Hooking sCallProcHook!\n"); + sCallProcHook = SetWindowsHookEx(WH_CALLWNDPROC, MozSpecialWndProc, nullptr, + GetCurrentThreadId()); +#ifdef POPUP_ROLLUP_DEBUG_OUTPUT + if (!sCallProcHook) { + MOZ_LOG( + gWindowsLog, LogLevel::Info, + ("***** SetWindowsHookEx is NOT installed for WH_CALLWNDPROC!\n")); + } +#endif + } + + // Install msg hook for the mouse + if (!sCallMouseHook) { + DISPLAY_NMM_PRT("***** Hooking sCallMouseHook!\n"); + sCallMouseHook = SetWindowsHookEx(WH_MOUSE, MozSpecialMouseProc, nullptr, + GetCurrentThreadId()); +#ifdef POPUP_ROLLUP_DEBUG_OUTPUT + if (!sCallMouseHook) { + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("***** SetWindowsHookEx is NOT installed for WH_MOUSE!\n")); + } +#endif + } +} + +// Unhook special message hooks for dropdowns. +void nsWindow::UnregisterSpecialDropdownHooks() { + DISPLAY_NMM_PRT( + "***************** De-installing Msg Hooks ***************\n"); + + if (sCallProcHook) { + DISPLAY_NMM_PRT("***** Unhooking sCallProcHook!\n"); + if (!::UnhookWindowsHookEx(sCallProcHook)) { + DISPLAY_NMM_PRT("***** UnhookWindowsHookEx failed for sCallProcHook!\n"); + } + sCallProcHook = nullptr; + } + + if (sMsgFilterHook) { + DISPLAY_NMM_PRT("***** Unhooking sMsgFilterHook!\n"); + if (!::UnhookWindowsHookEx(sMsgFilterHook)) { + DISPLAY_NMM_PRT("***** UnhookWindowsHookEx failed for sMsgFilterHook!\n"); + } + sMsgFilterHook = nullptr; + } + + if (sCallMouseHook) { + DISPLAY_NMM_PRT("***** Unhooking sCallMouseHook!\n"); + if (!::UnhookWindowsHookEx(sCallMouseHook)) { + DISPLAY_NMM_PRT("***** UnhookWindowsHookEx failed for sCallMouseHook!\n"); + } + sCallMouseHook = nullptr; + } +} + +// This timer is designed to only fire one time at most each time a "hook" +// function is used to rollup the dropdown. In some cases, the timer may be +// scheduled from the hook, but that hook event or a subsequent event may roll +// up the dropdown before this timer function is executed. +// +// For example, if an MFC control takes focus, the combobox will lose focus and +// rollup before this function fires. +VOID CALLBACK nsWindow::HookTimerForPopups(HWND hwnd, UINT uMsg, UINT idEvent, + DWORD dwTime) { + if (sHookTimerId != 0) { + // if the window is nullptr then we need to use the ID to kill the timer + DebugOnly<BOOL> status = ::KillTimer(nullptr, sHookTimerId); + NS_ASSERTION(status, "Hook Timer was not killed."); + sHookTimerId = 0; + } + + if (sRollupMsgId != 0) { + // Note: DealWithPopups does the check to make sure that the rollup widget + // is set. + LRESULT popupHandlingResult; + nsAutoRollup autoRollup; + DealWithPopups(sRollupMsgWnd, sRollupMsgId, 0, 0, &popupHandlingResult); + sRollupMsgId = 0; + sRollupMsgWnd = nullptr; + } +} + +static bool IsDifferentThreadWindow(HWND aWnd) { + return ::GetCurrentThreadId() != ::GetWindowThreadProcessId(aWnd, nullptr); +} + +// static +bool nsWindow::EventIsInsideWindow(nsWindow* aWindow, + Maybe<POINT> aEventPoint) { + RECT r; + ::GetWindowRect(aWindow->mWnd, &r); + POINT mp; + if (aEventPoint) { + mp = *aEventPoint; + } else { + DWORD pos = ::GetMessagePos(); + mp.x = GET_X_LPARAM(pos); + mp.y = GET_Y_LPARAM(pos); + } + + auto margin = aWindow->mInputRegion.mMargin; + if (margin > 0) { + r.top += margin; + r.bottom -= margin; + r.left += margin; + r.right -= margin; + } + + // was the event inside this window? + return static_cast<bool>(::PtInRect(&r, mp)); +} + +// static +bool nsWindow::GetPopupsToRollup(nsIRollupListener* aRollupListener, + uint32_t* aPopupsToRollup, + Maybe<POINT> aEventPoint) { + // If we're dealing with menus, we probably have submenus and we don't want + // to rollup some of them if the click is in a parent menu of the current + // submenu. + *aPopupsToRollup = UINT32_MAX; + AutoTArray<nsIWidget*, 5> widgetChain; + uint32_t sameTypeCount = aRollupListener->GetSubmenuWidgetChain(&widgetChain); + for (uint32_t i = 0; i < widgetChain.Length(); ++i) { + nsIWidget* widget = widgetChain[i]; + if (EventIsInsideWindow(static_cast<nsWindow*>(widget), aEventPoint)) { + // Don't roll up if the mouse event occurred within a menu of the + // same type. If the mouse event occurred in a menu higher than that, + // roll up, but pass the number of popups to Rollup so that only those + // of the same type close up. + if (i < sameTypeCount) { + return false; + } + + *aPopupsToRollup = sameTypeCount; + break; + } + } + return true; +} + +// static +bool nsWindow::NeedsToHandleNCActivateDelayed(HWND aWnd) { + // While popup is open, popup window might be activated by other application. + // At this time, we need to take back focus to the previous window but it + // causes flickering its nonclient area because WM_NCACTIVATE comes before + // WM_ACTIVATE and we cannot know which window will take focus at receiving + // WM_NCACTIVATE. Therefore, we need a hack for preventing the flickerling. + // + // If non-popup window receives WM_NCACTIVATE at deactivating, default + // wndproc shouldn't handle it as deactivating. Instead, at receiving + // WM_ACTIVIATE after that, WM_NCACTIVATE should be sent again manually. + // This returns true if the window needs to handle WM_NCACTIVATE later. + + nsWindow* window = WinUtils::GetNSWindowPtr(aWnd); + return window && !window->IsPopup(); +} + +static bool IsTouchSupportEnabled(HWND aWnd) { + nsWindow* topWindow = + WinUtils::GetNSWindowPtr(WinUtils::GetTopLevelHWND(aWnd, true)); + return topWindow ? topWindow->IsTouchWindow() : false; +} + +static Maybe<POINT> GetSingleTouch(WPARAM wParam, LPARAM lParam) { + Maybe<POINT> ret; + uint32_t cInputs = LOWORD(wParam); + if (cInputs != 1) { + return ret; + } + TOUCHINPUT input; + if (GetTouchInputInfo((HTOUCHINPUT)lParam, cInputs, &input, + sizeof(TOUCHINPUT))) { + ret.emplace(); + ret->x = TOUCH_COORD_TO_PIXEL(input.x); + ret->y = TOUCH_COORD_TO_PIXEL(input.y); + } + // Note that we don't call CloseTouchInputHandle here because we need + // to read the touch input info again in OnTouch later. + return ret; +} + +// static +bool nsWindow::DealWithPopups(HWND aWnd, UINT aMessage, WPARAM aWParam, + LPARAM aLParam, LRESULT* aResult) { + NS_ASSERTION(aResult, "Bad outResult"); + + // XXX Why do we use the return value of WM_MOUSEACTIVATE for all messages? + *aResult = MA_NOACTIVATE; + + if (!::IsWindowVisible(aWnd)) { + return false; + } + + if (MOZ_UNLIKELY(aMessage == WM_KILLFOCUS)) { + // NOTE: We deal with this here rather than on the switch below because we + // want to do this even if there are no menus to rollup (tooltips don't set + // the rollup listener etc). + if (RefPtr pm = nsXULPopupManager::GetInstance()) { + pm->RollupTooltips(); + } + } + + nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener(); + NS_ENSURE_TRUE(rollupListener, false); + + nsCOMPtr<nsIWidget> popup = rollupListener->GetRollupWidget(); + if (!popup) { + return false; + } + + static bool sSendingNCACTIVATE = false; + static bool sPendingNCACTIVATE = false; + uint32_t popupsToRollup = UINT32_MAX; + + bool consumeRollupEvent = false; + Maybe<POINT> touchPoint; // In screen coords. + + // If we rollup with animations but get occluded right away, we might not + // advance the refresh driver enough for the animation to finish. + auto allowAnimations = nsIRollupListener::AllowAnimations::Yes; + nsWindow* popupWindow = static_cast<nsWindow*>(popup.get()); + UINT nativeMessage = WinUtils::GetNativeMessage(aMessage); + switch (nativeMessage) { + case WM_TOUCH: + if (!IsTouchSupportEnabled(aWnd)) { + // If APZ is disabled, don't allow touch inputs to dismiss popups. The + // compatibility mouse events will do it instead. + return false; + } + touchPoint = GetSingleTouch(aWParam, aLParam); + if (!touchPoint) { + return false; + } + [[fallthrough]]; + case WM_LBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_NCLBUTTONDOWN: + case WM_NCRBUTTONDOWN: + case WM_NCMBUTTONDOWN: + if (nativeMessage != WM_TOUCH && IsTouchSupportEnabled(aWnd) && + MOUSE_INPUT_SOURCE() == MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + // If any of these mouse events are really compatibility events that + // Windows is sending for touch inputs, then don't allow them to dismiss + // popups when APZ is enabled (instead we do the dismissing as part of + // WM_TOUCH handling which is more correct). + // If we don't do this, then when the user lifts their finger after a + // long-press, the WM_RBUTTONDOWN compatibility event that Windows sends + // us will dismiss the contextmenu popup that we displayed as part of + // handling the long-tap-up. + return false; + } + if (!EventIsInsideWindow(popupWindow, touchPoint) && + GetPopupsToRollup(rollupListener, &popupsToRollup, touchPoint)) { + break; + } + return false; + case WM_POINTERDOWN: { + WinPointerEvents pointerEvents; + if (!pointerEvents.ShouldRollupOnPointerEvent(nativeMessage, aWParam)) { + return false; + } + POINT pt; + pt.x = GET_X_LPARAM(aLParam); + pt.y = GET_Y_LPARAM(aLParam); + if (!GetPopupsToRollup(rollupListener, &popupsToRollup, Some(pt))) { + return false; + } + if (EventIsInsideWindow(popupWindow, Some(pt))) { + // Don't roll up if the event is inside the popup window. + return false; + } + } break; + case MOZ_WM_DMANIP: { + POINT pt; + ::GetCursorPos(&pt); + if (!GetPopupsToRollup(rollupListener, &popupsToRollup, Some(pt))) { + return false; + } + if (EventIsInsideWindow(popupWindow, Some(pt))) { + // Don't roll up if the event is inside the popup window + return false; + } + } break; + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + // We need to check if the popup thinks that it should cause closing + // itself when mouse wheel events are fired outside the rollup widget. + if (!EventIsInsideWindow(popupWindow)) { + // Check if we should consume this event even if we don't roll-up: + consumeRollupEvent = rollupListener->ShouldConsumeOnMouseWheelEvent(); + *aResult = MA_ACTIVATE; + if (rollupListener->ShouldRollupOnMouseWheelEvent() && + GetPopupsToRollup(rollupListener, &popupsToRollup)) { + break; + } + } + return consumeRollupEvent; + + case WM_ACTIVATEAPP: + allowAnimations = nsIRollupListener::AllowAnimations::No; + break; + + case WM_ACTIVATE: { + WndProcUrgentInvocation::Marker _marker; + + // NOTE: Don't handle WA_INACTIVE for preventing popup taking focus + // because we cannot distinguish it's caused by mouse or not. + if (LOWORD(aWParam) == WA_ACTIVE && aLParam) { + nsWindow* window = WinUtils::GetNSWindowPtr(aWnd); + if (window && (window->IsPopup() || window->mIsAlert)) { + // Cancel notifying widget listeners of deactivating the previous + // active window (see WM_KILLFOCUS case in ProcessMessage()). + sJustGotDeactivate = false; + // Reactivate the window later. + ::PostMessageW(aWnd, MOZ_WM_REACTIVATE, aWParam, aLParam); + return true; + } + // Don't rollup the popup when focus moves back to the parent window + // from a popup because such case is caused by strange mouse drivers. + nsWindow* prevWindow = + WinUtils::GetNSWindowPtr(reinterpret_cast<HWND>(aLParam)); + if (prevWindow && prevWindow->IsPopup()) { + // Consume this message here since previous window must not have + // been inactivated since we've already stopped accepting the + // inactivation below. + return true; + } + } else if (LOWORD(aWParam) == WA_INACTIVE) { + nsWindow* activeWindow = + WinUtils::GetNSWindowPtr(reinterpret_cast<HWND>(aLParam)); + if (sPendingNCACTIVATE && NeedsToHandleNCActivateDelayed(aWnd)) { + // If focus moves to non-popup widget or focusable popup, the window + // needs to update its nonclient area. + if (!activeWindow || !activeWindow->IsPopup()) { + sSendingNCACTIVATE = true; + ::SendMessageW(aWnd, WM_NCACTIVATE, false, 0); + sSendingNCACTIVATE = false; + } + sPendingNCACTIVATE = false; + } + // If focus moves from/to popup, we don't need to rollup the popup + // because such case is caused by strange mouse drivers. And in + // such case, we should consume the message here since we need to + // hide this odd focus move from our content. (If we didn't consume + // the message here, ProcessMessage() will notify widget listener of + // inactivation and that causes unnecessary reflow for supporting + // -moz-window-inactive pseudo class. + if (activeWindow) { + if (activeWindow->IsPopup()) { + return true; + } + nsWindow* deactiveWindow = WinUtils::GetNSWindowPtr(aWnd); + if (deactiveWindow && deactiveWindow->IsPopup()) { + return true; + } + } + } else if (LOWORD(aWParam) == WA_CLICKACTIVE) { + // If the WM_ACTIVATE message is caused by a click in a popup, + // we should not rollup any popups. + nsWindow* window = WinUtils::GetNSWindowPtr(aWnd); + if ((window && window->IsPopup()) || + !GetPopupsToRollup(rollupListener, &popupsToRollup)) { + return false; + } + } + allowAnimations = nsIRollupListener::AllowAnimations::No; + } break; + + case MOZ_WM_REACTIVATE: + // The previous active window should take back focus. + if (::IsWindow(reinterpret_cast<HWND>(aLParam))) { + // FYI: Even without this API call, you see expected result (e.g., the + // owner window of the popup keeps active without flickering + // the non-client area). And also this causes initializing + // TSF and it causes using CPU time a lot. However, even if we + // consume WM_ACTIVE messages, native focus change has already + // been occurred. I.e., a popup window is active now. Therefore, + // you'll see some odd behavior if we don't reactivate the owner + // window here. For example, if you do: + // 1. Turn wheel on a bookmark panel. + // 2. Turn wheel on another window. + // then, you'll see that the another window becomes active but the + // owner window of the bookmark panel looks still active and the + // bookmark panel keeps open. The reason is that the first wheel + // operation gives focus to the bookmark panel. Therefore, when + // the next operation gives focus to the another window, previous + // focus window is the bookmark panel (i.e., a popup window). + // So, in this case, our hack around here prevents to inactivate + // the owner window and roll up the bookmark panel. + ::SetForegroundWindow(reinterpret_cast<HWND>(aLParam)); + } + return true; + + case WM_NCACTIVATE: + if (!aWParam && !sSendingNCACTIVATE && + NeedsToHandleNCActivateDelayed(aWnd)) { + // Don't just consume WM_NCACTIVATE. It doesn't handle only the + // nonclient area state change. + ::DefWindowProcW(aWnd, aMessage, TRUE, aLParam); + // Accept the deactivating because it's necessary to receive following + // WM_ACTIVATE. + *aResult = TRUE; + sPendingNCACTIVATE = true; + return true; + } + return false; + + case WM_MOUSEACTIVATE: + if (!EventIsInsideWindow(popupWindow) && + GetPopupsToRollup(rollupListener, &popupsToRollup)) { + // WM_MOUSEACTIVATE may be caused by moving the mouse (e.g., X-mouse + // of TweakUI is enabled. Then, check if the popup should be rolled up + // with rollup listener. If not, just consume the message. + if (HIWORD(aLParam) == WM_MOUSEMOVE && + !rollupListener->ShouldRollupOnMouseActivate()) { + return true; + } + // Otherwise, it should be handled by wndproc. + return false; + } + + // Prevent the click inside the popup from causing a change in window + // activation. Since the popup is shown non-activated, we need to eat any + // requests to activate the window while it is displayed. Windows will + // automatically activate the popup on the mousedown otherwise. + return true; + + case WM_SHOWWINDOW: + // If the window is being minimized, close popups. + if (aLParam == SW_PARENTCLOSING) { + allowAnimations = nsIRollupListener::AllowAnimations::No; + break; + } + return false; + + case WM_KILLFOCUS: + // If focus moves to other window created in different process/thread, + // e.g., a plugin window, popups should be rolled up. + if (IsDifferentThreadWindow(reinterpret_cast<HWND>(aWParam))) { + allowAnimations = nsIRollupListener::AllowAnimations::No; + break; + } + return false; + + case WM_MOVING: + case WM_MENUSELECT: + break; + + default: + return false; + } + + // Only need to deal with the last rollup for left mouse down events. + NS_ASSERTION(!nsAutoRollup::GetLastRollup(), "last rollup is null"); + + nsIRollupListener::RollupOptions rollupOptions{ + popupsToRollup, + nsIRollupListener::FlushViews::Yes, + /* mPoint = */ nullptr, + allowAnimations, + }; + + if (nativeMessage == WM_TOUCH || nativeMessage == WM_LBUTTONDOWN || + nativeMessage == WM_POINTERDOWN) { + LayoutDeviceIntPoint pos; + if (nativeMessage == WM_TOUCH) { + pos.x = touchPoint->x; + pos.y = touchPoint->y; + } else { + POINT pt; + pt.x = GET_X_LPARAM(aLParam); + pt.y = GET_Y_LPARAM(aLParam); + // POINTERDOWN is already in screen coords. + if (nativeMessage == WM_LBUTTONDOWN) { + ::ClientToScreen(aWnd, &pt); + } + pos = LayoutDeviceIntPoint(pt.x, pt.y); + } + + rollupOptions.mPoint = &pos; + nsIContent* lastRollup = nullptr; + consumeRollupEvent = rollupListener->Rollup(rollupOptions, &lastRollup); + nsAutoRollup::SetLastRollup(lastRollup); + } else { + consumeRollupEvent = rollupListener->Rollup(rollupOptions); + } + + // Tell hook to stop processing messages + sProcessHook = false; + sRollupMsgId = 0; + sRollupMsgWnd = nullptr; + + // If we are NOT supposed to be consuming events, let it go through + if (consumeRollupEvent && nativeMessage != WM_RBUTTONDOWN) { + *aResult = MA_ACTIVATE; + return true; + } + + return false; +} + +/************************************************************** + ************************************************************** + ** + ** BLOCK: Misc. utility methods and functions. + ** + ** General use. + ** + ************************************************************** + **************************************************************/ + +// Note that the result of GetTopLevelWindow method can be different from the +// result of WinUtils::GetTopLevelHWND(). The result can be non-floating +// window. Because our top level window may be contained in another window +// which is not managed by us. +nsWindow* nsWindow::GetTopLevelWindow(bool aStopOnDialogOrPopup) { + nsWindow* curWindow = this; + + while (true) { + if (aStopOnDialogOrPopup) { + switch (curWindow->mWindowType) { + case WindowType::Dialog: + case WindowType::Popup: + return curWindow; + default: + break; + } + } + + // Retrieve the top level parent or owner window + nsWindow* parentWindow = curWindow->GetParentWindow(true); + + if (!parentWindow) return curWindow; + + curWindow = parentWindow; + } +} + +// Set a flag if hwnd is a (non-popup) visible window from this process, +// and bail out of the enumeration. Otherwise leave the flag unmodified +// and continue the enumeration. +// lParam must be a bool* pointing at the flag to be set. +static BOOL CALLBACK EnumVisibleWindowsProc(HWND hwnd, LPARAM lParam) { + DWORD pid; + ::GetWindowThreadProcessId(hwnd, &pid); + if (pid == ::GetCurrentProcessId() && ::IsWindowVisible(hwnd)) { + // Don't count popups as visible windows, since they don't take focus, + // in case we only have a popup visible (see bug 1554490 where the gfx + // test window is an offscreen popup). + nsWindow* window = WinUtils::GetNSWindowPtr(hwnd); + if (!window || !window->IsPopup()) { + bool* windowsVisible = reinterpret_cast<bool*>(lParam); + *windowsVisible = true; + return FALSE; + } + } + return TRUE; +} + +// Determine if it would be ok to activate a window, taking focus. +// We want to avoid stealing focus from another app (bug 225305). +bool nsWindow::CanTakeFocus() { + HWND fgWnd = ::GetForegroundWindow(); + if (!fgWnd) { + // There is no foreground window, so don't worry about stealing focus. + return true; + } + // We can take focus if the current foreground window is already from + // this process. + DWORD pid; + ::GetWindowThreadProcessId(fgWnd, &pid); + if (pid == ::GetCurrentProcessId()) { + return true; + } + + bool windowsVisible = false; + ::EnumWindows(EnumVisibleWindowsProc, + reinterpret_cast<LPARAM>(&windowsVisible)); + + if (!windowsVisible) { + // We're probably creating our first visible window, allow that to + // take focus. + return true; + } + return false; +} + +/* static */ const wchar_t* nsWindow::GetMainWindowClass() { + static const wchar_t* sMainWindowClass = nullptr; + if (!sMainWindowClass) { + nsAutoString className; + Preferences::GetString("ui.window_class_override", className); + if (!className.IsEmpty()) { + sMainWindowClass = wcsdup(className.get()); + } else { + sMainWindowClass = kClassNameGeneral; + } + } + return sMainWindowClass; +} + +LPARAM nsWindow::lParamToScreen(LPARAM lParam) { + POINT pt; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + ::ClientToScreen(mWnd, &pt); + return MAKELPARAM(pt.x, pt.y); +} + +LPARAM nsWindow::lParamToClient(LPARAM lParam) { + POINT pt; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + ::ScreenToClient(mWnd, &pt); + return MAKELPARAM(pt.x, pt.y); +} + +WPARAM nsWindow::wParamFromGlobalMouseState() { + WPARAM result = 0; + + if (!!::GetKeyState(VK_CONTROL)) { + result |= MK_CONTROL; + } + + if (!!::GetKeyState(VK_SHIFT)) { + result |= MK_SHIFT; + } + + if (!!::GetKeyState(VK_LBUTTON)) { + result |= MK_LBUTTON; + } + + if (!!::GetKeyState(VK_MBUTTON)) { + result |= MK_MBUTTON; + } + + if (!!::GetKeyState(VK_RBUTTON)) { + result |= MK_RBUTTON; + } + + if (!!::GetKeyState(VK_XBUTTON1)) { + result |= MK_XBUTTON1; + } + + if (!!::GetKeyState(VK_XBUTTON2)) { + result |= MK_XBUTTON2; + } + + return result; +} + +void nsWindow::PickerOpen() { + AssertIsOnMainThread(); + mPickerDisplayCount++; +} + +void nsWindow::PickerClosed() { + AssertIsOnMainThread(); + NS_ASSERTION(mPickerDisplayCount > 0, "mPickerDisplayCount out of sync!"); + if (!mPickerDisplayCount) return; + mPickerDisplayCount--; + + // WORKAROUND FOR UNDOCUMENTED BEHAVIOR: `IFileDialog::Show` disables the + // top-level ancestor of its provided owner-window. If the modal window's + // container process crashes, it will never get a chance to undo that, so we + // do it manually here. + // + // Note that this may cause problems in the embedded case if you reparent a + // subtree of the native window hierarchy containing a Gecko window while that + // Gecko window has a file-dialog open. + if (!mPickerDisplayCount) { + ::EnableWindow(::GetAncestor(GetWindowHandle(), GA_ROOT), TRUE); + } + + if (!mPickerDisplayCount && mDestroyCalled) { + Destroy(); + } +} + +bool nsWindow::WidgetTypeSupportsAcceleration() { + // We don't currently support using an accelerated layer manager with + // transparent windows so don't even try. I'm also not sure if we even + // want to support this case. See bug 593471. + // + // Windows' support for transparent accelerated surfaces isn't great. + // Some possible approaches: + // - Readback the data and update it using + // UpdateLayeredWindow/UpdateLayeredWindowIndirect + // This is what WPF does. See + // CD3DDeviceLevel1::PresentWithGDI/CD3DSwapChainWithSwDC in WpfGfx. The + // rationale for not using IDirect3DSurface9::GetDC is explained here: + // https://web.archive.org/web/20160521191104/https://blogs.msdn.microsoft.com/dwayneneed/2008/09/08/transparent-windows-in-wpf/ + // - Use D3D11_RESOURCE_MISC_GDI_COMPATIBLE, IDXGISurface1::GetDC(), + // and UpdateLayeredWindowIndirect. + // This is suggested here: + // https://docs.microsoft.com/en-us/archive/msdn-magazine/2009/december/windows-with-c-layered-windows-with-direct2d + // but might have the same problem that IDirect3DSurface9::GetDC has. + // - Creating the window with the WS_EX_NOREDIRECTIONBITMAP flag and use + // DirectComposition. + // Not supported on Win7. + // - Using DwmExtendFrameIntoClientArea with negative margins and something + // to turn off the glass effect. + // This doesn't work when the DWM is not running (Win7) + // + // Also see bug 1150376, D3D11 composition can cause issues on some devices + // on Windows 7 where presentation fails randomly for windows with drop + // shadows. + return mTransparencyMode != TransparencyMode::Transparent && + !(IsPopup() && DeviceManagerDx::Get()->IsWARP()); +} + +bool nsWindow::DispatchTouchEventFromWMPointer( + UINT msg, LPARAM aLParam, const WinPointerInfo& aPointerInfo, + mozilla::MouseButton aButton) { + MultiTouchInput::MultiTouchType touchType; + switch (msg) { + case WM_POINTERDOWN: + touchType = MultiTouchInput::MULTITOUCH_START; + break; + case WM_POINTERUPDATE: + if (aPointerInfo.mPressure == 0) { + return false; // hover + } + touchType = MultiTouchInput::MULTITOUCH_MOVE; + break; + case WM_POINTERUP: + touchType = MultiTouchInput::MULTITOUCH_END; + break; + default: + return false; + } + + nsPointWin touchPoint; + touchPoint.x = GET_X_LPARAM(aLParam); + touchPoint.y = GET_Y_LPARAM(aLParam); + touchPoint.ScreenToClient(mWnd); + + SingleTouchData touchData(static_cast<int32_t>(aPointerInfo.pointerId), + ScreenIntPoint::FromUnknownPoint(touchPoint), + ScreenSize(1, 1), // pixel size radius for pen + 0.0f, // no radius rotation + aPointerInfo.mPressure); + touchData.mTiltX = aPointerInfo.tiltX; + touchData.mTiltY = aPointerInfo.tiltY; + touchData.mTwist = aPointerInfo.twist; + + MultiTouchInput touchInput; + touchInput.mType = touchType; + touchInput.mTimeStamp = GetMessageTimeStamp(::GetMessageTime()); + touchInput.mTouches.AppendElement(touchData); + touchInput.mButton = aButton; + touchInput.mButtons = aPointerInfo.mButtons; + + // POINTER_INFO.dwKeyStates can't be used as it only supports Shift and Ctrl + ModifierKeyState modifierKeyState; + touchInput.modifiers = modifierKeyState.GetModifiers(); + + DispatchTouchInput(touchInput, MouseEvent_Binding::MOZ_SOURCE_PEN); + return true; +} + +static MouseButton PenFlagsToMouseButton(PEN_FLAGS aPenFlags) { + // Theoretically flags can be set together but they do not + if (aPenFlags & PEN_FLAG_BARREL) { + return MouseButton::eSecondary; + } + if (aPenFlags & PEN_FLAG_ERASER) { + return MouseButton::eEraser; + } + return MouseButton::ePrimary; +} + +bool nsWindow::OnPointerEvents(UINT msg, WPARAM aWParam, LPARAM aLParam) { + if (!mAPZC) { + // APZ is not available on context menu. Follow the behavior of touch input + // which fallbacks to WM_LBUTTON* and WM_GESTURE, to keep consistency. + return false; + } + if (!mPointerEvents.ShouldHandleWinPointerMessages(msg, aWParam)) { + return false; + } + if (!mPointerEvents.ShouldFirePointerEventByWinPointerMessages()) { + // We have to handle WM_POINTER* to fetch and cache pen related information + // and fire WidgetMouseEvent with the cached information the WM_*BUTTONDOWN + // handler. This is because Windows doesn't support ::DoDragDrop in the + // touch or pen message handlers. + mPointerEvents.ConvertAndCachePointerInfo(msg, aWParam); + // Don't consume the Windows WM_POINTER* messages + return false; + } + + uint32_t pointerId = mPointerEvents.GetPointerId(aWParam); + POINTER_PEN_INFO penInfo{}; + if (!mPointerEvents.GetPointerPenInfo(pointerId, &penInfo)) { + return false; + } + + // When dispatching mouse events with pen, there may be some + // WM_POINTERUPDATE messages between WM_POINTERDOWN and WM_POINTERUP with + // small movements. Those events will reset sLastMousePoint and reset + // sLastClickCount. To prevent that, we keep the last pen down position + // and compare it with the subsequent WM_POINTERUPDATE. If the movement is + // smaller than GetSystemMetrics(SM_CXDRAG), then we suppress firing + // eMouseMove for WM_POINTERUPDATE. + static POINT sLastPointerDownPoint = {0}; + + // We don't support chorded buttons for pen. Keep the button at + // WM_POINTERDOWN. + static mozilla::MouseButton sLastPenDownButton = MouseButton::ePrimary; + static bool sPointerDown = false; + + EventMessage message; + mozilla::MouseButton button = MouseButton::ePrimary; + switch (msg) { + case WM_POINTERDOWN: { + LayoutDeviceIntPoint eventPoint(GET_X_LPARAM(aLParam), + GET_Y_LPARAM(aLParam)); + sLastPointerDownPoint.x = eventPoint.x; + sLastPointerDownPoint.y = eventPoint.y; + message = eMouseDown; + button = PenFlagsToMouseButton(penInfo.penFlags); + sLastPenDownButton = button; + sPointerDown = true; + } break; + case WM_POINTERUP: + message = eMouseUp; + MOZ_ASSERT(sPointerDown, "receive WM_POINTERUP w/o WM_POINTERDOWN"); + button = sPointerDown ? sLastPenDownButton : MouseButton::ePrimary; + sPointerDown = false; + break; + case WM_POINTERUPDATE: + message = eMouseMove; + if (sPointerDown) { + LayoutDeviceIntPoint eventPoint(GET_X_LPARAM(aLParam), + GET_Y_LPARAM(aLParam)); + int32_t movementX = sLastPointerDownPoint.x > eventPoint.x + ? sLastPointerDownPoint.x - eventPoint.x.value + : eventPoint.x.value - sLastPointerDownPoint.x; + int32_t movementY = sLastPointerDownPoint.y > eventPoint.y + ? sLastPointerDownPoint.y - eventPoint.y.value + : eventPoint.y.value - sLastPointerDownPoint.y; + bool insideMovementThreshold = + movementX < (int32_t)::GetSystemMetrics(SM_CXDRAG) && + movementY < (int32_t)::GetSystemMetrics(SM_CYDRAG); + + if (insideMovementThreshold) { + // Suppress firing eMouseMove for WM_POINTERUPDATE if the movement + // from last WM_POINTERDOWN is smaller than SM_CXDRAG / SM_CYDRAG + return false; + } + button = sLastPenDownButton; + } + break; + case WM_POINTERLEAVE: + message = eMouseExitFromWidget; + break; + default: + return false; + } + + // Windows defines the pen pressure is normalized to a range between 0 and + // 1024. Convert it to float. + float pressure = penInfo.pressure ? (float)penInfo.pressure / 1024 : 0; + int16_t buttons = sPointerDown + ? nsContentUtils::GetButtonsFlagForButton(button) + : MouseButtonsFlag::eNoButtons; + WinPointerInfo pointerInfo(pointerId, penInfo.tiltX, penInfo.tiltY, pressure, + buttons); + // Per + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-pointer_pen_info, + // the rotation is normalized in a range of 0 to 359. + MOZ_ASSERT(penInfo.rotation <= 359); + pointerInfo.twist = (int32_t)penInfo.rotation; + + // Fire touch events but not when the barrel button is pressed. + if (button != MouseButton::eSecondary && + StaticPrefs::dom_w3c_pointer_events_scroll_by_pen_enabled() && + DispatchTouchEventFromWMPointer(msg, aLParam, pointerInfo, button)) { + return true; + } + + // The aLParam of WM_POINTER* is the screen location. Convert it to client + // location + LPARAM newLParam = lParamToClient(aLParam); + DispatchMouseEvent(message, aWParam, newLParam, false, button, + MouseEvent_Binding::MOZ_SOURCE_PEN, &pointerInfo); + + if (button == MouseButton::eSecondary && message == eMouseUp) { + // Fire eContextMenu manually since consuming WM_POINTER* blocks + // WM_CONTEXTMENU + DispatchMouseEvent(eContextMenu, aWParam, newLParam, false, button, + MouseEvent_Binding::MOZ_SOURCE_PEN, &pointerInfo); + } + // Consume WM_POINTER* to stop Windows fires WM_*BUTTONDOWN / WM_*BUTTONUP + // WM_MOUSEMOVE. + return true; +} + +void nsWindow::GetCompositorWidgetInitData( + mozilla::widget::CompositorWidgetInitData* aInitData) { + *aInitData = WinCompositorWidgetInitData( + reinterpret_cast<uintptr_t>(mWnd), + reinterpret_cast<uintptr_t>(static_cast<nsIWidget*>(this)), + mTransparencyMode, mFrameState->GetSizeMode()); +} + +bool nsWindow::SynchronouslyRepaintOnResize() { return false; } + +void nsWindow::MaybeDispatchInitialFocusEvent() { + if (mIsShowingPreXULSkeletonUI && ::GetActiveWindow() == mWnd) { + DispatchFocusToTopLevelWindow(true); + } +} + +already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() { + nsCOMPtr<nsIWidget> window = new nsWindow(); + return window.forget(); +} + +already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() { + nsCOMPtr<nsIWidget> window = new nsWindow(true); + return window.forget(); +} + +// static +bool nsWindow::InitTouchInjection() { + if (!sTouchInjectInitialized) { + // Initialize touch injection on the first call + HMODULE hMod = LoadLibraryW(kUser32LibName); + if (!hMod) { + return false; + } + + InitializeTouchInjectionPtr func = + (InitializeTouchInjectionPtr)GetProcAddress(hMod, + "InitializeTouchInjection"); + if (!func) { + WinUtils::Log("InitializeTouchInjection not available."); + return false; + } + + if (!func(TOUCH_INJECT_MAX_POINTS, TOUCH_FEEDBACK_DEFAULT)) { + WinUtils::Log("InitializeTouchInjection failure. GetLastError=%d", + GetLastError()); + return false; + } + + sInjectTouchFuncPtr = + (InjectTouchInputPtr)GetProcAddress(hMod, "InjectTouchInput"); + if (!sInjectTouchFuncPtr) { + WinUtils::Log("InjectTouchInput not available."); + return false; + } + sTouchInjectInitialized = true; + } + return true; +} + +bool nsWindow::InjectTouchPoint(uint32_t aId, LayoutDeviceIntPoint& aPoint, + POINTER_FLAGS aFlags, uint32_t aPressure, + uint32_t aOrientation) { + if (aId > TOUCH_INJECT_MAX_POINTS) { + WinUtils::Log("Pointer ID exceeds maximum. See TOUCH_INJECT_MAX_POINTS."); + return false; + } + + POINTER_TOUCH_INFO info{}; + + info.touchFlags = TOUCH_FLAG_NONE; + info.touchMask = + TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE; + info.pressure = aPressure; + info.orientation = aOrientation; + + info.pointerInfo.pointerFlags = aFlags; + info.pointerInfo.pointerType = PT_TOUCH; + info.pointerInfo.pointerId = aId; + info.pointerInfo.ptPixelLocation.x = aPoint.x; + info.pointerInfo.ptPixelLocation.y = aPoint.y; + + info.rcContact.top = info.pointerInfo.ptPixelLocation.y - 2; + info.rcContact.bottom = info.pointerInfo.ptPixelLocation.y + 2; + info.rcContact.left = info.pointerInfo.ptPixelLocation.x - 2; + info.rcContact.right = info.pointerInfo.ptPixelLocation.x + 2; + + for (int i = 0; i < 3; i++) { + if (sInjectTouchFuncPtr(1, &info)) { + break; + } + DWORD error = GetLastError(); + if (error == ERROR_NOT_READY && i < 2) { + // We sent it too quickly after the previous injection (see bug 1535140 + // comment 10). On the first loop iteration we just yield (via Sleep(0)) + // and try again. If it happens again on the second loop iteration we + // explicitly Sleep(1) and try again. If that doesn't work either we just + // error out. + ::Sleep(i); + continue; + } + WinUtils::Log("InjectTouchInput failure. GetLastError=%d", error); + return false; + } + return true; +} + +void nsWindow::ChangedDPI() { + if (mWidgetListener) { + if (PresShell* presShell = mWidgetListener->GetPresShell()) { + presShell->BackingScaleFactorChanged(); + } + } +} + +static Result<POINTER_FLAGS, nsresult> PointerStateToFlag( + nsWindow::TouchPointerState aPointerState, bool isUpdate) { + bool hover = aPointerState & nsWindow::TOUCH_HOVER; + bool contact = aPointerState & nsWindow::TOUCH_CONTACT; + bool remove = aPointerState & nsWindow::TOUCH_REMOVE; + bool cancel = aPointerState & nsWindow::TOUCH_CANCEL; + + POINTER_FLAGS flags; + if (isUpdate) { + // We know about this pointer, send an update + flags = POINTER_FLAG_UPDATE; + if (hover) { + flags |= POINTER_FLAG_INRANGE; + } else if (contact) { + flags |= POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE; + } else if (remove) { + flags = POINTER_FLAG_UP; + } + + if (cancel) { + flags |= POINTER_FLAG_CANCELED; + } + } else { + // Missing init state, error out + if (remove || cancel) { + return Err(NS_ERROR_INVALID_ARG); + } + + // Create a new pointer + flags = POINTER_FLAG_INRANGE; + if (contact) { + flags |= POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN; + } + } + return flags; +} + +nsresult nsWindow::SynthesizeNativeTouchPoint( + uint32_t aPointerId, nsIWidget::TouchPointerState aPointerState, + LayoutDeviceIntPoint aPoint, double aPointerPressure, + uint32_t aPointerOrientation, nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "touchpoint"); + + if (StaticPrefs::apz_test_fails_with_native_injection() || + !InitTouchInjection()) { + // If we don't have touch injection from the OS, or if we are running a test + // that cannot properly inject events to satisfy the OS requirements (see + // bug 1313170) we can just fake it and synthesize the events from here. + MOZ_ASSERT(NS_IsMainThread()); + if (aPointerState == TOUCH_HOVER) { + return NS_ERROR_UNEXPECTED; + } + + if (!mSynthesizedTouchInput) { + mSynthesizedTouchInput = MakeUnique<MultiTouchInput>(); + } + + WidgetEventTime time = CurrentMessageWidgetEventTime(); + LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset(); + MultiTouchInput inputToDispatch = UpdateSynthesizedTouchState( + mSynthesizedTouchInput.get(), time.mTimeStamp, aPointerId, + aPointerState, pointInWindow, aPointerPressure, aPointerOrientation); + DispatchTouchInput(inputToDispatch); + return NS_OK; + } + + // win api expects a value from 0 to 1024. aPointerPressure is a value + // from 0.0 to 1.0. + uint32_t pressure = (uint32_t)ceil(aPointerPressure * 1024); + + // If we already know about this pointer id get it's record + return mActivePointers.WithEntryHandle(aPointerId, [&](auto&& entry) { + POINTER_FLAGS flags; + // Can't use MOZ_TRY_VAR because it confuses WithEntryHandle + auto result = PointerStateToFlag(aPointerState, !!entry); + if (result.isOk()) { + flags = result.unwrap(); + } else { + return result.unwrapErr(); + } + + if (!entry) { + entry.Insert(MakeUnique<PointerInfo>(aPointerId, aPoint, + PointerInfo::PointerType::TOUCH)); + } else { + if (entry.Data()->mType != PointerInfo::PointerType::TOUCH) { + return NS_ERROR_UNEXPECTED; + } + if (aPointerState & TOUCH_REMOVE) { + // Remove the pointer from our tracking list. This is UniquePtr wrapped, + // so shouldn't leak. + entry.Remove(); + } + } + + return !InjectTouchPoint(aPointerId, aPoint, flags, pressure, + aPointerOrientation) + ? NS_ERROR_UNEXPECTED + : NS_OK; + }); +} + +nsresult nsWindow::ClearNativeTouchSequence(nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "cleartouch"); + if (!sTouchInjectInitialized) { + return NS_OK; + } + + // cancel all input points + for (auto iter = mActivePointers.Iter(); !iter.Done(); iter.Next()) { + auto* info = iter.UserData(); + if (info->mType != PointerInfo::PointerType::TOUCH) { + continue; + } + InjectTouchPoint(info->mPointerId, info->mPosition, POINTER_FLAG_CANCELED); + iter.Remove(); + } + + nsBaseWidget::ClearNativeTouchSequence(nullptr); + + return NS_OK; +} + +#if !defined(NTDDI_WIN10_RS5) || (NTDDI_VERSION < NTDDI_WIN10_RS5) +static CreateSyntheticPointerDevicePtr CreateSyntheticPointerDevice; +static DestroySyntheticPointerDevicePtr DestroySyntheticPointerDevice; +static InjectSyntheticPointerInputPtr InjectSyntheticPointerInput; +#endif +static HSYNTHETICPOINTERDEVICE sSyntheticPenDevice; + +static bool InitPenInjection() { + if (sSyntheticPenDevice) { + return true; + } +#if !defined(NTDDI_WIN10_RS5) || (NTDDI_VERSION < NTDDI_WIN10_RS5) + HMODULE hMod = LoadLibraryW(kUser32LibName); + if (!hMod) { + return false; + } + CreateSyntheticPointerDevice = + (CreateSyntheticPointerDevicePtr)GetProcAddress( + hMod, "CreateSyntheticPointerDevice"); + if (!CreateSyntheticPointerDevice) { + WinUtils::Log("CreateSyntheticPointerDevice not available."); + return false; + } + DestroySyntheticPointerDevice = + (DestroySyntheticPointerDevicePtr)GetProcAddress( + hMod, "DestroySyntheticPointerDevice"); + if (!DestroySyntheticPointerDevice) { + WinUtils::Log("DestroySyntheticPointerDevice not available."); + return false; + } + InjectSyntheticPointerInput = (InjectSyntheticPointerInputPtr)GetProcAddress( + hMod, "InjectSyntheticPointerInput"); + if (!InjectSyntheticPointerInput) { + WinUtils::Log("InjectSyntheticPointerInput not available."); + return false; + } +#endif + sSyntheticPenDevice = + CreateSyntheticPointerDevice(PT_PEN, 1, POINTER_FEEDBACK_DEFAULT); + return !!sSyntheticPenDevice; +} + +nsresult nsWindow::SynthesizeNativePenInput( + uint32_t aPointerId, nsIWidget::TouchPointerState aPointerState, + LayoutDeviceIntPoint aPoint, double aPressure, uint32_t aRotation, + int32_t aTiltX, int32_t aTiltY, int32_t aButton, nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "peninput"); + if (!InitPenInjection()) { + return NS_ERROR_UNEXPECTED; + } + + // win api expects a value from 0 to 1024. aPointerPressure is a value + // from 0.0 to 1.0. + uint32_t pressure = (uint32_t)ceil(aPressure * 1024); + + // If we already know about this pointer id get it's record + return mActivePointers.WithEntryHandle(aPointerId, [&](auto&& entry) { + POINTER_FLAGS flags; + // Can't use MOZ_TRY_VAR because it confuses WithEntryHandle + auto result = PointerStateToFlag(aPointerState, !!entry); + if (result.isOk()) { + flags = result.unwrap(); + } else { + return result.unwrapErr(); + } + + if (!entry) { + entry.Insert(MakeUnique<PointerInfo>(aPointerId, aPoint, + PointerInfo::PointerType::PEN)); + } else { + if (entry.Data()->mType != PointerInfo::PointerType::PEN) { + return NS_ERROR_UNEXPECTED; + } + if (aPointerState & TOUCH_REMOVE) { + // Remove the pointer from our tracking list. This is UniquePtr wrapped, + // so shouldn't leak. + entry.Remove(); + } + } + + POINTER_TYPE_INFO info{}; + + info.type = PT_PEN; + info.penInfo.pointerInfo.pointerType = PT_PEN; + info.penInfo.pointerInfo.pointerFlags = flags; + info.penInfo.pointerInfo.pointerId = aPointerId; + info.penInfo.pointerInfo.ptPixelLocation.x = aPoint.x; + info.penInfo.pointerInfo.ptPixelLocation.y = aPoint.y; + + info.penInfo.penFlags = PEN_FLAG_NONE; + // PEN_FLAG_ERASER is not supported this way, unfortunately. + if (aButton == 2) { + info.penInfo.penFlags |= PEN_FLAG_BARREL; + } + info.penInfo.penMask = PEN_MASK_PRESSURE | PEN_MASK_ROTATION | + PEN_MASK_TILT_X | PEN_MASK_TILT_Y; + info.penInfo.pressure = pressure; + info.penInfo.rotation = aRotation; + info.penInfo.tiltX = aTiltX; + info.penInfo.tiltY = aTiltY; + + return InjectSyntheticPointerInput(sSyntheticPenDevice, &info, 1) + ? NS_OK + : NS_ERROR_UNEXPECTED; + }); +}; + +bool nsWindow::HandleAppCommandMsg(const MSG& aAppCommandMsg, + LRESULT* aRetValue) { + ModifierKeyState modKeyState; + NativeKey nativeKey(this, aAppCommandMsg, modKeyState); + bool consumed = nativeKey.HandleAppCommandMessage(); + *aRetValue = consumed ? 1 : 0; + return consumed; +} + +#ifdef DEBUG +nsresult nsWindow::SetHiDPIMode(bool aHiDPI) { + return WinUtils::SetHiDPIMode(aHiDPI); +} + +nsresult nsWindow::RestoreHiDPIMode() { return WinUtils::RestoreHiDPIMode(); } +#endif + +mozilla::Maybe<UINT> nsWindow::GetHiddenTaskbarEdge() { + HMONITOR windowMonitor = ::MonitorFromWindow(mWnd, MONITOR_DEFAULTTONEAREST); + + // Check all four sides of our monitor for an appbar. Skip any that aren't + // the system taskbar. + MONITORINFO mi; + mi.cbSize = sizeof(MONITORINFO); + ::GetMonitorInfo(windowMonitor, &mi); + + APPBARDATA appBarData; + appBarData.cbSize = sizeof(appBarData); + appBarData.rc = mi.rcMonitor; + const auto kEdges = {ABE_BOTTOM, ABE_TOP, ABE_LEFT, ABE_RIGHT}; + for (auto edge : kEdges) { + appBarData.uEdge = edge; + HWND appBarHwnd = (HWND)SHAppBarMessage(ABM_GETAUTOHIDEBAREX, &appBarData); + if (appBarHwnd) { + nsAutoString className; + if (WinUtils::GetClassName(appBarHwnd, className)) { + if (className.Equals(L"Shell_TrayWnd") || + className.Equals(L"Shell_SecondaryTrayWnd")) { + return Some(edge); + } + } + } + } + + return Nothing(); +} + +static nsSizeMode GetSizeModeForWindowFrame(HWND aWnd, bool aFullscreenMode) { + WINDOWPLACEMENT pl; + pl.length = sizeof(pl); + ::GetWindowPlacement(aWnd, &pl); + + if (pl.showCmd == SW_SHOWMINIMIZED) { + return nsSizeMode_Minimized; + } else if (aFullscreenMode) { + return nsSizeMode_Fullscreen; + } else if (pl.showCmd == SW_SHOWMAXIMIZED) { + return nsSizeMode_Maximized; + } else { + return nsSizeMode_Normal; + } +} + +static void ShowWindowWithMode(HWND aWnd, nsSizeMode aMode) { + // This will likely cause a callback to + // nsWindow::FrameState::{OnFrameChanging() and OnFrameChanged()} + switch (aMode) { + case nsSizeMode_Fullscreen: + ::ShowWindow(aWnd, SW_SHOW); + break; + + case nsSizeMode_Maximized: + ::ShowWindow(aWnd, SW_MAXIMIZE); + break; + + case nsSizeMode_Minimized: + ::ShowWindow(aWnd, SW_MINIMIZE); + break; + + default: + // Don't call ::ShowWindow if we're trying to "restore" a window that is + // already in a normal state. Prevents a bug where snapping to one side + // of the screen and then minimizing would cause Windows to forget our + // window's correct restored position/size. + if (GetCurrentShowCmd(aWnd) != SW_SHOWNORMAL) { + ::ShowWindow(aWnd, SW_RESTORE); + } + } +} + +nsWindow::FrameState::FrameState(nsWindow* aWindow) : mWindow(aWindow) {} + +nsSizeMode nsWindow::FrameState::GetSizeMode() const { return mSizeMode; } + +void nsWindow::FrameState::CheckInvariant() const { + MOZ_ASSERT(mSizeMode >= 0 && mSizeMode < nsSizeMode_Invalid); + MOZ_ASSERT(mLastSizeMode >= 0 && mLastSizeMode < nsSizeMode_Invalid); + MOZ_ASSERT(mPreFullscreenSizeMode >= 0 && + mPreFullscreenSizeMode < nsSizeMode_Invalid); + MOZ_ASSERT(mWindow); + + // We should never observe fullscreen sizemode unless fullscreen is enabled + MOZ_ASSERT_IF(mSizeMode == nsSizeMode_Fullscreen, mFullscreenMode); + MOZ_ASSERT_IF(!mFullscreenMode, mSizeMode != nsSizeMode_Fullscreen); + + // Something went wrong if we somehow saved fullscreen mode when we are + // changing into fullscreen mode + MOZ_ASSERT(mPreFullscreenSizeMode != nsSizeMode_Fullscreen); +} + +void nsWindow::FrameState::ConsumePreXULSkeletonState(bool aWasMaximized) { + mSizeMode = aWasMaximized ? nsSizeMode_Maximized : nsSizeMode_Normal; +} + +void nsWindow::FrameState::EnsureSizeMode(nsSizeMode aMode, + DoShowWindow aDoShowWindow) { + if (mSizeMode == aMode) { + return; + } + + if (StaticPrefs::widget_windows_fullscreen_remind_taskbar()) { + // If we're unminimizing a window, asynchronously notify the taskbar after + // the message has been processed. This redundant notification works around + // a race condition in explorer.exe. (See bug 1835851, or comments in + // TaskbarConcealer.) + // + // Note that we notify regardless of `aMode`: unminimizing a non-fullscreen + // window can also affect the correct taskbar state, yet fail to affect the + // current taskbar state. + if (mSizeMode == nsSizeMode_Minimized) { + ::PostMessage(mWindow->mWnd, MOZ_WM_FULLSCREEN_STATE_UPDATE, 0, 0); + } + } + + if (aMode == nsSizeMode_Fullscreen) { + EnsureFullscreenMode(true, aDoShowWindow); + MOZ_ASSERT(mSizeMode == nsSizeMode_Fullscreen); + } else if (mSizeMode == nsSizeMode_Fullscreen && aMode == nsSizeMode_Normal) { + // If we are in fullscreen mode, minimize should work like normal and + // return us to fullscreen mode when unminimized. Maximize isn't really + // available and won't do anything. "Restore" should do the same thing as + // requesting to end fullscreen. + EnsureFullscreenMode(false, aDoShowWindow); + } else { + SetSizeModeInternal(aMode, aDoShowWindow); + } +} + +void nsWindow::FrameState::EnsureFullscreenMode(bool aFullScreen, + DoShowWindow aDoShowWindow) { + const bool changed = aFullScreen != mFullscreenMode; + if (changed && aFullScreen) { + // Save the size mode from before fullscreen. + mPreFullscreenSizeMode = mSizeMode; + } + mFullscreenMode = aFullScreen; + if (changed || aFullScreen) { + // NOTE(emilio): When minimizing a fullscreen window we remain with + // mFullscreenMode = true, but mSizeMode = nsSizeMode_Minimized. We need to + // make sure to call SetSizeModeInternal even if mFullscreenMode didn't + // change, to ensure we actually end up with a fullscreen sizemode when + // restoring a window from that state. + SetSizeModeInternal( + aFullScreen ? nsSizeMode_Fullscreen : mPreFullscreenSizeMode, + aDoShowWindow); + } +} + +void nsWindow::FrameState::OnFrameChanging() { + const nsSizeMode newSizeMode = + GetSizeModeForWindowFrame(mWindow->mWnd, mFullscreenMode); + EnsureSizeMode(newSizeMode); + mWindow->UpdateNonClientMargins(false); +} + +void nsWindow::FrameState::OnFrameChanged() { + // We don't want to perform the ShowWindow ourselves if we're on the frame + // changed message. Windows has done the frame change for us, and we take care + // of activating as needed. We also don't want to potentially trigger + // more focus / restore. Among other things, this addresses a bug on Win7 + // related to window docking. (bug 489258) + const auto newSizeMode = + GetSizeModeForWindowFrame(mWindow->mWnd, mFullscreenMode); + EnsureSizeMode(newSizeMode, DoShowWindow::No); + + // If window was restored, activate the window now to get correct attributes. + if (mWindow->mIsVisible && mWindow->IsForegroundWindow() && + mLastSizeMode == nsSizeMode_Minimized && + mSizeMode != nsSizeMode_Minimized) { + mWindow->DispatchFocusToTopLevelWindow(true); + } + mLastSizeMode = mSizeMode; +} + +static void MaybeLogSizeMode(nsSizeMode aMode) { +#ifdef WINSTATE_DEBUG_OUTPUT + MOZ_LOG(gWindowsLog, LogLevel::Info, ("*** SizeMode: %d\n", int(aMode))); +#endif +} + +void nsWindow::FrameState::SetSizeModeInternal(nsSizeMode aMode, + DoShowWindow aDoShowWindow) { + if (mSizeMode == aMode) { + return; + } + + const auto oldSizeMode = mSizeMode; + const bool fullscreenChange = + mSizeMode == nsSizeMode_Fullscreen || aMode == nsSizeMode_Fullscreen; + const bool fullscreen = aMode == nsSizeMode_Fullscreen; + + mLastSizeMode = mSizeMode; + mSizeMode = aMode; + + MaybeLogSizeMode(mSizeMode); + + if (bool(aDoShowWindow) && mWindow->mIsVisible) { + ShowWindowWithMode(mWindow->mWnd, aMode); + } + + mWindow->UpdateNonClientMargins(false); + + if (fullscreenChange) { + mWindow->OnFullscreenChanged(oldSizeMode, fullscreen); + } + + mWindow->OnSizeModeChange(); +} + +void nsWindow::ContextMenuPreventer::Update( + const WidgetMouseEvent& aEvent, + const nsIWidget::ContentAndAPZEventStatus& aEventStatus) { + mNeedsToPreventContextMenu = + aEvent.mMessage == eMouseUp && + aEvent.mButton == MouseButton::eSecondary && + aEvent.mInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE && + aEventStatus.mApzStatus == nsEventStatus_eConsumeNoDefault; +} diff --git a/widget/windows/nsWindow.h b/widget/windows/nsWindow.h new file mode 100644 index 0000000000..c75c9d174d --- /dev/null +++ b/widget/windows/nsWindow.h @@ -0,0 +1,905 @@ +/* -*- 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_NSWINDOW_H_ +#define WIDGET_WINDOWS_NSWINDOW_H_ + +/* + * nsWindow - Native window management and event handling. + */ + +#include "mozilla/RefPtr.h" +#include "nsBaseWidget.h" +#include "CompositorWidget.h" +#include "mozilla/EventForwards.h" +#include "nsClassHashtable.h" +#include <windows.h> +#include "touchinjection_sdk80.h" +#include "nsdefs.h" +#include "nsUserIdleService.h" +#include "nsToolkit.h" +#include "nsString.h" +#include "nsTArray.h" +#include "gfxWindowsPlatform.h" +#include "gfxWindowsSurface.h" +#include "nsWindowDbg.h" +#include "cairo.h" +#include "nsRegion.h" +#include "mozilla/EnumeratedArray.h" +#include "mozilla/Maybe.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/webrender/WebRenderTypes.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/DataMutex.h" +#include "mozilla/UniquePtr.h" +#include "nsMargin.h" +#include "nsRegionFwd.h" + +#include "nsWinGesture.h" +#include "WinPointerEvents.h" +#include "WinUtils.h" +#include "WindowHook.h" +#include "TaskbarWindowPreview.h" + +#ifdef ACCESSIBILITY +# include "oleacc.h" +# include "mozilla/a11y/LocalAccessible.h" +#endif + +#include "nsUXThemeData.h" +#include "nsIUserIdleServiceInternal.h" + +#include "IMMHandler.h" +#include "CheckInvariantWrapper.h" + +/** + * Forward class definitions + */ + +class nsNativeDragTarget; +class nsIRollupListener; +class imgIContainer; + +namespace mozilla { +class WidgetMouseEvent; +namespace widget { +class NativeKey; +class InProcessWinCompositorWidget; +struct MSGResult; +class DirectManipulationOwner; +} // namespace widget +} // namespace mozilla + +/** + * Forward Windows-internal definitions of otherwise incomplete ones provided by + * the SDK. + */ +const CLSID CLSID_ImmersiveShell = { + 0xC2F03A33, + 0x21F5, + 0x47FA, + {0xB4, 0xBB, 0x15, 0x63, 0x62, 0xA2, 0xF2, 0x39}}; + +/** + * Native WIN32 window wrapper. + */ + +class nsWindow final : public nsBaseWidget { + public: + using WindowHook = mozilla::widget::WindowHook; + using IMEContext = mozilla::widget::IMEContext; + using WidgetEventTime = mozilla::WidgetEventTime; + + NS_INLINE_DECL_REFCOUNTING_INHERITED(nsWindow, nsBaseWidget) + + explicit nsWindow(bool aIsChildWindow = false); + + void SendAnAPZEvent(mozilla::InputData& aEvent); + + /* + * Init a standard gecko event for this widget. + * @param aEvent the event to initialize. + * @param aPoint message position in physical coordinates. + */ + void InitEvent(mozilla::WidgetGUIEvent& aEvent, + LayoutDeviceIntPoint* aPoint = nullptr); + + /* + * Returns WidgetEventTime instance which is initialized with current message + * time. + */ + WidgetEventTime CurrentMessageWidgetEventTime() const; + + /* + * Dispatch a gecko keyboard event for this widget. This + * is called by KeyboardLayout to dispatch gecko events. + * Returns true if it's consumed. Otherwise, false. + */ + bool DispatchKeyboardEvent(mozilla::WidgetKeyboardEvent* aEvent); + + /* + * Dispatch a gecko wheel event for this widget. This + * is called by ScrollHandler to dispatch gecko events. + * Returns true if it's consumed. Otherwise, false. + */ + bool DispatchWheelEvent(mozilla::WidgetWheelEvent* aEvent); + + /* + * Dispatch a gecko content command event for this widget. This + * is called by ScrollHandler to dispatch gecko events. + * Returns true if it's consumed. Otherwise, false. + */ + bool DispatchContentCommandEvent(mozilla::WidgetContentCommandEvent* aEvent); + + /* + * Return the parent window, if it exists. + */ + nsWindow* GetParentWindowBase(bool aIncludeOwner); + + /* + * Return true if this is a top level widget. + */ + bool IsTopLevelWidget() { return mIsTopWidgetWindow; } + + // nsIWidget interface + using nsBaseWidget::Create; // for Create signature not overridden here + [[nodiscard]] nsresult Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + InitData* aInitData = nullptr) override; + void Destroy() override; + void SetParent(nsIWidget* aNewParent) override; + nsIWidget* GetParent(void) override; + float GetDPI() override; + double GetDefaultScaleInternal() override; + int32_t LogToPhys(double aValue); + mozilla::DesktopToLayoutDeviceScale GetDesktopToDeviceScale() override { + if (mozilla::widget::WinUtils::IsPerMonitorDPIAware()) { + return mozilla::DesktopToLayoutDeviceScale(1.0); + } else { + return mozilla::DesktopToLayoutDeviceScale(GetDefaultScaleInternal()); + } + } + + void Show(bool aState) override; + bool IsVisible() const override; + void ConstrainPosition(DesktopIntPoint&) override; + void SetSizeConstraints(const SizeConstraints& aConstraints) override; + void LockAspectRatio(bool aShouldLock) override; + const SizeConstraints GetSizeConstraints() override; + void SetInputRegion(const InputRegion&) override; + void Move(double aX, double aY) override; + void Resize(double aWidth, double aHeight, bool aRepaint) override; + void Resize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint) override; + mozilla::Maybe<bool> IsResizingNativeWidget() override; + void PlaceBehind(nsTopLevelWidgetZPlacement aPlacement, nsIWidget* aWidget, + bool aActivate) override; + void SetSizeMode(nsSizeMode aMode) override; + nsSizeMode SizeMode() override; + void GetWorkspaceID(nsAString& workspaceID) override; + void MoveToWorkspace(const nsAString& workspaceID) override; + void SuppressAnimation(bool aSuppress) override; + void Enable(bool aState) override; + bool IsEnabled() const override; + void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override; + LayoutDeviceIntRect GetBounds() override; + LayoutDeviceIntRect GetScreenBounds() override; + [[nodiscard]] nsresult GetRestoredBounds(LayoutDeviceIntRect& aRect) override; + LayoutDeviceIntRect GetClientBounds() override; + LayoutDeviceIntPoint GetClientOffset() override; + void SetBackgroundColor(const nscolor& aColor) override; + void SetCursor(const Cursor&) override; + bool PrepareForFullscreenTransition(nsISupports** aData) override; + void PerformFullscreenTransition(FullscreenTransitionStage aStage, + uint16_t aDuration, nsISupports* aData, + nsIRunnable* aCallback) override; + void CleanupFullscreenTransition() override; + nsresult MakeFullScreen(bool aFullScreen) override; + void HideWindowChrome(bool aShouldHide) override; + void Invalidate(bool aEraseBackground = false, bool aUpdateNCArea = false, + bool aIncludeChildren = false); + void Invalidate(const LayoutDeviceIntRect& aRect) override; + void* GetNativeData(uint32_t aDataType) override; + void FreeNativeData(void* data, uint32_t aDataType) override; + nsresult SetTitle(const nsAString& aTitle) override; + void SetIcon(const nsAString& aIconSpec) override; + LayoutDeviceIntPoint WidgetToScreenOffset() override; + LayoutDeviceIntMargin ClientToWindowMargin() override; + nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) override; + void EnableDragDrop(bool aEnable) override; + void CaptureMouse(bool aCapture); + void CaptureRollupEvents(bool aDoCapture) override; + [[nodiscard]] nsresult GetAttention(int32_t aCycleCount) override; + bool HasPendingInputEvent() override; + WindowRenderer* GetWindowRenderer() override; + void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override; + [[nodiscard]] nsresult OnDefaultButtonLoaded( + const LayoutDeviceIntRect& aButtonRect) override; + nsresult SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout, + int32_t aNativeKeyCode, + uint32_t aModifierFlags, + const nsAString& aCharacters, + const nsAString& aUnmodifiedCharacters, + nsIObserver* aObserver) override; + nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint, + NativeMouseMessage aNativeMessage, + mozilla::MouseButton aButton, + nsIWidget::Modifiers aModifierFlags, + nsIObserver* aObserver) override; + + nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint, + nsIObserver* aObserver) override { + return SynthesizeNativeMouseEvent( + aPoint, NativeMouseMessage::Move, mozilla::MouseButton::eNotPressed, + nsIWidget::Modifiers::NO_MODIFIERS, aObserver); + } + + nsresult SynthesizeNativeMouseScrollEvent( + LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX, + double aDeltaY, double aDeltaZ, uint32_t aModifierFlags, + uint32_t aAdditionalFlags, nsIObserver* aObserver) override; + + nsresult SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase, + LayoutDeviceIntPoint aPoint, + double aDeltaX, double aDeltaY, + int32_t aModifierFlagsn, + nsIObserver* aObserver) override; + + void SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) override; + InputContext GetInputContext() override; + TextEventDispatcherListener* GetNativeTextEventDispatcherListener() override; + void SetTransparencyMode(TransparencyMode aMode) override; + TransparencyMode GetTransparencyMode() override; + nsresult SetNonClientMargins(const LayoutDeviceIntMargin&) override; + void SetResizeMargin(mozilla::LayoutDeviceIntCoord aResizeMargin) override; + void UpdateWindowDraggingRegion( + const LayoutDeviceIntRegion& aRegion) override; + + uint32_t GetMaxTouchPoints() const override; + void SetWindowClass(const nsAString& xulWinType, const nsAString& xulWinClass, + const nsAString& xulWinName) override; + + /** + * Event helpers + */ + bool DispatchMouseEvent(mozilla::EventMessage aEventMessage, WPARAM wParam, + LPARAM lParam, bool aIsContextMenuKey, + int16_t aButton, uint16_t aInputSource, + WinPointerInfo* aPointerInfo = nullptr, + bool aIgnoreAPZ = false); + void DispatchPendingEvents(); + void DispatchCustomEvent(const nsString& eventName); + +#ifdef ACCESSIBILITY + /** + * Return an accessible associated with the window. + */ + mozilla::a11y::LocalAccessible* GetAccessible(); +#endif // ACCESSIBILITY + + /** + * Window utilities + */ + nsWindow* GetTopLevelWindow(bool aStopOnDialogOrPopup); + WNDPROC GetPrevWindowProc() { return mPrevWndProc.valueOr(nullptr); } + WindowHook& GetWindowHook() { return mWindowHook; } + nsWindow* GetParentWindow(bool aIncludeOwner); + + /** + * Misc. + */ + bool WidgetTypeSupportsAcceleration() override; + + void ForcePresent(); + bool TouchEventShouldStartDrag(mozilla::EventMessage aEventMessage, + LayoutDeviceIntPoint aEventPoint); + + void SetSmallIcon(HICON aIcon); + void SetBigIcon(HICON aIcon); + void SetSmallIconNoData(); + void SetBigIconNoData(); + + static void SetIsRestoringSession(const bool aIsRestoringSession) { + sIsRestoringSession = aIsRestoringSession; + } + + bool IsRTL() const { return mIsRTL; } + + /** + * AssociateDefaultIMC() associates or disassociates the default IMC for + * the window. + * + * @param aAssociate TRUE, associates the default IMC with the window. + * Otherwise, disassociates the default IMC from the + * window. + * @return TRUE if this method associated the default IMC with + * disassociated window or disassociated the default IMC + * from associated window. + * Otherwise, i.e., if this method did nothing actually, + * FALSE. + */ + bool AssociateDefaultIMC(bool aAssociate); + + bool HasTaskbarIconBeenCreated() { return mHasTaskbarIconBeenCreated; } + // Called when either the nsWindow or an nsITaskbarTabPreview receives the + // noticiation that this window has its icon placed on the taskbar. + void SetHasTaskbarIconBeenCreated(bool created = true) { + mHasTaskbarIconBeenCreated = created; + } + + // Getter/setter for the nsITaskbarWindowPreview for this nsWindow + already_AddRefed<nsITaskbarWindowPreview> GetTaskbarPreview() { + nsCOMPtr<nsITaskbarWindowPreview> preview( + do_QueryReferent(mTaskbarPreview)); + return preview.forget(); + } + void SetTaskbarPreview(nsITaskbarWindowPreview* preview) { + mTaskbarPreview = do_GetWeakReference(preview); + } + + void ReparentNativeWidget(nsIWidget* aNewParent) override; + + // Open file picker tracking + void PickerOpen(); + void PickerClosed(); + + bool DestroyCalled() { return mDestroyCalled; } + + bool IsPopup(); + bool ShouldUseOffMainThreadCompositing() override; + + const IMEContext& DefaultIMC() const { return mDefaultIMC; } + + void GetCompositorWidgetInitData( + mozilla::widget::CompositorWidgetInitData* aInitData) override; + bool IsTouchWindow() const { return mTouchWindow; } + bool SynchronouslyRepaintOnResize() override; + void MaybeDispatchInitialFocusEvent() override; + + void LocalesChanged() override; + + void NotifyOcclusionState(mozilla::widget::OcclusionState aState) override; + void MaybeEnableWindowOcclusion(bool aEnable); + + /* + * Return the HWND or null for this widget. + */ + HWND GetWindowHandle() { + return static_cast<HWND>(GetNativeData(NS_NATIVE_WINDOW)); + } + + /* + * Touch input injection apis + */ + nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId, + TouchPointerState aPointerState, + LayoutDeviceIntPoint aPoint, + double aPointerPressure, + uint32_t aPointerOrientation, + nsIObserver* aObserver) override; + nsresult ClearNativeTouchSequence(nsIObserver* aObserver) override; + + nsresult SynthesizeNativePenInput(uint32_t aPointerId, + TouchPointerState aPointerState, + LayoutDeviceIntPoint aPoint, + double aPressure, uint32_t aRotation, + int32_t aTiltX, int32_t aTiltY, + int32_t aButton, + nsIObserver* aObserver) override; + + /* + * WM_APPCOMMAND common handler. + * Sends events via NativeKey::HandleAppCommandMessage(). + */ + bool HandleAppCommandMsg(const MSG& aAppCommandMsg, LRESULT* aRetValue); + + const InputContext& InputContextRef() const { return mInputContext; } + + private: + using TimeStamp = mozilla::TimeStamp; + using TimeDuration = mozilla::TimeDuration; + using TaskbarWindowPreview = mozilla::widget::TaskbarWindowPreview; + using NativeKey = mozilla::widget::NativeKey; + using MSGResult = mozilla::widget::MSGResult; + using PlatformCompositorWidgetDelegate = + mozilla::widget::PlatformCompositorWidgetDelegate; + + struct Desktop { + // Cached GUID of the virtual desktop this window should be on. + // This value may be stale. + nsString mID; + bool mUpdateIsQueued = false; + }; + + class PointerInfo { + public: + enum class PointerType : uint8_t { + TOUCH, + PEN, + }; + + PointerInfo(int32_t aPointerId, LayoutDeviceIntPoint& aPoint, + PointerType aType) + : mPointerId(aPointerId), mPosition(aPoint), mType(aType) {} + + int32_t mPointerId; + LayoutDeviceIntPoint mPosition; + PointerType mType; + }; + + class FrameState { + public: + explicit FrameState(nsWindow* aWindow); + + void ConsumePreXULSkeletonState(bool aWasMaximized); + + // Whether we should call ShowWindow with the relevant size mode if needed. + // We want to avoid that when Windows is already performing the change for + // us (via the SWP_FRAMECHANGED messages). + enum class DoShowWindow : bool { No, Yes }; + + void EnsureSizeMode(nsSizeMode, DoShowWindow = DoShowWindow::Yes); + void EnsureFullscreenMode(bool, DoShowWindow = DoShowWindow::Yes); + void OnFrameChanging(); + void OnFrameChanged(); + + nsSizeMode GetSizeMode() const; + + void CheckInvariant() const; + + private: + void SetSizeModeInternal(nsSizeMode, DoShowWindow); + + nsSizeMode mSizeMode = nsSizeMode_Normal; + // XXX mLastSizeMode is rather bizarre and needs some documentation. + nsSizeMode mLastSizeMode = nsSizeMode_Normal; + // The old size mode before going into fullscreen mode. This should never + // be nsSizeMode_Fullscreen. + nsSizeMode mPreFullscreenSizeMode = nsSizeMode_Normal; + // Whether we're in fullscreen. We need to keep this state out of band, + // rather than just using mSizeMode, because a window can be minimized + // while fullscreen, and we don't store the fullscreen state anywhere else. + bool mFullscreenMode = false; + nsWindow* mWindow; + }; + + // Manager for taskbar-hiding. No persistent state. + class TaskbarConcealer; + + // A magic number to identify the FAKETRACKPOINTSCROLLABLE window created + // when the trackpoint hack is enabled. + enum { eFakeTrackPointScrollableID = 0x46545053 }; + + // Used for displayport suppression during window resize + enum ResizeState { NOT_RESIZING, IN_SIZEMOVE, RESIZING, MOVING }; + + ~nsWindow() override; + + void WindowUsesOMTC() override; + void RegisterTouchWindow() override; + + /** + * Callbacks + */ + static LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam, + LPARAM lParam); + static LRESULT CALLBACK WindowProcInternal(HWND hWnd, UINT msg, WPARAM wParam, + LPARAM lParam); + + static BOOL CALLBACK DispatchStarvedPaints(HWND aTopWindow, LPARAM aMsg); + static BOOL CALLBACK RegisterTouchForDescendants(HWND aTopWindow, + LPARAM aMsg); + static BOOL CALLBACK UnregisterTouchForDescendants(HWND aTopWindow, + LPARAM aMsg); + static LRESULT CALLBACK MozSpecialMsgFilter(int code, WPARAM wParam, + LPARAM lParam); + static LRESULT CALLBACK MozSpecialWndProc(int code, WPARAM wParam, + LPARAM lParam); + static LRESULT CALLBACK MozSpecialMouseProc(int code, WPARAM wParam, + LPARAM lParam); + static VOID CALLBACK HookTimerForPopups(HWND hwnd, UINT uMsg, UINT idEvent, + DWORD dwTime); + + /** + * Window utilities + */ + LPARAM lParamToScreen(LPARAM lParam); + LPARAM lParamToClient(LPARAM lParam); + + WPARAM wParamFromGlobalMouseState(); + + bool AssociateWithNativeWindow(); + void DissociateFromNativeWindow(); + bool CanTakeFocus(); + bool UpdateNonClientMargins(bool aReflowWindow = true); + void UpdateDarkModeToolbar(); + void UpdateGetWindowInfoCaptionStatus(bool aActiveCaption); + void ResetLayout(); + void InvalidateNonClientRegion(); + static const wchar_t* GetMainWindowClass(); + HWND GetOwnerWnd() const { return ::GetWindow(mWnd, GW_OWNER); } + bool IsOwnerForegroundWindow() const { + HWND owner = GetOwnerWnd(); + return owner && owner == ::GetForegroundWindow(); + } + bool IsForegroundWindow() const { return mWnd == ::GetForegroundWindow(); } + bool IsPopup() const { return mWindowType == WindowType::Popup; } + bool IsCloaked() const { return mIsCloaked; } + + /** + * Event processing helpers + */ + HWND GetTopLevelForFocus(HWND aCurWnd); + void DispatchFocusToTopLevelWindow(bool aIsActivate); + bool DispatchStandardEvent(mozilla::EventMessage aMsg); + void RelayMouseEvent(UINT aMsg, WPARAM wParam, LPARAM lParam); + bool ProcessMessage(UINT msg, WPARAM& wParam, LPARAM& lParam, + LRESULT* aRetValue); + // We wrap this in ProcessMessage so we can log the return value + bool ProcessMessageInternal(UINT msg, WPARAM& wParam, LPARAM& lParam, + LRESULT* aRetValue); + bool ExternalHandlerProcessMessage(UINT aMessage, WPARAM& aWParam, + LPARAM& aLParam, MSGResult& aResult); + LRESULT ProcessCharMessage(const MSG& aMsg, bool* aEventDispatched); + LRESULT ProcessKeyUpMessage(const MSG& aMsg, bool* aEventDispatched); + LRESULT ProcessKeyDownMessage(const MSG& aMsg, bool* aEventDispatched); + static bool EventIsInsideWindow( + nsWindow* aWindow, + mozilla::Maybe<POINT> aEventPoint = mozilla::Nothing()); + static void PostSleepWakeNotification(const bool aIsSleepMode); + int32_t ClientMarginHitTestPoint(int32_t mx, int32_t my); + void SetWindowButtonRect(WindowButtonType aButtonType, + const LayoutDeviceIntRect& aClientRect) override { + mWindowBtnRect[aButtonType] = aClientRect; + } + TimeStamp GetMessageTimeStamp(LONG aEventTime) const; + static void UpdateFirstEventTime(DWORD aEventTime); + void FinishLiveResizing(ResizeState aNewState); + mozilla::Maybe<mozilla::PanGestureInput> ConvertTouchToPanGesture( + const mozilla::MultiTouchInput& aTouchInput, PTOUCHINPUT aOriginalEvent); + void DispatchTouchOrPanGestureInput(mozilla::MultiTouchInput& aTouchInput, + PTOUCHINPUT aOSEvent); + + /** + * Event handlers + */ + void OnDestroy() override; + bool OnResize(const LayoutDeviceIntSize& aSize); + void OnSizeModeChange(); + bool OnGesture(WPARAM wParam, LPARAM lParam); + bool OnTouch(WPARAM wParam, LPARAM lParam); + bool OnHotKey(WPARAM wParam, LPARAM lParam); + bool OnPaint(uint32_t aNestingLevel); + void OnWindowPosChanging(WINDOWPOS* info); + void OnWindowPosChanged(WINDOWPOS* wp); + void OnSysColorChanged(); + void OnDPIChanged(int32_t x, int32_t y, int32_t width, int32_t height); + bool OnPointerEvents(UINT msg, WPARAM wParam, LPARAM lParam); + + /** + * Function that registers when the user has been active (used for detecting + * when the user is idle). + */ + void UserActivity(); + + int32_t GetHeight(int32_t aProposedHeight); + + DWORD WindowStyle(); + DWORD WindowExStyle(); + + static const wchar_t* ChooseWindowClass(WindowType); + // This method registers the given window class, and returns the class name. + static const wchar_t* RegisterWindowClass(const wchar_t* aClassName, + UINT aExtraStyle, LPWSTR aIconID); + + /** + * Popup hooks + */ + static void ScheduleHookTimer(HWND aWnd, UINT aMsgId); + static void RegisterSpecialDropdownHooks(); + static void UnregisterSpecialDropdownHooks(); + static bool GetPopupsToRollup( + nsIRollupListener* aRollupListener, uint32_t* aPopupsToRollup, + mozilla::Maybe<POINT> aEventPoint = mozilla::Nothing()); + static bool NeedsToHandleNCActivateDelayed(HWND aWnd); + static bool DealWithPopups(HWND inWnd, UINT inMsg, WPARAM inWParam, + LPARAM inLParam, LRESULT* outResult); + + /** + * Window transparency helpers + */ + void SetWindowTranslucencyInner(TransparencyMode aMode); + TransparencyMode GetWindowTranslucencyInner() const { + return mTransparencyMode; + } + bool IsSimulatedClientArea(int32_t clientX, int32_t clientY); + bool IsWindowButton(int32_t hitTestResult); + + bool DispatchTouchEventFromWMPointer(UINT msg, LPARAM aLParam, + const WinPointerInfo& aPointerInfo, + mozilla::MouseButton aButton); + + static bool IsAsyncResponseEvent(UINT aMsg, LRESULT& aResult); + void IPCWindowProcHandler(UINT& msg, WPARAM& wParam, LPARAM& lParam); + + /** + * Misc. + */ + void StopFlashing(); + static HWND WindowAtMouse(); + static bool IsTopLevelMouseExit(HWND aWnd); + LayoutDeviceIntRegion GetRegionToPaint(bool aForceFullRepaint, PAINTSTRUCT ps, + HDC aDC); + nsIWidgetListener* GetPaintListener(); + + void CreateCompositor() override; + void DestroyCompositor() override; + void RequestFxrOutput() override; + + void RecreateDirectManipulationIfNeeded(); + void ResizeDirectManipulationViewport(); + void DestroyDirectManipulation(); + + bool NeedsToTrackWindowOcclusionState(); + + void AsyncUpdateWorkspaceID(Desktop& aDesktop); + + // See bug 603793 + static bool HasBogusPopupsDropShadowOnMultiMonitor(); + + static void InitMouseWheelScrollData(); + + void ChangedDPI(); + + static bool InitTouchInjection(); + + bool InjectTouchPoint(uint32_t aId, LayoutDeviceIntPoint& aPoint, + POINTER_FLAGS aFlags, uint32_t aPressure = 1024, + uint32_t aOrientation = 90); + + void OnFullscreenChanged(nsSizeMode aOldSizeMode, bool aFullScreen); + void TryDwmResizeHack(); + + static void OnCloakEvent(HWND aWnd, bool aCloaked); + void OnCloakChanged(bool aCloaked); + +#ifdef DEBUG + virtual nsresult SetHiDPIMode(bool aHiDPI) override; + virtual nsresult RestoreHiDPIMode() override; +#endif + + // Get the orientation of the hidden taskbar, on the screen that this window + // is on, or Nothing if taskbar isn't hidden. + mozilla::Maybe<UINT> GetHiddenTaskbarEdge(); + + static bool sTouchInjectInitialized; + static InjectTouchInputPtr sInjectTouchFuncPtr; + static uint32_t sInstanceCount; + static nsWindow* sCurrentWindow; + static bool sIsOleInitialized; + static Cursor sCurrentCursor; + static bool sJustGotDeactivate; + static bool sJustGotActivate; + static bool sIsInMouseCapture; + static bool sIsRestoringSession; + + // Message postponement hack. See the definition-site of + // WndProcUrgentInvocation::sDepth for details. + struct MOZ_STACK_CLASS WndProcUrgentInvocation { + struct Marker { + Marker() { ++sDepth; } + ~Marker() { --sDepth; } + }; + inline static bool IsActive() { return sDepth > 0; } + static size_t sDepth; + }; + + // Hook Data Members for Dropdowns. sProcessHook Tells the + // hook methods whether they should be processing the hook + // messages. + static HHOOK sMsgFilterHook; + static HHOOK sCallProcHook; + static HHOOK sCallMouseHook; + static bool sProcessHook; + static UINT sRollupMsgId; + static HWND sRollupMsgWnd; + static UINT sHookTimerId; + + // Used to prevent dispatching mouse events that do not originate from user + // input. + static POINT sLastMouseMovePoint; + + nsClassHashtable<nsUint32HashKey, PointerInfo> mActivePointers; + + // This is used by SynthesizeNativeTouchPoint to maintain state between + // multiple synthesized points, in the case where we can't call InjectTouch + // directly. + mozilla::UniquePtr<mozilla::MultiTouchInput> mSynthesizedTouchInput; + + InputContext mInputContext; + + nsCOMPtr<nsIWidget> mParent; + nsIntSize mLastSize = nsIntSize(0, 0); + nsIntPoint mLastPoint; + HWND mWnd = nullptr; + HWND mTransitionWnd = nullptr; + mozilla::Maybe<WNDPROC> mPrevWndProc; + HBRUSH mBrush; + IMEContext mDefaultIMC; + HDEVNOTIFY mDeviceNotifyHandle = nullptr; + bool mIsTopWidgetWindow = false; + bool mInDtor = false; + bool mIsVisible = false; + bool mIsCloaked = false; + bool mTouchWindow = false; + bool mDisplayPanFeedback = false; + bool mHideChrome = false; + bool mIsRTL; + bool mMousePresent = false; + bool mSimulatedClientArea = false; + bool mDestroyCalled = false; + bool mOpeningAnimationSuppressed; + bool mAlwaysOnTop; + bool mIsEarlyBlankWindow = false; + bool mIsShowingPreXULSkeletonUI = false; + bool mResizable = false; + // Whether we're an alert window. Alert windows don't have taskbar icons and + // don't steal focus from other windows when opened. They're also expected to + // be of type WindowType::Dialog. + bool mIsAlert = false; + bool mIsPerformingDwmFlushHack = false; + bool mDraggingWindowWithMouse = false; + DWORD_PTR mOldStyle = 0; + DWORD_PTR mOldExStyle = 0; + nsNativeDragTarget* mNativeDragTarget = nullptr; + HKL mLastKeyboardLayout = 0; + mozilla::CheckInvariantWrapper<FrameState> mFrameState; + WindowHook mWindowHook; + uint32_t mPickerDisplayCount = 0; + HICON mIconSmall = nullptr; + HICON mIconBig = nullptr; + HWND mLastKillFocusWindow = nullptr; + PlatformCompositorWidgetDelegate* mCompositorWidgetDelegate = nullptr; + + LayoutDeviceIntMargin NonClientSizeMargin() const { + return NonClientSizeMargin(mNonClientOffset); + } + LayoutDeviceIntMargin NonClientSizeMargin( + const LayoutDeviceIntMargin& aNonClientOffset) const; + LayoutDeviceIntMargin NormalWindowNonClientOffset() const; + + // Non-client margin settings + // Pre-calculated outward offset applied to default frames + LayoutDeviceIntMargin mNonClientOffset; + // Margins set by the owner + LayoutDeviceIntMargin mNonClientMargins; + // Margins we'd like to set once chrome is reshown: + LayoutDeviceIntMargin mFutureMarginsOnceChromeShows; + // Indicates we need to apply margins once toggling chrome into showing: + bool mFutureMarginsToUse = false; + + // Indicates custom frames are enabled + bool mCustomNonClient = false; + // Indicates custom resize margins are in effect + bool mUseResizeMarginOverrides = false; + // Width of the left and right portions of the resize region + mozilla::LayoutDeviceIntCoord mHorResizeMargin; + // Height of the top and bottom portions of the resize region + mozilla::LayoutDeviceIntCoord mVertResizeMargin; + // Height of the caption plus border + mozilla::LayoutDeviceIntCoord mCaptionHeight; + + // not yet set, will be calculated on first use + double mDefaultScale = -1.0; + + // not yet set, will be calculated on first use + float mAspectRatio = 0.0; + + nsCOMPtr<nsIUserIdleServiceInternal> mIdleService; + + // Draggable titlebar region maintained by UpdateWindowDraggingRegion + LayoutDeviceIntRegion mDraggableRegion; + + // Graphics + LayoutDeviceIntRect mLastPaintBounds; + + ResizeState mResizeState = NOT_RESIZING; + + // Transparency + TransparencyMode mTransparencyMode = TransparencyMode::Opaque; + nsIntRegion mPossiblyTransparentRegion; + + // Win7 Gesture processing and management + nsWinGesture mGesture; + + // Weak ref to the nsITaskbarWindowPreview associated with this window + nsWeakPtr mTaskbarPreview = nullptr; + + // The input region that determines whether mouse events should be ignored + // and pass through to the window below. This is currently only used for + // popups. + InputRegion mInputRegion; + + // True if the taskbar (possibly through the tab preview) tells us that the + // icon has been created on the taskbar. + bool mHasTaskbarIconBeenCreated = false; + + // Whether we're in the process of sending a WM_SETTEXT ourselves + bool mSendingSetText = false; + + // Whether we were created as a child window (aka ChildWindow) or not. + bool mIsChildWindow : 1; + + int32_t mCachedHitTestResult = 0; + + // The point in time at which the last paint completed. We use this to avoid + // painting too rapidly in response to frequent input events. + TimeStamp mLastPaintEndTime; + + // Caching for hit test results (in client coordinates) + LayoutDeviceIntPoint mCachedHitTestPoint; + TimeStamp mCachedHitTestTime; + + RefPtr<mozilla::widget::InProcessWinCompositorWidget> mBasicLayersSurface; + + double mSizeConstraintsScale; // scale in effect when setting constraints + + // Will be calculated when layer manager is created. + int32_t mMaxTextureSize = -1; + + // Pointer events processing and management + WinPointerEvents mPointerEvents; + + ScreenPoint mLastPanGestureFocus; + + // When true, used to indicate an async call to RequestFxrOutput to the GPU + // process after the Compositor is created + bool mRequestFxrOutputPending = false; + + // A stack based class used in DispatchMouseEvent() to tell whether we should + // NOT open context menu when we receives WM_CONTEXTMENU after the + // DispatchMouseEvent calls. + // This class now works only in the case where a mouse up event happened in + // the overscroll gutter. + class MOZ_STACK_CLASS ContextMenuPreventer final { + public: + explicit ContextMenuPreventer(nsWindow* aWindow) + : mWindow(aWindow), mNeedsToPreventContextMenu(false){}; + ~ContextMenuPreventer() { + mWindow->mNeedsToPreventContextMenu = mNeedsToPreventContextMenu; + } + void Update(const mozilla::WidgetMouseEvent& aEvent, + const nsIWidget::ContentAndAPZEventStatus& aEventStatus); + + private: + nsWindow* mWindow; + bool mNeedsToPreventContextMenu = false; + }; + friend class ContextMenuPreventer; + bool mNeedsToPreventContextMenu = false; + + mozilla::UniquePtr<mozilla::widget::DirectManipulationOwner> mDmOwner; + + // Client rect for minimize, maximize and close buttons. + mozilla::EnumeratedArray<WindowButtonType, WindowButtonType::Count, + LayoutDeviceIntRect> + mWindowBtnRect; + + mozilla::DataMutex<Desktop> mDesktopId; + + // If set, indicates the edge of the NC region we should clear to black + // on next paint. One of: ABE_TOP, ABE_BOTTOM, ABE_LEFT or ABE_RIGHT. + mozilla::Maybe<UINT> mClearNCEdge; + + friend class nsWindowGfx; + + static constexpr int kHiddenTaskbarSize = 2; +}; + +#endif // WIDGET_WINDOWS_NSWINDOW_H_ diff --git a/widget/windows/nsWindowDbg.cpp b/widget/windows/nsWindowDbg.cpp new file mode 100644 index 0000000000..8125a8532b --- /dev/null +++ b/widget/windows/nsWindowDbg.cpp @@ -0,0 +1,1612 @@ +/* -*- 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/. */ + +/* + * nsWindowDbg - Debug related utilities for nsWindow. + */ + +#include "nsWindowDbg.h" +#include "nsToolkit.h" +#include "WinPointerEvents.h" +#include "nsWindowLoggedMessages.h" +#include "mozilla/Logging.h" +#include "mozilla/Maybe.h" +#include "nsWindow.h" +#include "GeckoProfiler.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Document.h" + +#include <winuser.h> +#include <dbt.h> +#include <imm.h> +#include <tpcshrd.h> + +#include <unordered_set> + +using namespace mozilla; +using namespace mozilla::widget; +extern mozilla::LazyLogModule gWindowsLog; +static mozilla::LazyLogModule gWindowsEventLog("WindowsEvent"); + +// currently defined in widget/windows/nsAppShell.cpp +extern UINT sAppShellGeckoMsgId; + +#if defined(POPUP_ROLLUP_DEBUG_OUTPUT) +MSGFEventMsgInfo gMSGFEvents[] = { + "MSGF_DIALOGBOX", 0, "MSGF_MESSAGEBOX", 1, "MSGF_MENU", 2, + "MSGF_SCROLLBAR", 5, "MSGF_NEXTWINDOW", 6, "MSGF_MAX", 8, + "MSGF_USER", 4096, nullptr, 0}; +#endif + +static long gEventCounter = 0; +static UINT gLastEventMsg = 0; + +namespace geckoprofiler::markers { + +struct WindowProcMarker { + static constexpr Span<const char> MarkerTypeName() { + return MakeStringSpan("WindowProc"); + } + static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, + const ProfilerString8View& aMsgLoopName, + UINT aMsg, WPARAM aWParam, LPARAM aLParam) { + aWriter.StringProperty("messageLoop", aMsgLoopName); + aWriter.IntProperty("uMsg", aMsg); + const char* name; + if (aMsg < WM_USER) { + const auto eventMsgInfo = mozilla::widget::gAllEvents.find(aMsg); + if (eventMsgInfo != mozilla::widget::gAllEvents.end()) { + name = eventMsgInfo->second.mStr; + } else { + name = "ui message"; + } + } else if (aMsg >= WM_USER && aMsg < WM_APP) { + name = "WM_USER message"; + } else if (aMsg >= WM_APP && aMsg < 0xC000) { + name = "WM_APP message"; + } else if (aMsg >= 0xC000 && aMsg < 0x10000) { + if (aMsg == sAppShellGeckoMsgId) { + name = "nsAppShell:EventID"; + } else { + name = "registered Windows message"; + } + } else { + name = "system message"; + } + aWriter.StringProperty("name", MakeStringSpan(name)); + + if (aWParam) { + aWriter.IntProperty("wParam", aWParam); + } + if (aLParam) { + aWriter.IntProperty("lParam", aLParam); + } + } + + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyFormat("uMsg", MS::Format::Integer); + schema.SetChartLabel( + "{marker.data.messageLoop} | {marker.data.name} ({marker.data.uMsg})"); + schema.SetTableLabel( + "{marker.name} - {marker.data.messageLoop} - {marker.data.name} " + "({marker.data.uMsg})"); + schema.SetTooltipLabel( + "{marker.data.messageLoop} - {marker.name} - {marker.data.name}"); + schema.AddKeyFormat("wParam", MS::Format::Integer); + schema.AddKeyFormat("lParam", MS::Format::Integer); + return schema; + } +}; + +} // namespace geckoprofiler::markers + +namespace mozilla::widget { + +AutoProfilerMessageMarker::AutoProfilerMessageMarker( + Span<const char> aMsgLoopName, HWND hWnd, UINT msg, WPARAM wParam, + LPARAM lParam) + : mMsgLoopName(aMsgLoopName), mMsg(msg), mWParam(wParam), mLParam(lParam) { + if (profiler_thread_is_being_profiled_for_markers()) { + mOptions.emplace(MarkerOptions(MarkerTiming::IntervalStart())); + nsWindow* win = WinUtils::GetNSWindowPtr(hWnd); + if (win) { + nsIWidgetListener* wl = win->GetWidgetListener(); + if (wl) { + PresShell* presShell = wl->GetPresShell(); + if (presShell) { + dom::Document* doc = presShell->GetDocument(); + if (doc) { + mOptions->Set(MarkerInnerWindowId(doc->InnerWindowID())); + } + } + } + } + } +} + +AutoProfilerMessageMarker::~AutoProfilerMessageMarker() { + if (!profiler_thread_is_being_profiled_for_markers()) { + return; + } + + if (mOptions) { + mOptions->TimingRef().SetIntervalEnd(); + } else { + mOptions.emplace(MarkerOptions(MarkerTiming::IntervalEnd())); + } + profiler_add_marker( + "WindowProc", ::mozilla::baseprofiler::category::OTHER, + std::move(*mOptions), geckoprofiler::markers::WindowProcMarker{}, + ProfilerString8View::WrapNullTerminatedString(mMsgLoopName.data()), mMsg, + mWParam, mLParam); +} + +// Using an unordered_set so we can initialize this with nice syntax instead of +// having to add them one at a time to a mozilla::HashSet. +std::unordered_set<UINT> gEventsToLogOriginalParams = { + WM_WINDOWPOSCHANGING, // (dummy comments for clang-format) + WM_SIZING, // + WM_STYLECHANGING, + WM_GETTEXT, + WM_GETMINMAXINFO, + WM_MEASUREITEM, + WM_NCCALCSIZE, +}; + +// If you add an event here, you must add cases for these to +// MakeMessageSpecificData() and AppendFriendlyMessageSpecificData() +// in nsWindowLoggedMessages.cpp. +std::unordered_set<UINT> gEventsToRecordInAboutPage = { + WM_WINDOWPOSCHANGING, // (dummy comments for clang-format) + WM_WINDOWPOSCHANGED, // + WM_SIZING, + WM_SIZE, + WM_DPICHANGED, + WM_SETTINGCHANGE, + WM_NCCALCSIZE, + WM_MOVE, + WM_MOVING, + WM_GETMINMAXINFO, +}; + +NativeEventLogger::NativeEventLogger(Span<const char> aMsgLoopName, HWND hwnd, + UINT msg, WPARAM wParam, LPARAM lParam) + : mProfilerMarker(aMsgLoopName, hwnd, msg, wParam, lParam), + mMsgLoopName(aMsgLoopName.data()), + mHwnd(hwnd), + mMsg(msg), + mWParam(wParam), + mLParam(lParam), + mResult(mozilla::Nothing()), + mShouldLogPostCall(false) { + if (NativeEventLoggerInternal()) { + // this event was logged, so reserve this counter number for the post-call + mEventCounter = mozilla::Some(gEventCounter); + ++gEventCounter; + } +} + +NativeEventLogger::~NativeEventLogger() { + // If mResult is Nothing, perhaps an exception was thrown or something + // before SetResult() was supposed to be called. + if (mResult.isSome()) { + if (NativeEventLoggerInternal() && mEventCounter.isNothing()) { + // We didn't reserve a counter in the pre-call, so reserve it here. + ++gEventCounter; + } + } + if (mMsg == WM_DESTROY) { + // Remove any logged messages for this window. + WindowClosed(mHwnd); + } +} + +void EventMsgInfo::LogParameters(nsCString& str, WPARAM wParam, LPARAM lParam, + bool isPreCall) { + if (mParamInfoFn) { + str = mParamInfoFn(wParam, lParam, isPreCall); + } else { + if (mWParamInfoFn) { + mWParamInfoFn(str, wParam, mWParamName, isPreCall); + } + if (mLParamInfoFn) { + if (mWParamInfoFn) { + str.AppendASCII(" "); + } + mLParamInfoFn(str, lParam, mLParamName, isPreCall); + } + } +} + +nsAutoCString DefaultParamInfo(uint64_t wParam, uint64_t lParam, + bool /* isPreCall */) { + nsAutoCString result; + result.AppendPrintf("wParam=0x%08llX lParam=0x%08llX", wParam, lParam); + return result; +} + +void AppendEnumValueInfo( + nsCString& str, uint64_t value, + const std::unordered_map<uint64_t, const char*>& valuesAndNames, + const char* name) { + if (name != nullptr) { + str.AppendPrintf("%s=", name); + } + auto entry = valuesAndNames.find(value); + if (entry == valuesAndNames.end()) { + str.AppendPrintf("Unknown (0x%08llX)", value); + } else { + str.AppendASCII(entry->second); + } +} + +bool AppendFlagsInfo(nsCString& str, uint64_t flags, + const nsTArray<EnumValueAndName>& flagsAndNames, + const char* name) { + if (name != nullptr) { + str.AppendPrintf("%s=", name); + } + bool firstAppend = true; + for (const EnumValueAndName& flagAndName : flagsAndNames) { + if (MOZ_UNLIKELY(flagAndName.mFlag == 0)) { + // Special case - only want to write this if nothing else was set. + // For this to make sense, 0 values should come at the end of + // flagsAndNames. + if (flags == 0 && firstAppend) { + firstAppend = false; + str.AppendASCII(flagAndName.mName); + } + } else if ((flags & flagAndName.mFlag) == flagAndName.mFlag) { + if (MOZ_LIKELY(!firstAppend)) { + str.Append('|'); + } + firstAppend = false; + str.AppendASCII(flagAndName.mName); + flags = flags & ~flagAndName.mFlag; + } + } + if (flags != 0) { + if (MOZ_LIKELY(!firstAppend)) { + str.Append('|'); + } + firstAppend = false; + str.AppendPrintf("Unknown (0x%08llX)", flags); + } + return !firstAppend; +} + +// if mResult is not set, this is used to log the parameters passed in the +// message, otherwise we are logging the parameters after we have handled the +// message. This is useful for events where we might change the parameters while +// handling the message (for example WM_GETTEXT and WM_NCCALCSIZE) +// Returns whether this message was logged, so we need to reserve a +// counter number for it. +bool NativeEventLogger::NativeEventLoggerInternal() { + mozilla::LogLevel const targetLogLevel = [&] { + // These messages often take up more than 90% of logs if not filtered out. + if (mMsg == WM_SETCURSOR || mMsg == WM_MOUSEMOVE || mMsg == WM_NCHITTEST) { + return LogLevel::Verbose; + } + if (gLastEventMsg == mMsg) { + return LogLevel::Debug; + } + return LogLevel::Info; + }(); + + bool isPreCall = mResult.isNothing(); + if (isPreCall || mShouldLogPostCall) { + bool recordInAboutPage = gEventsToRecordInAboutPage.find(mMsg) != + gEventsToRecordInAboutPage.end(); + bool writeToWindowsLog; + if (isPreCall) { + writeToWindowsLog = MOZ_LOG_TEST(gWindowsEventLog, targetLogLevel); + bool shouldLogAtAll = recordInAboutPage || writeToWindowsLog; + // Since calling mParamInfoFn() allocates a string, only go down this code + // path if we're going to log this message to reduce allocations. + if (!shouldLogAtAll) { + return false; + } + mShouldLogPostCall = true; + bool shouldLogPreCall = gEventsToLogOriginalParams.find(mMsg) != + gEventsToLogOriginalParams.end(); + if (!shouldLogPreCall) { + // Pre-call and we don't want to log both, so skip this one. + return false; + } + } else { + writeToWindowsLog = true; + } + if (recordInAboutPage) { + LogWindowMessage(mHwnd, mMsg, isPreCall, + mEventCounter.valueOr(gEventCounter), mWParam, mLParam, + mResult, mRetValue); + } + gLastEventMsg = mMsg; + if (writeToWindowsLog) { + const auto& eventMsgInfo = gAllEvents.find(mMsg); + const char* msgText = eventMsgInfo != gAllEvents.end() + ? eventMsgInfo->second.mStr + : nullptr; + nsAutoCString paramInfo; + if (eventMsgInfo != gAllEvents.end()) { + eventMsgInfo->second.LogParameters(paramInfo, mWParam, mLParam, + isPreCall); + } else { + paramInfo = DefaultParamInfo(mWParam, mLParam, isPreCall); + } + const char* resultMsg = mResult.isSome() + ? (mResult.value() ? "true" : "false") + : "initial call"; + nsAutoCString logMessage; + logMessage.AppendPrintf( + "%s | %6ld %08" PRIX64 " - 0x%04X %s%s%s: 0x%08" PRIX64 " (%s)\n", + mMsgLoopName, mEventCounter.valueOr(gEventCounter), + reinterpret_cast<uint64_t>(mHwnd), mMsg, + msgText ? msgText : "Unknown", paramInfo.IsEmpty() ? "" : " ", + paramInfo.get(), + mResult.isSome() ? static_cast<uint64_t>(mRetValue) : 0, resultMsg); + const char* logMessageData = logMessage.Data(); + MOZ_LOG(gWindowsEventLog, targetLogLevel, ("%s", logMessageData)); + } + return true; + } + return false; +} + +void TrueFalseParamInfo(nsCString& result, uint64_t value, const char* name, + bool /* isPreCall */) { + result.AppendPrintf("%s=%s", name, value == TRUE ? "TRUE" : "FALSE"); +} + +void TrueFalseLowOrderWordParamInfo(nsCString& result, uint64_t value, + const char* name, bool /* isPreCall */) { + result.AppendPrintf("%s=%s", name, LOWORD(value) == TRUE ? "TRUE" : "FALSE"); +} + +void HexParamInfo(nsCString& result, uint64_t value, const char* name, + bool /* isPreCall */) { + result.AppendPrintf("%s=0x%08llX", name, value); +} + +void IntParamInfo(nsCString& result, uint64_t value, const char* name, + bool /* isPreCall */) { + result.AppendPrintf("%s=%lld", name, value); +} + +void RectParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + LPRECT rect = reinterpret_cast<LPRECT>(value); + if (rect == nullptr) { + str.AppendPrintf("NULL rect?"); + return; + } + if (name != nullptr) { + str.AppendPrintf("%s ", name); + } + str.AppendPrintf("left=%ld top=%ld right=%ld bottom=%ld", rect->left, + rect->top, rect->right, rect->bottom); +} + +#define VALANDNAME_ENTRY(_msg) \ + { _msg, #_msg } + +void CreateStructParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + CREATESTRUCT* createStruct = reinterpret_cast<CREATESTRUCT*>(value); + if (createStruct == nullptr) { + str.AppendASCII("NULL createStruct?"); + return; + } + str.AppendPrintf( + "%s: hInstance=%p hMenu=%p hwndParent=%p lpszName=%S lpszClass=%S x=%d " + "y=%d cx=%d cy=%d", + name, createStruct->hInstance, createStruct->hMenu, + createStruct->hwndParent, createStruct->lpszName, createStruct->lpszClass, + createStruct->x, createStruct->y, createStruct->cx, createStruct->cy); + str.AppendASCII(" "); + const static nsTArray<EnumValueAndName> windowStyles = { + // these combinations of other flags need to come first + VALANDNAME_ENTRY(WS_OVERLAPPEDWINDOW), VALANDNAME_ENTRY(WS_POPUPWINDOW), + VALANDNAME_ENTRY(WS_CAPTION), + // regular flags + VALANDNAME_ENTRY(WS_POPUP), VALANDNAME_ENTRY(WS_CHILD), + VALANDNAME_ENTRY(WS_MINIMIZE), VALANDNAME_ENTRY(WS_VISIBLE), + VALANDNAME_ENTRY(WS_DISABLED), VALANDNAME_ENTRY(WS_CLIPSIBLINGS), + VALANDNAME_ENTRY(WS_CLIPCHILDREN), VALANDNAME_ENTRY(WS_MAXIMIZE), + VALANDNAME_ENTRY(WS_BORDER), VALANDNAME_ENTRY(WS_DLGFRAME), + VALANDNAME_ENTRY(WS_VSCROLL), VALANDNAME_ENTRY(WS_HSCROLL), + VALANDNAME_ENTRY(WS_SYSMENU), VALANDNAME_ENTRY(WS_THICKFRAME), + VALANDNAME_ENTRY(WS_GROUP), VALANDNAME_ENTRY(WS_TABSTOP), + // zero value needs to come last + VALANDNAME_ENTRY(WS_OVERLAPPED)}; + AppendFlagsInfo(str, createStruct->style, windowStyles, "style"); + str.AppendASCII(" "); + const nsTArray<EnumValueAndName> extendedWindowStyles = { + // these combinations of other flags need to come first + VALANDNAME_ENTRY(WS_EX_OVERLAPPEDWINDOW), + VALANDNAME_ENTRY(WS_EX_PALETTEWINDOW), + // regular flags + VALANDNAME_ENTRY(WS_EX_DLGMODALFRAME), + VALANDNAME_ENTRY(WS_EX_NOPARENTNOTIFY), + VALANDNAME_ENTRY(WS_EX_TOPMOST), + VALANDNAME_ENTRY(WS_EX_ACCEPTFILES), + VALANDNAME_ENTRY(WS_EX_TRANSPARENT), + VALANDNAME_ENTRY(WS_EX_MDICHILD), + VALANDNAME_ENTRY(WS_EX_TOOLWINDOW), + VALANDNAME_ENTRY(WS_EX_WINDOWEDGE), + VALANDNAME_ENTRY(WS_EX_CLIENTEDGE), + VALANDNAME_ENTRY(WS_EX_CONTEXTHELP), + VALANDNAME_ENTRY(WS_EX_RIGHT), + VALANDNAME_ENTRY(WS_EX_LEFT), + VALANDNAME_ENTRY(WS_EX_RTLREADING), + VALANDNAME_ENTRY(WS_EX_LTRREADING), + VALANDNAME_ENTRY(WS_EX_LEFTSCROLLBAR), + VALANDNAME_ENTRY(WS_EX_RIGHTSCROLLBAR), + VALANDNAME_ENTRY(WS_EX_CONTROLPARENT), + VALANDNAME_ENTRY(WS_EX_STATICEDGE), + VALANDNAME_ENTRY(WS_EX_APPWINDOW), + VALANDNAME_ENTRY(WS_EX_LAYERED), + VALANDNAME_ENTRY(WS_EX_NOINHERITLAYOUT), + VALANDNAME_ENTRY(WS_EX_LAYOUTRTL), + VALANDNAME_ENTRY(WS_EX_NOACTIVATE), + VALANDNAME_ENTRY(WS_EX_COMPOSITED), + VALANDNAME_ENTRY(WS_EX_NOREDIRECTIONBITMAP), + }; + AppendFlagsInfo(str, createStruct->dwExStyle, extendedWindowStyles, + "dwExStyle"); +} + +void XLowWordYHighWordParamInfo(nsCString& str, uint64_t value, + const char* name, bool /* isPreCall */) { + str.AppendPrintf("%s: x=%d y=%d", name, static_cast<int>(LOWORD(value)), + static_cast<int>(HIWORD(value))); +} + +void PointParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + str.AppendPrintf("%s: x=%d y=%d", name, static_cast<int>(GET_X_LPARAM(value)), + static_cast<int>(GET_Y_LPARAM(value))); +} + +void PointExplicitParamInfo(nsCString& str, POINT point, const char* name) { + str.AppendPrintf("%s: x=%ld y=%ld", name, point.x, point.y); +} + +void PointsParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + PPOINTS points = reinterpret_cast<PPOINTS>(&value); + str.AppendPrintf("%s: x=%d y=%d", name, points->x, points->y); +} + +void VirtualKeyParamInfo(nsCString& result, uint64_t param, const char* name, + bool /* isPreCall */) { + const static std::unordered_map<uint64_t, const char*> virtualKeys{ + VALANDNAME_ENTRY(VK_LBUTTON), + VALANDNAME_ENTRY(VK_RBUTTON), + VALANDNAME_ENTRY(VK_CANCEL), + VALANDNAME_ENTRY(VK_MBUTTON), + VALANDNAME_ENTRY(VK_XBUTTON1), + VALANDNAME_ENTRY(VK_XBUTTON2), + VALANDNAME_ENTRY(VK_BACK), + VALANDNAME_ENTRY(VK_TAB), + VALANDNAME_ENTRY(VK_CLEAR), + VALANDNAME_ENTRY(VK_RETURN), + VALANDNAME_ENTRY(VK_SHIFT), + VALANDNAME_ENTRY(VK_CONTROL), + VALANDNAME_ENTRY(VK_MENU), + VALANDNAME_ENTRY(VK_PAUSE), + VALANDNAME_ENTRY(VK_CAPITAL), + VALANDNAME_ENTRY(VK_KANA), + VALANDNAME_ENTRY(VK_HANGUL), +#ifdef VK_IME_ON + VALANDNAME_ENTRY(VK_IME_ON), +#endif + VALANDNAME_ENTRY(VK_JUNJA), + VALANDNAME_ENTRY(VK_FINAL), + VALANDNAME_ENTRY(VK_HANJA), + VALANDNAME_ENTRY(VK_KANJI), +#ifdef VK_IME_OFF + VALANDNAME_ENTRY(VK_IME_OFF), +#endif + VALANDNAME_ENTRY(VK_ESCAPE), + VALANDNAME_ENTRY(VK_CONVERT), + VALANDNAME_ENTRY(VK_NONCONVERT), + VALANDNAME_ENTRY(VK_ACCEPT), + VALANDNAME_ENTRY(VK_MODECHANGE), + VALANDNAME_ENTRY(VK_SPACE), + VALANDNAME_ENTRY(VK_PRIOR), + VALANDNAME_ENTRY(VK_NEXT), + VALANDNAME_ENTRY(VK_END), + VALANDNAME_ENTRY(VK_HOME), + VALANDNAME_ENTRY(VK_LEFT), + VALANDNAME_ENTRY(VK_UP), + VALANDNAME_ENTRY(VK_RIGHT), + VALANDNAME_ENTRY(VK_DOWN), + VALANDNAME_ENTRY(VK_SELECT), + VALANDNAME_ENTRY(VK_PRINT), + VALANDNAME_ENTRY(VK_EXECUTE), + VALANDNAME_ENTRY(VK_SNAPSHOT), + VALANDNAME_ENTRY(VK_INSERT), + VALANDNAME_ENTRY(VK_DELETE), + VALANDNAME_ENTRY(VK_HELP), + VALANDNAME_ENTRY(VK_LWIN), + VALANDNAME_ENTRY(VK_RWIN), + VALANDNAME_ENTRY(VK_APPS), + VALANDNAME_ENTRY(VK_SLEEP), + VALANDNAME_ENTRY(VK_NUMPAD0), + VALANDNAME_ENTRY(VK_NUMPAD1), + VALANDNAME_ENTRY(VK_NUMPAD2), + VALANDNAME_ENTRY(VK_NUMPAD3), + VALANDNAME_ENTRY(VK_NUMPAD4), + VALANDNAME_ENTRY(VK_NUMPAD5), + VALANDNAME_ENTRY(VK_NUMPAD6), + VALANDNAME_ENTRY(VK_NUMPAD7), + VALANDNAME_ENTRY(VK_NUMPAD8), + VALANDNAME_ENTRY(VK_NUMPAD9), + VALANDNAME_ENTRY(VK_MULTIPLY), + VALANDNAME_ENTRY(VK_ADD), + VALANDNAME_ENTRY(VK_SEPARATOR), + VALANDNAME_ENTRY(VK_SUBTRACT), + VALANDNAME_ENTRY(VK_DECIMAL), + VALANDNAME_ENTRY(VK_DIVIDE), + VALANDNAME_ENTRY(VK_F1), + VALANDNAME_ENTRY(VK_F2), + VALANDNAME_ENTRY(VK_F3), + VALANDNAME_ENTRY(VK_F4), + VALANDNAME_ENTRY(VK_F5), + VALANDNAME_ENTRY(VK_F6), + VALANDNAME_ENTRY(VK_F7), + VALANDNAME_ENTRY(VK_F8), + VALANDNAME_ENTRY(VK_F9), + VALANDNAME_ENTRY(VK_F10), + VALANDNAME_ENTRY(VK_F11), + VALANDNAME_ENTRY(VK_F12), + VALANDNAME_ENTRY(VK_F13), + VALANDNAME_ENTRY(VK_F14), + VALANDNAME_ENTRY(VK_F15), + VALANDNAME_ENTRY(VK_F16), + VALANDNAME_ENTRY(VK_F17), + VALANDNAME_ENTRY(VK_F18), + VALANDNAME_ENTRY(VK_F19), + VALANDNAME_ENTRY(VK_F20), + VALANDNAME_ENTRY(VK_F21), + VALANDNAME_ENTRY(VK_F22), + VALANDNAME_ENTRY(VK_F23), + VALANDNAME_ENTRY(VK_F24), + VALANDNAME_ENTRY(VK_NUMLOCK), + VALANDNAME_ENTRY(VK_SCROLL), + VALANDNAME_ENTRY(VK_LSHIFT), + VALANDNAME_ENTRY(VK_RSHIFT), + VALANDNAME_ENTRY(VK_LCONTROL), + VALANDNAME_ENTRY(VK_RCONTROL), + VALANDNAME_ENTRY(VK_LMENU), + VALANDNAME_ENTRY(VK_RMENU), + VALANDNAME_ENTRY(VK_BROWSER_BACK), + VALANDNAME_ENTRY(VK_BROWSER_FORWARD), + VALANDNAME_ENTRY(VK_BROWSER_REFRESH), + VALANDNAME_ENTRY(VK_BROWSER_STOP), + VALANDNAME_ENTRY(VK_BROWSER_SEARCH), + VALANDNAME_ENTRY(VK_BROWSER_FAVORITES), + VALANDNAME_ENTRY(VK_BROWSER_HOME), + VALANDNAME_ENTRY(VK_VOLUME_MUTE), + VALANDNAME_ENTRY(VK_VOLUME_DOWN), + VALANDNAME_ENTRY(VK_VOLUME_UP), + VALANDNAME_ENTRY(VK_MEDIA_NEXT_TRACK), + VALANDNAME_ENTRY(VK_MEDIA_PREV_TRACK), + VALANDNAME_ENTRY(VK_MEDIA_STOP), + VALANDNAME_ENTRY(VK_MEDIA_PLAY_PAUSE), + VALANDNAME_ENTRY(VK_LAUNCH_MAIL), + VALANDNAME_ENTRY(VK_LAUNCH_MEDIA_SELECT), + VALANDNAME_ENTRY(VK_LAUNCH_APP1), + VALANDNAME_ENTRY(VK_LAUNCH_APP2), + VALANDNAME_ENTRY(VK_OEM_1), + VALANDNAME_ENTRY(VK_OEM_PLUS), + VALANDNAME_ENTRY(VK_OEM_COMMA), + VALANDNAME_ENTRY(VK_OEM_MINUS), + VALANDNAME_ENTRY(VK_OEM_PERIOD), + VALANDNAME_ENTRY(VK_OEM_2), + VALANDNAME_ENTRY(VK_OEM_3), + VALANDNAME_ENTRY(VK_OEM_4), + VALANDNAME_ENTRY(VK_OEM_5), + VALANDNAME_ENTRY(VK_OEM_6), + VALANDNAME_ENTRY(VK_OEM_7), + VALANDNAME_ENTRY(VK_OEM_8), + VALANDNAME_ENTRY(VK_OEM_102), + VALANDNAME_ENTRY(VK_PROCESSKEY), + VALANDNAME_ENTRY(VK_PACKET), + VALANDNAME_ENTRY(VK_ATTN), + VALANDNAME_ENTRY(VK_CRSEL), + VALANDNAME_ENTRY(VK_EXSEL), + VALANDNAME_ENTRY(VK_EREOF), + VALANDNAME_ENTRY(VK_PLAY), + VALANDNAME_ENTRY(VK_ZOOM), + VALANDNAME_ENTRY(VK_NONAME), + VALANDNAME_ENTRY(VK_PA1), + VALANDNAME_ENTRY(VK_OEM_CLEAR), + {0x30, "0"}, + {0x31, "1"}, + {0x32, "2"}, + {0x33, "3"}, + {0x34, "4"}, + {0x35, "5"}, + {0x36, "6"}, + {0x37, "7"}, + {0x38, "8"}, + {0x39, "9"}, + {0x41, "A"}, + {0x42, "B"}, + {0x43, "C"}, + {0x44, "D"}, + {0x45, "E"}, + {0x46, "F"}, + {0x47, "G"}, + {0x48, "H"}, + {0x49, "I"}, + {0x4A, "J"}, + {0x4B, "K"}, + {0x4C, "L"}, + {0x4D, "M"}, + {0x4E, "N"}, + {0x4F, "O"}, + {0x50, "P"}, + {0x51, "Q"}, + {0x52, "S"}, + {0x53, "T"}, + {0x54, "U"}, + {0x55, "V"}, + {0x56, "W"}, + {0x57, "X"}, + {0x58, "Y"}, + {0x59, "Z"}, + }; + AppendEnumValueInfo(result, param, virtualKeys, name); +} + +void VirtualModifierKeysParamInfo(nsCString& result, uint64_t param, + const char* name, bool /* isPreCall */) { + const static nsTArray<EnumValueAndName> virtualKeys{ + VALANDNAME_ENTRY(MK_CONTROL), VALANDNAME_ENTRY(MK_LBUTTON), + VALANDNAME_ENTRY(MK_MBUTTON), VALANDNAME_ENTRY(MK_RBUTTON), + VALANDNAME_ENTRY(MK_SHIFT), VALANDNAME_ENTRY(MK_XBUTTON1), + VALANDNAME_ENTRY(MK_XBUTTON2), {0, "(none)"}}; + AppendFlagsInfo(result, param, virtualKeys, name); +} + +void ParentNotifyEventParamInfo(nsCString& str, uint64_t param, + const char* /* name */, bool /* isPreCall */) { + const static std::unordered_map<uint64_t, const char*> eventValues{ + VALANDNAME_ENTRY(WM_CREATE), VALANDNAME_ENTRY(WM_DESTROY), + VALANDNAME_ENTRY(WM_LBUTTONDOWN), VALANDNAME_ENTRY(WM_MBUTTONDOWN), + VALANDNAME_ENTRY(WM_RBUTTONDOWN), VALANDNAME_ENTRY(WM_XBUTTONDOWN), + VALANDNAME_ENTRY(WM_POINTERDOWN)}; + AppendEnumValueInfo(str, LOWORD(param), eventValues, "event"); + str.AppendASCII(" "); + HexParamInfo(str, HIWORD(param), "hiWord", false); +} + +void KeystrokeFlagsParamInfo(nsCString& str, uint64_t param, + const char* /* name */, bool /* isPreCall */) { + WORD repeatCount = LOWORD(param); + WORD keyFlags = HIWORD(param); + WORD scanCode = LOBYTE(keyFlags); + bool isExtendedKey = (keyFlags & KF_EXTENDED) == KF_EXTENDED; + if (isExtendedKey) { + scanCode = MAKEWORD(scanCode, 0xE0); + } + bool contextCode = (keyFlags & KF_ALTDOWN) == KF_ALTDOWN; + bool wasKeyDown = (keyFlags & KF_REPEAT) == KF_REPEAT; + bool transitionState = (keyFlags & KF_UP) == KF_UP; + + str.AppendPrintf( + "repeatCount: %d scanCode: %d isExtended: %d, contextCode: %d " + "previousKeyState: %d transitionState: %d", + repeatCount, scanCode, isExtendedKey ? 1 : 0, contextCode ? 1 : 0, + wasKeyDown ? 1 : 0, transitionState ? 1 : 0); +}; + +void VirtualKeysLowWordDistanceHighWordParamInfo(nsCString& str, uint64_t value, + const char* /* name */, + bool isPreCall) { + VirtualModifierKeysParamInfo(str, LOWORD(value), "virtualKeys", isPreCall); + str.AppendASCII(" "); + IntParamInfo(str, HIWORD(value), "distance", isPreCall); +} + +void ShowWindowReasonParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + const static std::unordered_map<uint64_t, const char*> showWindowReasonValues{ + VALANDNAME_ENTRY(SW_OTHERUNZOOM), + VALANDNAME_ENTRY(SW_OTHERZOOM), + VALANDNAME_ENTRY(SW_PARENTCLOSING), + VALANDNAME_ENTRY(SW_PARENTOPENING), + {0, "Call to ShowWindow()"}}; + AppendEnumValueInfo(str, value, showWindowReasonValues, name); +} + +void WindowEdgeParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + const static std::unordered_map<uint64_t, const char*> windowEdgeValues{ + VALANDNAME_ENTRY(WMSZ_BOTTOM), VALANDNAME_ENTRY(WMSZ_BOTTOMLEFT), + VALANDNAME_ENTRY(WMSZ_BOTTOMRIGHT), VALANDNAME_ENTRY(WMSZ_LEFT), + VALANDNAME_ENTRY(WMSZ_RIGHT), VALANDNAME_ENTRY(WMSZ_TOP), + VALANDNAME_ENTRY(WMSZ_TOPLEFT), VALANDNAME_ENTRY(WMSZ_TOPRIGHT)}; + AppendEnumValueInfo(str, value, windowEdgeValues, name); +} + +void UiActionParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + const static std::unordered_map<uint64_t, const char*> uiActionValues{ + VALANDNAME_ENTRY(SPI_GETACCESSTIMEOUT), + VALANDNAME_ENTRY(SPI_GETAUDIODESCRIPTION), + VALANDNAME_ENTRY(SPI_GETCLIENTAREAANIMATION), + VALANDNAME_ENTRY(SPI_GETDISABLEOVERLAPPEDCONTENT), + VALANDNAME_ENTRY(SPI_GETFILTERKEYS), + VALANDNAME_ENTRY(SPI_GETFOCUSBORDERHEIGHT), + VALANDNAME_ENTRY(SPI_GETFOCUSBORDERWIDTH), + VALANDNAME_ENTRY(SPI_GETHIGHCONTRAST), + VALANDNAME_ENTRY(SPI_GETLOGICALDPIOVERRIDE), + VALANDNAME_ENTRY(SPI_SETLOGICALDPIOVERRIDE), + VALANDNAME_ENTRY(SPI_GETMESSAGEDURATION), + VALANDNAME_ENTRY(SPI_GETMOUSECLICKLOCK), + VALANDNAME_ENTRY(SPI_GETMOUSECLICKLOCKTIME), + VALANDNAME_ENTRY(SPI_GETMOUSEKEYS), + VALANDNAME_ENTRY(SPI_GETMOUSESONAR), + VALANDNAME_ENTRY(SPI_GETMOUSEVANISH), + VALANDNAME_ENTRY(SPI_GETSCREENREADER), + VALANDNAME_ENTRY(SPI_GETSERIALKEYS), + VALANDNAME_ENTRY(SPI_GETSHOWSOUNDS), + VALANDNAME_ENTRY(SPI_GETSOUNDSENTRY), + VALANDNAME_ENTRY(SPI_GETSTICKYKEYS), + VALANDNAME_ENTRY(SPI_GETTOGGLEKEYS), + VALANDNAME_ENTRY(SPI_SETACCESSTIMEOUT), + VALANDNAME_ENTRY(SPI_SETAUDIODESCRIPTION), + VALANDNAME_ENTRY(SPI_SETCLIENTAREAANIMATION), + VALANDNAME_ENTRY(SPI_SETDISABLEOVERLAPPEDCONTENT), + VALANDNAME_ENTRY(SPI_SETFILTERKEYS), + VALANDNAME_ENTRY(SPI_SETFOCUSBORDERHEIGHT), + VALANDNAME_ENTRY(SPI_SETFOCUSBORDERWIDTH), + VALANDNAME_ENTRY(SPI_SETHIGHCONTRAST), + VALANDNAME_ENTRY(SPI_SETMESSAGEDURATION), + VALANDNAME_ENTRY(SPI_SETMOUSECLICKLOCK), + VALANDNAME_ENTRY(SPI_SETMOUSECLICKLOCKTIME), + VALANDNAME_ENTRY(SPI_SETMOUSEKEYS), + VALANDNAME_ENTRY(SPI_SETMOUSESONAR), + VALANDNAME_ENTRY(SPI_SETMOUSEVANISH), + VALANDNAME_ENTRY(SPI_SETSCREENREADER), + VALANDNAME_ENTRY(SPI_SETSERIALKEYS), + VALANDNAME_ENTRY(SPI_SETSHOWSOUNDS), + VALANDNAME_ENTRY(SPI_SETSOUNDSENTRY), + VALANDNAME_ENTRY(SPI_SETSTICKYKEYS), + VALANDNAME_ENTRY(SPI_SETTOGGLEKEYS), + VALANDNAME_ENTRY(SPI_GETCLEARTYPE), + VALANDNAME_ENTRY(SPI_GETDESKWALLPAPER), + VALANDNAME_ENTRY(SPI_GETDROPSHADOW), + VALANDNAME_ENTRY(SPI_GETFLATMENU), + VALANDNAME_ENTRY(SPI_GETFONTSMOOTHING), + VALANDNAME_ENTRY(SPI_GETFONTSMOOTHINGCONTRAST), + VALANDNAME_ENTRY(SPI_GETFONTSMOOTHINGORIENTATION), + VALANDNAME_ENTRY(SPI_GETFONTSMOOTHINGTYPE), + VALANDNAME_ENTRY(SPI_GETWORKAREA), + VALANDNAME_ENTRY(SPI_SETCLEARTYPE), + VALANDNAME_ENTRY(SPI_SETCURSORS), + VALANDNAME_ENTRY(SPI_SETDESKPATTERN), + VALANDNAME_ENTRY(SPI_SETDESKWALLPAPER), + VALANDNAME_ENTRY(SPI_SETDROPSHADOW), + VALANDNAME_ENTRY(SPI_SETFLATMENU), + VALANDNAME_ENTRY(SPI_SETFONTSMOOTHING), + VALANDNAME_ENTRY(SPI_SETFONTSMOOTHINGCONTRAST), + VALANDNAME_ENTRY(SPI_SETFONTSMOOTHINGORIENTATION), + VALANDNAME_ENTRY(SPI_SETFONTSMOOTHINGTYPE), + VALANDNAME_ENTRY(SPI_SETWORKAREA), + VALANDNAME_ENTRY(SPI_GETICONMETRICS), + VALANDNAME_ENTRY(SPI_GETICONTITLELOGFONT), + VALANDNAME_ENTRY(SPI_GETICONTITLEWRAP), + VALANDNAME_ENTRY(SPI_ICONHORIZONTALSPACING), + VALANDNAME_ENTRY(SPI_ICONVERTICALSPACING), + VALANDNAME_ENTRY(SPI_SETICONMETRICS), + VALANDNAME_ENTRY(SPI_SETICONS), + VALANDNAME_ENTRY(SPI_SETICONTITLELOGFONT), + VALANDNAME_ENTRY(SPI_SETICONTITLEWRAP), + VALANDNAME_ENTRY(SPI_GETBEEP), + VALANDNAME_ENTRY(SPI_GETBLOCKSENDINPUTRESETS), + VALANDNAME_ENTRY(SPI_GETCONTACTVISUALIZATION), + VALANDNAME_ENTRY(SPI_SETCONTACTVISUALIZATION), + VALANDNAME_ENTRY(SPI_GETDEFAULTINPUTLANG), + VALANDNAME_ENTRY(SPI_GETGESTUREVISUALIZATION), + VALANDNAME_ENTRY(SPI_SETGESTUREVISUALIZATION), + VALANDNAME_ENTRY(SPI_GETKEYBOARDCUES), + VALANDNAME_ENTRY(SPI_GETKEYBOARDDELAY), + VALANDNAME_ENTRY(SPI_GETKEYBOARDPREF), + VALANDNAME_ENTRY(SPI_GETKEYBOARDSPEED), + VALANDNAME_ENTRY(SPI_GETMOUSE), + VALANDNAME_ENTRY(SPI_GETMOUSEHOVERHEIGHT), + VALANDNAME_ENTRY(SPI_GETMOUSEHOVERTIME), + VALANDNAME_ENTRY(SPI_GETMOUSEHOVERWIDTH), + VALANDNAME_ENTRY(SPI_GETMOUSESPEED), + VALANDNAME_ENTRY(SPI_GETMOUSETRAILS), + VALANDNAME_ENTRY(SPI_GETMOUSEWHEELROUTING), + VALANDNAME_ENTRY(SPI_SETMOUSEWHEELROUTING), + VALANDNAME_ENTRY(SPI_GETPENVISUALIZATION), + VALANDNAME_ENTRY(SPI_SETPENVISUALIZATION), + VALANDNAME_ENTRY(SPI_GETSNAPTODEFBUTTON), + VALANDNAME_ENTRY(SPI_GETSYSTEMLANGUAGEBAR), + VALANDNAME_ENTRY(SPI_SETSYSTEMLANGUAGEBAR), + VALANDNAME_ENTRY(SPI_GETTHREADLOCALINPUTSETTINGS), + VALANDNAME_ENTRY(SPI_SETTHREADLOCALINPUTSETTINGS), + VALANDNAME_ENTRY(SPI_GETWHEELSCROLLCHARS), + VALANDNAME_ENTRY(SPI_GETWHEELSCROLLLINES), + VALANDNAME_ENTRY(SPI_SETBEEP), + VALANDNAME_ENTRY(SPI_SETBLOCKSENDINPUTRESETS), + VALANDNAME_ENTRY(SPI_SETDEFAULTINPUTLANG), + VALANDNAME_ENTRY(SPI_SETDOUBLECLICKTIME), + VALANDNAME_ENTRY(SPI_SETDOUBLECLKHEIGHT), + VALANDNAME_ENTRY(SPI_SETDOUBLECLKWIDTH), + VALANDNAME_ENTRY(SPI_SETKEYBOARDCUES), + VALANDNAME_ENTRY(SPI_SETKEYBOARDDELAY), + VALANDNAME_ENTRY(SPI_SETKEYBOARDPREF), + VALANDNAME_ENTRY(SPI_SETKEYBOARDSPEED), + VALANDNAME_ENTRY(SPI_SETLANGTOGGLE), + VALANDNAME_ENTRY(SPI_SETMOUSE), + VALANDNAME_ENTRY(SPI_SETMOUSEBUTTONSWAP), + VALANDNAME_ENTRY(SPI_SETMOUSEHOVERHEIGHT), + VALANDNAME_ENTRY(SPI_SETMOUSEHOVERTIME), + VALANDNAME_ENTRY(SPI_SETMOUSEHOVERWIDTH), + VALANDNAME_ENTRY(SPI_SETMOUSESPEED), + VALANDNAME_ENTRY(SPI_SETMOUSETRAILS), + VALANDNAME_ENTRY(SPI_SETSNAPTODEFBUTTON), + VALANDNAME_ENTRY(SPI_SETWHEELSCROLLCHARS), + VALANDNAME_ENTRY(SPI_SETWHEELSCROLLLINES), + VALANDNAME_ENTRY(SPI_GETMENUDROPALIGNMENT), + VALANDNAME_ENTRY(SPI_GETMENUFADE), + VALANDNAME_ENTRY(SPI_GETMENUSHOWDELAY), + VALANDNAME_ENTRY(SPI_SETMENUDROPALIGNMENT), + VALANDNAME_ENTRY(SPI_SETMENUFADE), + VALANDNAME_ENTRY(SPI_SETMENUSHOWDELAY), + VALANDNAME_ENTRY(SPI_GETLOWPOWERACTIVE), + VALANDNAME_ENTRY(SPI_GETLOWPOWERTIMEOUT), + VALANDNAME_ENTRY(SPI_GETPOWEROFFACTIVE), + VALANDNAME_ENTRY(SPI_GETPOWEROFFTIMEOUT), + VALANDNAME_ENTRY(SPI_SETLOWPOWERACTIVE), + VALANDNAME_ENTRY(SPI_SETLOWPOWERTIMEOUT), + VALANDNAME_ENTRY(SPI_SETPOWEROFFACTIVE), + VALANDNAME_ENTRY(SPI_SETPOWEROFFTIMEOUT), + VALANDNAME_ENTRY(SPI_GETSCREENSAVEACTIVE), + VALANDNAME_ENTRY(SPI_GETSCREENSAVERRUNNING), + VALANDNAME_ENTRY(SPI_GETSCREENSAVESECURE), + VALANDNAME_ENTRY(SPI_GETSCREENSAVETIMEOUT), + VALANDNAME_ENTRY(SPI_SETSCREENSAVEACTIVE), + VALANDNAME_ENTRY(SPI_SETSCREENSAVERRUNNING), + VALANDNAME_ENTRY(SPI_SETSCREENSAVESECURE), + VALANDNAME_ENTRY(SPI_SETSCREENSAVETIMEOUT), + VALANDNAME_ENTRY(SPI_GETHUNGAPPTIMEOUT), + VALANDNAME_ENTRY(SPI_GETWAITTOKILLTIMEOUT), + VALANDNAME_ENTRY(SPI_GETWAITTOKILLSERVICETIMEOUT), + VALANDNAME_ENTRY(SPI_SETHUNGAPPTIMEOUT), + VALANDNAME_ENTRY(SPI_SETWAITTOKILLTIMEOUT), + VALANDNAME_ENTRY(SPI_SETWAITTOKILLSERVICETIMEOUT), + VALANDNAME_ENTRY(SPI_GETCOMBOBOXANIMATION), + VALANDNAME_ENTRY(SPI_GETCURSORSHADOW), + VALANDNAME_ENTRY(SPI_GETGRADIENTCAPTIONS), + VALANDNAME_ENTRY(SPI_GETHOTTRACKING), + VALANDNAME_ENTRY(SPI_GETLISTBOXSMOOTHSCROLLING), + VALANDNAME_ENTRY(SPI_GETMENUANIMATION), + VALANDNAME_ENTRY(SPI_GETMENUUNDERLINES), + VALANDNAME_ENTRY(SPI_GETSELECTIONFADE), + VALANDNAME_ENTRY(SPI_GETTOOLTIPANIMATION), + VALANDNAME_ENTRY(SPI_GETTOOLTIPFADE), + VALANDNAME_ENTRY(SPI_GETUIEFFECTS), + VALANDNAME_ENTRY(SPI_SETCOMBOBOXANIMATION), + VALANDNAME_ENTRY(SPI_SETCURSORSHADOW), + VALANDNAME_ENTRY(SPI_SETGRADIENTCAPTIONS), + VALANDNAME_ENTRY(SPI_SETHOTTRACKING), + VALANDNAME_ENTRY(SPI_SETLISTBOXSMOOTHSCROLLING), + VALANDNAME_ENTRY(SPI_SETMENUANIMATION), + VALANDNAME_ENTRY(SPI_SETMENUUNDERLINES), + VALANDNAME_ENTRY(SPI_SETSELECTIONFADE), + VALANDNAME_ENTRY(SPI_SETTOOLTIPANIMATION), + VALANDNAME_ENTRY(SPI_SETTOOLTIPFADE), + VALANDNAME_ENTRY(SPI_SETUIEFFECTS), + VALANDNAME_ENTRY(SPI_GETACTIVEWINDOWTRACKING), + VALANDNAME_ENTRY(SPI_GETACTIVEWNDTRKZORDER), + VALANDNAME_ENTRY(SPI_GETACTIVEWNDTRKTIMEOUT), + VALANDNAME_ENTRY(SPI_GETANIMATION), + VALANDNAME_ENTRY(SPI_GETBORDER), + VALANDNAME_ENTRY(SPI_GETCARETWIDTH), + VALANDNAME_ENTRY(SPI_GETDOCKMOVING), + VALANDNAME_ENTRY(SPI_GETDRAGFROMMAXIMIZE), + VALANDNAME_ENTRY(SPI_GETDRAGFULLWINDOWS), + VALANDNAME_ENTRY(SPI_GETFOREGROUNDFLASHCOUNT), + VALANDNAME_ENTRY(SPI_GETFOREGROUNDLOCKTIMEOUT), + VALANDNAME_ENTRY(SPI_GETMINIMIZEDMETRICS), + VALANDNAME_ENTRY(SPI_GETMOUSEDOCKTHRESHOLD), + VALANDNAME_ENTRY(SPI_GETMOUSEDRAGOUTTHRESHOLD), + VALANDNAME_ENTRY(SPI_GETMOUSESIDEMOVETHRESHOLD), + VALANDNAME_ENTRY(SPI_GETNONCLIENTMETRICS), + VALANDNAME_ENTRY(SPI_GETPENDOCKTHRESHOLD), + VALANDNAME_ENTRY(SPI_GETPENDRAGOUTTHRESHOLD), + VALANDNAME_ENTRY(SPI_GETPENSIDEMOVETHRESHOLD), + VALANDNAME_ENTRY(SPI_GETSHOWIMEUI), + VALANDNAME_ENTRY(SPI_GETSNAPSIZING), + VALANDNAME_ENTRY(SPI_GETWINARRANGING), + VALANDNAME_ENTRY(SPI_SETACTIVEWINDOWTRACKING), + VALANDNAME_ENTRY(SPI_SETACTIVEWNDTRKZORDER), + VALANDNAME_ENTRY(SPI_SETACTIVEWNDTRKTIMEOUT), + VALANDNAME_ENTRY(SPI_SETANIMATION), + VALANDNAME_ENTRY(SPI_SETBORDER), + VALANDNAME_ENTRY(SPI_SETCARETWIDTH), + VALANDNAME_ENTRY(SPI_SETDOCKMOVING), + VALANDNAME_ENTRY(SPI_SETDRAGFROMMAXIMIZE), + VALANDNAME_ENTRY(SPI_SETDRAGFULLWINDOWS), + VALANDNAME_ENTRY(SPI_SETFOREGROUNDFLASHCOUNT), + VALANDNAME_ENTRY(SPI_SETFOREGROUNDLOCKTIMEOUT), + VALANDNAME_ENTRY(SPI_SETMINIMIZEDMETRICS), + VALANDNAME_ENTRY(SPI_SETMOUSEDOCKTHRESHOLD), + VALANDNAME_ENTRY(SPI_SETMOUSEDRAGOUTTHRESHOLD), + VALANDNAME_ENTRY(SPI_SETMOUSESIDEMOVETHRESHOLD), + VALANDNAME_ENTRY(SPI_SETNONCLIENTMETRICS), + VALANDNAME_ENTRY(SPI_SETPENDOCKTHRESHOLD), + VALANDNAME_ENTRY(SPI_SETPENDRAGOUTTHRESHOLD), + VALANDNAME_ENTRY(SPI_SETPENSIDEMOVETHRESHOLD), + VALANDNAME_ENTRY(SPI_SETSHOWIMEUI), + VALANDNAME_ENTRY(SPI_SETSNAPSIZING), + VALANDNAME_ENTRY(SPI_SETWINARRANGING), + }; + AppendEnumValueInfo(str, value, uiActionValues, name); +} + +nsAutoCString WmSizeParamInfo(uint64_t wParam, uint64_t lParam, + bool /* isPreCall */) { + nsAutoCString result; + const static std::unordered_map<uint64_t, const char*> sizeValues{ + VALANDNAME_ENTRY(SIZE_RESTORED), VALANDNAME_ENTRY(SIZE_MINIMIZED), + VALANDNAME_ENTRY(SIZE_MAXIMIZED), VALANDNAME_ENTRY(SIZE_MAXSHOW), + VALANDNAME_ENTRY(SIZE_MAXHIDE)}; + AppendEnumValueInfo(result, wParam, sizeValues, "size"); + result.AppendPrintf(" width=%d height=%d", static_cast<int>(LOWORD(lParam)), + static_cast<int>(HIWORD(lParam))); + return result; +} + +const nsTArray<EnumValueAndName> windowPositionFlags = { + VALANDNAME_ENTRY(SWP_DRAWFRAME), VALANDNAME_ENTRY(SWP_HIDEWINDOW), + VALANDNAME_ENTRY(SWP_NOACTIVATE), VALANDNAME_ENTRY(SWP_NOCOPYBITS), + VALANDNAME_ENTRY(SWP_NOMOVE), VALANDNAME_ENTRY(SWP_NOOWNERZORDER), + VALANDNAME_ENTRY(SWP_NOREDRAW), VALANDNAME_ENTRY(SWP_NOSENDCHANGING), + VALANDNAME_ENTRY(SWP_NOSIZE), VALANDNAME_ENTRY(SWP_NOZORDER), + VALANDNAME_ENTRY(SWP_SHOWWINDOW), +}; + +void WindowPosParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + LPWINDOWPOS windowPos = reinterpret_cast<LPWINDOWPOS>(value); + if (windowPos == nullptr) { + str.AppendASCII("null windowPos?"); + return; + } + HexParamInfo(str, reinterpret_cast<uint64_t>(windowPos->hwnd), "hwnd", false); + str.AppendASCII(" "); + HexParamInfo(str, reinterpret_cast<uint64_t>(windowPos->hwndInsertAfter), + "hwndInsertAfter", false); + str.AppendASCII(" "); + IntParamInfo(str, windowPos->x, "x", false); + str.AppendASCII(" "); + IntParamInfo(str, windowPos->y, "y", false); + str.AppendASCII(" "); + IntParamInfo(str, windowPos->cx, "cx", false); + str.AppendASCII(" "); + IntParamInfo(str, windowPos->cy, "cy", false); + str.AppendASCII(" "); + AppendFlagsInfo(str, windowPos->flags, windowPositionFlags, "flags"); +} + +void StyleOrExtendedParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + const static std::unordered_map<uint64_t, const char*> styleOrExtended{ + VALANDNAME_ENTRY(GWL_EXSTYLE), VALANDNAME_ENTRY(GWL_STYLE)}; + AppendEnumValueInfo(str, value, styleOrExtended, name); +} + +void StyleStructParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + LPSTYLESTRUCT styleStruct = reinterpret_cast<LPSTYLESTRUCT>(value); + if (styleStruct == nullptr) { + str.AppendASCII("null STYLESTRUCT?"); + return; + } + HexParamInfo(str, styleStruct->styleOld, "styleOld", false); + str.AppendASCII(" "); + HexParamInfo(str, styleStruct->styleNew, "styleNew", false); +} + +void NcCalcSizeParamsParamInfo(nsCString& str, uint64_t value, const char* name, + bool /* isPreCall */) { + LPNCCALCSIZE_PARAMS params = reinterpret_cast<LPNCCALCSIZE_PARAMS>(value); + if (params == nullptr) { + str.AppendASCII("null NCCALCSIZE_PARAMS?"); + return; + } + str.AppendPrintf("%s[0]: ", name); + RectParamInfo(str, reinterpret_cast<uintptr_t>(¶ms->rgrc[0]), nullptr, + false); + str.AppendPrintf(" %s[1]: ", name); + RectParamInfo(str, reinterpret_cast<uintptr_t>(¶ms->rgrc[1]), nullptr, + false); + str.AppendPrintf(" %s[2]: ", name); + RectParamInfo(str, reinterpret_cast<uintptr_t>(¶ms->rgrc[2]), nullptr, + false); + str.AppendASCII(" "); + WindowPosParamInfo(str, reinterpret_cast<uintptr_t>(params->lppos), nullptr, + false); +} + +nsAutoCString WmNcCalcSizeParamInfo(uint64_t wParam, uint64_t lParam, + bool /* isPreCall */) { + nsAutoCString result; + TrueFalseParamInfo(result, wParam, "shouldIndicateValidArea", false); + result.AppendASCII(" "); + if (wParam == TRUE) { + NcCalcSizeParamsParamInfo(result, lParam, "ncCalcSizeParams", false); + } else { + RectParamInfo(result, lParam, "rect", false); + } + return result; +} + +void ActivateWParamInfo(nsCString& result, uint64_t wParam, const char* name, + bool /* isPreCall */) { + const static std::unordered_map<uint64_t, const char*> activateValues{ + VALANDNAME_ENTRY(WA_ACTIVE), VALANDNAME_ENTRY(WA_CLICKACTIVE), + VALANDNAME_ENTRY(WA_INACTIVE)}; + AppendEnumValueInfo(result, wParam, activateValues, name); +} + +void HitTestParamInfo(nsCString& result, uint64_t param, const char* name, + bool /* isPreCall */) { + const static std::unordered_map<uint64_t, const char*> hitTestResults{ + VALANDNAME_ENTRY(HTBORDER), VALANDNAME_ENTRY(HTBOTTOM), + VALANDNAME_ENTRY(HTBOTTOMLEFT), VALANDNAME_ENTRY(HTBOTTOMRIGHT), + VALANDNAME_ENTRY(HTCAPTION), VALANDNAME_ENTRY(HTCLIENT), + VALANDNAME_ENTRY(HTCLOSE), VALANDNAME_ENTRY(HTERROR), + VALANDNAME_ENTRY(HTGROWBOX), VALANDNAME_ENTRY(HTHELP), + VALANDNAME_ENTRY(HTHSCROLL), VALANDNAME_ENTRY(HTLEFT), + VALANDNAME_ENTRY(HTMENU), VALANDNAME_ENTRY(HTMAXBUTTON), + VALANDNAME_ENTRY(HTMINBUTTON), VALANDNAME_ENTRY(HTNOWHERE), + VALANDNAME_ENTRY(HTREDUCE), VALANDNAME_ENTRY(HTRIGHT), + VALANDNAME_ENTRY(HTSIZE), VALANDNAME_ENTRY(HTSYSMENU), + VALANDNAME_ENTRY(HTTOP), VALANDNAME_ENTRY(HTTOPLEFT), + VALANDNAME_ENTRY(HTTOPRIGHT), VALANDNAME_ENTRY(HTTRANSPARENT), + VALANDNAME_ENTRY(HTVSCROLL), VALANDNAME_ENTRY(HTZOOM), + }; + AppendEnumValueInfo(result, param, hitTestResults, name); +} + +void SetCursorLParamInfo(nsCString& result, uint64_t lParam, + const char* /* name */, bool /* isPreCall */) { + HitTestParamInfo(result, LOWORD(lParam), "hitTestResult", false); + result.AppendASCII(" "); + HexParamInfo(result, HIWORD(lParam), "message", false); +} + +void MinMaxInfoParamInfo(nsCString& result, uint64_t value, + const char* /* name */, bool /* isPreCall */) { + PMINMAXINFO minMaxInfo = reinterpret_cast<PMINMAXINFO>(value); + if (minMaxInfo == nullptr) { + result.AppendPrintf("NULL minMaxInfo?"); + return; + } + PointExplicitParamInfo(result, minMaxInfo->ptMaxSize, "maxSize"); + result.AppendASCII(" "); + PointExplicitParamInfo(result, minMaxInfo->ptMaxPosition, "maxPosition"); + result.AppendASCII(" "); + PointExplicitParamInfo(result, minMaxInfo->ptMinTrackSize, "minTrackSize"); + result.AppendASCII(" "); + PointExplicitParamInfo(result, minMaxInfo->ptMaxTrackSize, "maxTrackSize"); +} + +void WideStringParamInfo(nsCString& result, uint64_t value, const char* name, + bool /* isPreCall */) { + result.AppendPrintf("%s=%S", name, reinterpret_cast<LPCWSTR>(value)); +} + +void DeviceEventParamInfo(nsCString& result, uint64_t value, const char* name, + bool /* isPreCall */) { + const static std::unordered_map<uint64_t, const char*> deviceEventValues{ + VALANDNAME_ENTRY(DBT_DEVNODES_CHANGED), + VALANDNAME_ENTRY(DBT_QUERYCHANGECONFIG), + VALANDNAME_ENTRY(DBT_CONFIGCHANGED), + VALANDNAME_ENTRY(DBT_CONFIGCHANGECANCELED), + VALANDNAME_ENTRY(DBT_DEVICEARRIVAL), + VALANDNAME_ENTRY(DBT_DEVICEQUERYREMOVE), + VALANDNAME_ENTRY(DBT_DEVICEQUERYREMOVEFAILED), + VALANDNAME_ENTRY(DBT_DEVICEREMOVEPENDING), + VALANDNAME_ENTRY(DBT_DEVICEREMOVECOMPLETE), + VALANDNAME_ENTRY(DBT_DEVICETYPESPECIFIC), + VALANDNAME_ENTRY(DBT_CUSTOMEVENT), + VALANDNAME_ENTRY(DBT_USERDEFINED)}; + AppendEnumValueInfo(result, value, deviceEventValues, name); +} + +void ResolutionParamInfo(nsCString& result, uint64_t value, const char* name, + bool /* isPreCall */) { + result.AppendPrintf("horizontalRes=%d verticalRes=%d", LOWORD(value), + HIWORD(value)); +} + +// Window message with default wParam/lParam logging +#define ENTRY(_msg) \ + { \ + _msg, { #_msg, _msg, DefaultParamInfo } \ + } +// Window message with no parameters +#define ENTRY_WITH_NO_PARAM_INFO(_msg) \ + { \ + _msg, { #_msg, _msg, nullptr } \ + } +// Window message with custom parameter logging functions +#define ENTRY_WITH_CUSTOM_PARAM_INFO(_msg, paramInfoFn) \ + { \ + _msg, { #_msg, _msg, paramInfoFn } \ + } +// Window message with separate custom wParam and lParam logging functions +#define ENTRY_WITH_SPLIT_PARAM_INFOS(_msg, wParamInfoFn, wParamName, \ + lParamInfoFn, lParamName) \ + { \ + _msg, { \ + #_msg, _msg, nullptr, wParamInfoFn, wParamName, lParamInfoFn, lParamName \ + } \ + } +std::unordered_map<UINT, EventMsgInfo> gAllEvents = { + ENTRY_WITH_NO_PARAM_INFO(WM_NULL), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_CREATE, nullptr, nullptr, + CreateStructParamInfo, "createStruct"), + ENTRY_WITH_NO_PARAM_INFO(WM_DESTROY), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOVE, nullptr, nullptr, + XLowWordYHighWordParamInfo, "upperLeft"), + ENTRY_WITH_CUSTOM_PARAM_INFO(WM_SIZE, WmSizeParamInfo), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_ACTIVATE, ActivateWParamInfo, "wParam", + HexParamInfo, "handle"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SETFOCUS, HexParamInfo, "handle", nullptr, + nullptr), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_KILLFOCUS, HexParamInfo, "handle", nullptr, + nullptr), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_ENABLE, TrueFalseParamInfo, "enabled", + nullptr, nullptr), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SETREDRAW, TrueFalseParamInfo, + "redrawState", nullptr, nullptr), + ENTRY(WM_SETTEXT), + ENTRY(WM_GETTEXT), + ENTRY(WM_GETTEXTLENGTH), + ENTRY_WITH_NO_PARAM_INFO(WM_PAINT), + ENTRY_WITH_NO_PARAM_INFO(WM_CLOSE), + ENTRY(WM_QUERYENDSESSION), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_QUIT, HexParamInfo, "exitCode", nullptr, + nullptr), + ENTRY(WM_QUERYOPEN), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_ERASEBKGND, HexParamInfo, "deviceContext", + nullptr, nullptr), + ENTRY(WM_SYSCOLORCHANGE), + ENTRY(WM_ENDSESSION), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SHOWWINDOW, TrueFalseParamInfo, + "windowBeingShown", ShowWindowReasonParamInfo, + "status"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SETTINGCHANGE, UiActionParamInfo, + "uiAction", WideStringParamInfo, + "paramChanged"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DEVMODECHANGE, nullptr, nullptr, + WideStringParamInfo, "deviceName"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_ACTIVATEAPP, TrueFalseParamInfo, + "activated", HexParamInfo, "threadId"), + ENTRY(WM_FONTCHANGE), + ENTRY(WM_TIMECHANGE), + ENTRY(WM_CANCELMODE), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SETCURSOR, HexParamInfo, "windowHandle", + SetCursorLParamInfo, ""), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEACTIVATE, HexParamInfo, "windowHandle", + SetCursorLParamInfo, ""), + ENTRY(WM_CHILDACTIVATE), + ENTRY(WM_QUEUESYNC), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_GETMINMAXINFO, nullptr, nullptr, + MinMaxInfoParamInfo, ""), + ENTRY(WM_PAINTICON), + ENTRY(WM_ICONERASEBKGND), + ENTRY(WM_NEXTDLGCTL), + ENTRY(WM_SPOOLERSTATUS), + ENTRY(WM_DRAWITEM), + ENTRY(WM_MEASUREITEM), + ENTRY(WM_DELETEITEM), + ENTRY(WM_VKEYTOITEM), + ENTRY(WM_CHARTOITEM), + ENTRY(WM_SETFONT), + ENTRY(WM_GETFONT), + ENTRY(WM_SETHOTKEY), + ENTRY(WM_GETHOTKEY), + ENTRY(WM_QUERYDRAGICON), + ENTRY(WM_COMPAREITEM), + ENTRY(WM_GETOBJECT), + ENTRY(WM_COMPACTING), + ENTRY(WM_COMMNOTIFY), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_WINDOWPOSCHANGING, nullptr, nullptr, + WindowPosParamInfo, "newSizeAndPos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_WINDOWPOSCHANGED, nullptr, nullptr, + WindowPosParamInfo, "newSizeAndPos"), + ENTRY(WM_POWER), + ENTRY(WM_COPYDATA), + ENTRY(WM_CANCELJOURNAL), + ENTRY(WM_NOTIFY), + ENTRY(WM_INPUTLANGCHANGEREQUEST), + ENTRY(WM_INPUTLANGCHANGE), + ENTRY(WM_TCARD), + ENTRY(WM_HELP), + ENTRY(WM_USERCHANGED), + ENTRY(WM_NOTIFYFORMAT), + ENTRY(WM_CONTEXTMENU), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_STYLECHANGING, StyleOrExtendedParamInfo, + "styleOrExtended", StyleStructParamInfo, + "newStyles"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_STYLECHANGED, StyleOrExtendedParamInfo, + "styleOrExtended", StyleStructParamInfo, + "newStyles"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DISPLAYCHANGE, IntParamInfo, "bitsPerPixel", + ResolutionParamInfo, ""), + ENTRY(WM_GETICON), + ENTRY(WM_SETICON), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCCREATE, nullptr, nullptr, + CreateStructParamInfo, "createStruct"), + ENTRY_WITH_NO_PARAM_INFO(WM_NCDESTROY), + ENTRY_WITH_CUSTOM_PARAM_INFO(WM_NCCALCSIZE, WmNcCalcSizeParamInfo), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCHITTEST, nullptr, nullptr, + XLowWordYHighWordParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCPAINT, HexParamInfo, "updateRegionHandle", + nullptr, nullptr), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCACTIVATE, TrueFalseParamInfo, + "isTitleBarOrIconActive", HexParamInfo, + "updateRegion"), + ENTRY(WM_GETDLGCODE), + ENTRY(WM_SYNCPAINT), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMOUSEMOVE, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCLBUTTONDOWN, HitTestParamInfo, + "hitTestValue", PointsParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCLBUTTONUP, HitTestParamInfo, + "hitTestValue", PointsParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCLBUTTONDBLCLK, HitTestParamInfo, + "hitTestValue", PointsParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCRBUTTONDOWN, HitTestParamInfo, + "hitTestValue", PointsParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCRBUTTONUP, HitTestParamInfo, + "hitTestValue", PointsParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCRBUTTONDBLCLK, HitTestParamInfo, + "hitTestValue", PointsParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMBUTTONDOWN, HitTestParamInfo, + "hitTestValue", PointsParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMBUTTONUP, HitTestParamInfo, + "hitTestValue", PointsParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMBUTTONDBLCLK, HitTestParamInfo, + "hitTestValue", PointsParamInfo, "mousePos"), + ENTRY(EM_GETSEL), + ENTRY(EM_SETSEL), + ENTRY(EM_GETRECT), + ENTRY(EM_SETRECT), + ENTRY(EM_SETRECTNP), + ENTRY(EM_SCROLL), + ENTRY(EM_LINESCROLL), + ENTRY(EM_SCROLLCARET), + ENTRY(EM_GETMODIFY), + ENTRY(EM_SETMODIFY), + ENTRY(EM_GETLINECOUNT), + ENTRY(EM_LINEINDEX), + ENTRY(EM_SETHANDLE), + ENTRY(EM_GETHANDLE), + ENTRY(EM_GETTHUMB), + ENTRY(EM_LINELENGTH), + ENTRY(EM_REPLACESEL), + ENTRY(EM_GETLINE), + ENTRY(EM_LIMITTEXT), + ENTRY(EM_CANUNDO), + ENTRY(EM_UNDO), + ENTRY(EM_FMTLINES), + ENTRY(EM_LINEFROMCHAR), + ENTRY(EM_SETTABSTOPS), + ENTRY(EM_SETPASSWORDCHAR), + ENTRY(EM_EMPTYUNDOBUFFER), + ENTRY(EM_GETFIRSTVISIBLELINE), + ENTRY(EM_SETREADONLY), + ENTRY(EM_SETWORDBREAKPROC), + ENTRY(EM_GETWORDBREAKPROC), + ENTRY(EM_GETPASSWORDCHAR), + ENTRY(EM_SETMARGINS), + ENTRY(EM_GETMARGINS), + ENTRY(EM_GETLIMITTEXT), + ENTRY(EM_POSFROMCHAR), + ENTRY(EM_CHARFROMPOS), + ENTRY(EM_SETIMESTATUS), + ENTRY(EM_GETIMESTATUS), + ENTRY(SBM_SETPOS), + ENTRY(SBM_GETPOS), + ENTRY(SBM_SETRANGE), + ENTRY(SBM_SETRANGEREDRAW), + ENTRY(SBM_GETRANGE), + ENTRY(SBM_ENABLE_ARROWS), + ENTRY(SBM_SETSCROLLINFO), + ENTRY(SBM_GETSCROLLINFO), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_KEYDOWN, VirtualKeyParamInfo, "vKey", + KeystrokeFlagsParamInfo, ""), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_KEYUP, VirtualKeyParamInfo, "vKey", + KeystrokeFlagsParamInfo, ""), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_CHAR, IntParamInfo, "charCode", + KeystrokeFlagsParamInfo, ""), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DEADCHAR, IntParamInfo, "charCode", + KeystrokeFlagsParamInfo, ""), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SYSKEYDOWN, VirtualKeyParamInfo, "vKey", + KeystrokeFlagsParamInfo, ""), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SYSKEYUP, VirtualKeyParamInfo, "vKey", + KeystrokeFlagsParamInfo, ""), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SYSCHAR, IntParamInfo, "charCode", + KeystrokeFlagsParamInfo, ""), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SYSDEADCHAR, IntParamInfo, "charCode", + KeystrokeFlagsParamInfo, ""), + ENTRY(WM_KEYLAST), + ENTRY(WM_IME_STARTCOMPOSITION), + ENTRY(WM_IME_ENDCOMPOSITION), + ENTRY(WM_IME_COMPOSITION), + ENTRY(WM_INITDIALOG), + ENTRY(WM_COMMAND), + ENTRY(WM_SYSCOMMAND), + ENTRY(WM_TIMER), + ENTRY(WM_HSCROLL), + ENTRY(WM_VSCROLL), + ENTRY(WM_INITMENU), + ENTRY(WM_INITMENUPOPUP), + ENTRY(WM_MENUSELECT), + ENTRY(WM_MENUCHAR), + ENTRY(WM_ENTERIDLE), + ENTRY(WM_MENURBUTTONUP), + ENTRY(WM_MENUDRAG), + ENTRY(WM_MENUGETOBJECT), + ENTRY(WM_UNINITMENUPOPUP), + ENTRY(WM_MENUCOMMAND), + ENTRY(WM_CHANGEUISTATE), + ENTRY(WM_QUERYUISTATE), + ENTRY(WM_UPDATEUISTATE), + ENTRY(WM_CTLCOLORMSGBOX), + ENTRY(WM_CTLCOLOREDIT), + ENTRY(WM_CTLCOLORLISTBOX), + ENTRY(WM_CTLCOLORBTN), + ENTRY(WM_CTLCOLORDLG), + ENTRY(WM_CTLCOLORSCROLLBAR), + ENTRY(WM_CTLCOLORSTATIC), + ENTRY(CB_GETEDITSEL), + ENTRY(CB_LIMITTEXT), + ENTRY(CB_SETEDITSEL), + ENTRY(CB_ADDSTRING), + ENTRY(CB_DELETESTRING), + ENTRY(CB_DIR), + ENTRY(CB_GETCOUNT), + ENTRY(CB_GETCURSEL), + ENTRY(CB_GETLBTEXT), + ENTRY(CB_GETLBTEXTLEN), + ENTRY(CB_INSERTSTRING), + ENTRY(CB_RESETCONTENT), + ENTRY(CB_FINDSTRING), + ENTRY(CB_SELECTSTRING), + ENTRY(CB_SETCURSEL), + ENTRY(CB_SHOWDROPDOWN), + ENTRY(CB_GETITEMDATA), + ENTRY(CB_SETITEMDATA), + ENTRY(CB_GETDROPPEDCONTROLRECT), + ENTRY(CB_SETITEMHEIGHT), + ENTRY(CB_GETITEMHEIGHT), + ENTRY(CB_SETEXTENDEDUI), + ENTRY(CB_GETEXTENDEDUI), + ENTRY(CB_GETDROPPEDSTATE), + ENTRY(CB_FINDSTRINGEXACT), + ENTRY(CB_SETLOCALE), + ENTRY(CB_GETLOCALE), + ENTRY(CB_GETTOPINDEX), + ENTRY(CB_SETTOPINDEX), + ENTRY(CB_GETHORIZONTALEXTENT), + ENTRY(CB_SETHORIZONTALEXTENT), + ENTRY(CB_GETDROPPEDWIDTH), + ENTRY(CB_SETDROPPEDWIDTH), + ENTRY(CB_INITSTORAGE), + ENTRY(CB_MSGMAX), + ENTRY(LB_ADDSTRING), + ENTRY(LB_INSERTSTRING), + ENTRY(LB_DELETESTRING), + ENTRY(LB_SELITEMRANGEEX), + ENTRY(LB_RESETCONTENT), + ENTRY(LB_SETSEL), + ENTRY(LB_SETCURSEL), + ENTRY(LB_GETSEL), + ENTRY(LB_GETCURSEL), + ENTRY(LB_GETTEXT), + ENTRY(LB_GETTEXTLEN), + ENTRY(LB_GETCOUNT), + ENTRY(LB_SELECTSTRING), + ENTRY(LB_DIR), + ENTRY(LB_GETTOPINDEX), + ENTRY(LB_FINDSTRING), + ENTRY(LB_GETSELCOUNT), + ENTRY(LB_GETSELITEMS), + ENTRY(LB_SETTABSTOPS), + ENTRY(LB_GETHORIZONTALEXTENT), + ENTRY(LB_SETHORIZONTALEXTENT), + ENTRY(LB_SETCOLUMNWIDTH), + ENTRY(LB_ADDFILE), + ENTRY(LB_SETTOPINDEX), + ENTRY(LB_GETITEMRECT), + ENTRY(LB_GETITEMDATA), + ENTRY(LB_SETITEMDATA), + ENTRY(LB_SELITEMRANGE), + ENTRY(LB_SETANCHORINDEX), + ENTRY(LB_GETANCHORINDEX), + ENTRY(LB_SETCARETINDEX), + ENTRY(LB_GETCARETINDEX), + ENTRY(LB_SETITEMHEIGHT), + ENTRY(LB_GETITEMHEIGHT), + ENTRY(LB_FINDSTRINGEXACT), + ENTRY(LB_SETLOCALE), + ENTRY(LB_GETLOCALE), + ENTRY(LB_SETCOUNT), + ENTRY(LB_INITSTORAGE), + ENTRY(LB_ITEMFROMPOINT), + ENTRY(LB_MSGMAX), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEMOVE, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_LBUTTONDOWN, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_LBUTTONUP, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_LBUTTONDBLCLK, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_RBUTTONDOWN, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_RBUTTONUP, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_RBUTTONDBLCLK, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MBUTTONDOWN, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MBUTTONUP, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MBUTTONDBLCLK, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEWHEEL, + VirtualKeysLowWordDistanceHighWordParamInfo, + "", XLowWordYHighWordParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEHWHEEL, + VirtualKeysLowWordDistanceHighWordParamInfo, + "", XLowWordYHighWordParamInfo, "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_PARENTNOTIFY, ParentNotifyEventParamInfo, + "", PointParamInfo, "pointerLocation"), + ENTRY(WM_ENTERMENULOOP), + ENTRY(WM_EXITMENULOOP), + ENTRY(WM_NEXTMENU), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_SIZING, WindowEdgeParamInfo, "edge", + RectParamInfo, "rect"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_CAPTURECHANGED, nullptr, nullptr, + HexParamInfo, "windowHandle"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOVING, nullptr, nullptr, RectParamInfo, + "rect"), + ENTRY(WM_POWERBROADCAST), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DEVICECHANGE, DeviceEventParamInfo, "event", + HexParamInfo, "data"), + ENTRY(WM_MDICREATE), + ENTRY(WM_MDIDESTROY), + ENTRY(WM_MDIACTIVATE), + ENTRY(WM_MDIRESTORE), + ENTRY(WM_MDINEXT), + ENTRY(WM_MDIMAXIMIZE), + ENTRY(WM_MDITILE), + ENTRY(WM_MDICASCADE), + ENTRY(WM_MDIICONARRANGE), + ENTRY(WM_MDIGETACTIVE), + ENTRY(WM_MDISETMENU), + ENTRY(WM_ENTERSIZEMOVE), + ENTRY(WM_EXITSIZEMOVE), + ENTRY(WM_DROPFILES), + ENTRY(WM_MDIREFRESHMENU), + ENTRY(WM_IME_SETCONTEXT), + ENTRY(WM_IME_NOTIFY), + ENTRY(WM_IME_CONTROL), + ENTRY(WM_IME_COMPOSITIONFULL), + ENTRY(WM_IME_SELECT), + ENTRY(WM_IME_CHAR), + ENTRY(WM_IME_REQUEST), + ENTRY(WM_IME_KEYDOWN), + ENTRY(WM_IME_KEYUP), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_NCMOUSEHOVER, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_MOUSEHOVER, VirtualModifierKeysParamInfo, + "virtualKeys", XLowWordYHighWordParamInfo, + "mousePos"), + ENTRY_WITH_NO_PARAM_INFO(WM_NCMOUSELEAVE), + ENTRY_WITH_NO_PARAM_INFO(WM_MOUSELEAVE), + ENTRY(WM_CUT), + ENTRY(WM_COPY), + ENTRY(WM_PASTE), + ENTRY(WM_CLEAR), + ENTRY(WM_UNDO), + ENTRY(WM_RENDERFORMAT), + ENTRY(WM_RENDERALLFORMATS), + ENTRY(WM_DESTROYCLIPBOARD), + ENTRY(WM_DRAWCLIPBOARD), + ENTRY(WM_PAINTCLIPBOARD), + ENTRY(WM_VSCROLLCLIPBOARD), + ENTRY(WM_SIZECLIPBOARD), + ENTRY(WM_ASKCBFORMATNAME), + ENTRY(WM_CHANGECBCHAIN), + ENTRY(WM_HSCROLLCLIPBOARD), + ENTRY(WM_QUERYNEWPALETTE), + ENTRY(WM_PALETTEISCHANGING), + ENTRY(WM_PALETTECHANGED), + ENTRY(WM_HOTKEY), + ENTRY(WM_PRINT), + ENTRY(WM_PRINTCLIENT), + ENTRY(WM_THEMECHANGED), + ENTRY(WM_HANDHELDFIRST), + ENTRY(WM_HANDHELDLAST), + ENTRY(WM_AFXFIRST), + ENTRY(WM_AFXLAST), + ENTRY(WM_PENWINFIRST), + ENTRY(WM_PENWINLAST), + ENTRY(WM_APP), + ENTRY_WITH_NO_PARAM_INFO(WM_DWMCOMPOSITIONCHANGED), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DWMNCRENDERINGCHANGED, TrueFalseParamInfo, + "DwmNcRendering", nullptr, nullptr), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DWMCOLORIZATIONCOLORCHANGED, HexParamInfo, + "color:AARRGGBB", TrueFalseParamInfo, + "isOpaque"), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DWMWINDOWMAXIMIZEDCHANGE, + TrueFalseParamInfo, "maximized", nullptr, + nullptr), + ENTRY(WM_DWMSENDICONICTHUMBNAIL), // lParam: HIWORD is x, LOWORD is y + ENTRY_WITH_NO_PARAM_INFO(WM_DWMSENDICONICLIVEPREVIEWBITMAP), + ENTRY(WM_TABLET_QUERYSYSTEMGESTURESTATUS), + ENTRY(WM_GESTURE), + ENTRY(WM_GESTURENOTIFY), + ENTRY(WM_GETTITLEBARINFOEX), + ENTRY_WITH_SPLIT_PARAM_INFOS(WM_DPICHANGED, XLowWordYHighWordParamInfo, + "newDPI", RectParamInfo, + "suggestedSizeAndPos"), +}; +#undef ENTRY +#undef ENTRY_WITH_NO_PARAM_INFO +#undef ENTRY_WITH_CUSTOM_PARAM_INFO +#undef ENTRY_WITH_SPLIT_PARAM_INFO + +} // namespace mozilla::widget + +#ifdef DEBUG +void DDError(const char* msg, HRESULT hr) { + /*XXX make nicer */ + MOZ_LOG(gWindowsLog, LogLevel::Error, + ("DirectDraw error %s: 0x%08lx\n", msg, hr)); +} +#endif + +#ifdef DEBUG_VK +bool is_vk_down(int vk) { + SHORT st = GetKeyState(vk); +# ifdef DEBUG + MOZ_LOG(gWindowsLog, LogLevel::Info, ("is_vk_down vk=%x st=%x\n", vk, st)); +# endif + return (st < 0); +} +#endif diff --git a/widget/windows/nsWindowDbg.h b/widget/windows/nsWindowDbg.h new file mode 100644 index 0000000000..c739966fd5 --- /dev/null +++ b/widget/windows/nsWindowDbg.h @@ -0,0 +1,153 @@ +/* -*- 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 WindowDbg_h__ +#define WindowDbg_h__ + +/* + * nsWindowDbg - Debug related utilities for nsWindow. + */ + +#include "nsWindowDefs.h" +#include "mozilla/BaseProfilerMarkersPrerequisites.h" + +// Enables debug output for popup rollup hooks +// #define POPUP_ROLLUP_DEBUG_OUTPUT + +// Enable window size and state debug output +// #define WINSTATE_DEBUG_OUTPUT + +// nsIWidget defines a set of debug output statements +// that are called in various places within the code. +// #define WIDGET_DEBUG_OUTPUT + +// Enable IS_VK_DOWN debug output +// #define DEBUG_VK + +namespace mozilla::widget { + +class MOZ_RAII AutoProfilerMessageMarker { + public: + explicit AutoProfilerMessageMarker(Span<const char> aMsgLoopName, HWND hWnd, + UINT msg, WPARAM wParam, LPARAM lParam); + + ~AutoProfilerMessageMarker(); + + protected: + Maybe<MarkerOptions> mOptions; + Span<const char> mMsgLoopName; + UINT mMsg; + WPARAM mWParam; + LPARAM mLParam; +}; + +// Windows message debugging data +struct EventMsgInfo { + const char* mStr; + UINT mId; + std::function<nsAutoCString(WPARAM, LPARAM, bool)> mParamInfoFn; + std::function<void(nsCString&, WPARAM, const char*, bool)> mWParamInfoFn; + const char* mWParamName; + std::function<void(nsCString&, LPARAM, const char*, bool)> mLParamInfoFn; + const char* mLParamName; + void LogParameters(nsCString& str, WPARAM wParam, LPARAM lParam, + bool isPreCall); +}; +extern std::unordered_map<UINT, EventMsgInfo> gAllEvents; + +// RAII-style class to log before and after an event is handled. +class NativeEventLogger final { + public: + template <size_t N> + NativeEventLogger(const char (&aMsgLoopName)[N], HWND hwnd, UINT msg, + WPARAM wParam, LPARAM lParam) + : NativeEventLogger(Span(aMsgLoopName), hwnd, msg, wParam, lParam) {} + + void SetResult(LRESULT lresult, bool result) { + mRetValue = lresult; + mResult = mozilla::Some(result); + } + ~NativeEventLogger(); + + private: + NativeEventLogger(Span<const char> aMsgLoopName, HWND hwnd, UINT msg, + WPARAM wParam, LPARAM lParam); + bool NativeEventLoggerInternal(); + + AutoProfilerMessageMarker mProfilerMarker; + const char* mMsgLoopName; + const HWND mHwnd; + const UINT mMsg; + const WPARAM mWParam; + const LPARAM mLParam; + mozilla::Maybe<long> mEventCounter; + // not const because these will be set after the event is handled + mozilla::Maybe<bool> mResult; + LRESULT mRetValue = 0; + + bool mShouldLogPostCall; +}; + +struct EnumValueAndName { + uint64_t mFlag; + const char* mName; +}; + +// Appends to str a description of the flags passed in. +// flagsAndNames is a list of flag values with a string description +// for each one. These are processed in order, so if there are +// flag values that are combination of individual values (for example +// something like WS_OVERLAPPEDWINDOW) they need to come first +// in the flagsAndNames array. +// A 0 flag value will only be written if the flags input is exactly +// 0, and it must come last in the flagsAndNames array. +// Returns whether any info was appended to str. +bool AppendFlagsInfo(nsCString& str, uint64_t flags, + const nsTArray<EnumValueAndName>& flagsAndNames, + const char* name); + +nsAutoCString WmSizeParamInfo(uint64_t wParam, uint64_t lParam, bool isPreCall); +void XLowWordYHighWordParamInfo(nsCString& str, uint64_t value, + const char* name, bool isPreCall); +void WindowPosParamInfo(nsCString& str, uint64_t value, const char* name, + bool isPreCall); +void WindowEdgeParamInfo(nsCString& str, uint64_t value, const char* name, + bool isPreCall); +void RectParamInfo(nsCString& str, uint64_t value, const char* name, + bool isPreCall); +void UiActionParamInfo(nsCString& str, uint64_t value, const char* name, + bool isPreCall); +void WideStringParamInfo(nsCString& result, uint64_t value, const char* name, + bool isPreCall); +void MinMaxInfoParamInfo(nsCString& result, uint64_t value, const char* name, + bool isPreCall); +nsAutoCString WmNcCalcSizeParamInfo(uint64_t wParam, uint64_t lParam, + bool isPreCall); +} // namespace mozilla::widget + +#if defined(POPUP_ROLLUP_DEBUG_OUTPUT) +typedef struct { + char* mStr; + int mId; +} MSGFEventMsgInfo; + +# define DISPLAY_NMM_PRT(_arg) \ + MOZ_LOG(gWindowsLog, mozilla::LogLevel::Info, ((_arg))); +#else +# define DISPLAY_NMM_PRT(_arg) +#endif // defined(POPUP_ROLLUP_DEBUG_OUTPUT) + +#if defined(DEBUG) +void DDError(const char* msg, HRESULT hr); +#endif // defined(DEBUG) + +#if defined(DEBUG_VK) +bool is_vk_down(int vk); +# define IS_VK_DOWN is_vk_down +#else +# define IS_VK_DOWN(a) (GetKeyState(a) < 0) +#endif // defined(DEBUG_VK) + +#endif /* WindowDbg_h__ */ diff --git a/widget/windows/nsWindowDefs.h b/widget/windows/nsWindowDefs.h new file mode 100644 index 0000000000..320d6ef07b --- /dev/null +++ b/widget/windows/nsWindowDefs.h @@ -0,0 +1,119 @@ +/* -*- 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 WindowDefs_h__ +#define WindowDefs_h__ + +/* + * nsWindowDefs - nsWindow related definitions, consts, and macros. + */ + +#include "mozilla/widget/WinMessages.h" +#include "nsBaseWidget.h" +#include "nsdefs.h" +#include "resource.h" + +/************************************************************** + * + * SECTION: defines + * + **************************************************************/ + +// ConstrainPosition window positioning slop value +#define kWindowPositionSlop 20 + +// Origin of the system context menu when displayed in full screen mode +#define MOZ_SYSCONTEXT_X_POS 20 +#define MOZ_SYSCONTEXT_Y_POS 20 + +// Don't put more than this many rects in the dirty region, just fluff +// out to the bounding-box if there are more +#define MAX_RECTS_IN_REGION 100 + +// Tablet PC Mouse Input Source +#define TABLET_INK_SIGNATURE 0xFFFFFF00 +#define TABLET_INK_CHECK 0xFF515700 +#define TABLET_INK_TOUCH 0x00000080 +#define TABLET_INK_ID_MASK 0x0000007F +#define MOUSE_INPUT_SOURCE() WinUtils::GetMouseInputSource() +#define MOUSE_POINTERID() WinUtils::GetMousePointerID() + +/************************************************************** + * + * SECTION: constants + * + **************************************************************/ + +/* + * Native windows class names + * + * ::: IMPORTANT ::: + * + * External apps and drivers depend on window class names. + * For example, changing the window classes could break + * touchpad scrolling or screen readers. + * + * See bug 1776498. + */ +const wchar_t kClassNameHidden[] = L"MozillaHiddenWindowClass"; +const wchar_t kClassNameGeneral[] = L"MozillaWindowClass"; +const wchar_t kClassNameDialog[] = L"MozillaDialogClass"; +const wchar_t kClassNameDropShadow[] = L"MozillaDropShadowWindowClass"; +const wchar_t kClassNameTransition[] = L"MozillaTransitionWindowClass"; + +/************************************************************** + * + * SECTION: structs + * + **************************************************************/ + +// Used for synthesizing events +struct KeyPair { + uint8_t mGeneral; + uint8_t mSpecific; + uint16_t mScanCode; + KeyPair(uint32_t aGeneral, uint32_t aSpecific) + : mGeneral(aGeneral & 0xFF), + mSpecific(aSpecific & 0xFF), + mScanCode((aGeneral & 0xFFFF0000) >> 16) {} + KeyPair(uint8_t aGeneral, uint8_t aSpecific, uint16_t aScanCode) + : mGeneral(aGeneral), mSpecific(aSpecific), mScanCode(aScanCode) {} +}; + +namespace mozilla { +namespace widget { + +struct MSGResult { + // Result for the message. + LRESULT& mResult; + // If mConsumed is true, the caller shouldn't call next wndproc. + bool mConsumed; + + explicit MSGResult(LRESULT* aResult = nullptr) + : mResult(aResult ? *aResult : mDefaultResult), mConsumed(false) {} + + private: + LRESULT mDefaultResult; +}; + +} // namespace widget +} // namespace mozilla + +/************************************************************** + * + * SECTION: macros + * + **************************************************************/ + +#define NSRGB_2_COLOREF(color) \ + RGB(NS_GET_R(color), NS_GET_G(color), NS_GET_B(color)) +#define COLOREF_2_NSRGB(color) \ + NS_RGB(GetRValue(color), GetGValue(color), GetBValue(color)) + +#define VERIFY_WINDOW_STYLE(s) \ + NS_ASSERTION(((s) & (WS_CHILD | WS_POPUP)) != (WS_CHILD | WS_POPUP), \ + "WS_POPUP and WS_CHILD are mutually exclusive") + +#endif /* WindowDefs_h__ */ diff --git a/widget/windows/nsWindowGfx.cpp b/widget/windows/nsWindowGfx.cpp new file mode 100644 index 0000000000..c2a91dcf6b --- /dev/null +++ b/widget/windows/nsWindowGfx.cpp @@ -0,0 +1,718 @@ +/* -*- 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/. */ + +/* + * nsWindowGfx - Painting and aceleration. + */ + +/************************************************************** + ************************************************************** + ** + ** BLOCK: Includes + ** + ** Include headers. + ** + ************************************************************** + **************************************************************/ + +#include "mozilla/dom/ContentParent.h" + +#include "nsWindowGfx.h" +#include "nsAppRunner.h" +#include <windows.h> +#include <shellapi.h> +#include "gfxEnv.h" +#include "gfxImageSurface.h" +#include "gfxUtils.h" +#include "gfxConfig.h" +#include "gfxWindowsSurface.h" +#include "gfxWindowsPlatform.h" +#include "gfxDWriteFonts.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" +#include "mozilla/gfx/Tools.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "nsGfxCIID.h" +#include "gfxContext.h" +#include "WinUtils.h" +#include "WinWindowOcclusionTracker.h" +#include "nsIWidgetListener.h" +#include "mozilla/Unused.h" +#include "nsDebug.h" +#include "WindowRenderer.h" +#include "mozilla/layers/WebRenderLayerManager.h" + +#include "mozilla/gfx/GPUProcessManager.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/CompositorBridgeChild.h" +#include "InProcessWinCompositorWidget.h" + +#include "nsUXThemeData.h" +#include "nsUXThemeConstants.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::layers; +using namespace mozilla::widget; +using namespace mozilla::plugins; +extern mozilla::LazyLogModule gWindowsLog; + +/************************************************************** + ************************************************************** + ** + ** BLOCK: Variables + ** + ** nsWindow Class static initializations and global variables. + ** + ************************************************************** + **************************************************************/ + +/************************************************************** + * + * SECTION: nsWindow statics + * + **************************************************************/ + +struct IconMetrics { + int32_t xMetric; + int32_t yMetric; + int32_t defaultSize; +}; + +// Corresponds 1:1 to the IconSizeType enum +static IconMetrics sIconMetrics[] = { + {SM_CXSMICON, SM_CYSMICON, 16}, // small icon + {SM_CXICON, SM_CYICON, 32} // regular icon +}; + +/************************************************************** + ************************************************************** + ** + ** BLOCK: nsWindow impl. + ** + ** Paint related nsWindow methods. + ** + ************************************************************** + **************************************************************/ + +// GetRegionToPaint returns the invalidated region that needs to be painted +LayoutDeviceIntRegion nsWindow::GetRegionToPaint(bool aForceFullRepaint, + PAINTSTRUCT ps, HDC aDC) { + if (aForceFullRepaint) { + RECT paintRect; + ::GetClientRect(mWnd, &paintRect); + return LayoutDeviceIntRegion(WinUtils::ToIntRect(paintRect)); + } + + HRGN paintRgn = ::CreateRectRgn(0, 0, 0, 0); + if (paintRgn != nullptr) { + int result = GetRandomRgn(aDC, paintRgn, SYSRGN); + if (result == 1) { + POINT pt = {0, 0}; + ::MapWindowPoints(nullptr, mWnd, &pt, 1); + ::OffsetRgn(paintRgn, pt.x, pt.y); + } + LayoutDeviceIntRegion rgn(WinUtils::ConvertHRGNToRegion(paintRgn)); + ::DeleteObject(paintRgn); + return rgn; + } + return LayoutDeviceIntRegion(WinUtils::ToIntRect(ps.rcPaint)); +} + +nsIWidgetListener* nsWindow::GetPaintListener() { + if (mDestroyCalled) return nullptr; + return mAttachedWidgetListener ? mAttachedWidgetListener : mWidgetListener; +} + +void nsWindow::ForcePresent() { + if (mResizeState != RESIZING) { + if (CompositorBridgeChild* remoteRenderer = GetRemoteRenderer()) { + remoteRenderer->SendForcePresent(wr::RenderReasons::WIDGET); + } + } +} + +bool nsWindow::OnPaint(uint32_t aNestingLevel) { + DeviceResetReason resetReason = DeviceResetReason::OK; + if (gfxWindowsPlatform::GetPlatform()->DidRenderingDeviceReset( + &resetReason)) { + gfxCriticalNote << "(nsWindow) Detected device reset: " << (int)resetReason; + + gfxWindowsPlatform::GetPlatform()->UpdateRenderMode(); + + bool guilty; + switch (resetReason) { + case DeviceResetReason::HUNG: + case DeviceResetReason::RESET: + case DeviceResetReason::INVALID_CALL: + guilty = true; + break; + default: + guilty = false; + break; + } + + GPUProcessManager::Get()->OnInProcessDeviceReset(guilty); + + gfxCriticalNote << "(nsWindow) Finished device reset."; + return false; + } + + PAINTSTRUCT ps; + + // Avoid starting the GPU process for the initial navigator:blank window. + if (mIsEarlyBlankWindow) { + // Call BeginPaint/EndPaint or Windows will keep sending us messages. + ::BeginPaint(mWnd, &ps); + ::EndPaint(mWnd, &ps); + return true; + } + + WindowRenderer* renderer = GetWindowRenderer(); + KnowsCompositor* knowsCompositor = renderer->AsKnowsCompositor(); + WebRenderLayerManager* layerManager = renderer->AsWebRender(); + + if (mClearNCEdge) { + // We need to clear this edge of the non-client region to black (once). + HDC hdc; + RECT rect; + hdc = ::GetWindowDC(mWnd); + ::GetWindowRect(mWnd, &rect); + ::MapWindowPoints(nullptr, mWnd, (LPPOINT)&rect, 2); + switch (mClearNCEdge.value()) { + case ABE_TOP: + rect.bottom = rect.top + kHiddenTaskbarSize; + break; + case ABE_LEFT: + rect.right = rect.left + kHiddenTaskbarSize; + break; + case ABE_BOTTOM: + rect.top = rect.bottom - kHiddenTaskbarSize; + break; + case ABE_RIGHT: + rect.left = rect.right - kHiddenTaskbarSize; + break; + default: + MOZ_ASSERT_UNREACHABLE("Invalid edge value"); + break; + } + ::FillRect(hdc, &rect, + reinterpret_cast<HBRUSH>(::GetStockObject(BLACK_BRUSH))); + ::ReleaseDC(mWnd, hdc); + + mClearNCEdge.reset(); + } + + if (knowsCompositor && layerManager && + !mBounds.IsEqualEdges(mLastPaintBounds)) { + // Do an early async composite so that we at least have something on the + // screen in the right place, even if the content is out of date. + layerManager->ScheduleComposite(wr::RenderReasons::WIDGET); + } + mLastPaintBounds = mBounds; + + // For layered translucent windows all drawing should go to memory DC and no + // WM_PAINT messages are normally generated. To support asynchronous painting + // we force generation of WM_PAINT messages by invalidating window areas with + // RedrawWindow, InvalidateRect or InvalidateRgn function calls. + const bool usingMemoryDC = + renderer->GetBackendType() == LayersBackend::LAYERS_NONE && + mTransparencyMode == TransparencyMode::Transparent; + + HDC hDC = nullptr; + if (usingMemoryDC) { + // BeginPaint/EndPaint must be called to make Windows think that invalid + // area is painted. Otherwise it will continue sending the same message + // endlessly. + ::BeginPaint(mWnd, &ps); + ::EndPaint(mWnd, &ps); + + // We're guaranteed to have a widget proxy since we called + // GetLayerManager(). + hDC = mBasicLayersSurface->GetTransparentDC(); + } else { + hDC = ::BeginPaint(mWnd, &ps); + } + + const bool forceRepaint = mTransparencyMode == TransparencyMode::Transparent; + const LayoutDeviceIntRegion region = GetRegionToPaint(forceRepaint, ps, hDC); + + if (knowsCompositor && layerManager) { + // We need to paint to the screen even if nothing changed, since if we + // don't have a compositing window manager, our pixels could be stale. + layerManager->SetNeedsComposite(true); + layerManager->SendInvalidRegion(region.ToUnknownRegion()); + } + + RefPtr<nsWindow> strongThis(this); + + nsIWidgetListener* listener = GetPaintListener(); + if (listener) { + listener->WillPaintWindow(this); + } + // Re-get the listener since the will paint notification may have killed it. + listener = GetPaintListener(); + if (!listener) { + return false; + } + + if (knowsCompositor && layerManager && layerManager->NeedsComposite()) { + layerManager->ScheduleComposite(wr::RenderReasons::WIDGET); + layerManager->SetNeedsComposite(false); + } + + bool result = true; + if (!region.IsEmpty() && listener) { + // Should probably pass in a real region here, using GetRandomRgn + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/clipping_4q0e.asp + +#ifdef WIDGET_DEBUG_OUTPUT + debug_DumpPaintEvent(stdout, this, region.ToUnknownRegion(), "noname", + (int32_t)mWnd); +#endif // WIDGET_DEBUG_OUTPUT + + switch (renderer->GetBackendType()) { + case LayersBackend::LAYERS_NONE: { + RefPtr<gfxASurface> targetSurface; + + // don't support transparency for non-GDI rendering, for now + if (TransparencyMode::Transparent == mTransparencyMode) { + // This mutex needs to be held when EnsureTransparentSurface is + // called. + MutexAutoLock lock(mBasicLayersSurface->GetTransparentSurfaceLock()); + targetSurface = mBasicLayersSurface->EnsureTransparentSurface(); + } + + RefPtr<gfxWindowsSurface> targetSurfaceWin; + if (!targetSurface) { + uint32_t flags = (mTransparencyMode == TransparencyMode::Opaque) + ? 0 + : gfxWindowsSurface::FLAG_IS_TRANSPARENT; + targetSurfaceWin = new gfxWindowsSurface(hDC, flags); + targetSurface = targetSurfaceWin; + } + + if (!targetSurface) { + NS_ERROR("Invalid RenderMode!"); + return false; + } + + RECT paintRect; + ::GetClientRect(mWnd, &paintRect); + RefPtr<DrawTarget> dt = gfxPlatform::CreateDrawTargetForSurface( + targetSurface, IntSize(paintRect.right - paintRect.left, + paintRect.bottom - paintRect.top)); + if (!dt || !dt->IsValid()) { + gfxWarning() + << "nsWindow::OnPaint failed in CreateDrawTargetForSurface"; + return false; + } + + // don't need to double buffer with anything but GDI + BufferMode doubleBuffering = mozilla::layers::BufferMode::BUFFER_NONE; + switch (mTransparencyMode) { + case TransparencyMode::Transparent: + // If we're rendering with translucency, we're going to be + // rendering the whole window; make sure we clear it first + dt->ClearRect( + Rect(0.f, 0.f, dt->GetSize().width, dt->GetSize().height)); + break; + default: + // If we're not doing translucency, then double buffer + doubleBuffering = mozilla::layers::BufferMode::BUFFERED; + break; + } + + gfxContext thebesContext(dt); + + { + AutoLayerManagerSetup setupLayerManager(this, &thebesContext, + doubleBuffering); + result = listener->PaintWindow(this, region); + } + + if (TransparencyMode::Transparent == mTransparencyMode) { + // Data from offscreen drawing surface was copied to memory bitmap of + // transparent bitmap. Now it can be read from memory bitmap to apply + // alpha channel and after that displayed on the screen. + mBasicLayersSurface->RedrawTransparentWindow(); + } + } break; + case LayersBackend::LAYERS_WR: { + result = listener->PaintWindow(this, region); + if (!gfxEnv::MOZ_DISABLE_FORCE_PRESENT()) { + nsCOMPtr<nsIRunnable> event = NewRunnableMethod( + "nsWindow::ForcePresent", this, &nsWindow::ForcePresent); + NS_DispatchToMainThread(event); + } + } break; + default: + NS_ERROR("Unknown layers backend used!"); + break; + } + } + + if (!usingMemoryDC) { + ::EndPaint(mWnd, &ps); + } + + mLastPaintEndTime = TimeStamp::Now(); + + // Re-get the listener since painting may have killed it. + listener = GetPaintListener(); + if (listener) listener->DidPaintWindow(); + + if (aNestingLevel == 0 && ::GetUpdateRect(mWnd, nullptr, false)) { + OnPaint(1); + } + + return result; +} + +bool nsWindow::NeedsToTrackWindowOcclusionState() { + if (!WinWindowOcclusionTracker::Get()) { + return false; + } + + if (mCompositorSession && mWindowType == WindowType::TopLevel) { + return true; + } + + return false; +} + +void nsWindow::NotifyOcclusionState(mozilla::widget::OcclusionState aState) { + MOZ_ASSERT(NeedsToTrackWindowOcclusionState()); + + bool isFullyOccluded = aState == mozilla::widget::OcclusionState::OCCLUDED; + // When window is minimized, it is not set as fully occluded. + if (mFrameState->GetSizeMode() == nsSizeMode_Minimized) { + isFullyOccluded = false; + } + + // Don't dispatch if the new occlustion state is the same as the current + // state. + if (mIsFullyOccluded == isFullyOccluded) { + return; + } + + mIsFullyOccluded = isFullyOccluded; + + MOZ_LOG(gWindowsLog, LogLevel::Info, + ("nsWindow::NotifyOcclusionState() mIsFullyOccluded %d " + "mFrameState->GetSizeMode() %d", + mIsFullyOccluded, mFrameState->GetSizeMode())); + + wr::DebugFlags flags{0}; + flags._0 = gfx::gfxVars::WebRenderDebugFlags(); + bool debugEnabled = bool(flags & wr::DebugFlags::WINDOW_VISIBILITY_DBG); + if (debugEnabled && mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyVisibilityUpdated( + mFrameState->GetSizeMode(), mIsFullyOccluded); + } + + if (mWidgetListener) { + mWidgetListener->OcclusionStateChanged(mIsFullyOccluded); + } +} + +void nsWindow::MaybeEnableWindowOcclusion(bool aEnable) { + // WindowOcclusion is enabled/disabled only when compositor session exists. + // See nsWindow::NeedsToTrackWindowOcclusionState(). + if (!mCompositorSession) { + return; + } + + bool enabled = gfxConfig::IsEnabled(gfx::Feature::WINDOW_OCCLUSION); + + if (aEnable) { + // Enable window occlusion. + if (enabled && NeedsToTrackWindowOcclusionState()) { + WinWindowOcclusionTracker::Get()->Enable(this, mWnd); + + wr::DebugFlags flags{0}; + flags._0 = gfx::gfxVars::WebRenderDebugFlags(); + bool debugEnabled = bool(flags & wr::DebugFlags::WINDOW_VISIBILITY_DBG); + if (debugEnabled && mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyVisibilityUpdated( + mFrameState->GetSizeMode(), mIsFullyOccluded); + } + } + return; + } + + // Disable window occlusion. + MOZ_ASSERT(!aEnable); + + if (!NeedsToTrackWindowOcclusionState()) { + return; + } + + WinWindowOcclusionTracker::Get()->Disable(this, mWnd); + NotifyOcclusionState(OcclusionState::VISIBLE); + + wr::DebugFlags flags{0}; + flags._0 = gfx::gfxVars::WebRenderDebugFlags(); + bool debugEnabled = bool(flags & wr::DebugFlags::WINDOW_VISIBILITY_DBG); + if (debugEnabled && mCompositorWidgetDelegate) { + mCompositorWidgetDelegate->NotifyVisibilityUpdated( + mFrameState->GetSizeMode(), mIsFullyOccluded); + } +} + +// This override of CreateCompositor is to add support for sending the IPC +// call for RequesetFxrOutput as soon as the compositor for this widget is +// available. +void nsWindow::CreateCompositor() { + nsBaseWidget::CreateCompositor(); + + MaybeEnableWindowOcclusion(/* aEnable */ true); + + if (mRequestFxrOutputPending) { + GetRemoteRenderer()->SendRequestFxrOutput(); + } +} + +void nsWindow::DestroyCompositor() { + MaybeEnableWindowOcclusion(/* aEnable */ false); + + nsBaseWidget::DestroyCompositor(); +} + +void nsWindow::RequestFxrOutput() { + if (GetRemoteRenderer() != nullptr) { + MOZ_CRASH("RequestFxrOutput should happen before Compositor is created."); + } else { + // The compositor isn't ready, so indicate to make the IPC call when + // it is available. + mRequestFxrOutputPending = true; + } +} + +LayoutDeviceIntSize nsWindowGfx::GetIconMetrics(IconSizeType aSizeType) { + int32_t width = ::GetSystemMetrics(sIconMetrics[aSizeType].xMetric); + int32_t height = ::GetSystemMetrics(sIconMetrics[aSizeType].yMetric); + + if (width == 0 || height == 0) { + width = height = sIconMetrics[aSizeType].defaultSize; + } + + return LayoutDeviceIntSize(width, height); +} + +nsresult nsWindowGfx::CreateIcon(imgIContainer* aContainer, bool aIsCursor, + LayoutDeviceIntPoint aHotspot, + LayoutDeviceIntSize aScaledSize, + HICON* aIcon) { + MOZ_ASSERT(aHotspot.x >= 0 && aHotspot.y >= 0); + MOZ_ASSERT((aScaledSize.width > 0 && aScaledSize.height > 0) || + (aScaledSize.width == 0 && aScaledSize.height == 0)); + + // Get the image data + RefPtr<SourceSurface> surface = aContainer->GetFrame( + imgIContainer::FRAME_CURRENT, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + NS_ENSURE_TRUE(surface, NS_ERROR_NOT_AVAILABLE); + + IntSize frameSize = surface->GetSize(); + if (frameSize.IsEmpty()) { + return NS_ERROR_FAILURE; + } + + IntSize iconSize(aScaledSize.width, aScaledSize.height); + if (iconSize == IntSize(0, 0)) { // use frame's intrinsic size + iconSize = frameSize; + } + + RefPtr<DataSourceSurface> dataSurface; + bool mappedOK; + DataSourceSurface::MappedSurface map; + + if (iconSize != frameSize) { + // Scale the surface + dataSurface = + Factory::CreateDataSourceSurface(iconSize, SurfaceFormat::B8G8R8A8); + NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE); + mappedOK = dataSurface->Map(DataSourceSurface::MapType::READ_WRITE, &map); + NS_ENSURE_TRUE(mappedOK, NS_ERROR_FAILURE); + + RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData( + BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride, + SurfaceFormat::B8G8R8A8); + if (!dt) { + gfxWarning() + << "nsWindowGfx::CreatesIcon failed in CreateDrawTargetForData"; + return NS_ERROR_OUT_OF_MEMORY; + } + dt->DrawSurface(surface, Rect(0, 0, iconSize.width, iconSize.height), + Rect(0, 0, frameSize.width, frameSize.height), + DrawSurfaceOptions(), + DrawOptions(1.0f, CompositionOp::OP_SOURCE)); + } else if (surface->GetFormat() != SurfaceFormat::B8G8R8A8) { + // Convert format to SurfaceFormat::B8G8R8A8 + dataSurface = gfxUtils::CopySurfaceToDataSourceSurfaceWithFormat( + surface, SurfaceFormat::B8G8R8A8); + NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE); + mappedOK = dataSurface->Map(DataSourceSurface::MapType::READ, &map); + } else { + dataSurface = surface->GetDataSurface(); + NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE); + mappedOK = dataSurface->Map(DataSourceSurface::MapType::READ, &map); + } + NS_ENSURE_TRUE(dataSurface && mappedOK, NS_ERROR_FAILURE); + MOZ_ASSERT(dataSurface->GetFormat() == SurfaceFormat::B8G8R8A8); + + uint8_t* data = nullptr; + UniquePtr<uint8_t[]> autoDeleteArray; + if (map.mStride == BytesPerPixel(dataSurface->GetFormat()) * iconSize.width) { + // Mapped data is already packed + data = map.mData; + } else { + // We can't use map.mData since the pixels are not packed (as required by + // CreateDIBitmap, which is called under the DataToBitmap call below). + // + // We must unmap before calling SurfaceToPackedBGRA because it needs access + // to the pixel data. + dataSurface->Unmap(); + map.mData = nullptr; + + autoDeleteArray = SurfaceToPackedBGRA(dataSurface); + data = autoDeleteArray.get(); + NS_ENSURE_TRUE(data, NS_ERROR_FAILURE); + } + + HBITMAP bmp = DataToBitmap(data, iconSize.width, -iconSize.height, 32); + uint8_t* a1data = Data32BitTo1Bit(data, iconSize.width, iconSize.height); + if (map.mData) { + dataSurface->Unmap(); + } + if (!a1data) { + return NS_ERROR_FAILURE; + } + + HBITMAP mbmp = DataToBitmap(a1data, iconSize.width, -iconSize.height, 1); + free(a1data); + + ICONINFO info = {0}; + info.fIcon = !aIsCursor; + info.xHotspot = aHotspot.x; + info.yHotspot = aHotspot.y; + info.hbmMask = mbmp; + info.hbmColor = bmp; + + HCURSOR icon = ::CreateIconIndirect(&info); + ::DeleteObject(mbmp); + ::DeleteObject(bmp); + if (!icon) return NS_ERROR_FAILURE; + *aIcon = icon; + return NS_OK; +} + +// Adjust cursor image data +uint8_t* nsWindowGfx::Data32BitTo1Bit(uint8_t* aImageData, uint32_t aWidth, + uint32_t aHeight) { + // We need (aWidth + 7) / 8 bytes plus zero-padding up to a multiple of + // 4 bytes for each row (HBITMAP requirement). Bug 353553. + uint32_t outBpr = ((aWidth + 31) / 8) & ~3; + + // Allocate and clear mask buffer + uint8_t* outData = (uint8_t*)calloc(outBpr, aHeight); + if (!outData) return nullptr; + + int32_t* imageRow = (int32_t*)aImageData; + for (uint32_t curRow = 0; curRow < aHeight; curRow++) { + uint8_t* outRow = outData + curRow * outBpr; + uint8_t mask = 0x80; + for (uint32_t curCol = 0; curCol < aWidth; curCol++) { + // Use sign bit to test for transparency, as alpha byte is highest byte + if (*imageRow++ < 0) *outRow |= mask; + + mask >>= 1; + if (!mask) { + outRow++; + mask = 0x80; + } + } + } + + return outData; +} + +/** + * Convert the given image data to a HBITMAP. If the requested depth is + * 32 bit, a bitmap with an alpha channel will be returned. + * + * @param aImageData The image data to convert. Must use the format accepted + * by CreateDIBitmap. + * @param aWidth With of the bitmap, in pixels. + * @param aHeight Height of the image, in pixels. + * @param aDepth Image depth, in bits. Should be one of 1, 24 and 32. + * + * @return The HBITMAP representing the image. Caller should call + * DeleteObject when done with the bitmap. + * On failure, nullptr will be returned. + */ +HBITMAP nsWindowGfx::DataToBitmap(uint8_t* aImageData, uint32_t aWidth, + uint32_t aHeight, uint32_t aDepth) { + HDC dc = ::GetDC(nullptr); + + if (aDepth == 32) { + // Alpha channel. We need the new header. + BITMAPV4HEADER head = {0}; + head.bV4Size = sizeof(head); + head.bV4Width = aWidth; + head.bV4Height = aHeight; + head.bV4Planes = 1; + head.bV4BitCount = aDepth; + head.bV4V4Compression = BI_BITFIELDS; + head.bV4SizeImage = 0; // Uncompressed + head.bV4XPelsPerMeter = 0; + head.bV4YPelsPerMeter = 0; + head.bV4ClrUsed = 0; + head.bV4ClrImportant = 0; + + head.bV4RedMask = 0x00FF0000; + head.bV4GreenMask = 0x0000FF00; + head.bV4BlueMask = 0x000000FF; + head.bV4AlphaMask = 0xFF000000; + + HBITMAP bmp = ::CreateDIBitmap( + dc, reinterpret_cast<CONST BITMAPINFOHEADER*>(&head), CBM_INIT, + aImageData, reinterpret_cast<CONST BITMAPINFO*>(&head), DIB_RGB_COLORS); + ::ReleaseDC(nullptr, dc); + return bmp; + } + + char reserved_space[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 2]; + BITMAPINFOHEADER& head = *(BITMAPINFOHEADER*)reserved_space; + + head.biSize = sizeof(BITMAPINFOHEADER); + head.biWidth = aWidth; + head.biHeight = aHeight; + head.biPlanes = 1; + head.biBitCount = (WORD)aDepth; + head.biCompression = BI_RGB; + head.biSizeImage = 0; // Uncompressed + head.biXPelsPerMeter = 0; + head.biYPelsPerMeter = 0; + head.biClrUsed = 0; + head.biClrImportant = 0; + + BITMAPINFO& bi = *(BITMAPINFO*)reserved_space; + + if (aDepth == 1) { + RGBQUAD black = {0, 0, 0, 0}; + RGBQUAD white = {255, 255, 255, 0}; + + bi.bmiColors[0] = white; + bi.bmiColors[1] = black; + } + + HBITMAP bmp = + ::CreateDIBitmap(dc, &head, CBM_INIT, aImageData, &bi, DIB_RGB_COLORS); + ::ReleaseDC(nullptr, dc); + return bmp; +} diff --git a/widget/windows/nsWindowGfx.h b/widget/windows/nsWindowGfx.h new file mode 100644 index 0000000000..0d5fd9e01a --- /dev/null +++ b/widget/windows/nsWindowGfx.h @@ -0,0 +1,35 @@ +/* -*- 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 WindowGfx_h__ +#define WindowGfx_h__ + +/* + * nsWindowGfx - Painting and aceleration. + */ + +#include "nsWindow.h" +#include <imgIContainer.h> + +class nsWindowGfx { + public: + enum IconSizeType { kSmallIcon, kRegularIcon }; + static mozilla::LayoutDeviceIntSize GetIconMetrics(IconSizeType aSizeType); + static nsresult CreateIcon(imgIContainer* aContainer, bool aIsCursor, + mozilla::LayoutDeviceIntPoint aHotspot, + mozilla::LayoutDeviceIntSize aScaledSize, + HICON* aIcon); + + private: + /** + * Cursor helpers + */ + static uint8_t* Data32BitTo1Bit(uint8_t* aImageData, uint32_t aWidth, + uint32_t aHeight); + static HBITMAP DataToBitmap(uint8_t* aImageData, uint32_t aWidth, + uint32_t aHeight, uint32_t aDepth); +}; + +#endif // WindowGfx_h__ diff --git a/widget/windows/nsWindowLoggedMessages.cpp b/widget/windows/nsWindowLoggedMessages.cpp new file mode 100644 index 0000000000..ac0f05a875 --- /dev/null +++ b/widget/windows/nsWindowLoggedMessages.cpp @@ -0,0 +1,307 @@ +/* -*- 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 <windef.h> +#include <winuser.h> +#include "mozilla/StaticPrefs_storage.h" +#include "mozilla/StaticPrefs_widget.h" +#include "nsWindowLoggedMessages.h" +#include "nsWindow.h" +#include "WinUtils.h" +#include <map> +#include <algorithm> + +namespace mozilla::widget { + +// NCCALCSIZE_PARAMS and WINDOWPOS are relatively large structures, so store +// them as a pointer to save memory +using NcCalcSizeVariantData = + Variant<UniquePtr<std::pair<NCCALCSIZE_PARAMS, WINDOWPOS>>, RECT>; +// to save memory, hold the raw data and only convert to string +// when requested +using MessageSpecificData = + Variant<std::pair<WPARAM, LPARAM>, // WM_SIZE, WM_MOVE + WINDOWPOS, // WM_WINDOWPOSCHANGING, WM_WINDOWPOSCHANGED + std::pair<WPARAM, RECT>, // WM_SIZING, WM_DPICHANGED, WM_MOVING + std::pair<WPARAM, nsString>, // WM_SETTINGCHANGE + std::pair<bool, NcCalcSizeVariantData>, // WM_NCCALCSIZE + MINMAXINFO // WM_GETMINMAXINFO + >; + +struct WindowMessageData { + long mEventCounter; + bool mIsPreEvent; + MessageSpecificData mSpecificData; + mozilla::Maybe<bool> mResult; + LRESULT mRetValue; + WindowMessageData(long eventCounter, bool isPreEvent, + MessageSpecificData&& specificData, + mozilla::Maybe<bool> result, LRESULT retValue) + : mEventCounter(eventCounter), + mIsPreEvent(isPreEvent), + mSpecificData(std::move(specificData)), + mResult(result), + mRetValue(retValue) {} + // Disallow copy constructor/operator since MessageSpecificData has a + // UniquePtr + WindowMessageData(const WindowMessageData&) = delete; + WindowMessageData& operator=(const WindowMessageData&) = delete; + WindowMessageData(WindowMessageData&&) = default; + WindowMessageData& operator=(WindowMessageData&&) = default; +}; + +struct WindowMessageDataSortKey { + long mEventCounter; + bool mIsPreEvent; + explicit WindowMessageDataSortKey(const WindowMessageData& data) + : mEventCounter(data.mEventCounter), mIsPreEvent(data.mIsPreEvent) {} + bool operator<(const WindowMessageDataSortKey& other) const { + if (mEventCounter < other.mEventCounter) { + return true; + } + if (other.mEventCounter < mEventCounter) { + return false; + } + if (mIsPreEvent && !other.mIsPreEvent) { + return true; + } + if (other.mIsPreEvent && !mIsPreEvent) { + return false; + } + // they're equal + return false; + } +}; + +struct CircularMessageBuffer { + // Only used when the vector is at its maximum size + size_t mNextFreeIndex = 0; + std::vector<WindowMessageData> mMessages; +}; +static std::map<HWND, std::map<UINT, CircularMessageBuffer>> gWindowMessages; + +static HWND GetHwndFromWidget(nsIWidget* windowWidget) { + nsWindow* window = static_cast<nsWindow*>(windowWidget); + return window->GetWindowHandle(); +} + +MessageSpecificData MakeMessageSpecificData(UINT event, WPARAM wParam, + LPARAM lParam) { + // Since we store this data for every message we log, make sure it's of a + // reasonable size. Keep in mind we're storing up to 10 (number of message + // types) + // * 6 (default number of messages per type to keep) of these messages per + // window. + static_assert(sizeof(MessageSpecificData) <= 48); + switch (event) { + case WM_SIZE: + case WM_MOVE: + return MessageSpecificData(std::make_pair(wParam, lParam)); + case WM_WINDOWPOSCHANGING: + case WM_WINDOWPOSCHANGED: { + LPWINDOWPOS windowPosPtr = reinterpret_cast<LPWINDOWPOS>(lParam); + WINDOWPOS windowPos = *windowPosPtr; + return MessageSpecificData(std::move(windowPos)); + } + case WM_SIZING: + case WM_DPICHANGED: + case WM_MOVING: { + LPRECT rectPtr = reinterpret_cast<LPRECT>(lParam); + RECT rect = *rectPtr; + return MessageSpecificData(std::make_pair(wParam, std::move(rect))); + } + case WM_SETTINGCHANGE: { + LPCWSTR wideStrPtr = reinterpret_cast<LPCWSTR>(lParam); + nsString str(wideStrPtr); + return MessageSpecificData(std::make_pair(wParam, std::move(str))); + } + case WM_NCCALCSIZE: { + bool shouldIndicateValidArea = wParam == TRUE; + if (shouldIndicateValidArea) { + LPNCCALCSIZE_PARAMS ncCalcSizeParamsPtr = + reinterpret_cast<LPNCCALCSIZE_PARAMS>(lParam); + NCCALCSIZE_PARAMS ncCalcSizeParams = *ncCalcSizeParamsPtr; + WINDOWPOS windowPos = *ncCalcSizeParams.lppos; + UniquePtr<std::pair<NCCALCSIZE_PARAMS, WINDOWPOS>> ncCalcSizeData = + MakeUnique<std::pair<NCCALCSIZE_PARAMS, WINDOWPOS>>( + std::pair(std::move(ncCalcSizeParams), std::move(windowPos))); + return MessageSpecificData( + std::make_pair(shouldIndicateValidArea, + NcCalcSizeVariantData(std::move(ncCalcSizeData)))); + } else { + LPRECT rectPtr = reinterpret_cast<LPRECT>(lParam); + RECT rect = *rectPtr; + return MessageSpecificData(std::make_pair( + shouldIndicateValidArea, NcCalcSizeVariantData(std::move(rect)))); + } + } + case WM_GETMINMAXINFO: { + PMINMAXINFO minMaxInfoPtr = reinterpret_cast<PMINMAXINFO>(lParam); + MINMAXINFO minMaxInfo = *minMaxInfoPtr; + return MessageSpecificData(std::move(minMaxInfo)); + } + default: + MOZ_ASSERT_UNREACHABLE( + "Unhandled message type in MakeMessageSpecificData"); + return MessageSpecificData(std::make_pair(wParam, lParam)); + } +} + +void AppendFriendlyMessageSpecificData(nsCString& str, UINT event, + bool isPreEvent, + const MessageSpecificData& data) { + switch (event) { + case WM_SIZE: { + const auto& params = data.as<std::pair<WPARAM, LPARAM>>(); + nsAutoCString tempStr = + WmSizeParamInfo(params.first, params.second, isPreEvent); + str.AppendASCII(tempStr); + break; + } + case WM_MOVE: { + const auto& params = data.as<std::pair<WPARAM, LPARAM>>(); + XLowWordYHighWordParamInfo(str, params.second, "upperLeft", isPreEvent); + break; + } + case WM_WINDOWPOSCHANGING: + case WM_WINDOWPOSCHANGED: { + const auto& params = data.as<WINDOWPOS>(); + WindowPosParamInfo(str, reinterpret_cast<uint64_t>(¶ms), + "newSizeAndPos", isPreEvent); + break; + } + case WM_SIZING: { + const auto& params = data.as<std::pair<WPARAM, RECT>>(); + WindowEdgeParamInfo(str, params.first, "edge", isPreEvent); + str.AppendASCII(" "); + RectParamInfo(str, reinterpret_cast<uint64_t>(¶ms.second), "rect", + isPreEvent); + break; + } + case WM_DPICHANGED: { + const auto& params = data.as<std::pair<WPARAM, RECT>>(); + XLowWordYHighWordParamInfo(str, params.first, "newDPI", isPreEvent); + str.AppendASCII(" "); + RectParamInfo(str, reinterpret_cast<uint64_t>(¶ms.second), + "suggestedSizeAndPos", isPreEvent); + break; + } + case WM_MOVING: { + const auto& params = data.as<std::pair<WPARAM, RECT>>(); + RectParamInfo(str, reinterpret_cast<uint64_t>(¶ms.second), "rect", + isPreEvent); + break; + } + case WM_SETTINGCHANGE: { + const auto& params = data.as<std::pair<WPARAM, nsString>>(); + UiActionParamInfo(str, params.first, "uiAction", isPreEvent); + str.AppendASCII(" "); + WideStringParamInfo( + str, + reinterpret_cast<uint64_t>((const wchar_t*)(params.second.Data())), + "paramChanged", isPreEvent); + break; + } + case WM_NCCALCSIZE: { + const auto& params = data.as<std::pair<bool, NcCalcSizeVariantData>>(); + bool shouldIndicateValidArea = params.first; + if (shouldIndicateValidArea) { + const auto& validAreaParams = + params.second + .as<UniquePtr<std::pair<NCCALCSIZE_PARAMS, WINDOWPOS>>>(); + // Make pointer point to the cached data + validAreaParams->first.lppos = &validAreaParams->second; + nsAutoCString tempStr = WmNcCalcSizeParamInfo( + TRUE, reinterpret_cast<uint64_t>(&validAreaParams->first), + isPreEvent); + str.AppendASCII(tempStr); + } else { + RECT rect = params.second.as<RECT>(); + nsAutoCString tempStr = WmNcCalcSizeParamInfo( + FALSE, reinterpret_cast<uint64_t>(&rect), isPreEvent); + str.AppendASCII(tempStr); + } + break; + } + case WM_GETMINMAXINFO: { + const auto& params = data.as<MINMAXINFO>(); + MinMaxInfoParamInfo(str, reinterpret_cast<uint64_t>(¶ms), "", + isPreEvent); + break; + } + default: + MOZ_ASSERT(false, + "Unhandled message type in AppendFriendlyMessageSpecificData"); + str.AppendASCII("???"); + } +} + +nsCString MakeFriendlyMessage(UINT event, bool isPreEvent, long eventCounter, + const MessageSpecificData& data, + mozilla::Maybe<bool> result, LRESULT retValue) { + nsCString str; + const char* eventName = mozilla::widget::WinUtils::WinEventToEventName(event); + MOZ_ASSERT(eventName, "Unknown event name in MakeFriendlyMessage"); + eventName = eventName ? eventName : "(unknown)"; + str.AppendPrintf("%6ld %04x (%s) - ", eventCounter, event, eventName); + AppendFriendlyMessageSpecificData(str, event, isPreEvent, data); + const char* resultMsg = + result.isSome() ? (result.value() ? "true" : "false") : "initial call"; + str.AppendPrintf(" 0x%08llX (%s)", + result.isSome() ? static_cast<uint64_t>(retValue) : 0, + resultMsg); + return str; +} + +void WindowClosed(HWND hwnd) { gWindowMessages.erase(hwnd); } + +void LogWindowMessage(HWND hwnd, UINT event, bool isPreEvent, long eventCounter, + WPARAM wParam, LPARAM lParam, mozilla::Maybe<bool> result, + LRESULT retValue) { + auto& hwndMessages = gWindowMessages[hwnd]; + auto& hwndWindowMessages = hwndMessages[event]; + WindowMessageData messageData = { + eventCounter, isPreEvent, MakeMessageSpecificData(event, wParam, lParam), + result, retValue}; + uint32_t numberOfMessagesToKeep = + StaticPrefs::widget_windows_messages_to_log(); + if (hwndWindowMessages.mMessages.size() < numberOfMessagesToKeep) { + // haven't reached limit yet + hwndWindowMessages.mMessages.push_back(std::move(messageData)); + } else { + hwndWindowMessages.mMessages[hwndWindowMessages.mNextFreeIndex] = + std::move(messageData); + } + hwndWindowMessages.mNextFreeIndex = + (hwndWindowMessages.mNextFreeIndex + 1) % numberOfMessagesToKeep; +} + +void GetLatestWindowMessages(RefPtr<nsIWidget> windowWidget, + nsTArray<nsCString>& messages) { + HWND hwnd = GetHwndFromWidget(windowWidget); + const auto& rawMessages = gWindowMessages[hwnd]; + nsTArray<std::pair<WindowMessageDataSortKey, nsCString>> + sortKeyAndMessageArray; + sortKeyAndMessageArray.SetCapacity( + rawMessages.size() * StaticPrefs::widget_windows_messages_to_log()); + for (const auto& eventAndMessage : rawMessages) { + for (const auto& messageData : eventAndMessage.second.mMessages) { + nsCString message = MakeFriendlyMessage( + eventAndMessage.first, messageData.mIsPreEvent, + messageData.mEventCounter, messageData.mSpecificData, + messageData.mResult, messageData.mRetValue); + WindowMessageDataSortKey sortKey(messageData); + sortKeyAndMessageArray.AppendElement( + std::make_pair(sortKey, std::move(message))); + } + } + std::sort(sortKeyAndMessageArray.begin(), sortKeyAndMessageArray.end()); + messages.SetCapacity(sortKeyAndMessageArray.Length()); + for (const std::pair<WindowMessageDataSortKey, nsCString>& entry : + sortKeyAndMessageArray) { + messages.AppendElement(std::move(entry.second)); + } +} +} // namespace mozilla::widget diff --git a/widget/windows/nsWindowLoggedMessages.h b/widget/windows/nsWindowLoggedMessages.h new file mode 100644 index 0000000000..c0ea632bb0 --- /dev/null +++ b/widget/windows/nsWindowLoggedMessages.h @@ -0,0 +1,26 @@ +/* -*- 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 WindowLoggedMessages_h__ +#define WindowLoggedMessages_h__ + +#include "minwindef.h" +#include "wtypes.h" + +#include "nsIWidget.h" +#include "nsStringFwd.h" + +namespace mozilla::widget { + +void LogWindowMessage(HWND hwnd, UINT event, bool isPreEvent, long eventCounter, + WPARAM wParam, LPARAM lParam, mozilla::Maybe<bool> result, + LRESULT retValue); +void WindowClosed(HWND hwnd); +void GetLatestWindowMessages(RefPtr<nsIWidget> windowWidget, + nsTArray<nsCString>& messages); + +} // namespace mozilla::widget + +#endif /* WindowLoggedMessages */ diff --git a/widget/windows/nsWindowTaskbarConcealer.cpp b/widget/windows/nsWindowTaskbarConcealer.cpp new file mode 100644 index 0000000000..b2b6b13a1d --- /dev/null +++ b/widget/windows/nsWindowTaskbarConcealer.cpp @@ -0,0 +1,384 @@ +/* -*- 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 "nsWindowTaskbarConcealer.h" + +#include "nsIWinTaskbar.h" +#define NS_TASKBAR_CONTRACTID "@mozilla.org/windows-taskbar;1" + +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_widget.h" +#include "WinUtils.h" + +using namespace mozilla; + +/** + * TaskbarConcealerImpl + * + * Implement Windows-fullscreen marking. + * + * nsWindow::TaskbarConcealer implements logic determining _whether_ to tell + * Windows that a given window is fullscreen. TaskbarConcealerImpl performs the + * platform-specific work of actually communicating that fact to Windows. + * + * (This object is not persistent; it's constructed on the stack when needed.) + */ +struct TaskbarConcealerImpl { + void MarkAsHidingTaskbar(HWND aWnd, bool aMark); + + private: + nsCOMPtr<nsIWinTaskbar> mTaskbarInfo; +}; + +/** + * nsWindow::TaskbarConcealer + * + * Issue taskbar-hide requests to the OS as needed. + */ + +/* + Per MSDN [0], one should mark and unmark fullscreen windows via the + ITaskbarList2::MarkFullscreenWindow method. Unfortunately, Windows pays less + attention to this than one might prefer -- in particular, it typically fails + to show the taskbar when switching focus from a window marked as fullscreen to + one not thus marked. [1] + + Experimentation has (so far) suggested that its behavior is reasonable when + switching between multiple monitors, or between a set of windows which are all + from different processes [2]. This leaves us to handle the same-monitor, same- + process case. + + Rather than do anything subtle here, we take the blanket approach of simply + listening for every potentially-relevant state change, and then explicitly + marking or unmarking every potentially-visible toplevel window. + + ---- + + [0] Relevant link: + https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist2-markfullscreenwindow + + The "NonRudeHWND" property described therein doesn't help with anything + in this comment, unfortunately. (See its use in MarkAsHidingTaskbar for + more details.) + + [1] This is an oversimplification; Windows' actual behavior here is... + complicated. See bug 1732517 comment 6 for some examples. + + [2] A comment in Chromium asserts that this is actually different threads. For + us, of course, that makes no difference. + https://github.com/chromium/chromium/blob/2b822268bd3/ui/views/win/hwnd_message_handler.cc#L1342 +*/ + +/************************************************************** + * + * SECTION: TaskbarConcealer utilities + * + **************************************************************/ + +static mozilla::LazyLogModule sTaskbarConcealerLog("TaskbarConcealer"); + +// Map of all relevant Gecko windows, along with the monitor on which each +// window was last known to be located. +/* static */ +nsTHashMap<HWND, HMONITOR> nsWindow::TaskbarConcealer::sKnownWindows; + +// Returns Nothing if the window in question is irrelevant (for any reason), +// or Some(the window's current state) otherwise. +/* static */ +Maybe<nsWindow::TaskbarConcealer::WindowState> +nsWindow::TaskbarConcealer::GetWindowState(HWND aWnd) { + // Classical Win32 visibility conditions. + if (!::IsWindowVisible(aWnd)) { + return Nothing(); + } + if (::IsIconic(aWnd)) { + return Nothing(); + } + + // Non-nsWindow windows associated with this thread may include file dialogs + // and IME input popups. + nsWindow* pWin = widget::WinUtils::GetNSWindowPtr(aWnd); + if (!pWin) { + return Nothing(); + } + + // nsWindows of other window-classes include tooltips and drop-shadow-bearing + // menus. + if (pWin->mWindowType != WindowType::TopLevel) { + return Nothing(); + } + + // Cloaked windows are (presumably) on a different virtual desktop. + // https://devblogs.microsoft.com/oldnewthing/20200302-00/?p=103507 + if (pWin->mIsCloaked) { + return Nothing(); + } + + return Some( + WindowState{::MonitorFromWindow(aWnd, MONITOR_DEFAULTTONULL), + pWin->mFrameState->GetSizeMode() == nsSizeMode_Fullscreen}); +} + +/************************************************************** + * + * SECTION: TaskbarConcealer::UpdateAllState + * + **************************************************************/ + +// Update all Windows-fullscreen-marking state and internal caches to represent +// the current state of the system. +/* static */ +void nsWindow::TaskbarConcealer::UpdateAllState( + HWND destroyedHwnd /* = nullptr */ +) { + // sKnownWindows is otherwise-unprotected shared state + MOZ_ASSERT(NS_IsMainThread(), + "TaskbarConcealer can only be used from the main thread!"); + + if (MOZ_LOG_TEST(sTaskbarConcealerLog, LogLevel::Info)) { + static size_t sLogCounter = 0; + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, + ("Calling UpdateAllState() for the %zuth time", sLogCounter++)); + + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("Last known state:")); + if (sKnownWindows.IsEmpty()) { + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, + (" none (no windows known)")); + } else { + for (const auto& entry : sKnownWindows) { + MOZ_LOG( + sTaskbarConcealerLog, LogLevel::Info, + (" window %p was on monitor %p", entry.GetKey(), entry.GetData())); + } + } + } + + // Array of all our potentially-relevant HWNDs, in Z-order (topmost first), + // along with their associated relevant state. + struct Item { + HWND hwnd; + HMONITOR monitor; + bool isGkFullscreen; + }; + const nsTArray<Item> windows = [&] { + nsTArray<Item> windows; + + // USE OF UNDOCUMENTED BEHAVIOR: The EnumWindows family of functions + // enumerates windows in Z-order, topmost first. (This has been true since + // at least Windows 2000, and possibly since Windows 3.0.) + // + // It's necessarily unreliable if windows are reordered while being + // enumerated; but in that case we'll get a message informing us of that + // fact, and can redo our state-calculations then. + // + // There exists no documented interface to acquire this information (other + // than ::GetWindow(), which is racy). + mozilla::EnumerateThreadWindows([&](HWND hwnd) { + // Depending on details of window-destruction that probably shouldn't be + // relied on, this HWND may or may not still be in the window list. + // Pretend it's not. + if (hwnd == destroyedHwnd) { + return; + } + + const auto maybeState = GetWindowState(hwnd); + if (!maybeState) { + return; + } + const WindowState& state = *maybeState; + + windows.AppendElement(Item{.hwnd = hwnd, + .monitor = state.monitor, + .isGkFullscreen = state.isGkFullscreen}); + }); + + return windows; + }(); + + // Relevant monitors are exactly those with relevant windows. + const nsTHashSet<HMONITOR> relevantMonitors = [&]() { + nsTHashSet<HMONITOR> relevantMonitors; + for (const Item& item : windows) { + relevantMonitors.Insert(item.monitor); + } + return relevantMonitors; + }(); + + // Update the cached mapping from windows to monitors. (This is only used as + // an optimization in TaskbarConcealer::OnWindowPosChanged().) + sKnownWindows.Clear(); + for (const Item& item : windows) { + MOZ_LOG( + sTaskbarConcealerLog, LogLevel::Debug, + ("Found relevant window %p on monitor %p", item.hwnd, item.monitor)); + sKnownWindows.InsertOrUpdate(item.hwnd, item.monitor); + } + + // Auxiliary function. Does what it says on the tin. + const auto FindUppermostWindowOn = [&windows](HMONITOR aMonitor) -> HWND { + for (const Item& item : windows) { + if (item.monitor == aMonitor) { + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, + ("on monitor %p, uppermost relevant HWND is %p", aMonitor, + item.hwnd)); + return item.hwnd; + } + } + + // This should never happen, since we're drawing our monitor-set from the + // set of relevant windows. + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Warning, + ("on monitor %p, no relevant windows were found", aMonitor)); + return nullptr; + }; + + TaskbarConcealerImpl impl; + + // Mark all relevant windows as not hiding the taskbar, unless they're both + // Gecko-fullscreen and the uppermost relevant window on their monitor. + for (HMONITOR monitor : relevantMonitors) { + const HWND topmost = FindUppermostWindowOn(monitor); + + for (const Item& item : windows) { + if (item.monitor != monitor) continue; + impl.MarkAsHidingTaskbar(item.hwnd, + item.isGkFullscreen && item.hwnd == topmost); + } + } +} // nsWindow::TaskbarConcealer::UpdateAllState() + +// Mark this window as requesting to occlude the taskbar. (The caller is +// responsible for keeping any local state up-to-date.) +void TaskbarConcealerImpl::MarkAsHidingTaskbar(HWND aWnd, bool aMark) { + const char* const sMark = aMark ? "true" : "false"; + + if (!mTaskbarInfo) { + mTaskbarInfo = do_GetService(NS_TASKBAR_CONTRACTID); + + if (!mTaskbarInfo) { + MOZ_LOG( + sTaskbarConcealerLog, LogLevel::Warning, + ("could not acquire IWinTaskbar (aWnd %p, aMark %s)", aWnd, sMark)); + return; + } + } + + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, + ("Calling PrepareFullScreen(%p, %s)", aWnd, sMark)); + + const nsresult hr = mTaskbarInfo->PrepareFullScreen(aWnd, aMark); + + if (FAILED(hr)) { + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Error, + ("Call to PrepareFullScreen(%p, %s) failed with nsresult %x", aWnd, + sMark, uint32_t(hr))); + } +}; + +/************************************************************** + * + * SECTION: TaskbarConcealer event callbacks + * + **************************************************************/ + +void nsWindow::TaskbarConcealer::OnWindowDestroyed(HWND aWnd) { + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, + ("==> OnWindowDestroyed() for HWND %p", aWnd)); + + UpdateAllState(aWnd); +} + +void nsWindow::TaskbarConcealer::OnFocusAcquired(nsWindow* aWin) { + // Update state unconditionally. + // + // This is partially because focus-acquisition only updates the z-order, which + // we don't cache and therefore can't notice changes to -- but also because + // it's probably a good idea to give the user a natural way to refresh the + // current fullscreen-marking state if it's somehow gone bad. + + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, + ("==> OnFocusAcquired() for HWND %p on HMONITOR %p", aWin->mWnd, + ::MonitorFromWindow(aWin->mWnd, MONITOR_DEFAULTTONULL))); + + UpdateAllState(); +} + +void nsWindow::TaskbarConcealer::OnFullscreenChanged(nsWindow* aWin, + bool enteredFullscreen) { + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, + ("==> OnFullscreenChanged() for HWND %p on HMONITOR %p", aWin->mWnd, + ::MonitorFromWindow(aWin->mWnd, MONITOR_DEFAULTTONULL))); + + UpdateAllState(); +} + +void nsWindow::TaskbarConcealer::OnWindowPosChanged(nsWindow* aWin) { + // Optimization: don't bother updating the state if the window hasn't moved + // (including appearances and disappearances). + const HWND myHwnd = aWin->mWnd; + const HMONITOR oldMonitor = sKnownWindows.Get(myHwnd); // or nullptr + const HMONITOR newMonitor = GetWindowState(myHwnd) + .map([](auto state) { return state.monitor; }) + .valueOr(nullptr); + + if (oldMonitor == newMonitor) { + return; + } + + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, + ("==> OnWindowPosChanged() for HWND %p (HMONITOR %p -> %p)", myHwnd, + oldMonitor, newMonitor)); + + UpdateAllState(); +} + +void nsWindow::TaskbarConcealer::OnAsyncStateUpdateRequest(HWND hwnd) { + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, + ("==> OnAsyncStateUpdateRequest()")); + + // Work around a race condition in explorer.exe. + // + // When a window is unminimized (and on several other events), the taskbar + // receives a notification that it needs to recalculate the current + // is-a-fullscreen-window-active-here-state ("rudeness") of each monitor. + // Unfortunately, this notification is sent concurrently with the + // WM_WINDOWPOSCHANGING message that performs the unminimization. + // + // Until that message is resolved, the window's position is still "minimized". + // If the taskbar processes its notification faster than the window handles + // its WM_WINDOWPOSCHANGING message, then the window will appear to the + // taskbar to still be minimized, and won't be taken into account for + // computing rudeness. This usually presents as a just-unminimized Firefox + // fullscreen-window occasionally having the taskbar stuck above it. + // + // Unfortunately, it's a bit difficult to improve Firefox's speed-of-response + // to WM_WINDOWPOSCHANGING messages (we can, and do, execute JavaScript during + // these), and even if we could that wouldn't always fix it. We instead adopt + // a variant of a strategy by Etienne Duchamps, who has investigated and + // documented this issue extensively[0]: we simply send another signal to the + // shell to notify it to recalculate the current rudeness state of all + // monitors. + // + // [0] + // https://github.com/dechamps/RudeWindowFixer#a-race-condition-activating-a-minimized-window + // + static UINT const shellHookMsg = ::RegisterWindowMessageW(L"SHELLHOOK"); + if (shellHookMsg != 0) { + // Identifying the particular thread of the particular instance of the + // shell associated with our current desktop is probably possible, but + // also probably not worth the effort. Just broadcast the message + // globally. + DWORD info = BSM_APPLICATIONS; + ::BroadcastSystemMessage(BSF_POSTMESSAGE | BSF_IGNORECURRENTTASK, &info, + shellHookMsg, HSHELL_WINDOWACTIVATED, + (LPARAM)hwnd); + } +} + +void nsWindow::TaskbarConcealer::OnCloakChanged() { + MOZ_LOG(sTaskbarConcealerLog, LogLevel::Info, ("==> OnCloakChanged()")); + + UpdateAllState(); +} diff --git a/widget/windows/nsWindowTaskbarConcealer.h b/widget/windows/nsWindowTaskbarConcealer.h new file mode 100644 index 0000000000..84567fc857 --- /dev/null +++ b/widget/windows/nsWindowTaskbarConcealer.h @@ -0,0 +1,53 @@ +/* -*- 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_NSWINDOWTASKBARCONCEALER_H_ +#define WIDGET_WINDOWS_NSWINDOWTASKBARCONCEALER_H_ + +#include "nsWindow.h" +#include "mozilla/Maybe.h" + +/** + * nsWindow::TaskbarConcealer + * + * Fullscreen-state (and, thus, taskbar-occlusion) manager. + */ +class nsWindow::TaskbarConcealer { + public: + // To be called when a window acquires focus. (Note that no action need be + // taken when focus is lost.) + static void OnFocusAcquired(nsWindow* aWin); + + // To be called during or after a window's destruction. The corresponding + // nsWindow pointer is not needed, and will not be acquired or accessed. + static void OnWindowDestroyed(HWND aWnd); + + // To be called when the Gecko-fullscreen state of a window changes. + static void OnFullscreenChanged(nsWindow* aWin, bool enteredFullscreen); + + // To be called when the position of a window changes. (Performs its own + // batching; irrelevant movements will be cheap.) + static void OnWindowPosChanged(nsWindow* aWin); + + // To be called when the cloaking state of any window changes. (Expects that + // all windows' internal cloaking-state mirror variables are up-to-date.) + static void OnCloakChanged(); + + // To be called upon receipt of MOZ_WM_FULLSCREEN_STATE_UPDATE. + static void OnAsyncStateUpdateRequest(HWND); + + private: + static void UpdateAllState(HWND destroyedHwnd = nullptr); + + struct WindowState { + HMONITOR monitor; + bool isGkFullscreen; + }; + static mozilla::Maybe<WindowState> GetWindowState(HWND); + + static nsTHashMap<HWND, HMONITOR> sKnownWindows; +}; + +#endif // WIDGET_WINDOWS_NSWINDOWTASKBARCONCEALER_H_ diff --git a/widget/windows/nsdefs.h b/widget/windows/nsdefs.h new file mode 100644 index 0000000000..ebf7892fda --- /dev/null +++ b/widget/windows/nsdefs.h @@ -0,0 +1,58 @@ +/* -*- 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 NSDEFS_H +#define NSDEFS_H + +#include <windows.h> + +#ifdef _DEBUG +# define BREAK_TO_DEBUGGER DebugBreak() +#else +# define BREAK_TO_DEBUGGER +#endif + +#ifdef _DEBUG +# define VERIFY(exp) \ + if (!(exp)) { \ + GetLastError(); \ + BREAK_TO_DEBUGGER; \ + } +#else // !_DEBUG +# define VERIFY(exp) (exp) +#endif // !_DEBUG + +// inttypes.h-like macro for LONG_PTR/LPARAM formatting. +#ifdef HAVE_64BIT_BUILD +# define PRIdLPTR "lld" +# define PRIxLPTR "llx" +# define PRIXLPTR "llX" +#else +# define PRIdLPTR "ld" +# define PRIxLPTR "lx" +# define PRIXLPTR "lX" +#endif + +// Win32 logging modules: +// nsWindow, nsSound, and nsClipboard +// +// Logging can be changed at runtime without recompiling in the General +// property page of Visual Studio under the "Environment" property. +// +// Two variables are of importance to be set: +// MOZ_LOG and MOZ_LOG_FILE +// +// MOZ_LOG: +// MOZ_LOG=all:5 (To log everything completely) +// MOZ_LOG=nsWindow:5,nsSound:5,nsClipboard:5 +// (To log windows widget stuff) +// MOZ_LOG= (To turn off logging) +// +// MOZ_LOG_FILE: +// MOZ_LOG_FILE=C:\log.txt (To a file on disk) +// MOZ_LOG_FILE=WinDebug (To the debug window) +// MOZ_LOG_FILE= (To stdout/stderr) + +#endif // NSDEFS_H diff --git a/widget/windows/res/aliasb.cur b/widget/windows/res/aliasb.cur Binary files differnew file mode 100644 index 0000000000..8d9ac9478c --- /dev/null +++ b/widget/windows/res/aliasb.cur diff --git a/widget/windows/res/cell.cur b/widget/windows/res/cell.cur Binary files differnew file mode 100644 index 0000000000..decfbdcac5 --- /dev/null +++ b/widget/windows/res/cell.cur diff --git a/widget/windows/res/col_resize.cur b/widget/windows/res/col_resize.cur Binary files differnew file mode 100644 index 0000000000..8f7f675122 --- /dev/null +++ b/widget/windows/res/col_resize.cur diff --git a/widget/windows/res/copy.cur b/widget/windows/res/copy.cur Binary files differnew file mode 100644 index 0000000000..87f1519cd1 --- /dev/null +++ b/widget/windows/res/copy.cur diff --git a/widget/windows/res/grab.cur b/widget/windows/res/grab.cur Binary files differnew file mode 100644 index 0000000000..db7ad5aed3 --- /dev/null +++ b/widget/windows/res/grab.cur diff --git a/widget/windows/res/grabbing.cur b/widget/windows/res/grabbing.cur Binary files differnew file mode 100644 index 0000000000..e0dfd04e4d --- /dev/null +++ b/widget/windows/res/grabbing.cur diff --git a/widget/windows/res/none.cur b/widget/windows/res/none.cur Binary files differnew file mode 100644 index 0000000000..2114dfaee3 --- /dev/null +++ b/widget/windows/res/none.cur diff --git a/widget/windows/res/row_resize.cur b/widget/windows/res/row_resize.cur Binary files differnew file mode 100644 index 0000000000..a7369d32d1 --- /dev/null +++ b/widget/windows/res/row_resize.cur diff --git a/widget/windows/res/select.cur b/widget/windows/res/select.cur Binary files differnew file mode 100644 index 0000000000..5a88b3707e --- /dev/null +++ b/widget/windows/res/select.cur diff --git a/widget/windows/res/vertical_text.cur b/widget/windows/res/vertical_text.cur Binary files differnew file mode 100644 index 0000000000..3de04ebec3 --- /dev/null +++ b/widget/windows/res/vertical_text.cur diff --git a/widget/windows/res/zoom_in.cur b/widget/windows/res/zoom_in.cur Binary files differnew file mode 100644 index 0000000000..b594d79271 --- /dev/null +++ b/widget/windows/res/zoom_in.cur diff --git a/widget/windows/res/zoom_out.cur b/widget/windows/res/zoom_out.cur Binary files differnew file mode 100644 index 0000000000..7e495fbaa4 --- /dev/null +++ b/widget/windows/res/zoom_out.cur diff --git a/widget/windows/resource.h b/widget/windows/resource.h new file mode 100644 index 0000000000..a367e9f3e9 --- /dev/null +++ b/widget/windows/resource.h @@ -0,0 +1,16 @@ +/* -*- 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 IDC_GRAB 4101 +#define IDC_GRABBING 4102 +#define IDC_CELL 4103 +#define IDC_COPY 4104 +#define IDC_ALIAS 4105 +#define IDC_ZOOMIN 4106 +#define IDC_ZOOMOUT 4107 +#define IDC_COLRESIZE 4108 +#define IDC_ROWRESIZE 4109 +#define IDC_VERTICALTEXT 4110 +#define IDC_DUMMY_CE_MENUBAR 4111 +#define IDC_NONE 4112 diff --git a/widget/windows/tests/TestUriValidation.cpp b/widget/windows/tests/TestUriValidation.cpp new file mode 100644 index 0000000000..d8a0ca09ce --- /dev/null +++ b/widget/windows/tests/TestUriValidation.cpp @@ -0,0 +1,135 @@ +/* -*- 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/. */ + +#define MOZ_USE_LAUNCHER_ERROR + +#define UNICODE +#include "mozilla/UrlmonHeaderOnlyUtils.h" +#include "TestUrisToValidate.h" + +#include <urlmon.h> + +using namespace mozilla; + +static LauncherResult<_bstr_t> ShellValidateUri(const wchar_t* aUri) { + LauncherResult<UniqueAbsolutePidl> pidlResult = ShellParseDisplayName(aUri); + if (pidlResult.isErr()) { + return pidlResult.propagateErr(); + } + UniqueAbsolutePidl pidl = pidlResult.unwrap(); + + // |pidl| is an absolute path. IShellFolder::GetDisplayNameOf requires a + // valid child ID, so the first thing we need to resolve is the IShellFolder + // for |pidl|'s parent, as well as the childId that represents |pidl|. + // Fortunately SHBindToParent does exactly that! + PCUITEMID_CHILD childId = nullptr; + RefPtr<IShellFolder> parentFolder; + HRESULT hr = SHBindToParent(pidl.get(), IID_IShellFolder, + getter_AddRefs(parentFolder), &childId); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + // Now we retrieve the display name of |childId|, telling the shell that we + // plan to have the string parsed. + STRRET strret; + hr = parentFolder->GetDisplayNameOf(childId, SHGDN_FORPARSING, &strret); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + // StrRetToBSTR automatically takes care of freeing any dynamically + // allocated memory in |strret|. + _bstr_t bstrUri; + hr = StrRetToBSTR(&strret, nullptr, bstrUri.GetAddress()); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + return bstrUri; +} + +static LauncherResult<_bstr_t> GetFragment(const wchar_t* aUri) { + constexpr DWORD flags = + Uri_CREATE_NO_DECODE_EXTRA_INFO | Uri_CREATE_CANONICALIZE | + Uri_CREATE_CRACK_UNKNOWN_SCHEMES | Uri_CREATE_PRE_PROCESS_HTML_URI | + Uri_CREATE_IE_SETTINGS; + RefPtr<IUri> uri; + HRESULT hr = CreateUri(aUri, flags, 0, getter_AddRefs(uri)); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + + _bstr_t bstrFragment; + hr = uri->GetFragment(bstrFragment.GetAddress()); + if (FAILED(hr)) { + return LAUNCHER_ERROR_FROM_HRESULT(hr); + } + return bstrFragment; +} + +static bool RunSingleTest(const wchar_t* aUri) { + LauncherResult<_bstr_t> uriOld = ShellValidateUri(aUri), + uriNew = UrlmonValidateUri(aUri); + if (uriOld.isErr() != uriNew.isErr()) { + printf("TEST-FAILED | UriValidation | Validation result mismatch on %S\n", + aUri); + return false; + } + + if (uriOld.isErr()) { + if (uriOld.unwrapErr().mError != uriNew.unwrapErr().mError) { + printf("TEST-FAILED | UriValidation | Error code mismatch on %S\n", aUri); + return false; + } + return true; + } + + LauncherResult<_bstr_t> bstrFragment = GetFragment(aUri); + if (bstrFragment.isErr()) { + printf("TEST-FAILED | UriValidation | Failed to get a fragment from %S\n", + aUri); + return false; + } + + // We validate a uri with two logics: the current one UrlmonValidateUri and + // the older one ShellValidateUri, to make sure the same validation result. + // We introduced UrlmonValidateUri because ShellValidateUri drops a fragment + // in a uri due to the design of Windows. To bypass the fragment issue, we + // extract a fragment and appends it into the validated string, and compare. + _bstr_t bstrUriOldCorrected = uriOld.unwrap() + bstrFragment.unwrap(); + const _bstr_t& bstrUriNew = uriNew.unwrap(); + if (bstrUriOldCorrected != bstrUriNew) { + printf("TEST-FAILED | UriValidation | %S %S %S\n", aUri, + static_cast<const wchar_t*>(bstrUriOldCorrected), + static_cast<const wchar_t*>(bstrUriNew)); + return false; + } + + return true; +} + +int wmain(int argc, wchar_t* argv[]) { + HRESULT hr = CoInitialize(nullptr); + if (FAILED(hr)) { + return 1; + } + + bool isOk = true; + + if (argc == 2) { + isOk = RunSingleTest(argv[1]); + } else { + for (const wchar_t*& testUri : kTestUris) { + if (!RunSingleTest(testUri)) { + isOk = false; + } + } + } + + CoUninitialize(); + return isOk ? 0 : 1; +} diff --git a/widget/windows/tests/TestUrisToValidate.h b/widget/windows/tests/TestUrisToValidate.h new file mode 100644 index 0000000000..cb00366d1e --- /dev/null +++ b/widget/windows/tests/TestUrisToValidate.h @@ -0,0 +1,471 @@ +/* -*- 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_TestUrisToValidate_h
+#define mozilla_TestUrisToValidate_h
+
+const wchar_t* kTestUris[] = {
+ L"callto:%.txt",
+ L"callto:%00.txt",
+ L"callto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"fdaction:%.txt",
+ L"fdaction:%00.txt",
+ L"fdaction:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"feed:%.txt",
+ L"feed:%00.txt",
+ L"feed:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"feeds:%.txt",
+ L"feeds:%00.txt",
+ L"feeds:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"file:///%.txt",
+ L"file:///%00.txt",
+ L"file:///%41%2D%31%5Ftest%22ing?%41%31%00.txt",
+ L"firefox.url:%.txt",
+ L"firefox.url:%00.txt",
+ L"firefox.url:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"firefoxurl:%.txt",
+ L"firefoxurl:%00.txt",
+ L"firefoxurl:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ftp:%.txt",
+ L"ftp:%00.txt",
+ L"ftp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gopher:%.txt",
+ L"gopher:%00.txt",
+ L"gopher:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gtalk:%.txt",
+ L"gtalk:%00.txt",
+ L"gtalk:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"HTTP:%.txt",
+ L"HTTP:%00.txt",
+ L"HTTP:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"http:%.txt",
+ L"http:%00.txt",
+ L"http:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"https://bug389580.bmoattachments.org/%.txt",
+ L"https://bug389580.bmoattachments.org/%00.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"%41%2D%31%5Ftest%22ing?%41%31%00.txt",
+ L"ie.ftp:%.txt",
+ L"ie.ftp:%00.txt",
+ L"ie.ftp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ie.http:%.txt",
+ L"ie.http:%00.txt",
+ L"ie.http:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ie.https:%.txt",
+ L"ie.https:%00.txt",
+ L"ie.https:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"irc:%.txt",
+ L"irc:%00.txt",
+ L"irc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ircs:%.txt",
+ L"ircs:%00.txt",
+ L"ircs:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itms:%.txt",
+ L"itms:%00.txt",
+ L"itms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itmss:%.txt",
+ L"itmss:%00.txt",
+ L"itmss:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itpc:%.txt",
+ L"itpc:%00.txt",
+ L"itpc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itms:%.txt",
+ L"itunes.assocprotocol.itms:%00.txt",
+ L"itunes.assocprotocol.itms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itmss:%.txt",
+ L"itunes.assocprotocol.itmss:%00.txt",
+ L"itunes.assocprotocol.itmss:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.itpc:%.txt",
+ L"itunes.assocprotocol.itpc:%00.txt",
+ L"itunes.assocprotocol.itpc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ldap:%.txt",
+ L"ldap:%00.txt",
+ L"ldap:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mailto:%.txt",
+ L"mailto:%00.txt",
+ L"mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mms:%.txt",
+ L"mms:%00.txt",
+ L"mms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mmst:%.txt",
+ L"mmst:%00.txt",
+ L"mmst:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mmst:%.txt",
+ L"mmst:%00.txt",
+ L"mmst:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mmsu:%.txt",
+ L"mmsu:%00.txt",
+ L"mmsu:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mmsu:%.txt",
+ L"mmsu:%00.txt",
+ L"mmsu:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"Mozilla%20Thunderbird.Url.Mailto:%.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"Mozilla%20Thunderbird.Url.Mailto:%00.txt",
+ L"https://bug389580.bmoattachments.org/"
+ L"Mozilla%20Thunderbird.Url.Mailto:%41%2D%31%5Ftest%22ing?%41%31%00.txt",
+ L"navigatorurl:%.txt",
+ L"navigatorurl:%00.txt",
+ L"navigatorurl:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"news:%.txt",
+ L"news:%00.txt",
+ L"news:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"nntp:%.txt",
+ L"nntp:%00.txt",
+ L"nntp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"oms:%.txt",
+ L"oms:%00.txt",
+ L"oms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook:%.txt",
+ L"outlook:%00.txt",
+ L"outlook:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook.url.feed:%.txt",
+ L"outlook.url.feed:%00.txt",
+ L"outlook.url.feed:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook.url.mailto:%.txt",
+ L"outlook.url.mailto:%00.txt",
+ L"outlook.url.mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook.url.webcal:%.txt",
+ L"outlook.url.webcal:%00.txt",
+ L"outlook.url.webcal:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlookfeed:%.txt",
+ L"outlookfeed:%00.txt",
+ L"outlookfeed:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlookfeeds:%.txt",
+ L"outlookfeeds:%00.txt",
+ L"outlookfeeds:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"pnm:%.txt",
+ L"pnm:%00.txt",
+ L"pnm:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"prls.intappfile.ftp:%.txt",
+ L"prls.intappfile.ftp:%00.txt",
+ L"prls.intappfile.ftp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"prls.intappfile.http:%.txt",
+ L"prls.intappfile.http:%00.txt",
+ L"prls.intappfile.http:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"prls.intappfile.https:%.txt",
+ L"prls.intappfile.https:%00.txt",
+ L"prls.intappfile.https:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"prls.intappfile.mailto:%.txt",
+ L"prls.intappfile.mailto:%00.txt",
+ L"prls.intappfile.mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"rlogin:%.txt",
+ L"rlogin:%00.txt",
+ L"rlogin:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"rtsp:%.txt",
+ L"rtsp:%00.txt",
+ L"rtsp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"scp:%.txt",
+ L"scp:%00.txt",
+ L"scp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sftp:%.txt",
+ L"sftp:%00.txt",
+ L"sftp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sip:%.txt",
+ L"sip:%00.txt",
+ L"sip:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"skype:%.txt",
+ L"skype:%00.txt",
+ L"skype:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"snews:%.txt",
+ L"snews:%00.txt",
+ L"snews:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"telnet:%.txt",
+ L"telnet:%00.txt",
+ L"telnet:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"thunderbird.url.mailto:%.txt",
+ L"thunderbird.url.mailto:%00.txt",
+ L"thunderbird.url.mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"thunderbird.url.news:%.txt",
+ L"thunderbird.url.news:%00.txt",
+ L"thunderbird.url.news:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tn3270:%.txt",
+ L"tn3270:%00.txt",
+ L"tn3270:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tscrec4:%.txt",
+ L"tscrec4:%00.txt",
+ L"tscrec4:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"webcal:%.txt",
+ L"webcal:%00.txt",
+ L"webcal:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"webcal:%.txt",
+ L"webcal:%00.txt",
+ L"webcal:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"webcals:%.txt",
+ L"webcals:%00.txt",
+ L"webcals:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowscalendar.urlwebcal.1:%.txt",
+ L"windowscalendar.urlwebcal.1:%00.txt",
+ L"windowscalendar.urlwebcal.1:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowsmail.url.mailto:%.txt",
+ L"windowsmail.url.mailto:%00.txt",
+ L"windowsmail.url.mailto:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowsmail.url.news:%.txt",
+ L"windowsmail.url.news:%00.txt",
+ L"windowsmail.url.news:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowsmail.url.nntp:%.txt",
+ L"windowsmail.url.nntp:%00.txt",
+ L"windowsmail.url.nntp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"windowsmail.url.snews:%.txt",
+ L"windowsmail.url.snews:%00.txt",
+ L"windowsmail.url.snews:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"wmp11.assocprotocol.mms:%.txt",
+ L"wmp11.assocprotocol.mms:%00.txt",
+ L"wmp11.assocprotocol.mms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"wpc:%.txt",
+ L"wpc:%00.txt",
+ L"wpc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ymsgr:%.txt",
+ L"ymsgr:%00.txt",
+ L"ymsgr:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"acrobat:%.txt",
+ L"acrobat:%00.txt",
+ L"acrobat:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"acsui:%.txt",
+ L"acsui:%00.txt",
+ L"acsui:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"aim:%.txt",
+ L"aim:%00.txt",
+ L"aim:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"aim:%.txt",
+ L"aim:%00.txt",
+ L"aim:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"allc8.commands.2:%.txt",
+ L"allc8.commands.2:%00.txt",
+ L"allc8.commands.2:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"allholdem.commands.2:%.txt",
+ L"allholdem.commands.2:%00.txt",
+ L"allholdem.commands.2:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"allpoker.commands.2:%.txt",
+ L"allpoker.commands.2:%00.txt",
+ L"allpoker.commands.2:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"aolautofix:%.txt",
+ L"aolautofix:%00.txt",
+ L"aolautofix:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"aolds:%.txt",
+ L"aolds:%00.txt",
+ L"aolds:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"bc:%.txt",
+ L"bc:%00.txt",
+ L"bc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"bctp:%.txt",
+ L"bctp:%00.txt",
+ L"bctp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"bittorrent:%.txt",
+ L"bittorrent:%00.txt",
+ L"bittorrent:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"camfrog:%.txt",
+ L"camfrog:%00.txt",
+ L"camfrog:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"csi:%.txt",
+ L"csi:%00.txt",
+ L"csi:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"cvs:%.txt",
+ L"cvs:%00.txt",
+ L"cvs:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"daap:%.txt",
+ L"daap:%00.txt",
+ L"daap:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ed2k:%.txt",
+ L"ed2k:%00.txt",
+ L"ed2k:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"explorer.assocprotocol.search-ms:%.txt",
+ L"explorer.assocprotocol.search-ms:%00.txt",
+ L"explorer.assocprotocol.search-ms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gizmoproject:%.txt",
+ L"gizmoproject:%00.txt",
+ L"gizmoproject:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gnet:%.txt",
+ L"gnet:%00.txt",
+ L"gnet:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gnutella:%.txt",
+ L"gnutella:%00.txt",
+ L"gnutella:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"gsarcade:%.txt",
+ L"gsarcade:%00.txt",
+ L"gsarcade:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"hcp:%.txt",
+ L"hcp:%00.txt",
+ L"hcp:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"icquser:%.txt",
+ L"icquser:%00.txt",
+ L"icquser:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"icy:%.txt",
+ L"icy:%00.txt",
+ L"icy:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"imesync:%.txt",
+ L"imesync:%00.txt",
+ L"imesync:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.daap:%.txt",
+ L"itunes.assocprotocol.daap:%00.txt",
+ L"itunes.assocprotocol.daap:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"itunes.assocprotocol.pcast:%.txt",
+ L"itunes.assocprotocol.pcast:%00.txt",
+ L"itunes.assocprotocol.pcast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"joost:%.txt",
+ L"joost:%00.txt",
+ L"joost:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"m4macdrive:%.txt",
+ L"m4macdrive:%00.txt",
+ L"m4macdrive:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"magnet:%.txt",
+ L"magnet:%00.txt",
+ L"magnet:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mapi:%.txt",
+ L"mapi:%00.txt",
+ L"mapi:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mc12:%.txt",
+ L"mc12:%00.txt",
+ L"mc12:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mediajukebox:%.txt",
+ L"mediajukebox:%00.txt",
+ L"mediajukebox:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"morpheus:%.txt",
+ L"morpheus:%00.txt",
+ L"morpheus:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mp2p:%.txt",
+ L"mp2p:%00.txt",
+ L"mp2p:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"mpodcast:%.txt",
+ L"mpodcast:%00.txt",
+ L"mpodcast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"msbd:%.txt",
+ L"msbd:%00.txt",
+ L"msbd:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"msbd:%.txt",
+ L"msbd:%00.txt",
+ L"msbd:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"msdigitallocker:%.txt",
+ L"msdigitallocker:%00.txt",
+ L"msdigitallocker:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"outlook.url.stssync:%.txt",
+ L"outlook.url.stssync:%00.txt",
+ L"outlook.url.stssync:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"p2p:%.txt",
+ L"p2p:%00.txt",
+ L"p2p:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"pando:%.txt",
+ L"pando:%00.txt",
+ L"pando:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"pcast:%.txt",
+ L"pcast:%00.txt",
+ L"pcast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"picasa:%.txt",
+ L"picasa:%00.txt",
+ L"picasa:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"plaxo:%.txt",
+ L"plaxo:%00.txt",
+ L"plaxo:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"play:%.txt",
+ L"play:%00.txt",
+ L"play:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"podcast:%.txt",
+ L"podcast:%00.txt",
+ L"podcast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ppmate:%.txt",
+ L"ppmate:%00.txt",
+ L"ppmate:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ppmates:%.txt",
+ L"ppmates:%00.txt",
+ L"ppmates:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ppstream:%.txt",
+ L"ppstream:%00.txt",
+ L"ppstream:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"quicktime:%.txt",
+ L"quicktime:%00.txt",
+ L"quicktime:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"realplayer.autoplay.6:%.txt",
+ L"realplayer.autoplay.6:%00.txt",
+ L"realplayer.autoplay.6:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"realplayer.cdburn.6:%.txt",
+ L"realplayer.cdburn.6:%00.txt",
+ L"realplayer.cdburn.6:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"rhap:%.txt",
+ L"rhap:%00.txt",
+ L"rhap:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sc:%.txt",
+ L"sc:%00.txt",
+ L"sc:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"search-ms:%.txt",
+ L"search-ms:%00.txt",
+ L"search-ms:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"shareaza:%.txt",
+ L"shareaza:%00.txt",
+ L"shareaza:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"shell:%.txt",
+ L"shell:%00.txt",
+ L"shell:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"shout:%.txt",
+ L"shout:%00.txt",
+ L"shout:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sig2dat:%.txt",
+ L"sig2dat:%00.txt",
+ L"sig2dat:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"sop:%.txt",
+ L"sop:%00.txt",
+ L"sop:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"steam:%.txt",
+ L"steam:%00.txt",
+ L"steam:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"stssync:%.txt",
+ L"stssync:%00.txt",
+ L"stssync:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"svn:%.txt",
+ L"svn:%00.txt",
+ L"svn:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"svn+ssh:%.txt",
+ L"svn+ssh:%00.txt",
+ L"svn+ssh:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"synacast:%.txt",
+ L"synacast:%00.txt",
+ L"synacast:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"torrent:%.txt",
+ L"torrent:%00.txt",
+ L"torrent:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tsvn:%.txt",
+ L"tsvn:%00.txt",
+ L"tsvn:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tvants:%.txt",
+ L"tvants:%00.txt",
+ L"tvants:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"tvu:%.txt",
+ L"tvu:%00.txt",
+ L"tvu:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"unsv:%.txt",
+ L"unsv:%00.txt",
+ L"unsv:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"uvox:%.txt",
+ L"uvox:%00.txt",
+ L"uvox:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"ventrilo:%.txt",
+ L"ventrilo:%00.txt",
+ L"ventrilo:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"vs:%.txt",
+ L"vs:%00.txt",
+ L"vs:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"zune:%.txt",
+ L"zune:%00.txt",
+ L"zune:%41%2D%31%5Ftest"ing?%41%31%00.txt",
+ L"https://example.com/?a=123&b=456",
+ L"https://example.com/#123?a=123&b=456",
+ L"https://example.com/?#123a=123&b=456",
+ L"https://example.com/?a=123&b=456#123",
+ L"mailto:%41%42%23%31",
+ L"mailto:%41%42%23%31#fragment",
+ L"news:%41%42%23%31",
+ L"news:%41%42%23%31#fragment",
+ L"microsoft-edge:%41%42%23%31",
+ L"microsoft-edge:%41%42%23%31#fragment",
+ L"microsoft-edge:%41%42%23%31#fragment#",
+ L"microsoft-edge:%41%42%23%31####",
+ L"something-unknown:",
+ L"something-unknown:x=123",
+ L"something-unknown:?=123",
+ L"something-unknown:#code=0123456789%200123456789&x=01234567890123456789",
+};
+
+#endif // mozilla_TestUrisToValidate_h
diff --git a/widget/windows/tests/gtest/TestJumpListBuilder.cpp b/widget/windows/tests/gtest/TestJumpListBuilder.cpp new file mode 100644 index 0000000000..5494c42d37 --- /dev/null +++ b/widget/windows/tests/gtest/TestJumpListBuilder.cpp @@ -0,0 +1,823 @@ +/* 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 <objectarray.h> +#include <shobjidl.h> +#include <windows.h> +#include <string.h> +#include <propvarutil.h> +#include <propkey.h> + +#ifdef __MINGW32__ +// MinGW-w64 headers are missing PropVariantToString. +# include <propsys.h> +PSSTDAPI PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch); +#endif + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/WindowsJumpListShortcutDescriptionBinding.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "JumpListBuilder.h" + +using namespace mozilla; +using namespace testing; +using mozilla::dom::AutoJSAPI; +using mozilla::dom::Promise; +using mozilla::dom::PromiseNativeHandler; +using mozilla::dom::ToJSValue; +using mozilla::dom::WindowsJumpListShortcutDescription; +using mozilla::widget::JumpListBackend; +using mozilla::widget::JumpListBuilder; + +/** + * GMock matcher that ensures that two LPCWSTRs match. + */ +MATCHER_P(LPCWSTREq, value, "The equivalent of StrEq for LPCWSTRs") { + return (wcscmp(arg, value)) == 0; +} + +/** + * GMock matcher that ensures that a IObjectArray* contains nsIShellLinkW's + * that match an equivalent set of nsTArray<WindowsJumpListShortcutDescriptions> + */ +MATCHER_P(ShellLinksEq, descs, + "Comparing generated IShellLinkW with " + "WindowsJumpListShortcutDescription definitions") { + uint32_t count = 0; + HRESULT hr = arg->GetCount(&count); + if (FAILED(hr) || count != descs->Length()) { + return false; + } + + for (uint32_t i = 0; i < descs->Length(); ++i) { + RefPtr<IShellLinkW> link; + if (FAILED(arg->GetAt(i, IID_IShellLinkW, + static_cast<void**>(getter_AddRefs(link))))) { + return false; + } + + if (!link) { + return false; + } + + const WindowsJumpListShortcutDescription& desc = descs->ElementAt(i); + + // We'll now compare each member of the WindowsJumpListShortcutDescription + // with what is stored in the IShellLink. + + // WindowsJumpListShortcutDescription.title + IPropertyStore* propStore = nullptr; + hr = link->QueryInterface(IID_IPropertyStore, (LPVOID*)&propStore); + if (FAILED(hr)) { + return false; + } + + PROPVARIANT pv; + hr = propStore->GetValue(PKEY_Title, &pv); + if (FAILED(hr)) { + return false; + } + + wchar_t title[PKEYSTR_MAX]; + hr = PropVariantToString(pv, title, PKEYSTR_MAX); + if (FAILED(hr)) { + return false; + } + + if (!desc.mTitle.Equals(title)) { + return false; + } + + // WindowsJumpListShortcutDescription.path + wchar_t pathBuf[MAX_PATH]; + hr = link->GetPath(pathBuf, MAX_PATH, nullptr, SLGP_SHORTPATH); + if (FAILED(hr)) { + return false; + } + + if (!desc.mPath.Equals(pathBuf)) { + return false; + } + + // WindowsJumpListShortcutDescription.arguments (optional) + wchar_t argsBuf[MAX_PATH]; + hr = link->GetArguments(argsBuf, MAX_PATH); + if (FAILED(hr)) { + return false; + } + + if (desc.mArguments.WasPassed()) { + if (!desc.mArguments.Value().Equals(argsBuf)) { + return false; + } + } else { + // Otherwise, the arguments should be empty. + if (wcsnlen(argsBuf, MAX_PATH) != 0) { + return false; + } + } + + // WindowsJumpListShortcutDescription.description + wchar_t descBuf[MAX_PATH]; + hr = link->GetDescription(descBuf, MAX_PATH); + if (FAILED(hr)) { + return false; + } + + if (!desc.mDescription.Equals(descBuf)) { + return false; + } + + // WindowsJumpListShortcutDescription.iconPath and + // WindowsJumpListShortcutDescription.fallbackIconIndex + int iconIdx = 0; + wchar_t iconPathBuf[MAX_PATH]; + hr = link->GetIconLocation(iconPathBuf, MAX_PATH, &iconIdx); + if (FAILED(hr)) { + return false; + } + + if (desc.mIconPath.WasPassed() && !desc.mIconPath.Value().IsEmpty()) { + // If the WindowsJumpListShortcutDescription supplied an iconPath, + // then it should match iconPathBuf and have an icon index of 0. + if (!desc.mIconPath.Value().Equals(iconPathBuf) || iconIdx != 0) { + return false; + } + } else { + // Otherwise, the iconPathBuf should equal the + // WindowsJumpListShortcutDescription path, and the iconIdx should match + // the fallbackIconIndex. + if (!desc.mPath.Equals(iconPathBuf) || + desc.mFallbackIconIndex != iconIdx) { + return false; + } + } + } + + return true; +} + +/** + * This is a helper class that allows our tests to wait for a native DOM Promise + * to resolve, and get the JS::Value that the Promise resolves with. This is + * expected to run on the main thread. + */ +class WaitForResolver : public PromiseNativeHandler { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaitForResolver, override) + + NS_IMETHODIMP QueryInterface(REFNSIID aIID, void** aInstancePtr) override { + nsresult rv = NS_ERROR_UNEXPECTED; + NS_INTERFACE_TABLE0(WaitForResolver) + + return rv; + } + + WaitForResolver() : mIsDone(false) {} + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aError) override { + mResult = aValue; + mIsDone = true; + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aError) override { + ASSERT_TRUE(false); // Should never reach here. + } + + /** + * Spins a nested event loop and blocks until the Promise has resolved. + */ + void SpinUntilResolved() { + SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns, + [&]() { return mIsDone; }); + } + + /** + * Spins a nested event loop and blocks until the Promise has resolved, + * after which the JS::Value that the Promise resolves with is returned via + * the aRetval outparam. + * + * @param {JS::MutableHandle<JS::Value>} aRetval + * The outparam for the JS::Value that the Promise resolves with. + */ + void SpinUntilResolvedWithResult(JS::MutableHandle<JS::Value> aRetval) { + SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns, + [&]() { return mIsDone; }); + aRetval.set(mResult); + } + + private: + virtual ~WaitForResolver() = default; + + JS::Heap<JS::Value> mResult; + bool mIsDone; +}; + +/** + * An implementation of JumpListBackend that is instrumented using the GMock + * framework to record calls. Unlike the NativeJumpListBackend, this backend + * is expected to be instantiated on the main thread and passed as an argument + * to the JumpListBuilder's worker thread. Testers should wait for the methods + * that call these functions to resolve their Promises before checking the + * recorded values. + */ +class TestingJumpListBackend : public JumpListBackend { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JumpListBackend, override) + + TestingJumpListBackend() : mMonitor("TestingJumpListBackend::mMonitor") {} + + virtual bool IsAvailable() override { return true; } + + MOCK_METHOD(HRESULT, SetAppID, (LPCWSTR)); + MOCK_METHOD(HRESULT, BeginList, (UINT*, REFIID, void**)); + MOCK_METHOD(HRESULT, AddUserTasks, (IObjectArray*)); + MOCK_METHOD(HRESULT, AppendCategory, (LPCWSTR, IObjectArray*)); + MOCK_METHOD(HRESULT, CommitList, ()); + MOCK_METHOD(HRESULT, AbortList, ()); + MOCK_METHOD(HRESULT, DeleteList, (LPCWSTR)); + + virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) override { + return 0; + } + + // In one case (construction), an operation occurs off of the main thread that + // we must wait for without an associated Promise. + Monitor& GetMonitor() { return mMonitor; } + + protected: + virtual ~TestingJumpListBackend() override{}; + + private: + Monitor mMonitor; +}; + +/** + * A helper function that creates some fake WindowsJumpListShortcutDescription + * objects as well as JS::Value representations of those objects. These are + * returned to the caller through outparams. + * + * @param {JSContext*} aCx + * The current JSContext in the execution environment. + * @param {uint32_t} howMany + * The number of WindowsJumpListShortcutDescriptions to generate. + * @param {boolean} longDescription + * True if the description should be greater than MAX_PATH (260 characters). + * @param {nsTArray<WindowsJumpListShortcutDescription>&} aArray + * The outparam for the array of generated + * WindowsJumpListShortcutDescriptions. + * @param {nsTArray<JS::Value>&} aJSValArray + * The outparam for the array of JS::Value's representing the generated + * WindowsJumpListShortcutDescriptions. + */ +void GenerateWindowsJumpListShortcutDescriptions( + JSContext* aCx, uint32_t howMany, bool longDescription, + nsTArray<WindowsJumpListShortcutDescription>& aArray, + nsTArray<JS::Value>& aJSValArray) { + for (uint32_t i = 0; i < howMany; ++i) { + WindowsJumpListShortcutDescription desc; + nsAutoString title(u"Test Task #"); + title.AppendInt(i); + desc.mTitle = title; + + nsAutoString path(u"C:\\Some\\Test\\Path.exe"); + desc.mPath = path; + nsAutoString description; + + if (longDescription) { + description.AppendPrintf( + "For item #%i, this is a very very very very VERY VERY very very " + "very very very very very very very very very very VERY VERY very " + "very very very very very very very very very very very VERY VERY " + "very very very very very very very very very very very very VERY " + "VERY very very very very very very very very very very very very " + "VERY VERY very very very very very very very very very very very " + "very VERY VERY very very very very very very very very long test " + "description for an item", + i); + } else { + description.AppendPrintf("This is a test description for an item #%i", i); + } + + desc.mDescription = description; + desc.mFallbackIconIndex = 0; + + if (!(i % 2)) { + nsAutoString arguments(u"-arg1 -arg2 -arg3"); + desc.mArguments.Construct(arguments); + nsAutoString iconPath(u"C:\\Some\\icon.png"); + desc.mIconPath.Construct(iconPath); + } + + aArray.AppendElement(desc); + JS::Rooted<JS::Value> descJSValue(aCx); + ASSERT_TRUE(ToJSValue(aCx, desc, &descJSValue)); + aJSValArray.AppendElement(std::move(descJSValue)); + } +} + +/** + * Tests construction and that the application ID is properly passed to the + * backend. + */ +TEST(JumpListBuilder, Construction) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + ASSERT_TRUE(testBackend); + + nsAutoString aumid(u"TestApplicationID"); + LPCWSTR passedID = aumid.get(); + // Construction of our class (or any class of that matter) does not return a + // Promise that we can wait on to ensure that the background thread got the + // right information. We therefore use a monitor on the testing backend as + // well as an EXPECT_CALL to block execution of the test until the background + // work has completed. + Monitor& mon = testBackend->GetMonitor(); + MonitorAutoLock lock(mon); + EXPECT_CALL(*testBackend, SetAppID(LPCWSTREq(passedID))).WillOnce([&mon] { + MonitorAutoLock lock(mon); + mon.Notify(); + return S_OK; + }); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + // This is the amount of time that we will wait for the background thread to + // respond before considering it a timeout failure. + const int kWaitTimeoutMs = 5000; + + ASSERT_TRUE(mon.Wait(TimeDuration::FromMilliseconds(kWaitTimeoutMs)) != + CVStatus::Timeout); +} + +/** + * Tests calling CheckForRemovals and receiving a series of removed jump list + * entries. Calling CheckForRemovals should call the following methods on the + * backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - AbortList + */ +TEST(JumpListBuilder, CheckForRemovals) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + EXPECT_CALL(*testBackend, AbortList()).Times(2); + + // Let's prepare BeginList to return a two entry collection of IShellLinks. + // The first IShellLink will have the URL be https://example.com, the second + // will have the URL be https://mozilla.org. + EXPECT_CALL(*testBackend, BeginList) + .WillOnce([](UINT* pcMinSlots, REFIID riid, void** ppv) { + RefPtr<IObjectCollection> collection; + DebugOnly<HRESULT> hr = CoCreateInstance( + CLSID_EnumerableObjectCollection, nullptr, CLSCTX_INPROC_SERVER, + IID_IObjectCollection, getter_AddRefs(collection)); + MOZ_ASSERT(SUCCEEDED(hr)); + + RefPtr<IShellLinkW> link; + hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLinkW, getter_AddRefs(link)); + MOZ_ASSERT(SUCCEEDED(hr)); + + nsAutoString firstLinkHref(u"https://example.com"_ns); + link->SetArguments(firstLinkHref.get()); + + nsAutoString appPath(u"C:\\Tmp\\firefox.exe"_ns); + link->SetIconLocation(appPath.get(), 0); + + collection->AddObject(link); + + // Let's re-use the same IShellLink, but change the URL to add our + // second entry. The values of the IShellLink are ultimately copied + // over to the items being added to the collection. + nsAutoString secondLinkHref(u"https://mozilla.org"_ns); + link->SetArguments(secondLinkHref.get()); + collection->AddObject(link); + + RefPtr<IObjectArray> pArray; + hr = collection->QueryInterface(IID_IObjectArray, + getter_AddRefs(pArray)); + MOZ_ASSERT(SUCCEEDED(hr)); + + *ppv = static_cast<IObjectArray*>(pArray); + (static_cast<IUnknown*>(*ppv))->AddRef(); + + // This is the default value to return, according to + // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist + *pcMinSlots = 10; + + return S_OK; + }); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + nsresult rv = builder->CheckForRemovals(cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolvedWithResult(&result); + + ASSERT_TRUE(result.isObject()); + JS::Rooted<JSObject*> obj(cx, result.toObjectOrNull()); + + bool isArray; + ASSERT_TRUE(JS::IsArrayObject(cx, obj, &isArray)); + ASSERT_TRUE(isArray); + + // We should expect to see 2 URL strings returned in the array. + uint32_t length = 0; + ASSERT_TRUE(JS::GetArrayLength(cx, obj, &length)); + ASSERT_EQ(length, 2U); + + // The first one should be https://example.com + JS::Rooted<JS::Value> firstURLValue(cx); + ASSERT_TRUE(JS_GetElement(cx, obj, 0, &firstURLValue)); + JS::Rooted<JSString*> firstURLJSString(cx, firstURLValue.toString()); + nsAutoJSString firstURLAutoString; + ASSERT_TRUE(firstURLAutoString.init(cx, firstURLJSString)); + + ASSERT_TRUE(firstURLAutoString.EqualsLiteral("https://example.com")); + + // The second one should be https://mozilla.org + JS::Rooted<JS::Value> secondURLValue(cx); + ASSERT_TRUE(JS_GetElement(cx, obj, 1, &secondURLValue)); + JS::Rooted<JSString*> secondURLJSString(cx, secondURLValue.toString()); + nsAutoJSString secondURLAutoString; + ASSERT_TRUE(secondURLAutoString.init(cx, secondURLJSString)); + + ASSERT_TRUE(secondURLAutoString.EqualsLiteral("https://mozilla.org")); +} + +/** + * Tests calling PopulateJumpList with empty arguments, which should call the + * following methods on the backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - CommitList + * + * This should result in an empty jump list for the user. + */ +TEST(JumpListBuilder, PopulateJumpListEmpty) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<JS::Value> taskDescJSVals; + nsAutoString customTitle(u""); + nsTArray<JS::Value> customDescJSVals; + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Tests calling PopulateJumpList with only tasks, and no custom items. + * This should call the following methods on the backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - AddUserTasks + * - CommitList + * + * This should result in a jump list with just tasks shown to the user, and + * no custom section. + */ +TEST(JumpListBuilder, PopulateJumpListOnlyTasks) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<JS::Value> taskDescJSVals; + nsTArray<WindowsJumpListShortcutDescription> taskDescs; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs, + taskDescJSVals); + + nsAutoString customTitle(u""); + nsTArray<JS::Value> customDescJSVals; + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1); + + EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Tests calling PopulateJumpList with only custom items, and no tasks. + * This should call the following methods on the backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - AppendCategory + * - CommitList + * + * This should result in a jump list with just custom items shown to the user, + * and no tasks. + */ +TEST(JumpListBuilder, PopulateJumpListOnlyCustomItems) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<WindowsJumpListShortcutDescription> descs; + nsTArray<JS::Value> customDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, descs, + customDescJSVals); + + nsAutoString customTitle(u"My custom title"); + nsTArray<JS::Value> taskDescJSVals; + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0); + + EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()), + ShellLinksEq(&descs))) + .Times(1); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Tests calling PopulateJumpList with tasks and custom items. + * This should call the following methods on the backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - AddUserTasks + * - AppendCategory + * - CommitList + * + * This should result in a jump list with both built-in tasks as well as + * custom tasks with a custom label. + */ +TEST(JumpListBuilder, PopulateJumpList) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<WindowsJumpListShortcutDescription> taskDescs; + nsTArray<JS::Value> taskDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs, + taskDescJSVals); + + nsTArray<WindowsJumpListShortcutDescription> customDescs; + nsTArray<JS::Value> customDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, customDescs, + customDescJSVals); + + nsAutoString customTitle(u"My custom title"); + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1); + + EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()), + ShellLinksEq(&customDescs))) + .Times(1); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Tests calling ClearJumpList calls the following: + * + * - SetAppID + * - DeleteList (passing the aumid) + * + * This results in an empty jump list for the user. + */ +TEST(JumpListBuilder, ClearJumpList) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + EXPECT_CALL(*testBackend, AbortList()).Times(0); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(0); + EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0); + + EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0); + EXPECT_CALL(*testBackend, CommitList()).Times(0); + EXPECT_CALL(*testBackend, DeleteList(LPCWSTREq(aumid.get()))).Times(1); + + nsresult rv = builder->ClearJumpList(cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Test that a WindowsJumpListShortcutDescription with a description + * longer than MAX_PATH gets truncated to MAX_PATH. This is because a + * description longer than MAX_PATH will cause CommitList to fail. + */ +TEST(JumpListBuilder, TruncateDescription) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<WindowsJumpListShortcutDescription> taskDescs; + nsTArray<JS::Value> taskDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, taskDescs, + taskDescJSVals); + + nsTArray<WindowsJumpListShortcutDescription> customDescs; + nsTArray<JS::Value> customDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, customDescs, + customDescJSVals); + // We expect the long descriptions to be truncated to 260 characters, so + // we'll truncate the descriptions here ourselves. + for (auto& taskDesc : taskDescs) { + taskDesc.mDescription.SetLength(MAX_PATH - 1); + } + for (auto& customDesc : customDescs) { + customDesc.mDescription.SetLength(MAX_PATH - 1); + } + + nsAutoString customTitle(u"My custom title"); + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1); + + EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()), + ShellLinksEq(&customDescs))) + .Times(1); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} diff --git a/widget/windows/tests/gtest/TestWinDND.cpp b/widget/windows/tests/gtest/TestWinDND.cpp new file mode 100644 index 0000000000..fb7849fd79 --- /dev/null +++ b/widget/windows/tests/gtest/TestWinDND.cpp @@ -0,0 +1,728 @@ +/* -*- 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 <windows.h> +#include <ole2.h> +#include <shlobj.h> + +#include "nsArray.h" +#include "nsArrayUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsISupportsPrimitives.h" +#include "nsITransferable.h" + +#include "nsClipboard.h" +#include "nsDataObjCollection.h" + +#include "gtest/gtest.h" + +// shims for conversion from cppunittest to gtest +template <size_t N> +void fail(const char (&msg)[N]) { + ADD_FAILURE() << "TEST-UNEXPECTED-FAIL | " << msg; +} +template <size_t N> +void passed(const char (&msg)[N]) { + GTEST_SUCCEED() << "TEST-PASS | " << msg; +} + +nsIFile* xferFile; + +nsresult CheckValidHDROP(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + DROPFILES* pDropFiles; + pDropFiles = (DROPFILES*)GlobalLock(hGlobal); + if (!pDropFiles) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + if (pDropFiles->pFiles != sizeof(DROPFILES)) { + fail("DROPFILES struct has wrong size"); + } + + if (!pDropFiles->fWide) { + fail("Received data is not Unicode"); + return NS_ERROR_UNEXPECTED; + } + nsString s; + unsigned long offset = 0; + while (true) { + s = (char16_t*)((char*)pDropFiles + pDropFiles->pFiles + offset); + if (s.IsEmpty()) break; + nsresult rv; + nsCOMPtr<nsIFile> localFile( + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + rv = localFile->InitWithPath(s); + if (NS_FAILED(rv)) { + fail("File could not be opened"); + return NS_ERROR_UNEXPECTED; + } + offset += sizeof(char16_t) * (s.Length() + 1); + } + return NS_OK; +} + +nsresult CheckValidTEXT(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + char* pText; + pText = (char*)GlobalLock(hGlobal); + if (!pText) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + nsCString string; + string = pText; + + if (!string.EqualsLiteral("Mozilla can drag and drop")) { + fail("Text passed through drop object wrong"); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult CheckValidTEXTTwo(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + char* pText; + pText = (char*)GlobalLock(hGlobal); + if (!pText) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + nsCString string; + string = pText; + + if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) { + fail("Text passed through drop object wrong"); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult CheckValidUNICODE(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + char16_t* pText; + pText = (char16_t*)GlobalLock(hGlobal); + if (!pText) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + nsString string; + string = pText; + + if (!string.EqualsLiteral("Mozilla can drag and drop")) { + fail("Text passed through drop object wrong"); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult CheckValidUNICODETwo(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + char16_t* pText; + pText = (char16_t*)GlobalLock(hGlobal); + if (!pText) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + nsString string; + string = pText; + + if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) { + fail("Text passed through drop object wrong"); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult GetTransferableFile(nsCOMPtr<nsITransferable>& pTransferable) { + nsresult rv; + + nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferFile); + + pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + pTransferable->Init(nullptr); + rv = pTransferable->SetTransferData("application/x-moz-file", genericWrapper); + return rv; +} + +nsresult GetTransferableText(nsCOMPtr<nsITransferable>& pTransferable) { + nsresult rv; + constexpr auto mozString = u"Mozilla can drag and drop"_ns; + nsCOMPtr<nsISupportsString> xferString = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + rv = xferString->SetData(mozString); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString); + + pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + pTransferable->Init(nullptr); + rv = pTransferable->SetTransferData("text/plain", genericWrapper); + return rv; +} + +nsresult GetTransferableTextTwo(nsCOMPtr<nsITransferable>& pTransferable) { + nsresult rv; + constexpr auto mozString = u" twice over"_ns; + nsCOMPtr<nsISupportsString> xferString = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + rv = xferString->SetData(mozString); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString); + + pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + pTransferable->Init(nullptr); + rv = pTransferable->SetTransferData("text/plain", genericWrapper); + return rv; +} + +nsresult GetTransferableURI(nsCOMPtr<nsITransferable>& pTransferable) { + nsresult rv; + + nsCOMPtr<nsIURI> xferURI; + + rv = NS_NewURI(getter_AddRefs(xferURI), "http://www.mozilla.org"); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferURI); + + pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + pTransferable->Init(nullptr); + rv = pTransferable->SetTransferData("text/x-moz-url", genericWrapper); + return rv; +} + +nsresult MakeDataObject(nsIArray* transferableArray, + RefPtr<IDataObject>& itemToDrag) { + nsresult rv; + uint32_t itemCount = 0; + + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), "http://www.mozilla.org"); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transferableArray->GetLength(&itemCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Copied more or less exactly from nsDragService::InvokeDragSession + // This is what lets us play fake Drag Service for the test + if (itemCount > 1) { + nsDataObjCollection* dataObjCollection = new nsDataObjCollection(); + if (!dataObjCollection) return NS_ERROR_OUT_OF_MEMORY; + itemToDrag = dataObjCollection; + for (uint32_t i = 0; i < itemCount; ++i) { + nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, i); + if (trans) { + RefPtr<IDataObject> dataObj; + rv = nsClipboard::CreateNativeDataObject(trans, getter_AddRefs(dataObj), + uri); + NS_ENSURE_SUCCESS(rv, rv); + // Add the flavors to the collection object too + rv = nsClipboard::SetupNativeDataObject(trans, dataObjCollection); + NS_ENSURE_SUCCESS(rv, rv); + + dataObjCollection->AddDataObject(dataObj); + } + } + } // if dragging multiple items + else { + nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, 0); + if (trans) { + rv = nsClipboard::CreateNativeDataObject(trans, + getter_AddRefs(itemToDrag), uri); + NS_ENSURE_SUCCESS(rv, rv); + } + } // else dragging a single object + return rv; +} + +nsresult Do_CheckOneFile() { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableFile(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + FORMATETC fe; + SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("File data object does not support the file data type!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg; + stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("File data object did not provide data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidHDROP(stg); + if (NS_FAILED(rv)) { + fail("HDROP was invalid"); + return rv; + } + + ReleaseStgMedium(stg); + + return NS_OK; +} + +nsresult Do_CheckTwoFiles() { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableFile(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = GetTransferableFile(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + FORMATETC fe; + SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("File data object does not support the file data type!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg; + stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("File data object did not provide data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidHDROP(stg); + if (NS_FAILED(rv)) { + fail("HDROP was invalid"); + return rv; + } + + ReleaseStgMedium(stg); + + return NS_OK; +} + +nsresult Do_CheckOneString() { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableText(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + FORMATETC fe; + SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("String data object does not support the ASCII text data type!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg; + stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("String data object did not provide ASCII data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidTEXT(stg); + if (NS_FAILED(rv)) { + fail("TEXT was invalid"); + return rv; + } + + ReleaseStgMedium(stg); + + SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("String data object does not support the wide text data type!"); + return NS_ERROR_UNEXPECTED; + } + + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("String data object did not provide wide data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidUNICODE(stg); + if (NS_FAILED(rv)) { + fail("UNICODE was invalid"); + return rv; + } + + return NS_OK; +} + +nsresult Do_CheckTwoStrings() { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableText(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = GetTransferableTextTwo(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + FORMATETC fe; + SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("String data object does not support the ASCII text data type!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg; + stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("String data object did not provide ASCII data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidTEXTTwo(stg); + if (NS_FAILED(rv)) { + fail("TEXT was invalid"); + return rv; + } + + ReleaseStgMedium(stg); + + SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("String data object does not support the wide text data type!"); + return NS_ERROR_UNEXPECTED; + } + + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("String data object did not provide wide data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidUNICODETwo(stg); + if (NS_FAILED(rv)) { + fail("UNICODE was invalid"); + return rv; + } + + return NS_OK; +} + +nsresult Do_CheckSetArbitraryData(bool aMultiple) { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableText(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + if (aMultiple) { + rv = GetTransferableText(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + static CLIPFORMAT mozArbitraryFormat = + ::RegisterClipboardFormatW(L"MozillaTestFormat"); + FORMATETC fe; + STGMEDIUM stg; + SET_FORMATETC(fe, mozArbitraryFormat, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + + HGLOBAL hg = GlobalAlloc(GPTR, 1024); + stg.tymed = TYMED_HGLOBAL; + stg.hGlobal = hg; + stg.pUnkForRelease = nullptr; + + if (dataObj->SetData(&fe, &stg, true) != S_OK) { + if (aMultiple) { + fail("Unable to set arbitrary data type on data object collection!"); + } else { + fail("Unable to set arbitrary data type on data object!"); + } + return NS_ERROR_UNEXPECTED; + } + + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("Arbitrary data set on data object is not advertised!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg2; + stg2 = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg2) != S_OK) { + fail("Data object did not provide arbitrary data upon request!"); + return NS_ERROR_UNEXPECTED; + } + + if (stg2->hGlobal != hg) { + fail("Arbitrary data was not returned properly!"); + return rv; + } + ReleaseStgMedium(stg2); + + return NS_OK; +} + +// This function performs basic drop tests, testing a data object consisting +// of one transferable +nsresult Do_Test1() { + nsresult rv = NS_OK; + nsresult workingrv; + + workingrv = Do_CheckOneFile(); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on a single file"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully created a working file drag object!"); + } + + workingrv = Do_CheckOneString(); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on a single string"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully created a working string drag object!"); + } + + workingrv = Do_CheckSetArbitraryData(false); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on setting arbitrary data"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully set arbitrary data on a drag object"); + } + + return rv; +} + +// This function performs basic drop tests, testing a data object consisting of +// two transferables. +nsresult Do_Test2() { + nsresult rv = NS_OK; + nsresult workingrv; + + workingrv = Do_CheckTwoFiles(); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on multiple files"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully created a working multiple file drag object!"); + } + + workingrv = Do_CheckTwoStrings(); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on multiple strings"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully created a working multiple string drag object!"); + } + + workingrv = Do_CheckSetArbitraryData(true); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on setting arbitrary data"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully set arbitrary data on a drag object"); + } + + return rv; +} + +// This function performs advanced drag and drop tests, testing a data object +// consisting of multiple transferables that have different data types +nsresult Do_Test3() { + nsresult rv = NS_OK; + // nsresult workingrv; + + // XXX TODO Write more advanced tests in Bug 535860 + return rv; +} + +nsCOMPtr<nsIFile> GetTemporaryDirectory() { + nsCOMPtr<nsIFile> tmpdir; + +#define ENSURE(expr) NS_ENSURE_SUCCESS(expr, nullptr); + + ENSURE(NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpdir))); + MOZ_ASSERT(tmpdir); + + ENSURE(tmpdir->AppendNative("TestWinDND"_ns)); + ENSURE(tmpdir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0777)); + +#undef ENSURE + + return tmpdir; +} + +TEST(TestWinDND, All) +{ + nsCOMPtr<nsIFile> file = GetTemporaryDirectory(); + if (!file) { + fail("could not create temporary directory!"); + return; + } + xferFile = file; + + if (NS_SUCCEEDED(Do_Test1())) { + passed( + "Basic Drag and Drop data type tests (single transferable) succeeded!"); + } + + if (NS_SUCCEEDED(Do_Test2())) { + passed( + "Basic Drag and Drop data type tests (multiple transferables) " + "succeeded!"); + } + + // if (NS_SUCCEEDED(Do_Test3())) + // passed("Advanced Drag and Drop data type tests succeeded!"); +} diff --git a/widget/windows/tests/gtest/moz.build b/widget/windows/tests/gtest/moz.build new file mode 100644 index 0000000000..4057b7ae57 --- /dev/null +++ b/widget/windows/tests/gtest/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "TestJumpListBuilder.cpp", + "TestWinDND.cpp", +] + +LOCAL_INCLUDES += [ + "/widget", + "/widget/windows", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/widget/windows/tests/moz.build b/widget/windows/tests/moz.build new file mode 100644 index 0000000000..2c7d200571 --- /dev/null +++ b/widget/windows/tests/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +GeckoCppUnitTests( + [ + "TestUriValidation", + ], + linkage=None, +) + +DIRS = ["gtest"] + +LOCAL_INCLUDES += [] + +OS_LIBS += [ + "oleaut32", + "ole32", + "shell32", + "shlwapi", + "urlmon", + "uuid", +] + +if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CC_TYPE"] in ("gcc", "clang"): + # This allows us to use wmain as the entry point on mingw + LDFLAGS += [ + "-municode", + ] + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"] diff --git a/widget/windows/tests/unit/test_windows_alert_service.js b/widget/windows/tests/unit/test_windows_alert_service.js new file mode 100644 index 0000000000..0ba0d2a4d4 --- /dev/null +++ b/widget/windows/tests/unit/test_windows_alert_service.js @@ -0,0 +1,667 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test that Windows alert notifications generate expected XML. + */ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +let gProfD = do_get_profile(); + +// Setup that allows to use the profile service in xpcshell tests, +// lifted from `toolkit/profile/xpcshell/head.js`. +function setupProfileService() { + let gDataHome = gProfD.clone(); + gDataHome.append("data"); + gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + let gDataHomeLocal = gProfD.clone(); + gDataHomeLocal.append("local"); + gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + + let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService( + Ci.nsIXREDirProvider + ); + xreDirProvider.setUserDataDirectory(gDataHome, false); + xreDirProvider.setUserDataDirectory(gDataHomeLocal, true); +} + +add_setup(setupProfileService); + +function makeAlert(options) { + var alert = Cc["@mozilla.org/alert-notification;1"].createInstance( + Ci.nsIAlertNotification + ); + alert.init( + options.name, + options.imageURL, + options.title, + options.text, + options.textClickable, + options.cookie, + options.dir, + options.lang, + options.data, + options.principal, + options.inPrivateBrowsing, + options.requireInteraction, + options.silent, + options.vibrate || [] + ); + if (options.actions) { + alert.actions = options.actions; + } + if (options.opaqueRelaunchData) { + alert.opaqueRelaunchData = options.opaqueRelaunchData; + } + return alert; +} + +/** + * Take a `key1\nvalue1\n...` string encoding as used by the Windows native + * notification server DLL, and split it into an object, keeping `action\n...` + * intact. + * + * @param {string} t string encoding. + * @returns {object} an object with keys and values. + */ +function parseOneEncoded(t) { + var launch = {}; + + var lines = t.split("\n"); + while (lines.length) { + var key = lines.shift(); + var value; + if (key === "action") { + value = lines.join("\n"); + lines = []; + } else { + value = lines.shift(); + } + launch[key] = value; + } + + return launch; +} + +/** + * This complicated-looking function takes a (XML) string representation of a + * Windows alert (toast notification), parses it into XML, extracts and further + * parses internal data, and returns a simplified XML representation together + * with the parsed internals. + * + * Doing this lets us compare JSON objects rather than stringified-JSON further + * encoded as XML strings, which have lots of slashes and `"` characters to + * contend with. + * + * @param {string} s XML string for Windows alert. + + * @returns {Array} a pair of a simplified XML string and an object with + * `launch` and `actions` keys. + */ +function parseLaunchAndActions(s) { + var document = new DOMParser().parseFromString(s, "text/xml"); + var root = document.documentElement; + + var launchString = root.getAttribute("launch"); + root.setAttribute("launch", "launch"); + var launch = parseOneEncoded(launchString); + + // `actions` is keyed by "content" attribute. + let actions = {}; + for (var actionElement of root.querySelectorAll("action")) { + // `activationType="system"` is special. Leave them alone. + let systemActivationType = + actionElement.getAttribute("activationType") === "system"; + + let action = {}; + let names = [...actionElement.attributes].map(attribute => attribute.name); + + for (var name of names) { + let value = actionElement.getAttribute(name); + + // Here is where we parse stringified-JSON to simplify comparisons. + if (value.startsWith("{")) { + value = JSON.parse(value); + if ("opaqueRelaunchData" in value) { + value.opaqueRelaunchData = JSON.parse(value.opaqueRelaunchData); + } + } + + if (name == "arguments" && !systemActivationType) { + action[name] = parseOneEncoded(value); + } else { + action[name] = value; + } + + if (name != "content" && !systemActivationType) { + actionElement.removeAttribute(name); + } + } + + let actionName = actionElement.getAttribute("content"); + actions[actionName] = action; + } + + return [new XMLSerializer().serializeToString(document), { launch, actions }]; +} + +function escape(s) { + return s + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/\n/g, "
"); +} + +function unescape(s) { + return s + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/
/g, "\n"); +} + +function testAlert(when, { serverEnabled, profD, isBackgroundTaskMode } = {}) { + let argumentString = action => { + // 
 is "\n". + let s = ``; + if (serverEnabled) { + s += `program
${AppConstants.MOZ_APP_NAME}`; + } else { + s += `invalid key
invalid value`; + } + if (serverEnabled && profD) { + s += `
profile
${profD.path}`; + } + if (serverEnabled) { + s += "
windowsTag
"; + } + if (action) { + s += `
action
${escape(JSON.stringify(action))}`; + } + + return s; + }; + + let parsedArgumentString = action => + parseOneEncoded(unescape(argumentString(action))); + + let settingsAction = isBackgroundTaskMode + ? "" + : `<action content="Notification settings"/>`; + + let parsedSettingsAction = hostport => { + if (isBackgroundTaskMode) { + return []; + } + let content = "Notification settings"; + return [ + content, + { + content, + arguments: parsedArgumentString( + Object.assign( + { + action: "settings", + }, + hostport && { + launchUrl: hostport, + } + ) + ), + placement: "contextmenu", + }, + ]; + }; + + let parsedSnoozeAction = hostport => { + let content = `Disable notifications from ${hostport}`; + return [ + content, + { + content, + arguments: parsedArgumentString( + Object.assign( + { + action: "snooze", + }, + hostport && { + launchUrl: hostport, + } + ) + ), + placement: "contextmenu", + }, + ]; + }; + + let alertsService = Cc["@mozilla.org/system-alerts-service;1"] + .getService(Ci.nsIAlertsService) + .QueryInterface(Ci.nsIWindowsAlertsService); + + let name = "name"; + let title = "title"; + let text = "text"; + let imageURL = "file:///image.png"; + let actions = [ + { action: "action1", title: "title1", iconURL: "file:///iconURL1.png" }, + { action: "action2", title: "title2", iconURL: "file:///iconURL2.png" }, + ]; + let opaqueRelaunchData = { foo: 1, bar: "two" }; + + let alert = makeAlert({ name, title, text }); + let expected = `<toast launch="launch"><visual><binding template="ToastText03"><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "" }), + actions: Object.fromEntries( + [parsedSettingsAction()].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + alert = makeAlert({ name, title, text, imageURL }); + expected = `<toast launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "" }), + actions: Object.fromEntries( + [parsedSettingsAction()].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + alert = makeAlert({ name, title, text, imageURL, requireInteraction: true }); + expected = `<toast scenario="reminder" launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="Dismiss" arguments="dismiss" activationType="system"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "" }), + actions: Object.fromEntries( + [ + parsedSettingsAction(), + [ + "Dismiss", + { + content: "Dismiss", + arguments: "dismiss", + activationType: "system", + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + alert = makeAlert({ name, title, text, imageURL, actions }); + expected = `<toast launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="title1"/><action content="title2"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "" }), + actions: Object.fromEntries( + [ + parsedSettingsAction(), + [ + "title1", + { + content: "title1", + arguments: parsedArgumentString({ action: "action1" }), + }, + ], + [ + "title2", + { + content: "title2", + arguments: parsedArgumentString({ action: "action2" }), + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // Chrome privileged alerts can use `windowsSystemActivationType`. + let systemActions = [ + { + action: "dismiss", + title: "dismissTitle", + windowsSystemActivationType: true, + }, + { + action: "snooze", + title: "snoozeTitle", + windowsSystemActivationType: true, + }, + ]; + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + alert = makeAlert({ + name, + title, + text, + imageURL, + principal: systemPrincipal, + actions: systemActions, + }); + let parsedSettingsActionWithPrivilegedName = isBackgroundTaskMode + ? [] + : [ + "Notification settings", + { + content: "Notification settings", + arguments: parsedArgumentString({ + action: "settings", + privilegedName: name, + }), + placement: "contextmenu", + }, + ]; + + expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="dismissTitle" arguments="dismiss" activationType="system"/><action content="snoozeTitle" arguments="snooze" activationType="system"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "", privilegedName: name }), + actions: Object.fromEntries( + [ + parsedSettingsActionWithPrivilegedName, + [ + "dismissTitle", + { + content: "dismissTitle", + arguments: "dismiss", + activationType: "system", + }, + ], + [ + "snoozeTitle", + { + content: "snoozeTitle", + arguments: "snooze", + activationType: "system", + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // But content unprivileged alerts can't use `windowsSystemActivationType`. + let launchUrl = "https://example.com/foo/bar.html"; + const principaluri = Services.io.newURI(launchUrl); + const principal = Services.scriptSecurityManager.createContentPrincipal( + principaluri, + {} + ); + + alert = makeAlert({ + name, + title, + text, + imageURL, + actions: systemActions, + principal, + }); + expected = `<toast launch="launch"><visual><binding template="ToastImageAndText04"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text><text id="3" placement="attribution">via example.com</text></binding></visual><actions><action content="Disable notifications from example.com"/>${settingsAction}<action content="dismissTitle"/><action content="snoozeTitle"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ + action: "", + launchUrl: principaluri.hostPort, + }), + actions: Object.fromEntries( + [ + parsedSnoozeAction(principaluri.hostPort), + parsedSettingsAction(principaluri.hostPort), + [ + "dismissTitle", + { + content: "dismissTitle", + arguments: parsedArgumentString({ + action: "dismiss", + launchUrl: principaluri.hostPort, + }), + }, + ], + [ + "snoozeTitle", + { + content: "snoozeTitle", + arguments: parsedArgumentString({ + action: "snooze", + launchUrl: principaluri.hostPort, + }), + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // Chrome privileged alerts can set `opaqueRelaunchData`. + alert = makeAlert({ + name, + title, + text, + imageURL, + principal: systemPrincipal, + opaqueRelaunchData: JSON.stringify(opaqueRelaunchData), + }); + expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ + action: "", + opaqueRelaunchData: JSON.stringify(opaqueRelaunchData), + privilegedName: name, + }), + actions: Object.fromEntries( + [parsedSettingsActionWithPrivilegedName].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // But content unprivileged alerts can't set `opaqueRelaunchData`. + alert = makeAlert({ + name, + title, + text, + imageURL, + principal, + opaqueRelaunchData: JSON.stringify(opaqueRelaunchData), + }); + expected = `<toast launch="launch"><visual><binding template="ToastImageAndText04"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text><text id="3" placement="attribution">via example.com</text></binding></visual><actions><action content="Disable notifications from example.com"/>${settingsAction}</actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ + action: "", + launchUrl: principaluri.hostPort, + }), + actions: Object.fromEntries( + [ + parsedSnoozeAction(principaluri.hostPort), + parsedSettingsAction(principaluri.hostPort), + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // Chrome privileged alerts can set action-specific relaunch parameters. + let systemRelaunchActions = [ + { + action: "action1", + title: "title1", + opaqueRelaunchData: JSON.stringify({ json: "data1" }), + }, + { + action: "action2", + title: "title2", + opaqueRelaunchData: JSON.stringify({ json: "data2" }), + }, + ]; + systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + alert = makeAlert({ + name, + title, + text, + imageURL, + principal: systemPrincipal, + actions: systemRelaunchActions, + }); + expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="title1"/><action content="title2"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "", privilegedName: name }), + actions: Object.fromEntries( + [ + parsedSettingsActionWithPrivilegedName, + [ + "title1", + { + content: "title1", + arguments: parsedArgumentString( + { + action: "action1", + opaqueRelaunchData: JSON.stringify({ json: "data1" }), + privilegedName: name, + }, + null, + name + ), + }, + ], + + [ + "title2", + { + content: "title2", + arguments: parsedArgumentString( + { + action: "action2", + opaqueRelaunchData: JSON.stringify({ json: "data2" }), + privilegedName: name, + }, + null, + name + ), + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); +} + +add_task(async () => { + Services.prefs.deleteBranch( + "alerts.useSystemBackend.windows.notificationserver.enabled" + ); + testAlert("when notification server pref is unset", { + profD: gProfD, + }); + + Services.prefs.setBoolPref( + "alerts.useSystemBackend.windows.notificationserver.enabled", + false + ); + testAlert("when notification server pref is false", { profD: gProfD }); + + Services.prefs.setBoolPref( + "alerts.useSystemBackend.windows.notificationserver.enabled", + true + ); + testAlert("when notification server pref is true", { + serverEnabled: true, + profD: gProfD, + }); +}); + +let condition = { + skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS, +}; + +add_task(condition, async () => { + const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( + Ci.nsIBackgroundTasks + ); + + // Pretend that this is a background task. + bts.overrideBackgroundTaskNameForTesting("taskname"); + + Services.prefs.setBoolPref( + "alerts.useSystemBackend.windows.notificationserver.enabled", + true + ); + testAlert( + "when notification server pref is true in background task, no default profile", + { serverEnabled: true, isBackgroundTaskMode: true } + ); + + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let profilePath = do_get_profile(); + profilePath.append(`test_windows_alert_service`); + let profile = profileService.createUniqueProfile( + profilePath, + "test_windows_alert_service" + ); + + profileService.defaultProfile = profile; + + testAlert( + "when notification server pref is true in background task, default profile", + { serverEnabled: true, isBackgroundTaskMode: true, profD: profilePath } + ); + + // No longer a background task, + bts.overrideBackgroundTaskNameForTesting(""); +}); diff --git a/widget/windows/tests/unit/xpcshell.toml b/widget/windows/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..9943d5510e --- /dev/null +++ b/widget/windows/tests/unit/xpcshell.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_windows_alert_service.js"] diff --git a/widget/windows/touchinjection_sdk80.h b/widget/windows/touchinjection_sdk80.h new file mode 100644 index 0000000000..7e9f5410d2 --- /dev/null +++ b/widget/windows/touchinjection_sdk80.h @@ -0,0 +1,171 @@ +/* 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 touchinjection_sdk80_h +#define touchinjection_sdk80_h + +#include <windows.h> + +// Note, this isn't inclusive of all touch injection header info. +// You may need to add more to expand on current apis. + +#ifndef TOUCH_FEEDBACK_DEFAULT + +# define TOUCH_FEEDBACK_DEFAULT 0x1 +# define TOUCH_FEEDBACK_INDIRECT 0x2 +# define TOUCH_FEEDBACK_NONE 0x3 + +enum POINTER_FEEDBACK_MODE { + POINTER_FEEDBACK_DEFAULT = + 1, // The injected pointer input feedback may get suppressed by the + // end-user settings in the Pen and Touch control panel. + POINTER_FEEDBACK_INDIRECT = + 2, // The injected pointer input feedback overrides the end-user settings + // in the Pen and Touch control panel. + POINTER_FEEDBACK_NONE = 3, // No touch visualizations. +}; + +enum { + PT_POINTER = 0x00000001, // Generic pointer + PT_TOUCH = 0x00000002, // Touch + PT_PEN = 0x00000003, // Pen + PT_MOUSE = 0x00000004, // Mouse + PT_TOUCHPAD = 0x00000005, // Touch pad +}; + +using POINTER_INPUT_TYPE = DWORD; +using POINTER_FLAGS = UINT32; + +enum POINTER_BUTTON_CHANGE_TYPE { + POINTER_CHANGE_NONE, + POINTER_CHANGE_FIRSTBUTTON_DOWN, + POINTER_CHANGE_FIRSTBUTTON_UP, + POINTER_CHANGE_SECONDBUTTON_DOWN, + POINTER_CHANGE_SECONDBUTTON_UP, + POINTER_CHANGE_THIRDBUTTON_DOWN, + POINTER_CHANGE_THIRDBUTTON_UP, + POINTER_CHANGE_FOURTHBUTTON_DOWN, + POINTER_CHANGE_FOURTHBUTTON_UP, + POINTER_CHANGE_FIFTHBUTTON_DOWN, + POINTER_CHANGE_FIFTHBUTTON_UP, +}; + +struct POINTER_INFO { + POINTER_INPUT_TYPE pointerType; + UINT32 pointerId; + UINT32 frameId; + POINTER_FLAGS pointerFlags; + HANDLE sourceDevice; + HWND hwndTarget; + POINT ptPixelLocation; + POINT ptHimetricLocation; + POINT ptPixelLocationRaw; + POINT ptHimetricLocationRaw; + DWORD dwTime; + UINT32 historyCount; + INT32 InputData; + DWORD dwKeyStates; + UINT64 PerformanceCount; + POINTER_BUTTON_CHANGE_TYPE ButtonChangeType; +}; + +using TOUCH_FLAGS = UINT32; +using TOUCH_MASK = UINT32; + +struct POINTER_TOUCH_INFO { + POINTER_INFO pointerInfo; + TOUCH_FLAGS touchFlags; + TOUCH_MASK touchMask; + RECT rcContact; + RECT rcContactRaw; + UINT32 orientation; + UINT32 pressure; +}; + +# define PEN_FLAG_NONE 0x00000000 // Default +# define PEN_FLAG_BARREL 0x00000001 // The barrel button is pressed +# define PEN_FLAG_INVERTED 0x00000002 // The pen is inverted +# define PEN_FLAG_ERASER 0x00000004 // The eraser button is pressed + +# define PEN_MASK_NONE \ + 0x00000000 // Default - none of the optional fields are valid +# define PEN_MASK_PRESSURE 0x00000001 // The pressure field is valid +# define PEN_MASK_ROTATION 0x00000002 // The rotation field is valid +# define PEN_MASK_TILT_X 0x00000004 // The tiltX field is valid +# define PEN_MASK_TILT_Y 0x00000008 // The tiltY field is valid + +using PEN_FLAGS = UINT32; +using PEN_MASK = UINT32; + +struct POINTER_PEN_INFO { + POINTER_INFO pointerInfo; + PEN_FLAGS penFlags; + PEN_MASK penMask; + UINT32 pressure; + UINT32 rotation; + INT32 tiltX; + INT32 tiltY; +}; + +struct POINTER_TYPE_INFO { + POINTER_INPUT_TYPE type; + union { + POINTER_TOUCH_INFO touchInfo; + POINTER_PEN_INFO penInfo; + }; +}; + +# define TOUCH_FLAG_NONE 0x00000000 // Default + +# define TOUCH_MASK_NONE \ + 0x00000000 // Default - none of the optional fields are valid +# define TOUCH_MASK_CONTACTAREA 0x00000001 // The rcContact field is valid +# define TOUCH_MASK_ORIENTATION 0x00000002 // The orientation field is valid +# define TOUCH_MASK_PRESSURE 0x00000004 // The pressure field is valid + +# define POINTER_FLAG_NONE 0x00000000 // Default +# define POINTER_FLAG_NEW 0x00000001 // New pointer +# define POINTER_FLAG_INRANGE 0x00000002 // Pointer has not departed +# define POINTER_FLAG_INCONTACT 0x00000004 // Pointer is in contact +# define POINTER_FLAG_FIRSTBUTTON 0x00000010 // Primary action +# define POINTER_FLAG_SECONDBUTTON 0x00000020 // Secondary action +# define POINTER_FLAG_THIRDBUTTON 0x00000040 // Third button +# define POINTER_FLAG_FOURTHBUTTON 0x00000080 // Fourth button +# define POINTER_FLAG_FIFTHBUTTON 0x00000100 // Fifth button +# define POINTER_FLAG_PRIMARY 0x00002000 // Pointer is primary +# define POINTER_FLAG_CONFIDENCE \ + 0x00004000 // Pointer is considered unlikely to be accidental +# define POINTER_FLAG_CANCELED \ + 0x00008000 // Pointer is departing in an abnormal manner +# define POINTER_FLAG_DOWN \ + 0x00010000 // Pointer transitioned to down state (made contact) +# define POINTER_FLAG_UPDATE 0x00020000 // Pointer update +# define POINTER_FLAG_UP \ + 0x00040000 // Pointer transitioned from down state (broke contact) +# define POINTER_FLAG_WHEEL 0x00080000 // Vertical wheel +# define POINTER_FLAG_HWHEEL 0x00100000 // Horizontal wheel +# define POINTER_FLAG_CAPTURECHANGED 0x00200000 // Lost capture + +#endif // TOUCH_FEEDBACK_DEFAULT + +#define TOUCH_FLAGS_CONTACTUPDATE \ + (POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT) +#define TOUCH_FLAGS_CONTACTDOWN \ + (POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT) + +using InitializeTouchInjectionPtr = BOOL(WINAPI*)(UINT32, DWORD); +using InjectTouchInputPtr = BOOL(WINAPI*)(UINT32, const POINTER_TOUCH_INFO*); + +#if !defined(NTDDI_WIN10_RS5) || (NTDDI_VERSION < NTDDI_WIN10_RS5) +# define HSYNTHETICPOINTERDEVICE intptr_t +#endif // NTDDI_VERSION < NTDDI_WIN10_RS5 + +using CreateSyntheticPointerDevicePtr = HSYNTHETICPOINTERDEVICE(WINAPI*)( + POINTER_INPUT_TYPE, ULONG, POINTER_FEEDBACK_MODE); +using DestroySyntheticPointerDevicePtr = void(WINAPI*)(HSYNTHETICPOINTERDEVICE); +using InjectSyntheticPointerInputPtr = BOOL(WINAPI*)(HSYNTHETICPOINTERDEVICE, + const POINTER_TYPE_INFO*, + UINT32); + +#endif // touchinjection_sdk80_h diff --git a/widget/windows/widget.rc b/widget/windows/widget.rc new file mode 100644 index 0000000000..9361f9e48c --- /dev/null +++ b/widget/windows/widget.rc @@ -0,0 +1,30 @@ +/* -*- 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 "resource.h" +#include <winresrc.h> +#include <dlgs.h> + +IDC_GRAB CURSOR DISCARDABLE "res/grab.cur" +IDC_GRABBING CURSOR DISCARDABLE "res/grabbing.cur" +IDC_CELL CURSOR DISCARDABLE "res/cell.cur" +IDC_COPY CURSOR DISCARDABLE "res/copy.cur" +IDC_ALIAS CURSOR DISCARDABLE "res/aliasb.cur" +IDC_ZOOMIN CURSOR DISCARDABLE "res/zoom_in.cur" +IDC_ZOOMOUT CURSOR DISCARDABLE "res/zoom_out.cur" +IDC_COLRESIZE CURSOR DISCARDABLE "res/col_resize.cur" +IDC_ROWRESIZE CURSOR DISCARDABLE "res/row_resize.cur" +IDC_VERTICALTEXT CURSOR DISCARDABLE "res/vertical_text.cur" +IDC_NONE CURSOR DISCARDABLE "res/none.cur" + +OPTPROPSHEET DIALOG DISCARDABLE 32, 32, 288, 226 +STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | + WS_CAPTION | WS_SYSMENU +CAPTION "Options" +FONT 8, "MS Sans Serif" +BEGIN + +END |