diff options
Diffstat (limited to '')
-rw-r--r-- | widget/headless/HeadlessWidget.cpp | 625 |
1 files changed, 625 insertions, 0 deletions
diff --git a/widget/headless/HeadlessWidget.cpp b/widget/headless/HeadlessWidget.cpp new file mode 100644 index 0000000000..083d026d3c --- /dev/null +++ b/widget/headless/HeadlessWidget.cpp @@ -0,0 +1,625 @@ +/* -*- 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 "HeadlessWidget.h" +#include "ErrorList.h" +#include "HeadlessCompositorWidget.h" +#include "BasicEvents.h" +#include "MouseEvents.h" +#include "mozilla/gfx/gfxVars.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Maybe.h" +#include "mozilla/NativeKeyBindingsType.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/WritingModes.h" +#include "mozilla/widget/HeadlessWidgetTypes.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "mozilla/widget/Screen.h" +#include "nsIScreen.h" +#include "HeadlessKeyBindings.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::layers; + +using mozilla::LogLevel; + +#ifdef MOZ_LOGGING + +# include "mozilla/Logging.h" +static mozilla::LazyLogModule sWidgetLog("Widget"); +static mozilla::LazyLogModule sWidgetFocusLog("WidgetFocus"); +# define LOG(args) MOZ_LOG(sWidgetLog, mozilla::LogLevel::Debug, args) +# define LOGFOCUS(args) \ + MOZ_LOG(sWidgetFocusLog, mozilla::LogLevel::Debug, args) + +#else + +# define LOG(args) +# define LOGFOCUS(args) + +#endif /* MOZ_LOGGING */ + +/*static*/ +already_AddRefed<nsIWidget> nsIWidget::CreateHeadlessWidget() { + nsCOMPtr<nsIWidget> widget = new mozilla::widget::HeadlessWidget(); + return widget.forget(); +} + +namespace mozilla { +namespace widget { + +StaticAutoPtr<nsTArray<HeadlessWidget*>> HeadlessWidget::sActiveWindows; + +already_AddRefed<HeadlessWidget> HeadlessWidget::GetActiveWindow() { + if (!sActiveWindows) { + return nullptr; + } + auto length = sActiveWindows->Length(); + if (length == 0) { + return nullptr; + } + RefPtr<HeadlessWidget> widget = sActiveWindows->ElementAt(length - 1); + return widget.forget(); +} + +HeadlessWidget::HeadlessWidget() + : mEnabled(true), + mVisible(false), + mDestroyed(false), + mAlwaysOnTop(false), + mTopLevel(nullptr), + mCompositorWidget(nullptr), + mSizeMode(nsSizeMode_Normal), + mLastSizeMode(nsSizeMode_Normal), + mEffectiveSizeMode(nsSizeMode_Normal), + mRestoreBounds(0, 0, 0, 0) { + if (!sActiveWindows) { + sActiveWindows = new nsTArray<HeadlessWidget*>(); + ClearOnShutdown(&sActiveWindows); + } +} + +HeadlessWidget::~HeadlessWidget() { + LOG(("HeadlessWidget::~HeadlessWidget() [%p]\n", (void*)this)); + + Destroy(); +} + +void HeadlessWidget::Destroy() { + if (mDestroyed) { + return; + } + LOG(("HeadlessWidget::Destroy [%p]\n", (void*)this)); + mDestroyed = true; + + if (sActiveWindows) { + int32_t index = sActiveWindows->IndexOf(this); + if (index != -1) { + RefPtr<HeadlessWidget> activeWindow = GetActiveWindow(); + sActiveWindows->RemoveElementAt(index); + // If this is the currently active widget and there's a previously active + // widget, activate the previous widget. + RefPtr<HeadlessWidget> previousActiveWindow = GetActiveWindow(); + if (this == activeWindow && previousActiveWindow && + previousActiveWindow->mWidgetListener) { + previousActiveWindow->mWidgetListener->WindowActivated(); + } + } + } + + nsBaseWidget::OnDestroy(); + + nsBaseWidget::Destroy(); +} + +nsresult HeadlessWidget::Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + widget::InitData* aInitData) { + MOZ_ASSERT(!aNativeParent, "No native parents for headless widgets."); + + BaseCreate(nullptr, aInitData); + + mBounds = aRect; + mRestoreBounds = aRect; + + mAlwaysOnTop = aInitData && aInitData->mAlwaysOnTop; + + if (aParent) { + mTopLevel = aParent->GetTopLevelWidget(); + } else { + mTopLevel = this; + } + + return NS_OK; +} + +already_AddRefed<nsIWidget> HeadlessWidget::CreateChild( + const LayoutDeviceIntRect& aRect, widget::InitData* aInitData, + bool aForceUseIWidgetParent) { + nsCOMPtr<nsIWidget> widget = nsIWidget::CreateHeadlessWidget(); + if (!widget) { + return nullptr; + } + if (NS_FAILED(widget->Create(this, nullptr, aRect, aInitData))) { + return nullptr; + } + return widget.forget(); +} + +void HeadlessWidget::GetCompositorWidgetInitData( + mozilla::widget::CompositorWidgetInitData* aInitData) { + *aInitData = + mozilla::widget::HeadlessCompositorWidgetInitData(GetClientSize()); +} + +nsIWidget* HeadlessWidget::GetTopLevelWidget() { return mTopLevel; } + +void HeadlessWidget::RaiseWindow() { + MOZ_ASSERT(mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog || + mWindowType == WindowType::Sheet, + "Raising a non-toplevel window."); + + // Do nothing if this is the currently active window. + RefPtr<HeadlessWidget> activeWindow = GetActiveWindow(); + if (activeWindow == this) { + return; + } + + // Raise the window to the top of the stack. + nsWindowZ placement = nsWindowZTop; + nsCOMPtr<nsIWidget> actualBelow; + if (mWidgetListener) + mWidgetListener->ZLevelChanged(true, &placement, nullptr, + getter_AddRefs(actualBelow)); + + // Deactivate the last active window. + if (activeWindow && activeWindow->mWidgetListener) { + activeWindow->mWidgetListener->WindowDeactivated(); + } + + // Remove this window if it's already tracked. + int32_t index = sActiveWindows->IndexOf(this); + if (index != -1) { + sActiveWindows->RemoveElementAt(index); + } + + // Activate this window. + sActiveWindows->AppendElement(this); + if (mWidgetListener) mWidgetListener->WindowActivated(); +} + +void HeadlessWidget::Show(bool aState) { + mVisible = aState; + + LOG(("HeadlessWidget::Show [%p] state %d\n", (void*)this, aState)); + + // Top-level window and dialogs are activated/raised when shown. + // NB: alwaysontop windows are generally used for peripheral indicators, + // so we don't focus them by default. + if (aState && !mAlwaysOnTop && + (mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog || mWindowType == WindowType::Sheet)) { + RaiseWindow(); + } + + ApplySizeModeSideEffects(); +} + +bool HeadlessWidget::IsVisible() const { return mVisible; } + +void HeadlessWidget::SetFocus(Raise aRaise, + mozilla::dom::CallerType aCallerType) { + LOGFOCUS((" SetFocus %d [%p]\n", aRaise == Raise::Yes, (void*)this)); + + // This means we request activation of our toplevel window. + if (aRaise == Raise::Yes) { + HeadlessWidget* topLevel = (HeadlessWidget*)GetTopLevelWidget(); + + // The toplevel only becomes active if it's currently visible; otherwise, it + // will be activated anyway when it's shown. + if (topLevel->IsVisible()) topLevel->RaiseWindow(); + } +} + +void HeadlessWidget::Enable(bool aState) { mEnabled = aState; } + +bool HeadlessWidget::IsEnabled() const { return mEnabled; } + +void HeadlessWidget::Move(double aX, double aY) { + LOG(("HeadlessWidget::Move [%p] %f %f\n", (void*)this, aX, aY)); + + double scale = + BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0; + int32_t x = NSToIntRound(aX * scale); + int32_t y = NSToIntRound(aY * scale); + + if (mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog) { + SetSizeMode(nsSizeMode_Normal); + } + + MoveInternal(x, y); +} + +void HeadlessWidget::MoveInternal(int32_t aX, int32_t aY) { + // Since a popup window's x/y coordinates are in relation to + // the parent, the parent might have moved so we always move a + // popup window. + if (mBounds.IsEqualXY(aX, aY) && mWindowType != WindowType::Popup) { + return; + } + + mBounds.MoveTo(aX, aY); + NotifyWindowMoved(aX, aY); +} + +LayoutDeviceIntPoint HeadlessWidget::WidgetToScreenOffset() { + return mTopLevel->GetBounds().TopLeft(); +} + +WindowRenderer* HeadlessWidget::GetWindowRenderer() { + return nsBaseWidget::GetWindowRenderer(); +} + +void HeadlessWidget::SetCompositorWidgetDelegate( + CompositorWidgetDelegate* delegate) { + if (delegate) { + mCompositorWidget = delegate->AsHeadlessCompositorWidget(); + MOZ_ASSERT(mCompositorWidget, + "HeadlessWidget::SetCompositorWidgetDelegate called with a " + "non-HeadlessCompositorWidget"); + } else { + mCompositorWidget = nullptr; + } +} + +void HeadlessWidget::Resize(double aWidth, double aHeight, bool aRepaint) { + int32_t width = NSToIntRound(aWidth); + int32_t height = NSToIntRound(aHeight); + ResizeInternal(width, height, aRepaint); +} + +void HeadlessWidget::ResizeInternal(int32_t aWidth, int32_t aHeight, + bool aRepaint) { + ConstrainSize(&aWidth, &aHeight); + mBounds.SizeTo(LayoutDeviceIntSize(aWidth, aHeight)); + + if (mCompositorWidget) { + mCompositorWidget->NotifyClientSizeChanged( + LayoutDeviceIntSize(mBounds.Width(), mBounds.Height())); + } + if (mWidgetListener) { + mWidgetListener->WindowResized(this, mBounds.Width(), mBounds.Height()); + } + if (mAttachedWidgetListener) { + mAttachedWidgetListener->WindowResized(this, mBounds.Width(), + mBounds.Height()); + } +} + +void HeadlessWidget::Resize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint) { + MoveInternal(NSToIntRound(aX), NSToIntRound(aY)); + Resize(aWidth, aHeight, aRepaint); +} + +void HeadlessWidget::SetSizeMode(nsSizeMode aMode) { + LOG(("HeadlessWidget::SetSizeMode [%p] %d\n", (void*)this, aMode)); + + if (aMode == mSizeMode) { + return; + } + + if (aMode == nsSizeMode_Normal && mSizeMode == nsSizeMode_Fullscreen) { + MakeFullScreen(false); + return; + } + + mSizeMode = aMode; + + // Normally in real widget backends a window event would be triggered that + // would cause the window manager to handle resizing the window. In headless + // the window must manually be resized. + ApplySizeModeSideEffects(); +} + +void HeadlessWidget::ApplySizeModeSideEffects() { + if (!mVisible || mEffectiveSizeMode == mSizeMode) { + return; + } + + if (mEffectiveSizeMode == nsSizeMode_Normal) { + // Store the last normal size bounds so it can be restored when entering + // normal mode again. + mRestoreBounds = mBounds; + } + + switch (mSizeMode) { + case nsSizeMode_Normal: { + MoveInternal(mRestoreBounds.X(), mRestoreBounds.Y()); + ResizeInternal(mRestoreBounds.Width(), mRestoreBounds.Height(), false); + break; + } + case nsSizeMode_Minimized: + break; + case nsSizeMode_Maximized: { + nsCOMPtr<nsIScreen> screen = GetWidgetScreen(); + if (screen) { + int32_t left, top, width, height; + if (NS_SUCCEEDED( + screen->GetRectDisplayPix(&left, &top, &width, &height))) { + MoveInternal(0, 0); + ResizeInternal(width, height, true); + } + } + break; + } + case nsSizeMode_Fullscreen: + // This will take care of resizing the window. + nsBaseWidget::InfallibleMakeFullScreen(true); + break; + default: + break; + } + + mEffectiveSizeMode = mSizeMode; + if (mWidgetListener) { + mWidgetListener->SizeModeChanged(mSizeMode); + } +} + +nsresult HeadlessWidget::MakeFullScreen(bool aFullScreen) { + // Directly update the size mode here so a later call SetSizeMode does + // nothing. + if (aFullScreen) { + if (mSizeMode != nsSizeMode_Fullscreen) { + mLastSizeMode = mSizeMode; + } + mSizeMode = nsSizeMode_Fullscreen; + } else { + mSizeMode = mLastSizeMode; + } + + // Notify the listener first so size mode change events are triggered before + // resize events. + if (mWidgetListener) { + mWidgetListener->SizeModeChanged(mSizeMode); + } + + // Real widget backends don't seem to follow a common approach for + // when and how many resize events are triggered during fullscreen + // transitions. InfallibleMakeFullScreen will trigger a resize, but it + // will be ignored if still transitioning to fullscreen, so it must be + // triggered on the next tick. + RefPtr<HeadlessWidget> self(this); + NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "HeadlessWidget::MakeFullScreen", [self, aFullScreen]() -> void { + self->InfallibleMakeFullScreen(aFullScreen); + })); + + return NS_OK; +} + +nsresult HeadlessWidget::AttachNativeKeyEvent(WidgetKeyboardEvent& aEvent) { + HeadlessKeyBindings& bindings = HeadlessKeyBindings::GetInstance(); + return bindings.AttachNativeKeyEvent(aEvent); +} + +bool HeadlessWidget::GetEditCommands(NativeKeyBindingsType aType, + const WidgetKeyboardEvent& aEvent, + nsTArray<CommandInt>& aCommands) { + // Validate the arguments. + if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) { + return false; + } + + Maybe<WritingMode> writingMode; + if (aEvent.NeedsToRemapNavigationKey()) { + if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) { + writingMode = dispatcher->MaybeQueryWritingModeAtSelection(); + } + } + + HeadlessKeyBindings& bindings = HeadlessKeyBindings::GetInstance(); + bindings.GetEditCommands(aType, aEvent, writingMode, aCommands); + return true; +} + +nsresult HeadlessWidget::DispatchEvent(WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) { +#ifdef DEBUG + debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "HeadlessWidget", 0); +#endif + + aStatus = nsEventStatus_eIgnore; + + if (mAttachedWidgetListener) { + aStatus = mAttachedWidgetListener->HandleEvent(aEvent, mUseAttachedEvents); + } else if (mWidgetListener) { + aStatus = mWidgetListener->HandleEvent(aEvent, mUseAttachedEvents); + } + + return NS_OK; +} + +nsresult HeadlessWidget::SynthesizeNativeMouseEvent( + LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage, + MouseButton aButton, nsIWidget::Modifiers aModifierFlags, + nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "mouseevent"); + EventMessage msg; + switch (aNativeMessage) { + case NativeMouseMessage::Move: + msg = eMouseMove; + break; + case NativeMouseMessage::ButtonDown: + msg = eMouseDown; + break; + case NativeMouseMessage::ButtonUp: + msg = eMouseUp; + break; + case NativeMouseMessage::EnterWindow: + case NativeMouseMessage::LeaveWindow: + MOZ_ASSERT_UNREACHABLE("Unsupported synthesized mouse event"); + return NS_ERROR_UNEXPECTED; + } + WidgetMouseEvent event(true, msg, this, WidgetMouseEvent::eReal); + event.mRefPoint = aPoint - WidgetToScreenOffset(); + if (msg == eMouseDown || msg == eMouseUp) { + event.mButton = aButton; + } + if (msg == eMouseDown) { + event.mClickCount = 1; + } + event.AssignEventTime(WidgetEventTime()); + DispatchInputEvent(&event); + return NS_OK; +} + +nsresult HeadlessWidget::SynthesizeNativeMouseScrollEvent( + mozilla::LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, + double aDeltaX, double aDeltaY, double aDeltaZ, uint32_t aModifierFlags, + uint32_t aAdditionalFlags, nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "mousescrollevent"); + printf(">>> DEBUG_ME: Synth: aDeltaY=%f\n", aDeltaY); + // The various platforms seem to handle scrolling deltas differently, + // but the following seems to emulate it well enough. + WidgetWheelEvent event(true, eWheel, this); + event.mDeltaMode = MOZ_HEADLESS_SCROLL_DELTA_MODE; + event.mIsNoLineOrPageDelta = true; + event.mDeltaX = -aDeltaX * MOZ_HEADLESS_SCROLL_MULTIPLIER; + event.mDeltaY = -aDeltaY * MOZ_HEADLESS_SCROLL_MULTIPLIER; + event.mDeltaZ = -aDeltaZ * MOZ_HEADLESS_SCROLL_MULTIPLIER; + event.mRefPoint = aPoint - WidgetToScreenOffset(); + event.AssignEventTime(WidgetEventTime()); + DispatchInputEvent(&event); + return NS_OK; +} + +nsresult HeadlessWidget::SynthesizeNativeTouchPoint( + uint32_t aPointerId, TouchPointerState aPointerState, + LayoutDeviceIntPoint aPoint, double aPointerPressure, + uint32_t aPointerOrientation, nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "touchpoint"); + + MOZ_ASSERT(NS_IsMainThread()); + if (aPointerState == TOUCH_HOVER) { + return NS_ERROR_UNEXPECTED; + } + + if (!mSynthesizedTouchInput) { + mSynthesizedTouchInput = MakeUnique<MultiTouchInput>(); + } + + LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset(); + MultiTouchInput inputToDispatch = UpdateSynthesizedTouchState( + mSynthesizedTouchInput.get(), TimeStamp::Now(), aPointerId, aPointerState, + pointInWindow, aPointerPressure, aPointerOrientation); + DispatchTouchInput(inputToDispatch); + return NS_OK; +} + +nsresult HeadlessWidget::SynthesizeNativeTouchPadPinch( + TouchpadGesturePhase aEventPhase, float aScale, LayoutDeviceIntPoint aPoint, + int32_t aModifierFlags) { + MOZ_ASSERT(NS_IsMainThread()); + + PinchGestureInput::PinchGestureType pinchGestureType = + PinchGestureInput::PINCHGESTURE_SCALE; + ScreenCoord CurrentSpan; + ScreenCoord PreviousSpan; + switch (aEventPhase) { + case PHASE_BEGIN: + pinchGestureType = PinchGestureInput::PINCHGESTURE_START; + CurrentSpan = aScale; + PreviousSpan = 0.999; + break; + + case PHASE_UPDATE: + pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE; + if (aScale == mLastPinchSpan) { + return NS_ERROR_INVALID_ARG; + } + CurrentSpan = aScale; + PreviousSpan = mLastPinchSpan; + break; + + case PHASE_END: + pinchGestureType = PinchGestureInput::PINCHGESTURE_END; + CurrentSpan = aScale; + PreviousSpan = mLastPinchSpan; + break; + + default: + return NS_ERROR_INVALID_ARG; + } + + ScreenPoint touchpadPoint = ViewAs<ScreenPixel>( + aPoint - WidgetToScreenOffset(), + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); + // The headless widget does not support modifiers. + // Do not pass `aModifierFlags` because it contains native modifier values. + PinchGestureInput inputToDispatch( + pinchGestureType, PinchGestureInput::TRACKPAD, TimeStamp::Now(), + ExternalPoint(0, 0), touchpadPoint, + 100.0 * ((aEventPhase == PHASE_END) ? ScreenCoord(1.f) : CurrentSpan), + 100.0 * ((aEventPhase == PHASE_END) ? ScreenCoord(1.f) : PreviousSpan), + 0); + + if (!inputToDispatch.SetLineOrPageDeltaY(this)) { + return NS_ERROR_INVALID_ARG; + } + + mLastPinchSpan = aScale; + DispatchPinchGestureInput(inputToDispatch); + return NS_OK; +} + +nsresult HeadlessWidget::SynthesizeNativeTouchpadPan( + TouchpadGesturePhase aEventPhase, LayoutDeviceIntPoint aPoint, + double aDeltaX, double aDeltaY, int32_t aModifierFlags, + nsIObserver* aObserver) { + AutoObserverNotifier notifier(aObserver, "touchpadpanevent"); + + MOZ_ASSERT(NS_IsMainThread()); + + PanGestureInput::PanGestureType eventType = PanGestureInput::PANGESTURE_PAN; + switch (aEventPhase) { + case PHASE_BEGIN: + eventType = PanGestureInput::PANGESTURE_START; + break; + case PHASE_UPDATE: + eventType = PanGestureInput::PANGESTURE_PAN; + break; + case PHASE_END: + eventType = PanGestureInput::PANGESTURE_END; + break; + default: + return NS_ERROR_INVALID_ARG; + } + + ScreenPoint touchpadPoint = ViewAs<ScreenPixel>( + aPoint - WidgetToScreenOffset(), + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); + PanGestureInput input(eventType, TimeStamp::Now(), touchpadPoint, + ScreenPoint(float(aDeltaX), float(aDeltaY)), + // Same as SynthesizeNativeTouchPadPinch case we ignore + // aModifierFlags. + 0); + + input.mSimulateMomentum = + Preferences::GetBool("apz.test.headless.simulate_momentum"); + + DispatchPanGestureInput(input); + + return NS_OK; +} + +} // namespace widget +} // namespace mozilla |