From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- widget/gtk/CompositorWidgetChild.cpp | 42 + widget/gtk/CompositorWidgetChild.h | 39 + widget/gtk/CompositorWidgetParent.cpp | 44 + widget/gtk/CompositorWidgetParent.h | 38 + widget/gtk/DMABufLibWrapper.cpp | 300 + widget/gtk/DMABufLibWrapper.h | 168 + widget/gtk/DMABufSurface.cpp | 1002 +++ widget/gtk/DMABufSurface.h | 290 + widget/gtk/GRefPtr.h | 32 + widget/gtk/GtkCompositorWidget.cpp | 147 + widget/gtk/GtkCompositorWidget.h | 103 + widget/gtk/IMContextWrapper.cpp | 3169 ++++++++ widget/gtk/IMContextWrapper.h | 685 ++ widget/gtk/InProcessGtkCompositorWidget.cpp | 45 + widget/gtk/InProcessGtkCompositorWidget.h | 30 + widget/gtk/MPRISInterfaceDescription.h | 91 + widget/gtk/MPRISServiceHandler.cpp | 851 ++ widget/gtk/MPRISServiceHandler.h | 188 + widget/gtk/MediaKeysEventSourceFactory.cpp | 14 + widget/gtk/MozContainer.cpp | 376 + widget/gtk/MozContainer.h | 90 + widget/gtk/MozContainerWayland.cpp | 514 ++ widget/gtk/MozContainerWayland.h | 83 + widget/gtk/NativeKeyBindings.cpp | 346 + widget/gtk/NativeKeyBindings.h | 44 + widget/gtk/PCompositorWidget.ipdl | 30 + widget/gtk/PlatformWidgetTypes.ipdlh | 31 + widget/gtk/ScreenHelperGTK.cpp | 195 + widget/gtk/ScreenHelperGTK.h | 45 + widget/gtk/TaskbarProgress.cpp | 108 + widget/gtk/TaskbarProgress.h | 33 + widget/gtk/WakeLockListener.cpp | 500 ++ widget/gtk/WakeLockListener.h | 55 + widget/gtk/WaylandVsyncSource.cpp | 214 + widget/gtk/WaylandVsyncSource.h | 96 + widget/gtk/WidgetStyleCache.cpp | 1464 ++++ widget/gtk/WidgetStyleCache.h | 59 + widget/gtk/WidgetTraceEvent.cpp | 68 + widget/gtk/WidgetUtilsGtk.cpp | 52 + widget/gtk/WidgetUtilsGtk.h | 26 + widget/gtk/WindowSurfaceProvider.cpp | 131 + widget/gtk/WindowSurfaceProvider.h | 81 + widget/gtk/WindowSurfaceWayland.cpp | 1116 +++ widget/gtk/WindowSurfaceWayland.h | 269 + widget/gtk/WindowSurfaceX11.cpp | 50 + widget/gtk/WindowSurfaceX11.h | 40 + widget/gtk/WindowSurfaceX11Image.cpp | 263 + widget/gtk/WindowSurfaceX11Image.h | 48 + widget/gtk/WindowSurfaceXRender.cpp | 75 + widget/gtk/WindowSurfaceXRender.h | 37 + widget/gtk/compat-gtk3/gdk/gdkversionmacros.h | 32 + widget/gtk/compat/gdk/gdkdnd.h | 29 + widget/gtk/compat/gdk/gdkkeysyms.h | 266 + widget/gtk/compat/gdk/gdkvisual.h | 15 + widget/gtk/compat/gdk/gdkwindow.h | 27 + widget/gtk/compat/gdk/gdkx.h | 46 + widget/gtk/compat/glib/gmem.h | 48 + widget/gtk/compat/gtk/gtkwidget.h | 43 + widget/gtk/compat/gtk/gtkwindow.h | 26 + widget/gtk/components.conf | 166 + widget/gtk/crashtests/540078-1.xhtml | 1 + widget/gtk/crashtests/673390-1.html | 1 + widget/gtk/crashtests/crashtests.list | 2 + widget/gtk/gtk3drawing.cpp | 3214 ++++++++ widget/gtk/gtkdrawing.h | 634 ++ widget/gtk/maiRedundantObjectFactory.c | 81 + widget/gtk/maiRedundantObjectFactory.h | 30 + widget/gtk/moz.build | 178 + widget/gtk/mozgtk/gtk2/moz.build | 40 + widget/gtk/mozgtk/gtk3/moz.build | 38 + widget/gtk/mozgtk/moz.build | 7 + widget/gtk/mozgtk/mozgtk.c | 676 ++ widget/gtk/mozgtk/stub/moz.build | 16 + widget/gtk/mozwayland/moz.build | 16 + widget/gtk/mozwayland/mozwayland.c | 201 + widget/gtk/mozwayland/mozwayland.h | 134 + widget/gtk/nsAppShell.cpp | 253 + widget/gtk/nsAppShell.h | 34 + widget/gtk/nsApplicationChooser.cpp | 131 + widget/gtk/nsApplicationChooser.h | 32 + widget/gtk/nsBidiKeyboard.cpp | 52 + widget/gtk/nsBidiKeyboard.h | 25 + widget/gtk/nsClipboard.cpp | 822 ++ widget/gtk/nsClipboard.h | 90 + widget/gtk/nsClipboardWayland.cpp | 915 +++ widget/gtk/nsClipboardWayland.h | 162 + widget/gtk/nsClipboardX11.cpp | 340 + widget/gtk/nsClipboardX11.h | 73 + widget/gtk/nsColorPicker.cpp | 242 + widget/gtk/nsColorPicker.h | 72 + widget/gtk/nsDeviceContextSpecG.cpp | 341 + widget/gtk/nsDeviceContextSpecG.h | 62 + widget/gtk/nsDragService.cpp | 2122 +++++ widget/gtk/nsDragService.h | 210 + widget/gtk/nsFilePicker.cpp | 646 ++ widget/gtk/nsFilePicker.h | 87 + widget/gtk/nsGTKToolkit.h | 53 + widget/gtk/nsGtkCursors.h | 416 + widget/gtk/nsGtkKeyUtils.cpp | 2377 ++++++ widget/gtk/nsGtkKeyUtils.h | 467 ++ widget/gtk/nsGtkUtils.h | 23 + widget/gtk/nsIImageToPixbuf.h | 38 + widget/gtk/nsImageToPixbuf.cpp | 108 + widget/gtk/nsImageToPixbuf.h | 47 + widget/gtk/nsLookAndFeel.cpp | 1536 ++++ widget/gtk/nsLookAndFeel.h | 131 + widget/gtk/nsNativeBasicThemeGTK.cpp | 125 + widget/gtk/nsNativeBasicThemeGTK.h | 44 + widget/gtk/nsNativeThemeGTK.cpp | 2019 +++++ widget/gtk/nsNativeThemeGTK.h | 116 + widget/gtk/nsPrintDialogGTK.cpp | 1054 +++ widget/gtk/nsPrintDialogGTK.h | 37 + widget/gtk/nsPrintSettingsGTK.cpp | 685 ++ widget/gtk/nsPrintSettingsGTK.h | 145 + widget/gtk/nsPrintSettingsServiceGTK.cpp | 78 + widget/gtk/nsPrintSettingsServiceGTK.h | 33 + widget/gtk/nsSound.cpp | 398 + widget/gtk/nsSound.h | 33 + widget/gtk/nsToolkit.cpp | 31 + widget/gtk/nsUserIdleServiceGTK.cpp | 115 + widget/gtk/nsUserIdleServiceGTK.h | 50 + widget/gtk/nsWaylandDisplay.cpp | 309 + widget/gtk/nsWaylandDisplay.h | 130 + widget/gtk/nsWidgetFactory.cpp | 74 + widget/gtk/nsWidgetFactory.h | 22 + widget/gtk/nsWindow.cpp | 8420 ++++++++++++++++++++ widget/gtk/nsWindow.h | 719 ++ widget/gtk/wayland/gbm.h | 480 ++ .../gtk-primary-selection-client-protocol.h | 580 ++ .../gtk/wayland/gtk-primary-selection-protocol.c | 115 + .../idle-inhibit-unstable-v1-client-protocol.h | 228 + .../wayland/idle-inhibit-unstable-v1-protocol.c | 60 + .../linux-dmabuf-unstable-v1-client-protocol.h | 650 ++ .../wayland/linux-dmabuf-unstable-v1-protocol.c | 81 + widget/gtk/wayland/moz.build | 35 + ...primary-selection-unstable-v1-client-protocol.h | 578 ++ .../primary-selection-unstable-v1-protocol.c | 115 + widget/gtk/wayland/va_drmcommon.h | 156 + .../xdg-output-unstable-v1-client-protocol.h | 392 + .../gtk/wayland/xdg-output-unstable-v1-protocol.c | 74 + 140 files changed, 49872 insertions(+) create mode 100644 widget/gtk/CompositorWidgetChild.cpp create mode 100644 widget/gtk/CompositorWidgetChild.h create mode 100644 widget/gtk/CompositorWidgetParent.cpp create mode 100644 widget/gtk/CompositorWidgetParent.h create mode 100644 widget/gtk/DMABufLibWrapper.cpp create mode 100644 widget/gtk/DMABufLibWrapper.h create mode 100644 widget/gtk/DMABufSurface.cpp create mode 100644 widget/gtk/DMABufSurface.h create mode 100644 widget/gtk/GRefPtr.h create mode 100644 widget/gtk/GtkCompositorWidget.cpp create mode 100644 widget/gtk/GtkCompositorWidget.h create mode 100644 widget/gtk/IMContextWrapper.cpp create mode 100644 widget/gtk/IMContextWrapper.h create mode 100644 widget/gtk/InProcessGtkCompositorWidget.cpp create mode 100644 widget/gtk/InProcessGtkCompositorWidget.h create mode 100644 widget/gtk/MPRISInterfaceDescription.h create mode 100644 widget/gtk/MPRISServiceHandler.cpp create mode 100644 widget/gtk/MPRISServiceHandler.h create mode 100644 widget/gtk/MediaKeysEventSourceFactory.cpp create mode 100644 widget/gtk/MozContainer.cpp create mode 100644 widget/gtk/MozContainer.h create mode 100644 widget/gtk/MozContainerWayland.cpp create mode 100644 widget/gtk/MozContainerWayland.h create mode 100644 widget/gtk/NativeKeyBindings.cpp create mode 100644 widget/gtk/NativeKeyBindings.h create mode 100644 widget/gtk/PCompositorWidget.ipdl create mode 100644 widget/gtk/PlatformWidgetTypes.ipdlh create mode 100644 widget/gtk/ScreenHelperGTK.cpp create mode 100644 widget/gtk/ScreenHelperGTK.h create mode 100644 widget/gtk/TaskbarProgress.cpp create mode 100644 widget/gtk/TaskbarProgress.h create mode 100644 widget/gtk/WakeLockListener.cpp create mode 100644 widget/gtk/WakeLockListener.h create mode 100644 widget/gtk/WaylandVsyncSource.cpp create mode 100644 widget/gtk/WaylandVsyncSource.h create mode 100644 widget/gtk/WidgetStyleCache.cpp create mode 100644 widget/gtk/WidgetStyleCache.h create mode 100644 widget/gtk/WidgetTraceEvent.cpp create mode 100644 widget/gtk/WidgetUtilsGtk.cpp create mode 100644 widget/gtk/WidgetUtilsGtk.h create mode 100644 widget/gtk/WindowSurfaceProvider.cpp create mode 100644 widget/gtk/WindowSurfaceProvider.h create mode 100644 widget/gtk/WindowSurfaceWayland.cpp create mode 100644 widget/gtk/WindowSurfaceWayland.h create mode 100644 widget/gtk/WindowSurfaceX11.cpp create mode 100644 widget/gtk/WindowSurfaceX11.h create mode 100644 widget/gtk/WindowSurfaceX11Image.cpp create mode 100644 widget/gtk/WindowSurfaceX11Image.h create mode 100644 widget/gtk/WindowSurfaceXRender.cpp create mode 100644 widget/gtk/WindowSurfaceXRender.h create mode 100644 widget/gtk/compat-gtk3/gdk/gdkversionmacros.h create mode 100644 widget/gtk/compat/gdk/gdkdnd.h create mode 100644 widget/gtk/compat/gdk/gdkkeysyms.h create mode 100644 widget/gtk/compat/gdk/gdkvisual.h create mode 100644 widget/gtk/compat/gdk/gdkwindow.h create mode 100644 widget/gtk/compat/gdk/gdkx.h create mode 100644 widget/gtk/compat/glib/gmem.h create mode 100644 widget/gtk/compat/gtk/gtkwidget.h create mode 100644 widget/gtk/compat/gtk/gtkwindow.h create mode 100644 widget/gtk/components.conf create mode 100644 widget/gtk/crashtests/540078-1.xhtml create mode 100644 widget/gtk/crashtests/673390-1.html create mode 100644 widget/gtk/crashtests/crashtests.list create mode 100644 widget/gtk/gtk3drawing.cpp create mode 100644 widget/gtk/gtkdrawing.h create mode 100644 widget/gtk/maiRedundantObjectFactory.c create mode 100644 widget/gtk/maiRedundantObjectFactory.h create mode 100644 widget/gtk/moz.build create mode 100644 widget/gtk/mozgtk/gtk2/moz.build create mode 100644 widget/gtk/mozgtk/gtk3/moz.build create mode 100644 widget/gtk/mozgtk/moz.build create mode 100644 widget/gtk/mozgtk/mozgtk.c create mode 100644 widget/gtk/mozgtk/stub/moz.build create mode 100644 widget/gtk/mozwayland/moz.build create mode 100644 widget/gtk/mozwayland/mozwayland.c create mode 100644 widget/gtk/mozwayland/mozwayland.h create mode 100644 widget/gtk/nsAppShell.cpp create mode 100644 widget/gtk/nsAppShell.h create mode 100644 widget/gtk/nsApplicationChooser.cpp create mode 100644 widget/gtk/nsApplicationChooser.h create mode 100644 widget/gtk/nsBidiKeyboard.cpp create mode 100644 widget/gtk/nsBidiKeyboard.h create mode 100644 widget/gtk/nsClipboard.cpp create mode 100644 widget/gtk/nsClipboard.h create mode 100644 widget/gtk/nsClipboardWayland.cpp create mode 100644 widget/gtk/nsClipboardWayland.h create mode 100644 widget/gtk/nsClipboardX11.cpp create mode 100644 widget/gtk/nsClipboardX11.h create mode 100644 widget/gtk/nsColorPicker.cpp create mode 100644 widget/gtk/nsColorPicker.h create mode 100644 widget/gtk/nsDeviceContextSpecG.cpp create mode 100644 widget/gtk/nsDeviceContextSpecG.h create mode 100644 widget/gtk/nsDragService.cpp create mode 100644 widget/gtk/nsDragService.h create mode 100644 widget/gtk/nsFilePicker.cpp create mode 100644 widget/gtk/nsFilePicker.h create mode 100644 widget/gtk/nsGTKToolkit.h create mode 100644 widget/gtk/nsGtkCursors.h create mode 100644 widget/gtk/nsGtkKeyUtils.cpp create mode 100644 widget/gtk/nsGtkKeyUtils.h create mode 100644 widget/gtk/nsGtkUtils.h create mode 100644 widget/gtk/nsIImageToPixbuf.h create mode 100644 widget/gtk/nsImageToPixbuf.cpp create mode 100644 widget/gtk/nsImageToPixbuf.h create mode 100644 widget/gtk/nsLookAndFeel.cpp create mode 100644 widget/gtk/nsLookAndFeel.h create mode 100644 widget/gtk/nsNativeBasicThemeGTK.cpp create mode 100644 widget/gtk/nsNativeBasicThemeGTK.h create mode 100644 widget/gtk/nsNativeThemeGTK.cpp create mode 100644 widget/gtk/nsNativeThemeGTK.h create mode 100644 widget/gtk/nsPrintDialogGTK.cpp create mode 100644 widget/gtk/nsPrintDialogGTK.h create mode 100644 widget/gtk/nsPrintSettingsGTK.cpp create mode 100644 widget/gtk/nsPrintSettingsGTK.h create mode 100644 widget/gtk/nsPrintSettingsServiceGTK.cpp create mode 100644 widget/gtk/nsPrintSettingsServiceGTK.h create mode 100644 widget/gtk/nsSound.cpp create mode 100644 widget/gtk/nsSound.h create mode 100644 widget/gtk/nsToolkit.cpp create mode 100644 widget/gtk/nsUserIdleServiceGTK.cpp create mode 100644 widget/gtk/nsUserIdleServiceGTK.h create mode 100644 widget/gtk/nsWaylandDisplay.cpp create mode 100644 widget/gtk/nsWaylandDisplay.h create mode 100644 widget/gtk/nsWidgetFactory.cpp create mode 100644 widget/gtk/nsWidgetFactory.h create mode 100644 widget/gtk/nsWindow.cpp create mode 100644 widget/gtk/nsWindow.h create mode 100644 widget/gtk/wayland/gbm.h create mode 100644 widget/gtk/wayland/gtk-primary-selection-client-protocol.h create mode 100644 widget/gtk/wayland/gtk-primary-selection-protocol.c create mode 100644 widget/gtk/wayland/idle-inhibit-unstable-v1-client-protocol.h create mode 100644 widget/gtk/wayland/idle-inhibit-unstable-v1-protocol.c create mode 100644 widget/gtk/wayland/linux-dmabuf-unstable-v1-client-protocol.h create mode 100644 widget/gtk/wayland/linux-dmabuf-unstable-v1-protocol.c create mode 100644 widget/gtk/wayland/moz.build create mode 100644 widget/gtk/wayland/primary-selection-unstable-v1-client-protocol.h create mode 100644 widget/gtk/wayland/primary-selection-unstable-v1-protocol.c create mode 100644 widget/gtk/wayland/va_drmcommon.h create mode 100644 widget/gtk/wayland/xdg-output-unstable-v1-client-protocol.h create mode 100644 widget/gtk/wayland/xdg-output-unstable-v1-protocol.c (limited to 'widget/gtk') diff --git a/widget/gtk/CompositorWidgetChild.cpp b/widget/gtk/CompositorWidgetChild.cpp new file mode 100644 index 0000000000..ba51dda7a5 --- /dev/null +++ b/widget/gtk/CompositorWidgetChild.cpp @@ -0,0 +1,42 @@ +/* -*- 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 "gfxPlatform.h" + +namespace mozilla { +namespace widget { + +CompositorWidgetChild::CompositorWidgetChild( + RefPtr aVsyncDispatcher, + RefPtr aVsyncObserver, + const CompositorWidgetInitData&) + : mVsyncDispatcher(aVsyncDispatcher), mVsyncObserver(aVsyncObserver) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!gfxPlatform::IsHeadless()); +} + +CompositorWidgetChild::~CompositorWidgetChild() = default; + +bool CompositorWidgetChild::Initialize() { return true; } + +mozilla::ipc::IPCResult CompositorWidgetChild::RecvObserveVsync() { + mVsyncDispatcher->SetCompositorVsyncObserver(mVsyncObserver); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetChild::RecvUnobserveVsync() { + mVsyncDispatcher->SetCompositorVsyncObserver(nullptr); + return IPC_OK(); +} + +void CompositorWidgetChild::NotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) { + Unused << SendNotifyClientSizeChanged(aClientSize); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/CompositorWidgetChild.h b/widget/gtk/CompositorWidgetChild.h new file mode 100644 index 0000000000..76adc95baf --- /dev/null +++ b/widget/gtk/CompositorWidgetChild.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 widget_gtk_CompositorWidgetChild_h +#define widget_gtk_CompositorWidgetChild_h + +#include "GtkCompositorWidget.h" +#include "mozilla/widget/PCompositorWidgetChild.h" +#include "mozilla/widget/CompositorWidgetVsyncObserver.h" + +namespace mozilla { +namespace widget { + +class CompositorWidgetChild final : public PCompositorWidgetChild, + public PlatformCompositorWidgetDelegate { + public: + CompositorWidgetChild(RefPtr aVsyncDispatcher, + RefPtr aVsyncObserver, + const CompositorWidgetInitData&); + ~CompositorWidgetChild() override; + + bool Initialize(); + + mozilla::ipc::IPCResult RecvObserveVsync() override; + mozilla::ipc::IPCResult RecvUnobserveVsync() override; + + void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize) override; + + private: + RefPtr mVsyncDispatcher; + RefPtr mVsyncObserver; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_gtk_CompositorWidgetChild_h diff --git a/widget/gtk/CompositorWidgetParent.cpp b/widget/gtk/CompositorWidgetParent.cpp new file mode 100644 index 0000000000..adf49f3f13 --- /dev/null +++ b/widget/gtk/CompositorWidgetParent.cpp @@ -0,0 +1,44 @@ +/* -*- 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/widget/PlatformWidgetTypes.h" + +namespace mozilla { +namespace widget { + +CompositorWidgetParent::CompositorWidgetParent( + const CompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions) + : GtkCompositorWidget(aInitData.get_GtkCompositorWidgetInitData(), aOptions, + nullptr) { + MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU); +} + +CompositorWidgetParent::~CompositorWidgetParent() = default; + +void CompositorWidgetParent::ObserveVsync(VsyncObserver* aObserver) { + if (aObserver) { + Unused << SendObserveVsync(); + } else { + Unused << SendUnobserveVsync(); + } + mVsyncObserver = aObserver; +} + +RefPtr CompositorWidgetParent::GetVsyncObserver() const { + MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU); + return mVsyncObserver; +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvNotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) { + NotifyClientSizeChanged(aClientSize); + return IPC_OK(); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/CompositorWidgetParent.h b/widget/gtk/CompositorWidgetParent.h new file mode 100644 index 0000000000..87e8d31d69 --- /dev/null +++ b/widget/gtk/CompositorWidgetParent.h @@ -0,0 +1,38 @@ +/* -*- 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_gtk_CompositorWidgetParent_h +#define widget_gtk_CompositorWidgetParent_h + +#include "GtkCompositorWidget.h" +#include "mozilla/VsyncDispatcher.h" +#include "mozilla/widget/PCompositorWidgetParent.h" + +namespace mozilla { +namespace widget { + +class CompositorWidgetParent final : public PCompositorWidgetParent, + public GtkCompositorWidget { + public: + explicit CompositorWidgetParent(const CompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions); + ~CompositorWidgetParent() override; + + void ActorDestroy(ActorDestroyReason aWhy) override {} + + void ObserveVsync(VsyncObserver* aObserver) override; + RefPtr GetVsyncObserver() const override; + + mozilla::ipc::IPCResult RecvNotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) override; + + private: + RefPtr mVsyncObserver; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_gtk_CompositorWidgetParent_h diff --git a/widget/gtk/DMABufLibWrapper.cpp b/widget/gtk/DMABufLibWrapper.cpp new file mode 100644 index 0000000000..7a9ae98ff8 --- /dev/null +++ b/widget/gtk/DMABufLibWrapper.cpp @@ -0,0 +1,300 @@ +/* -*- 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 "nsWaylandDisplay.h" +#include "DMABufLibWrapper.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/gfx/gfxVars.h" + +#include +#include + +#include +#include +#include +#include + +namespace mozilla { +namespace widget { + +#define GBMLIB_NAME "libgbm.so.1" +#define DRMLIB_NAME "libdrm.so.2" + +void* nsGbmLib::sGbmLibHandle = nullptr; +void* nsGbmLib::sXf86DrmLibHandle = nullptr; +bool nsGbmLib::sLibLoaded = false; +CreateDeviceFunc nsGbmLib::sCreateDevice; +CreateFunc nsGbmLib::sCreate; +CreateWithModifiersFunc nsGbmLib::sCreateWithModifiers; +GetModifierFunc nsGbmLib::sGetModifier; +GetStrideFunc nsGbmLib::sGetStride; +GetFdFunc nsGbmLib::sGetFd; +DestroyFunc nsGbmLib::sDestroy; +MapFunc nsGbmLib::sMap; +UnmapFunc nsGbmLib::sUnmap; +GetPlaneCountFunc nsGbmLib::sGetPlaneCount; +GetHandleForPlaneFunc nsGbmLib::sGetHandleForPlane; +GetStrideForPlaneFunc nsGbmLib::sGetStrideForPlane; +GetOffsetFunc nsGbmLib::sGetOffset; +DeviceIsFormatSupportedFunc nsGbmLib::sDeviceIsFormatSupported; +DrmPrimeHandleToFDFunc nsGbmLib::sDrmPrimeHandleToFD; + +bool nsGbmLib::IsLoaded() { + return sCreateDevice != nullptr && sCreate != nullptr && + sCreateWithModifiers != nullptr && sGetModifier != nullptr && + sGetStride != nullptr && sGetFd != nullptr && sDestroy != nullptr && + sMap != nullptr && sUnmap != nullptr && sGetPlaneCount != nullptr && + sGetHandleForPlane != nullptr && sGetStrideForPlane != nullptr && + sGetOffset != nullptr && sDeviceIsFormatSupported != nullptr && + sDrmPrimeHandleToFD != nullptr; +} + +bool nsGbmLib::IsAvailable() { + if (!Load()) { + return false; + } + return IsLoaded(); +} + +bool nsGbmLib::Load() { + if (!sGbmLibHandle && !sLibLoaded) { + sLibLoaded = true; + + sGbmLibHandle = dlopen(GBMLIB_NAME, RTLD_LAZY | RTLD_LOCAL); + if (!sGbmLibHandle) { + LOGDMABUF(("Failed to load %s, dmabuf isn't available.\n", GBMLIB_NAME)); + return false; + } + + sCreateDevice = (CreateDeviceFunc)dlsym(sGbmLibHandle, "gbm_create_device"); + sCreate = (CreateFunc)dlsym(sGbmLibHandle, "gbm_bo_create"); + sCreateWithModifiers = (CreateWithModifiersFunc)dlsym( + sGbmLibHandle, "gbm_bo_create_with_modifiers"); + sGetModifier = (GetModifierFunc)dlsym(sGbmLibHandle, "gbm_bo_get_modifier"); + sGetStride = (GetStrideFunc)dlsym(sGbmLibHandle, "gbm_bo_get_stride"); + sGetFd = (GetFdFunc)dlsym(sGbmLibHandle, "gbm_bo_get_fd"); + sDestroy = (DestroyFunc)dlsym(sGbmLibHandle, "gbm_bo_destroy"); + sMap = (MapFunc)dlsym(sGbmLibHandle, "gbm_bo_map"); + sUnmap = (UnmapFunc)dlsym(sGbmLibHandle, "gbm_bo_unmap"); + sGetPlaneCount = + (GetPlaneCountFunc)dlsym(sGbmLibHandle, "gbm_bo_get_plane_count"); + sGetHandleForPlane = (GetHandleForPlaneFunc)dlsym( + sGbmLibHandle, "gbm_bo_get_handle_for_plane"); + sGetStrideForPlane = (GetStrideForPlaneFunc)dlsym( + sGbmLibHandle, "gbm_bo_get_stride_for_plane"); + sGetOffset = (GetOffsetFunc)dlsym(sGbmLibHandle, "gbm_bo_get_offset"); + sDeviceIsFormatSupported = (DeviceIsFormatSupportedFunc)dlsym( + sGbmLibHandle, "gbm_device_is_format_supported"); + + sXf86DrmLibHandle = dlopen(DRMLIB_NAME, RTLD_LAZY | RTLD_LOCAL); + if (!sXf86DrmLibHandle) { + LOGDMABUF(("Failed to load %s, dmabuf isn't available.\n", DRMLIB_NAME)); + return false; + } + sDrmPrimeHandleToFD = + (DrmPrimeHandleToFDFunc)dlsym(sXf86DrmLibHandle, "drmPrimeHandleToFD"); + if (!IsLoaded()) { + LOGDMABUF(("Failed to load all symbols from %s\n", GBMLIB_NAME)); + } + } + + return sGbmLibHandle; +} + +gbm_device* nsDMABufDevice::GetGbmDevice() { + return IsDMABufEnabled() ? mGbmDevice : nullptr; +} + +int nsDMABufDevice::GetGbmDeviceFd() { return IsDMABufEnabled() ? mGbmFd : -1; } + +static void dmabuf_modifiers(void* data, + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf, + uint32_t format, uint32_t modifier_hi, + uint32_t modifier_lo) { + auto* device = static_cast(data); + switch (format) { + case GBM_FORMAT_ARGB8888: + device->AddFormatModifier(true, format, modifier_hi, modifier_lo); + break; + case GBM_FORMAT_XRGB8888: + device->AddFormatModifier(false, format, modifier_hi, modifier_lo); + break; + default: + break; + } +} + +static void dmabuf_format(void* data, + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf, + uint32_t format) { + // XXX: deprecated +} + +static const struct zwp_linux_dmabuf_v1_listener dmabuf_listener = { + dmabuf_format, dmabuf_modifiers}; + +static void global_registry_handler(void* data, wl_registry* registry, + uint32_t id, const char* interface, + uint32_t version) { + auto* device = static_cast(data); + if (strcmp(interface, "zwp_linux_dmabuf_v1") == 0 && version > 2) { + auto* dmabuf = WaylandRegistryBind( + registry, id, &zwp_linux_dmabuf_v1_interface, 3); + LOGDMABUF(("zwp_linux_dmabuf_v1 is available.")); + device->ResetFormatsModifiers(); + zwp_linux_dmabuf_v1_add_listener(dmabuf, &dmabuf_listener, data); + } else if (strcmp(interface, "wl_drm") == 0) { + LOGDMABUF(("wl_drm is available.")); + } +} + +static void global_registry_remover(void* data, wl_registry* registry, + uint32_t id) {} + +static const struct wl_registry_listener registry_listener = { + global_registry_handler, global_registry_remover}; + +nsDMABufDevice::nsDMABufDevice() + : mXRGBFormat({true, false, GBM_FORMAT_XRGB8888, nullptr, 0}), + mARGBFormat({true, true, GBM_FORMAT_ARGB8888, nullptr, 0}), + mGbmDevice(nullptr), + mGbmFd(-1) { + if (gdk_display_get_default() && + !GDK_IS_X11_DISPLAY(gdk_display_get_default())) { + wl_display* display = WaylandDisplayGetWLDisplay(); + mRegistry = (void*)wl_display_get_registry(display); + wl_registry_add_listener((wl_registry*)mRegistry, ®istry_listener, this); + wl_display_roundtrip(display); + wl_display_roundtrip(display); + } +} + +nsDMABufDevice::~nsDMABufDevice() { + if (mRegistry) { + wl_registry_destroy((wl_registry*)mRegistry); + mRegistry = nullptr; + } +} + +bool nsDMABufDevice::Configure() { + bool isDMABufUsed = ( +#ifdef NIGHTLY_BUILD + StaticPrefs::widget_dmabuf_textures_enabled() || +#endif + StaticPrefs::widget_dmabuf_webgl_enabled() || + StaticPrefs::media_ffmpeg_vaapi_enabled() || + StaticPrefs::media_ffmpeg_vaapi_drm_display_enabled()); + + if (!isDMABufUsed) { + // Disabled by user, just quit. + LOGDMABUF(("IsDMABufEnabled(): Disabled by preferences.")); + return false; + } + + if (!nsGbmLib::IsAvailable()) { + LOGDMABUF(("nsGbmLib is not available!")); + return false; + } + + nsAutoCString drm_render_node(getenv("MOZ_WAYLAND_DRM_DEVICE")); + if (drm_render_node.IsEmpty()) { + drm_render_node.Assign(gfx::gfxVars::DrmRenderDevice()); + if (drm_render_node.IsEmpty()) { + return false; + } + } + + mGbmFd = open(drm_render_node.get(), O_RDWR); + if (mGbmFd < 0) { + LOGDMABUF(("Failed to open drm render node %s\n", drm_render_node.get())); + return false; + } + + mGbmDevice = nsGbmLib::CreateDevice(mGbmFd); + if (!mGbmDevice) { + LOGDMABUF( + ("Failed to create drm render device %s\n", drm_render_node.get())); + close(mGbmFd); + mGbmFd = -1; + return false; + } + + LOGDMABUF(("GBM device initialized")); + return true; +} + +bool nsDMABufDevice::IsDMABufEnabled() { + static bool isDMABufEnabled = Configure(); + return isDMABufEnabled; +} + +#ifdef NIGHTLY_BUILD +bool nsDMABufDevice::IsDMABufTexturesEnabled() { + return gfx::gfxVars::UseEGL() && IsDMABufEnabled() && + StaticPrefs::widget_dmabuf_textures_enabled(); +} +#else +bool nsDMABufDevice::IsDMABufTexturesEnabled() { return false; } +#endif +bool nsDMABufDevice::IsDMABufVAAPIEnabled() { + return gfx::gfxVars::UseEGL() && IsDMABufEnabled() && + StaticPrefs::media_ffmpeg_vaapi_enabled() && + gfx::gfxVars::CanUseHardwareVideoDecoding() && !XRE_IsRDDProcess(); +} +bool nsDMABufDevice::IsDMABufWebGLEnabled() { + return gfx::gfxVars::UseEGL() && IsDMABufEnabled() && + StaticPrefs::widget_dmabuf_webgl_enabled(); +} + +GbmFormat* nsDMABufDevice::GetGbmFormat(bool aHasAlpha) { + GbmFormat* format = aHasAlpha ? &mARGBFormat : &mXRGBFormat; + return format->mIsSupported ? format : nullptr; +} + +GbmFormat* nsDMABufDevice::GetExactGbmFormat(int aFormat) { + if (aFormat == mARGBFormat.mFormat) { + return &mARGBFormat; + } else if (aFormat == mXRGBFormat.mFormat) { + return &mXRGBFormat; + } + + return nullptr; +} + +void nsDMABufDevice::AddFormatModifier(bool aHasAlpha, int aFormat, + uint32_t mModifierHi, + uint32_t mModifierLo) { + GbmFormat* format = aHasAlpha ? &mARGBFormat : &mXRGBFormat; + format->mIsSupported = true; + format->mHasAlpha = aHasAlpha; + format->mFormat = aFormat; + format->mModifiersCount++; + format->mModifiers = + (uint64_t*)realloc(format->mModifiers, + format->mModifiersCount * sizeof(*format->mModifiers)); + format->mModifiers[format->mModifiersCount - 1] = + ((uint64_t)mModifierHi << 32) | mModifierLo; +} + +void nsDMABufDevice::ResetFormatsModifiers() { + mARGBFormat.mModifiersCount = 0; + free(mARGBFormat.mModifiers); + mARGBFormat.mModifiers = nullptr; + + mXRGBFormat.mModifiersCount = 0; + free(mXRGBFormat.mModifiers); + mXRGBFormat.mModifiers = nullptr; +} + +nsDMABufDevice* GetDMABufDevice() { + static nsDMABufDevice dmaBufDevice; + return &dmaBufDevice; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/DMABufLibWrapper.h b/widget/gtk/DMABufLibWrapper.h new file mode 100644 index 0000000000..99e82609ef --- /dev/null +++ b/widget/gtk/DMABufLibWrapper.h @@ -0,0 +1,168 @@ +/* -*- 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 __MOZ_DMABUF_LIB_WRAPPER_H__ +#define __MOZ_DMABUF_LIB_WRAPPER_H__ + +#include "mozilla/widget/gbm.h" + +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +extern mozilla::LazyLogModule gDmabufLog; +# define LOGDMABUF(args) MOZ_LOG(gDmabufLog, mozilla::LogLevel::Debug, args) +#else +# define LOGDMABUF(args) +#endif /* MOZ_LOGGING */ + +namespace mozilla { +namespace widget { + +typedef struct gbm_device* (*CreateDeviceFunc)(int); +typedef struct gbm_bo* (*CreateFunc)(struct gbm_device*, uint32_t, uint32_t, + uint32_t, uint32_t); +typedef struct gbm_bo* (*CreateWithModifiersFunc)(struct gbm_device*, uint32_t, + uint32_t, uint32_t, + const uint64_t*, + const unsigned int); +typedef uint64_t (*GetModifierFunc)(struct gbm_bo*); +typedef uint32_t (*GetStrideFunc)(struct gbm_bo*); +typedef int (*GetFdFunc)(struct gbm_bo*); +typedef void (*DestroyFunc)(struct gbm_bo*); +typedef void* (*MapFunc)(struct gbm_bo*, uint32_t, uint32_t, uint32_t, uint32_t, + uint32_t, uint32_t*, void**); +typedef void (*UnmapFunc)(struct gbm_bo*, void*); +typedef int (*GetPlaneCountFunc)(struct gbm_bo*); +typedef union gbm_bo_handle (*GetHandleForPlaneFunc)(struct gbm_bo*, int); +typedef uint32_t (*GetStrideForPlaneFunc)(struct gbm_bo*, int); +typedef uint32_t (*GetOffsetFunc)(struct gbm_bo*, int); +typedef int (*DeviceIsFormatSupportedFunc)(struct gbm_device*, uint32_t, + uint32_t); +typedef int (*DrmPrimeHandleToFDFunc)(int, uint32_t, uint32_t, int*); + +class nsGbmLib { + public: + static bool Load(); + static bool IsLoaded(); + static bool IsAvailable(); + static bool IsModifierAvailable(); + + static struct gbm_device* CreateDevice(int fd) { return sCreateDevice(fd); }; + static struct gbm_bo* Create(struct gbm_device* gbm, uint32_t width, + uint32_t height, uint32_t format, + uint32_t flags) { + return sCreate(gbm, width, height, format, flags); + } + static void Destroy(struct gbm_bo* bo) { sDestroy(bo); } + static uint32_t GetStride(struct gbm_bo* bo) { return sGetStride(bo); } + static int GetFd(struct gbm_bo* bo) { return sGetFd(bo); } + static void* Map(struct gbm_bo* bo, uint32_t x, uint32_t y, uint32_t width, + uint32_t height, uint32_t flags, uint32_t* stride, + void** map_data) { + return sMap(bo, x, y, width, height, flags, stride, map_data); + } + static void Unmap(struct gbm_bo* bo, void* map_data) { sUnmap(bo, map_data); } + static struct gbm_bo* CreateWithModifiers(struct gbm_device* gbm, + uint32_t width, uint32_t height, + uint32_t format, + const uint64_t* modifiers, + const unsigned int count) { + return sCreateWithModifiers(gbm, width, height, format, modifiers, count); + } + static uint64_t GetModifier(struct gbm_bo* bo) { return sGetModifier(bo); } + static int GetPlaneCount(struct gbm_bo* bo) { return sGetPlaneCount(bo); } + static union gbm_bo_handle GetHandleForPlane(struct gbm_bo* bo, int plane) { + return sGetHandleForPlane(bo, plane); + } + static uint32_t GetStrideForPlane(struct gbm_bo* bo, int plane) { + return sGetStrideForPlane(bo, plane); + } + static uint32_t GetOffset(struct gbm_bo* bo, int plane) { + return sGetOffset(bo, plane); + } + static int DeviceIsFormatSupported(struct gbm_device* gbm, uint32_t format, + uint32_t usage) { + return sDeviceIsFormatSupported(gbm, format, usage); + } + + static int DrmPrimeHandleToFD(int fd, uint32_t handle, uint32_t flags, + int* prime_fd) { + return sDrmPrimeHandleToFD(fd, handle, flags, prime_fd); + } + + private: + static CreateDeviceFunc sCreateDevice; + static CreateFunc sCreate; + static CreateWithModifiersFunc sCreateWithModifiers; + static GetModifierFunc sGetModifier; + static GetStrideFunc sGetStride; + static GetFdFunc sGetFd; + static DestroyFunc sDestroy; + static MapFunc sMap; + static UnmapFunc sUnmap; + static GetPlaneCountFunc sGetPlaneCount; + static GetHandleForPlaneFunc sGetHandleForPlane; + static GetStrideForPlaneFunc sGetStrideForPlane; + static GetOffsetFunc sGetOffset; + static DeviceIsFormatSupportedFunc sDeviceIsFormatSupported; + static DrmPrimeHandleToFDFunc sDrmPrimeHandleToFD; + + static void* sGbmLibHandle; + static void* sXf86DrmLibHandle; + static bool sLibLoaded; +}; + +struct GbmFormat { + bool mIsSupported; + bool mHasAlpha; + int mFormat; + uint64_t* mModifiers; + int mModifiersCount; +}; + +class nsDMABufDevice { + public: + nsDMABufDevice(); + ~nsDMABufDevice(); + + gbm_device* GetGbmDevice(); + // Returns -1 if we fails to gbm device file descriptor. + int GetGbmDeviceFd(); + + // Use dmabuf for WebRender general web content + bool IsDMABufTexturesEnabled(); + // Use dmabuf for VA-API video playback + bool IsDMABufVAAPIEnabled(); + // Use dmabuf for WebGL content + bool IsDMABufWebGLEnabled(); + + GbmFormat* GetGbmFormat(bool aHasAlpha); + GbmFormat* GetExactGbmFormat(int aFormat); + void ResetFormatsModifiers(); + void AddFormatModifier(bool aHasAlpha, int aFormat, uint32_t mModifierHi, + uint32_t mModifierLo); + + private: + bool IsDMABufEnabled(); + bool Configure(); + + void* mRegistry; + + GbmFormat mXRGBFormat; + GbmFormat mARGBFormat; + + gbm_device* mGbmDevice; + int mGbmFd; +}; + +nsDMABufDevice* GetDMABufDevice(); + +} // namespace widget +} // namespace mozilla + +#endif // __MOZ_DMABUF_LIB_WRAPPER_H__ diff --git a/widget/gtk/DMABufSurface.cpp b/widget/gtk/DMABufSurface.cpp new file mode 100644 index 0000000000..1e1719780f --- /dev/null +++ b/widget/gtk/DMABufSurface.cpp @@ -0,0 +1,1002 @@ +/* -*- 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 "DMABufSurface.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mozilla/widget/gbm.h" +#include "mozilla/widget/va_drmcommon.h" +#include "GLContextTypes.h" // for GLContext, etc +#include "GLContextEGL.h" +#include "GLContextProvider.h" +#include "ScopedGLHelpers.h" + +#include "mozilla/layers/LayersSurfaces.h" +#include "mozilla/ScopeExit.h" + +/* +TODO: +DRM device selection: +https://lists.freedesktop.org/archives/wayland-devel/2018-November/039660.html +*/ + +/* C++ / C typecast macros for special EGL handle values */ +#if defined(__cplusplus) +# define EGL_CAST(type, value) (static_cast(value)) +#else +# define EGL_CAST(type, value) ((type)(value)) +#endif + +using namespace mozilla; +using namespace mozilla::widget; +using namespace mozilla::gl; +using namespace mozilla::layers; + +#ifndef DRM_FORMAT_MOD_INVALID +# define DRM_FORMAT_MOD_INVALID ((1ULL << 56) - 1) +#endif +#define BUFFER_FLAGS 0 + +#ifndef GBM_BO_USE_TEXTURING +# define GBM_BO_USE_TEXTURING (1 << 5) +#endif + +#ifndef VA_FOURCC_NV12 +# define VA_FOURCC_NV12 0x3231564E +#endif + +#ifndef VA_FOURCC_YV12 +# define VA_FOURCC_YV12 0x32315659 +#endif + +static Atomic gNewSurfaceUID(1); + +bool DMABufSurface::IsGlobalRefSet() const { + if (!mGlobalRefCountFd) { + return false; + } + struct pollfd pfd; + pfd.fd = mGlobalRefCountFd; + pfd.events = POLLIN; + return poll(&pfd, 1, 0) == 1; +} + +void DMABufSurface::GlobalRefRelease() { + MOZ_ASSERT(mGlobalRefCountFd); + uint64_t counter; + if (read(mGlobalRefCountFd, &counter, sizeof(counter)) != sizeof(counter)) { + // EAGAIN means the refcount is already zero. It happens when we release + // last reference to the surface. + if (errno != EAGAIN) { + NS_WARNING("Failed to unref dmabuf global ref count!"); + } + } +} + +void DMABufSurface::GlobalRefAdd() { + MOZ_ASSERT(mGlobalRefCountFd); + uint64_t counter = 1; + if (write(mGlobalRefCountFd, &counter, sizeof(counter)) != sizeof(counter)) { + NS_WARNING("Failed to ref dmabuf global ref count!"); + } +} + +void DMABufSurface::GlobalRefCountCreate() { + MOZ_ASSERT(!mGlobalRefCountFd); + mGlobalRefCountFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK | EFD_SEMAPHORE); + if (mGlobalRefCountFd < 0) { + NS_WARNING("Failed to create dmabuf global ref count!"); + mGlobalRefCountFd = 0; + return; + } +} + +void DMABufSurface::GlobalRefCountImport(int aFd) { + MOZ_ASSERT(!mGlobalRefCountFd); + mGlobalRefCountFd = aFd; + GlobalRefAdd(); +} + +void DMABufSurface::GlobalRefCountDelete() { + if (mGlobalRefCountFd) { + GlobalRefRelease(); + close(mGlobalRefCountFd); + mGlobalRefCountFd = 0; + } +} + +void DMABufSurface::ReleaseDMABuf() { + for (int i = 0; i < mBufferPlaneCount; i++) { + Unmap(i); + + if (mDmabufFds[i] >= 0) { + close(mDmabufFds[i]); + mDmabufFds[i] = -1; + } + } + + if (mGbmBufferObject[0]) { + nsGbmLib::Destroy(mGbmBufferObject[0]); + mGbmBufferObject[0] = nullptr; + } +} + +DMABufSurface::DMABufSurface(SurfaceType aSurfaceType) + : mSurfaceType(aSurfaceType), + mBufferModifier(DRM_FORMAT_MOD_INVALID), + mBufferPlaneCount(0), + mDrmFormats(), + mStrides(), + mOffsets(), + mGbmBufferObject(), + mMappedRegion(), + mMappedRegionStride(), + mSyncFd(-1), + mSync(0), + mGlobalRefCountFd(0), + mUID(gNewSurfaceUID++) { + for (auto& slot : mDmabufFds) { + slot = -1; + } +} + +DMABufSurface::~DMABufSurface() { + FenceDelete(); + GlobalRefCountDelete(); +} + +already_AddRefed DMABufSurface::CreateDMABufSurface( + const mozilla::layers::SurfaceDescriptor& aDesc) { + const SurfaceDescriptorDMABuf& desc = aDesc.get_SurfaceDescriptorDMABuf(); + RefPtr surf; + + switch (desc.bufferType()) { + case SURFACE_RGBA: + surf = new DMABufSurfaceRGBA(); + break; + case SURFACE_NV12: + case SURFACE_YUV420: + surf = new DMABufSurfaceYUV(); + break; + default: + return nullptr; + } + + if (!surf->Create(desc)) { + return nullptr; + } + return surf.forget(); +} + +void DMABufSurface::FenceDelete() { + if (!mGL) return; + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (mSyncFd > 0) { + close(mSyncFd); + mSyncFd = -1; + } + + if (mSync) { + egl->fDestroySync(mSync); + mSync = nullptr; + } +} + +void DMABufSurface::FenceSet() { + if (!mGL || !mGL->MakeCurrent()) { + return; + } + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (egl->IsExtensionSupported(EGLExtension::KHR_fence_sync) && + egl->IsExtensionSupported(EGLExtension::ANDROID_native_fence_sync)) { + FenceDelete(); + + mSync = egl->fCreateSync(LOCAL_EGL_SYNC_NATIVE_FENCE_ANDROID, nullptr); + if (mSync) { + mSyncFd = egl->fDupNativeFenceFDANDROID(mSync); + mGL->fFlush(); + return; + } + } + + // ANDROID_native_fence_sync may not be supported so call glFinish() + // as a slow path. + mGL->fFinish(); +} + +void DMABufSurface::FenceWait() { + if (!mGL) return; + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (!mSync && mSyncFd > 0) { + FenceImportFromFd(); + } + + // Wait on the fence, because presumably we're going to want to read this + // surface + if (mSync) { + egl->fClientWaitSync(mSync, 0, LOCAL_EGL_FOREVER); + } +} + +bool DMABufSurface::FenceImportFromFd() { + if (!mGL) return false; + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + const EGLint attribs[] = {LOCAL_EGL_SYNC_NATIVE_FENCE_FD_ANDROID, mSyncFd, + LOCAL_EGL_NONE}; + mSync = egl->fCreateSync(LOCAL_EGL_SYNC_NATIVE_FENCE_ANDROID, attribs); + close(mSyncFd); + mSyncFd = -1; + + if (!mSync) { + MOZ_ASSERT(false, "Failed to create GLFence!"); + return false; + } + + return true; +} + +DMABufSurfaceRGBA::DMABufSurfaceRGBA() + : DMABufSurface(SURFACE_RGBA), + mSurfaceFlags(0), + mWidth(0), + mHeight(0), + mGmbFormat(nullptr), + mEGLImage(LOCAL_EGL_NO_IMAGE), + mTexture(0), + mGbmBufferFlags(0) {} + +DMABufSurfaceRGBA::~DMABufSurfaceRGBA() { ReleaseSurface(); } + +bool DMABufSurfaceRGBA::Create(int aWidth, int aHeight, + int aDMABufSurfaceFlags) { + MOZ_ASSERT(mGbmBufferObject[0] == nullptr, "Already created?"); + + mSurfaceFlags = aDMABufSurfaceFlags; + mWidth = aWidth; + mHeight = aHeight; + + LOGDMABUF(("DMABufSurfaceRGBA::Create() UID %d size %d x %d\n", mUID, mWidth, + mHeight)); + + mGmbFormat = GetDMABufDevice()->GetGbmFormat(mSurfaceFlags & DMABUF_ALPHA); + if (!mGmbFormat) { + // Requested DRM format is not supported. + return false; + } + + bool useModifiers = (aDMABufSurfaceFlags & DMABUF_USE_MODIFIERS) && + mGmbFormat->mModifiersCount > 0; + if (useModifiers) { + LOGDMABUF((" Creating with modifiers\n")); + mGbmBufferObject[0] = nsGbmLib::CreateWithModifiers( + GetDMABufDevice()->GetGbmDevice(), mWidth, mHeight, mGmbFormat->mFormat, + mGmbFormat->mModifiers, mGmbFormat->mModifiersCount); + if (mGbmBufferObject[0]) { + mBufferModifier = nsGbmLib::GetModifier(mGbmBufferObject[0]); + } + } + + // Create without modifiers - use plain/linear format. + if (!mGbmBufferObject[0]) { + LOGDMABUF((" Creating without modifiers\n")); + mGbmBufferFlags = (GBM_BO_USE_SCANOUT | GBM_BO_USE_LINEAR); + if (mSurfaceFlags & DMABUF_TEXTURE) { + mGbmBufferFlags |= GBM_BO_USE_TEXTURING; + } + + if (!nsGbmLib::DeviceIsFormatSupported(GetDMABufDevice()->GetGbmDevice(), + mGmbFormat->mFormat, + mGbmBufferFlags)) { + mGbmBufferFlags &= ~GBM_BO_USE_SCANOUT; + } + + mGbmBufferObject[0] = + nsGbmLib::Create(GetDMABufDevice()->GetGbmDevice(), mWidth, mHeight, + mGmbFormat->mFormat, mGbmBufferFlags); + + mBufferModifier = DRM_FORMAT_MOD_INVALID; + } + + if (!mGbmBufferObject[0]) { + LOGDMABUF((" Failed to create GbmBufferObject\n")); + return false; + } + + if (mBufferModifier != DRM_FORMAT_MOD_INVALID) { + mBufferPlaneCount = nsGbmLib::GetPlaneCount(mGbmBufferObject[0]); + if (mBufferPlaneCount > DMABUF_BUFFER_PLANES) { + NS_WARNING("There's too many dmabuf planes!"); + ReleaseSurface(); + return false; + } + + for (int i = 0; i < mBufferPlaneCount; i++) { + uint32_t handle = nsGbmLib::GetHandleForPlane(mGbmBufferObject[0], i).u32; + int ret = nsGbmLib::DrmPrimeHandleToFD( + GetDMABufDevice()->GetGbmDeviceFd(), handle, 0, &mDmabufFds[i]); + if (ret < 0 || mDmabufFds[i] < 0) { + ReleaseSurface(); + return false; + } + mStrides[i] = nsGbmLib::GetStrideForPlane(mGbmBufferObject[0], i); + mOffsets[i] = nsGbmLib::GetOffset(mGbmBufferObject[0], i); + } + } else { + mBufferPlaneCount = 1; + mStrides[0] = nsGbmLib::GetStride(mGbmBufferObject[0]); + mDmabufFds[0] = nsGbmLib::GetFd(mGbmBufferObject[0]); + if (mDmabufFds[0] < 0) { + ReleaseSurface(); + return false; + } + } + + LOGDMABUF((" Success\n")); + return true; +} + +void DMABufSurfaceRGBA::ImportSurfaceDescriptor( + const SurfaceDescriptor& aDesc) { + const SurfaceDescriptorDMABuf& desc = aDesc.get_SurfaceDescriptorDMABuf(); + + mWidth = desc.width()[0]; + mHeight = desc.height()[0]; + mBufferModifier = desc.modifier(); + if (mBufferModifier != DRM_FORMAT_MOD_INVALID) { + mGmbFormat = GetDMABufDevice()->GetExactGbmFormat(desc.format()[0]); + } else { + mDrmFormats[0] = desc.format()[0]; + } + mBufferPlaneCount = desc.fds().Length(); + mGbmBufferFlags = desc.flags(); + MOZ_RELEASE_ASSERT(mBufferPlaneCount <= DMABUF_BUFFER_PLANES); + mUID = desc.uid(); + + for (int i = 0; i < mBufferPlaneCount; i++) { + mDmabufFds[i] = desc.fds()[i].ClonePlatformHandle().release(); + mStrides[i] = desc.strides()[i]; + mOffsets[i] = desc.offsets()[i]; + } + + if (desc.fence().Length() > 0) { + mSyncFd = desc.fence()[0].ClonePlatformHandle().release(); + } + + if (desc.refCount().Length() > 0) { + GlobalRefCountImport(desc.refCount()[0].ClonePlatformHandle().release()); + } + + LOGDMABUF(("DMABufSurfaceRGBA::Import() UID %d\n", mUID)); +} + +bool DMABufSurfaceRGBA::Create(const SurfaceDescriptor& aDesc) { + ImportSurfaceDescriptor(aDesc); + return true; +} + +bool DMABufSurfaceRGBA::Serialize( + mozilla::layers::SurfaceDescriptor& aOutDescriptor) { + AutoTArray width; + AutoTArray height; + AutoTArray format; + AutoTArray fds; + AutoTArray strides; + AutoTArray offsets; + AutoTArray images; + AutoTArray fenceFDs; + AutoTArray refCountFDs; + + LOGDMABUF(("DMABufSurfaceRGBA::Serialize() UID %d\n", mUID)); + + width.AppendElement(mWidth); + height.AppendElement(mHeight); + format.AppendElement(mGmbFormat->mFormat); + for (int i = 0; i < mBufferPlaneCount; i++) { + fds.AppendElement(ipc::FileDescriptor(mDmabufFds[i])); + strides.AppendElement(mStrides[i]); + offsets.AppendElement(mOffsets[i]); + } + + if (mSync) { + fenceFDs.AppendElement(ipc::FileDescriptor(mSyncFd)); + } + + if (mGlobalRefCountFd) { + refCountFDs.AppendElement(ipc::FileDescriptor(mGlobalRefCountFd)); + } + + aOutDescriptor = + SurfaceDescriptorDMABuf(mSurfaceType, mBufferModifier, mGbmBufferFlags, + fds, width, height, format, strides, offsets, + GetYUVColorSpace(), fenceFDs, mUID, refCountFDs); + return true; +} + +bool DMABufSurfaceRGBA::CreateTexture(GLContext* aGLContext, int aPlane) { + MOZ_ASSERT(!mEGLImage && !mTexture, "EGLImage is already created!"); + + nsTArray attribs; + attribs.AppendElement(LOCAL_EGL_WIDTH); + attribs.AppendElement(mWidth); + attribs.AppendElement(LOCAL_EGL_HEIGHT); + attribs.AppendElement(mHeight); + attribs.AppendElement(LOCAL_EGL_LINUX_DRM_FOURCC_EXT); + if (mGmbFormat) { + attribs.AppendElement(mGmbFormat->mFormat); + } else { + attribs.AppendElement(mDrmFormats[0]); + } +#define ADD_PLANE_ATTRIBS(plane_idx) \ + { \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_FD_EXT); \ + attribs.AppendElement(mDmabufFds[plane_idx]); \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_OFFSET_EXT); \ + attribs.AppendElement((int)mOffsets[plane_idx]); \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_PITCH_EXT); \ + attribs.AppendElement((int)mStrides[plane_idx]); \ + if (mBufferModifier != DRM_FORMAT_MOD_INVALID) { \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_LO_EXT); \ + attribs.AppendElement(mBufferModifier & 0xFFFFFFFF); \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_HI_EXT); \ + attribs.AppendElement(mBufferModifier >> 32); \ + } \ + } + ADD_PLANE_ATTRIBS(0); + if (mBufferPlaneCount > 1) ADD_PLANE_ATTRIBS(1); + if (mBufferPlaneCount > 2) ADD_PLANE_ATTRIBS(2); + if (mBufferPlaneCount > 3) ADD_PLANE_ATTRIBS(3); +#undef ADD_PLANE_ATTRIBS + attribs.AppendElement(LOCAL_EGL_NONE); + + if (!aGLContext) return false; + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + mEGLImage = + egl->fCreateImage(LOCAL_EGL_NO_CONTEXT, LOCAL_EGL_LINUX_DMA_BUF_EXT, + nullptr, attribs.Elements()); + if (mEGLImage == LOCAL_EGL_NO_IMAGE) { + NS_WARNING("EGLImageKHR creation failed"); + return false; + } + + aGLContext->MakeCurrent(); + aGLContext->fGenTextures(1, &mTexture); + const ScopedBindTexture savedTex(aGLContext, mTexture); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, + LOCAL_GL_CLAMP_TO_EDGE); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, + LOCAL_GL_CLAMP_TO_EDGE); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, + LOCAL_GL_LINEAR); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, + LOCAL_GL_LINEAR); + aGLContext->fEGLImageTargetTexture2D(LOCAL_GL_TEXTURE_2D, mEGLImage); + mGL = aGLContext; + + return true; +} + +void DMABufSurfaceRGBA::ReleaseTextures() { + FenceDelete(); + + if (!mGL) return; + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (mTexture && mGL->MakeCurrent()) { + mGL->fDeleteTextures(1, &mTexture); + mTexture = 0; + mGL = nullptr; + } + + if (mEGLImage) { + egl->fDestroyImage(mEGLImage); + mEGLImage = nullptr; + } +} + +void DMABufSurfaceRGBA::ReleaseSurface() { + MOZ_ASSERT(!IsMapped(), "We can't release mapped buffer!"); + + ReleaseTextures(); + ReleaseDMABuf(); +} + +void* DMABufSurface::MapInternal(uint32_t aX, uint32_t aY, uint32_t aWidth, + uint32_t aHeight, uint32_t* aStride, + int aGbmFlags, int aPlane) { + NS_ASSERTION(!IsMapped(aPlane), "Already mapped!"); + if (!mGbmBufferObject[aPlane]) { + NS_WARNING("We can't map DMABufSurfaceRGBA without mGbmBufferObject"); + return nullptr; + } + + LOGDMABUF( + ("DMABufSurfaceRGBA::MapInternal() UID %d size %d x %d -> %d x %d\n", + mUID, aX, aY, aWidth, aHeight)); + + mMappedRegionStride[aPlane] = 0; + mMappedRegionData[aPlane] = nullptr; + mMappedRegion[aPlane] = nsGbmLib::Map( + mGbmBufferObject[aPlane], aX, aY, aWidth, aHeight, aGbmFlags, + &mMappedRegionStride[aPlane], &mMappedRegionData[aPlane]); + if (aStride) { + *aStride = mMappedRegionStride[aPlane]; + } + return mMappedRegion[aPlane]; +} + +void* DMABufSurfaceRGBA::MapReadOnly(uint32_t aX, uint32_t aY, uint32_t aWidth, + uint32_t aHeight, uint32_t* aStride) { + return MapInternal(aX, aY, aWidth, aHeight, aStride, GBM_BO_TRANSFER_READ); +} + +void* DMABufSurfaceRGBA::MapReadOnly(uint32_t* aStride) { + return MapInternal(0, 0, mWidth, mHeight, aStride, GBM_BO_TRANSFER_READ); +} + +void* DMABufSurfaceRGBA::Map(uint32_t aX, uint32_t aY, uint32_t aWidth, + uint32_t aHeight, uint32_t* aStride) { + return MapInternal(aX, aY, aWidth, aHeight, aStride, + GBM_BO_TRANSFER_READ_WRITE); +} + +void* DMABufSurfaceRGBA::Map(uint32_t* aStride) { + return MapInternal(0, 0, mWidth, mHeight, aStride, + GBM_BO_TRANSFER_READ_WRITE); +} + +void DMABufSurface::Unmap(int aPlane) { + if (mMappedRegion[aPlane]) { + nsGbmLib::Unmap(mGbmBufferObject[aPlane], mMappedRegionData[aPlane]); + mMappedRegion[aPlane] = nullptr; + mMappedRegionData[aPlane] = nullptr; + mMappedRegionStride[aPlane] = 0; + } +} + +#ifdef DEBUG +void DMABufSurfaceRGBA::DumpToFile(const char* pFile) { + uint32_t stride; + + if (!MapReadOnly(&stride)) { + return; + } + cairo_surface_t* surface = nullptr; + + auto unmap = MakeScopeExit([&] { + if (surface) { + cairo_surface_destroy(surface); + } + Unmap(); + }); + + surface = cairo_image_surface_create_for_data( + (unsigned char*)mMappedRegion[0], CAIRO_FORMAT_ARGB32, mWidth, mHeight, + stride); + if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) { + cairo_surface_write_to_png(surface, pFile); + } +} +#endif + +#if 0 +// Copy from source surface by GL +# include "GLBlitHelper.h" + +bool DMABufSurfaceRGBA::CopyFrom(class DMABufSurface* aSourceSurface, + GLContext* aGLContext) { + MOZ_ASSERT(aSourceSurface->GetTexture()); + MOZ_ASSERT(GetTexture()); + + gfx::IntSize size(GetWidth(), GetHeight()); + aGLContext->BlitHelper()->BlitTextureToTexture(aSourceSurface->GetTexture(), + GetTexture(), size, size); + return true; +} +#endif + +// TODO - Clear the surface by EGL +void DMABufSurfaceRGBA::Clear() { + uint32_t destStride; + void* destData = Map(&destStride); + memset(destData, 0, GetHeight() * destStride); + Unmap(); +} + +bool DMABufSurfaceRGBA::HasAlpha() { + return mGmbFormat ? mGmbFormat->mHasAlpha : true; +} + +gfx::SurfaceFormat DMABufSurfaceRGBA::GetFormat() { + return HasAlpha() ? gfx::SurfaceFormat::B8G8R8A8 + : gfx::SurfaceFormat::B8G8R8X8; +} + +// GL uses swapped R and B components so report accordingly. +gfx::SurfaceFormat DMABufSurfaceRGBA::GetFormatGL() { + return HasAlpha() ? gfx::SurfaceFormat::R8G8B8A8 + : gfx::SurfaceFormat::R8G8B8X8; +} + +already_AddRefed DMABufSurfaceRGBA::CreateDMABufSurface( + int aWidth, int aHeight, int aDMABufSurfaceFlags) { + RefPtr surf = new DMABufSurfaceRGBA(); + if (!surf->Create(aWidth, aHeight, aDMABufSurfaceFlags)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed DMABufSurfaceYUV::CreateYUVSurface( + const VADRMPRIMESurfaceDescriptor& aDesc) { + RefPtr surf = new DMABufSurfaceYUV(); + if (!surf->UpdateYUVData(aDesc)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed DMABufSurfaceYUV::CreateYUVSurface( + int aWidth, int aHeight, void** aPixelData, int* aLineSizes) { + RefPtr surf = new DMABufSurfaceYUV(); + if (!surf->Create(aWidth, aHeight, aPixelData, aLineSizes)) { + return nullptr; + } + return surf.forget(); +} + +DMABufSurfaceYUV::DMABufSurfaceYUV() + : DMABufSurface(SURFACE_NV12), + mWidth(), + mHeight(), + mTexture(), + mColorSpace(mozilla::gfx::YUVColorSpace::UNKNOWN) { + for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) { + mEGLImage[i] = LOCAL_EGL_NO_IMAGE; + } +} + +DMABufSurfaceYUV::~DMABufSurfaceYUV() { ReleaseSurface(); } + +bool DMABufSurfaceYUV::UpdateYUVData(const VADRMPRIMESurfaceDescriptor& aDesc) { + if (aDesc.num_layers > DMABUF_BUFFER_PLANES || + aDesc.num_objects > DMABUF_BUFFER_PLANES) { + return false; + } + if (mDmabufFds[0] >= 0) { + NS_WARNING("DMABufSurfaceYUV is already created!"); + return false; + } + if (aDesc.fourcc == VA_FOURCC_NV12) { + mSurfaceType = SURFACE_NV12; + } else if (aDesc.fourcc == VA_FOURCC_YV12) { + mSurfaceType = SURFACE_YUV420; + } else { + NS_WARNING( + nsPrintfCString( + "UpdateYUVData(): Can't import surface data of 0x%x format\n", + aDesc.fourcc) + .get()); + return false; + } + + mBufferPlaneCount = aDesc.num_layers; + mBufferModifier = aDesc.objects[0].drm_format_modifier; + + for (unsigned int i = 0; i < aDesc.num_layers; i++) { + // Intel exports VA-API surfaces in one object,planes have the same FD. + // AMD exports surfaces in two objects with different FDs. + bool dupFD = (aDesc.layers[i].object_index[0] != i); + int fd = aDesc.objects[aDesc.layers[i].object_index[0]].fd; + mDmabufFds[i] = dupFD ? dup(fd) : fd; + + mDrmFormats[i] = aDesc.layers[i].drm_format; + mOffsets[i] = aDesc.layers[i].offset[0]; + mStrides[i] = aDesc.layers[i].pitch[0]; + mWidth[i] = aDesc.width >> i; + mHeight[i] = aDesc.height >> i; + } + + return true; +} + +bool DMABufSurfaceYUV::CreateYUVPlane(int aPlane, int aWidth, int aHeight, + int aDrmFormat) { + mWidth[aPlane] = aWidth; + mHeight[aPlane] = aHeight; + mDrmFormats[aPlane] = aDrmFormat; + + mGbmBufferObject[aPlane] = + nsGbmLib::Create(GetDMABufDevice()->GetGbmDevice(), aWidth, aHeight, + aDrmFormat, GBM_BO_USE_LINEAR | GBM_BO_USE_TEXTURING); + if (!mGbmBufferObject[aPlane]) { + NS_WARNING("Failed to create GbmBufferObject!"); + return false; + } + + mStrides[aPlane] = nsGbmLib::GetStride(mGbmBufferObject[aPlane]); + mDmabufFds[aPlane] = nsGbmLib::GetFd(mGbmBufferObject[aPlane]); + + return true; +} + +void DMABufSurfaceYUV::UpdateYUVPlane(int aPlane, void* aPixelData, + int aLineSize) { + if (aLineSize == mWidth[aPlane] && + (int)mMappedRegionStride[aPlane] == mWidth[aPlane]) { + memcpy(mMappedRegion[aPlane], aPixelData, aLineSize * mHeight[aPlane]); + } else { + char* src = (char*)aPixelData; + char* dest = (char*)mMappedRegion[aPlane]; + for (int i = 0; i < mHeight[aPlane]; i++) { + memcpy(dest, src, mWidth[aPlane]); + src += aLineSize; + dest += mMappedRegionStride[aPlane]; + } + } +} + +bool DMABufSurfaceYUV::UpdateYUVData(void** aPixelData, int* aLineSizes) { + if (mSurfaceType != SURFACE_YUV420) { + NS_WARNING("UpdateYUVData can upload YUV420 surface type only!"); + return false; + } + + if (mBufferPlaneCount != 3) { + NS_WARNING("DMABufSurfaceYUV planes does not match!"); + return false; + } + + auto unmapBuffers = MakeScopeExit([&] { + Unmap(0); + Unmap(1); + Unmap(2); + }); + + // Map planes + for (int i = 0; i < mBufferPlaneCount; i++) { + MapInternal(0, 0, mWidth[i], mHeight[i], nullptr, GBM_BO_TRANSFER_WRITE, i); + if (!mMappedRegion[i]) { + NS_WARNING("DMABufSurfaceYUV plane can't be mapped!"); + return false; + } + if ((int)mMappedRegionStride[i] < mWidth[i]) { + NS_WARNING("DMABufSurfaceYUV plane size stride does not match!"); + return false; + } + } + + // Copy planes + for (int i = 0; i < mBufferPlaneCount; i++) { + UpdateYUVPlane(i, aPixelData[i], aLineSizes[i]); + } + + return true; +} + +bool DMABufSurfaceYUV::Create(int aWidth, int aHeight, void** aPixelData, + int* aLineSizes) { + mSurfaceType = SURFACE_YUV420; + mBufferPlaneCount = 3; + + if (!CreateYUVPlane(0, aWidth, aHeight, GBM_FORMAT_R8)) { + return false; + } + if (!CreateYUVPlane(1, aWidth >> 1, aHeight >> 1, GBM_FORMAT_R8)) { + return false; + } + if (!CreateYUVPlane(2, aWidth >> 1, aHeight >> 1, GBM_FORMAT_R8)) { + return false; + } + + return aPixelData != nullptr && aLineSizes != nullptr + ? UpdateYUVData(aPixelData, aLineSizes) + : true; +} + +bool DMABufSurfaceYUV::Create(const SurfaceDescriptor& aDesc) { + ImportSurfaceDescriptor(aDesc); + return true; +} + +void DMABufSurfaceYUV::ImportSurfaceDescriptor( + const SurfaceDescriptorDMABuf& aDesc) { + mBufferPlaneCount = aDesc.fds().Length(); + mSurfaceType = (mBufferPlaneCount == 2) ? SURFACE_NV12 : SURFACE_YUV420; + mBufferModifier = aDesc.modifier(); + mColorSpace = aDesc.yUVColorSpace(); + mUID = aDesc.uid(); + + MOZ_RELEASE_ASSERT(mBufferPlaneCount <= DMABUF_BUFFER_PLANES); + for (int i = 0; i < mBufferPlaneCount; i++) { + mDmabufFds[i] = aDesc.fds()[i].ClonePlatformHandle().release(); + mWidth[i] = aDesc.width()[i]; + mHeight[i] = aDesc.height()[i]; + mDrmFormats[i] = aDesc.format()[i]; + mStrides[i] = aDesc.strides()[i]; + mOffsets[i] = aDesc.offsets()[i]; + } + + if (aDesc.fence().Length() > 0) { + mSyncFd = aDesc.fence()[0].ClonePlatformHandle().release(); + } + + if (aDesc.refCount().Length() > 0) { + GlobalRefCountImport(aDesc.refCount()[0].ClonePlatformHandle().release()); + } +} + +bool DMABufSurfaceYUV::Serialize( + mozilla::layers::SurfaceDescriptor& aOutDescriptor) { + AutoTArray width; + AutoTArray height; + AutoTArray format; + AutoTArray fds; + AutoTArray strides; + AutoTArray offsets; + AutoTArray fenceFDs; + AutoTArray refCountFDs; + + for (int i = 0; i < mBufferPlaneCount; i++) { + width.AppendElement(mWidth[i]); + height.AppendElement(mHeight[i]); + format.AppendElement(mDrmFormats[i]); + fds.AppendElement(ipc::FileDescriptor(mDmabufFds[i])); + strides.AppendElement(mStrides[i]); + offsets.AppendElement(mOffsets[i]); + } + + if (mSync) { + fenceFDs.AppendElement(ipc::FileDescriptor(mSyncFd)); + } + + if (mGlobalRefCountFd) { + refCountFDs.AppendElement(ipc::FileDescriptor(mGlobalRefCountFd)); + } + + aOutDescriptor = SurfaceDescriptorDMABuf( + mSurfaceType, mBufferModifier, 0, fds, width, height, format, strides, + offsets, GetYUVColorSpace(), fenceFDs, mUID, refCountFDs); + return true; +} + +bool DMABufSurfaceYUV::CreateTexture(GLContext* aGLContext, int aPlane) { + MOZ_ASSERT(!mEGLImage[aPlane] && !mTexture[aPlane], + "EGLImage/Texture is already created!"); + + if (!aGLContext) return false; + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + + nsTArray attribs; + attribs.AppendElement(LOCAL_EGL_WIDTH); + attribs.AppendElement(mWidth[aPlane]); + attribs.AppendElement(LOCAL_EGL_HEIGHT); + attribs.AppendElement(mHeight[aPlane]); + attribs.AppendElement(LOCAL_EGL_LINUX_DRM_FOURCC_EXT); + attribs.AppendElement(mDrmFormats[aPlane]); +#define ADD_PLANE_ATTRIBS_NV12(plane_idx) \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_FD_EXT); \ + attribs.AppendElement(mDmabufFds[aPlane]); \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_OFFSET_EXT); \ + attribs.AppendElement((int)mOffsets[aPlane]); \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_PITCH_EXT); \ + attribs.AppendElement((int)mStrides[aPlane]); + ADD_PLANE_ATTRIBS_NV12(0); +#undef ADD_PLANE_ATTRIBS_NV12 + attribs.AppendElement(LOCAL_EGL_NONE); + + mEGLImage[aPlane] = + egl->fCreateImage(LOCAL_EGL_NO_CONTEXT, LOCAL_EGL_LINUX_DMA_BUF_EXT, + nullptr, attribs.Elements()); + if (mEGLImage[aPlane] == LOCAL_EGL_NO_IMAGE) { + NS_WARNING("EGLImageKHR creation failed"); + return false; + } + + aGLContext->MakeCurrent(); + aGLContext->fGenTextures(1, &mTexture[aPlane]); + const ScopedBindTexture savedTex(aGLContext, mTexture[aPlane]); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, + LOCAL_GL_CLAMP_TO_EDGE); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, + LOCAL_GL_CLAMP_TO_EDGE); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, + LOCAL_GL_LINEAR); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, + LOCAL_GL_LINEAR); + aGLContext->fEGLImageTargetTexture2D(LOCAL_GL_TEXTURE_2D, mEGLImage[aPlane]); + mGL = aGLContext; + return true; +} + +void DMABufSurfaceYUV::ReleaseTextures() { + FenceDelete(); + + bool textureActive = false; + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mTexture[i]) { + textureActive = true; + break; + } + } + + if (!mGL) return; + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (textureActive && mGL->MakeCurrent()) { + mGL->fDeleteTextures(DMABUF_BUFFER_PLANES, mTexture); + for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) { + mTexture[i] = 0; + } + mGL = nullptr; + } + + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mEGLImage[i]) { + egl->fDestroyImage(mEGLImage[i]); + mEGLImage[i] = nullptr; + } + } +} + +gfx::SurfaceFormat DMABufSurfaceYUV::GetFormat() { + switch (mSurfaceType) { + case SURFACE_NV12: + return gfx::SurfaceFormat::NV12; + case SURFACE_YUV420: + return gfx::SurfaceFormat::YUV; + default: + NS_WARNING("DMABufSurfaceYUV::GetFormat(): Wrong surface format!"); + return gfx::SurfaceFormat::UNKNOWN; + } +} + +// GL uses swapped R and B components so report accordingly. +gfx::SurfaceFormat DMABufSurfaceYUV::GetFormatGL() { return GetFormat(); } + +uint32_t DMABufSurfaceYUV::GetTextureCount() { + switch (mSurfaceType) { + case SURFACE_NV12: + return 2; + case SURFACE_YUV420: + return 3; + default: + NS_WARNING("DMABufSurfaceYUV::GetTextureCount(): Wrong surface format!"); + return 1; + } +} + +void DMABufSurfaceYUV::ReleaseSurface() { + ReleaseTextures(); + ReleaseDMABuf(); +} diff --git a/widget/gtk/DMABufSurface.h b/widget/gtk/DMABufSurface.h new file mode 100644 index 0000000000..936c3c75a8 --- /dev/null +++ b/widget/gtk/DMABufSurface.h @@ -0,0 +1,290 @@ +/* -*- 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 DMABufSurface_h__ +#define DMABufSurface_h__ + +#include +#include "mozilla/widget/nsWaylandDisplay.h" +#include "mozilla/widget/va_drmcommon.h" +#include "GLTypes.h" + +typedef void* EGLImageKHR; +typedef void* EGLSyncKHR; + +#define DMABUF_BUFFER_PLANES 4 + +namespace mozilla { +namespace layers { +class SurfaceDescriptor; +class SurfaceDescriptorDMABuf; +} // namespace layers +namespace gl { +class GLContext; +} +} // namespace mozilla + +typedef enum { + // Use alpha pixel format + DMABUF_ALPHA = 1 << 0, + // Surface is used as texture and may be also shared + DMABUF_TEXTURE = 1 << 1, + // Use modifiers. Such dmabuf surface may have more planes + // and complex internal structure (tiling/compression/etc.) + // so we can't do direct rendering to it. + DMABUF_USE_MODIFIERS = 1 << 3, +} DMABufSurfaceFlags; + +class DMABufSurfaceRGBA; +class DMABufSurfaceYUV; + +class DMABufSurface { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DMABufSurface) + + enum SurfaceType { + SURFACE_RGBA, + SURFACE_NV12, + SURFACE_YUV420, + }; + + // Import surface from SurfaceDescriptor. This is usually + // used to copy surface from another process over IPC. + // When a global reference counter was created for the surface + // (see bellow) it's automatically referenced. + static already_AddRefed CreateDMABufSurface( + const mozilla::layers::SurfaceDescriptor& aDesc); + + // Export surface to another process via. SurfaceDescriptor. + virtual bool Serialize( + mozilla::layers::SurfaceDescriptor& aOutDescriptor) = 0; + + virtual int GetWidth(int aPlane = 0) = 0; + virtual int GetHeight(int aPlane = 0) = 0; + virtual mozilla::gfx::SurfaceFormat GetFormat() = 0; + virtual mozilla::gfx::SurfaceFormat GetFormatGL() = 0; + + virtual bool CreateTexture(mozilla::gl::GLContext* aGLContext, + int aPlane = 0) = 0; + virtual void ReleaseTextures() = 0; + virtual GLuint GetTexture(int aPlane = 0) = 0; + virtual EGLImageKHR GetEGLImage(int aPlane = 0) = 0; + + SurfaceType GetSurfaceType() { return mSurfaceType; }; + virtual uint32_t GetTextureCount() = 0; + + bool IsMapped(int aPlane = 0) { return (mMappedRegion[aPlane] != nullptr); }; + void Unmap(int aPlane = 0); + + virtual DMABufSurfaceRGBA* GetAsDMABufSurfaceRGBA() { return nullptr; } + virtual DMABufSurfaceYUV* GetAsDMABufSurfaceYUV() { return nullptr; } + + virtual mozilla::gfx::YUVColorSpace GetYUVColorSpace() { + return mozilla::gfx::YUVColorSpace::UNKNOWN; + }; + virtual bool IsFullRange() { return false; }; + + void FenceSet(); + void FenceWait(); + void FenceDelete(); + + // Set and get a global surface UID. The UID is shared across process + // and it's used to track surface lifetime in various parts of rendering + // engine. + uint32_t GetUID() const { return mUID; }; + + // Creates a global reference counter objects attached to the surface. + // It's created as unreferenced, i.e. IsGlobalRefSet() returns false + // right after GlobalRefCountCreate() call. + // + // The counter is shared by all surface instances across processes + // so it tracks global surface usage. + // + // The counter is automatically referenced when a new surface instance is + // created with SurfaceDescriptor (usually copied to another process over IPC) + // and it's unreferenced when surface is deleted. + // + // So without any additional GlobalRefAdd()/GlobalRefRelease() calls + // the IsGlobalRefSet() returns true if any other process use the surface. + void GlobalRefCountCreate(); + + // If global reference counter was created by GlobalRefCountCreate() + // returns true when there's an active surface reference. + bool IsGlobalRefSet() const; + + // Add/Remove additional reference to the surface global reference counter. + void GlobalRefAdd(); + void GlobalRefRelease(); + + // Release all underlying data. + virtual void ReleaseSurface() = 0; + +#ifdef DEBUG + virtual void DumpToFile(const char* pFile){}; +#endif + + DMABufSurface(SurfaceType aSurfaceType); + + protected: + virtual bool Create(const mozilla::layers::SurfaceDescriptor& aDesc) = 0; + bool FenceImportFromFd(); + + void GlobalRefCountImport(int aFd); + void GlobalRefCountDelete(); + + void ReleaseDMABuf(); + + void* MapInternal(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight, + uint32_t* aStride, int aGbmFlags, int aPlane = 0); + + virtual ~DMABufSurface(); + + SurfaceType mSurfaceType; + uint64_t mBufferModifier; + + int mBufferPlaneCount; + int mDmabufFds[DMABUF_BUFFER_PLANES]; + uint32_t mDrmFormats[DMABUF_BUFFER_PLANES]; + uint32_t mStrides[DMABUF_BUFFER_PLANES]; + uint32_t mOffsets[DMABUF_BUFFER_PLANES]; + + struct gbm_bo* mGbmBufferObject[DMABUF_BUFFER_PLANES]; + void* mMappedRegion[DMABUF_BUFFER_PLANES]; + void* mMappedRegionData[DMABUF_BUFFER_PLANES]; + uint32_t mMappedRegionStride[DMABUF_BUFFER_PLANES]; + + int mSyncFd; + EGLSyncKHR mSync; + RefPtr mGL; + + int mGlobalRefCountFd; + uint32_t mUID; +}; + +class DMABufSurfaceRGBA : public DMABufSurface { + public: + static already_AddRefed CreateDMABufSurface( + int aWidth, int aHeight, int aDMABufSurfaceFlags); + + bool Serialize(mozilla::layers::SurfaceDescriptor& aOutDescriptor); + + DMABufSurfaceRGBA* GetAsDMABufSurfaceRGBA() { return this; } + + void Clear(); + + void ReleaseSurface(); + + bool CopyFrom(class DMABufSurface* aSourceSurface); + + int GetWidth(int aPlane = 0) { return mWidth; }; + int GetHeight(int aPlane = 0) { return mHeight; }; + mozilla::gfx::SurfaceFormat GetFormat(); + mozilla::gfx::SurfaceFormat GetFormatGL(); + bool HasAlpha(); + + void* MapReadOnly(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight, + uint32_t* aStride = nullptr); + void* MapReadOnly(uint32_t* aStride = nullptr); + void* Map(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight, + uint32_t* aStride = nullptr); + void* Map(uint32_t* aStride = nullptr); + void* GetMappedRegion(int aPlane = 0) { return mMappedRegion[aPlane]; }; + uint32_t GetMappedRegionStride(int aPlane = 0) { + return mMappedRegionStride[aPlane]; + }; + + bool CreateTexture(mozilla::gl::GLContext* aGLContext, int aPlane = 0); + void ReleaseTextures(); + GLuint GetTexture(int aPlane = 0) { return mTexture; }; + EGLImageKHR GetEGLImage(int aPlane = 0) { return mEGLImage; }; + + uint32_t GetTextureCount() { return 1; }; + +#ifdef DEBUG + virtual void DumpToFile(const char* pFile); +#endif + + DMABufSurfaceRGBA(); + + private: + ~DMABufSurfaceRGBA(); + + bool Create(int aWidth, int aHeight, int aDMABufSurfaceFlags); + bool Create(const mozilla::layers::SurfaceDescriptor& aDesc); + + void ImportSurfaceDescriptor(const mozilla::layers::SurfaceDescriptor& aDesc); + + private: + int mSurfaceFlags; + + int mWidth; + int mHeight; + mozilla::widget::GbmFormat* mGmbFormat; + + EGLImageKHR mEGLImage; + GLuint mTexture; + uint32_t mGbmBufferFlags; +}; + +class DMABufSurfaceYUV : public DMABufSurface { + public: + static already_AddRefed CreateYUVSurface( + int aWidth, int aHeight, void** aPixelData = nullptr, + int* aLineSizes = nullptr); + + static already_AddRefed CreateYUVSurface( + const VADRMPRIMESurfaceDescriptor& aDesc); + + bool Serialize(mozilla::layers::SurfaceDescriptor& aOutDescriptor); + + DMABufSurfaceYUV* GetAsDMABufSurfaceYUV() { return this; }; + + int GetWidth(int aPlane = 0) { return mWidth[aPlane]; } + int GetHeight(int aPlane = 0) { return mHeight[aPlane]; } + mozilla::gfx::SurfaceFormat GetFormat(); + mozilla::gfx::SurfaceFormat GetFormatGL(); + + bool CreateTexture(mozilla::gl::GLContext* aGLContext, int aPlane = 0); + void ReleaseTextures(); + + void ReleaseSurface(); + + GLuint GetTexture(int aPlane = 0) { return mTexture[aPlane]; }; + EGLImageKHR GetEGLImage(int aPlane = 0) { return mEGLImage[aPlane]; }; + + uint32_t GetTextureCount(); + + void SetYUVColorSpace(mozilla::gfx::YUVColorSpace aColorSpace) { + mColorSpace = aColorSpace; + } + mozilla::gfx::YUVColorSpace GetYUVColorSpace() { return mColorSpace; } + + bool IsFullRange() { return true; } + + DMABufSurfaceYUV(); + + bool UpdateYUVData(void** aPixelData, int* aLineSizes); + bool UpdateYUVData(const VADRMPRIMESurfaceDescriptor& aDesc); + + private: + ~DMABufSurfaceYUV(); + + bool Create(const mozilla::layers::SurfaceDescriptor& aDesc); + bool Create(int aWidth, int aHeight, void** aPixelData, int* aLineSizes); + bool CreateYUVPlane(int aPlane, int aWidth, int aHeight, int aDrmFormat); + void UpdateYUVPlane(int aPlane, void* aPixelData, int aLineSize); + + void ImportSurfaceDescriptor( + const mozilla::layers::SurfaceDescriptorDMABuf& aDesc); + + int mWidth[DMABUF_BUFFER_PLANES]; + int mHeight[DMABUF_BUFFER_PLANES]; + EGLImageKHR mEGLImage[DMABUF_BUFFER_PLANES]; + GLuint mTexture[DMABUF_BUFFER_PLANES]; + mozilla::gfx::YUVColorSpace mColorSpace; +}; + +#endif diff --git a/widget/gtk/GRefPtr.h b/widget/gtk/GRefPtr.h new file mode 100644 index 0000000000..3705d5354d --- /dev/null +++ b/widget/gtk/GRefPtr.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GRefPtr_h_ +#define GRefPtr_h_ + +// Allows to use RefPtr with various kinds of GObjects + +#include +#include +#include "mozilla/RefPtr.h" + +namespace mozilla { + +template +struct GObjectRefPtrTraits { + static void AddRef(T* aObject) { g_object_ref(aObject); } + static void Release(T* aObject) { g_object_unref(aObject); } +}; + +template <> +struct RefPtrTraits : public GObjectRefPtrTraits {}; + +template <> +struct RefPtrTraits + : public GObjectRefPtrTraits {}; + +} // namespace mozilla + +#endif diff --git a/widget/gtk/GtkCompositorWidget.cpp b/widget/gtk/GtkCompositorWidget.cpp new file mode 100644 index 0000000000..bfb970ec47 --- /dev/null +++ b/widget/gtk/GtkCompositorWidget.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 "GtkCompositorWidget.h" + +#include "gfxPlatformGtk.h" +#include "mozilla/layers/CompositorThread.h" +#include "mozilla/widget/InProcessCompositorWidget.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "nsWindow.h" + +namespace mozilla { +namespace widget { + +GtkCompositorWidget::GtkCompositorWidget( + const GtkCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, nsWindow* aWindow) + : CompositorWidget(aOptions), + mWidget(aWindow), + mClientSize("GtkCompositorWidget::mClientSize") { +#if defined(MOZ_WAYLAND) + if (!aInitData.IsX11Display()) { + if (!aWindow) { + NS_WARNING("GtkCompositorWidget: We're missing nsWindow!"); + } + mProvider.Initialize(aWindow); + } +#endif +#if defined(MOZ_X11) + if (aInitData.IsX11Display()) { + // If we have a nsWindow, then grab the already existing display connection + // If we don't, then use the init data to connect to the display + if (aWindow) { + mXDisplay = aWindow->XDisplay(); + } else { + mXDisplay = XOpenDisplay(aInitData.XDisplayString().get()); + } + mXWindow = (Window)aInitData.XWindow(); + + // Grab the window's visual and depth + XWindowAttributes windowAttrs; + if (!XGetWindowAttributes(mXDisplay, mXWindow, &windowAttrs)) { + NS_WARNING("GtkCompositorWidget(): XGetWindowAttributes() failed!"); + } + + Visual* visual = windowAttrs.visual; + mDepth = windowAttrs.depth; + + // Initialize the window surface provider + mProvider.Initialize(mXDisplay, mXWindow, visual, mDepth, + aInitData.Shaped()); + } +#endif + auto size = mClientSize.Lock(); + *size = aInitData.InitialClientSize(); +} + +GtkCompositorWidget::~GtkCompositorWidget() { + mProvider.CleanupResources(); + +#if defined(MOZ_X11) + // If we created our own display connection, we need to destroy it + if (!mWidget && mXDisplay) { + XCloseDisplay(mXDisplay); + mXDisplay = nullptr; + } +#endif +} + +already_AddRefed GtkCompositorWidget::StartRemoteDrawing() { + return nullptr; +} +void GtkCompositorWidget::EndRemoteDrawing() {} + +already_AddRefed +GtkCompositorWidget::StartRemoteDrawingInRegion( + LayoutDeviceIntRegion& aInvalidRegion, layers::BufferMode* aBufferMode) { + return mProvider.StartRemoteDrawingInRegion(aInvalidRegion, aBufferMode); +} + +void GtkCompositorWidget::EndRemoteDrawingInRegion( + gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) { + mProvider.EndRemoteDrawingInRegion(aDrawTarget, aInvalidRegion); +} + +nsIWidget* GtkCompositorWidget::RealWidget() { return mWidget; } + +void GtkCompositorWidget::NotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) { + auto size = mClientSize.Lock(); + *size = aClientSize; +} + +LayoutDeviceIntSize GtkCompositorWidget::GetClientSize() { + auto size = mClientSize.Lock(); + return *size; +} + +uintptr_t GtkCompositorWidget::GetWidgetKey() { + return reinterpret_cast(mWidget); +} + +EGLNativeWindowType GtkCompositorWidget::GetEGLNativeWindow() { + if (mWidget) { + return (EGLNativeWindowType)mWidget->GetNativeData(NS_NATIVE_EGL_WINDOW); + } +#if defined(MOZ_X11) + if (mXWindow) { + return (EGLNativeWindowType)mXWindow; + } +#endif + return nullptr; +} + +int32_t GtkCompositorWidget::GetDepth() { return mDepth; } + +#if defined(MOZ_WAYLAND) +void GtkCompositorWidget::SetEGLNativeWindowSize( + const LayoutDeviceIntSize& aEGLWindowSize) { + if (mWidget) { + mWidget->SetEGLNativeWindowSize(aEGLWindowSize); + } +} +#endif + +void GtkCompositorWidget::ClearBeforePaint( + RefPtr aTarget, const LayoutDeviceIntRegion& aRegion) { + // We need to clear target buffer alpha values of popup windows as + // SW-WR paints with alpha blending (see Bug 1674473). + if (mWidget->IsPopup()) { + for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) { + aTarget->ClearRect(gfx::Rect(iter.Get().ToUnknownRect())); + } + } + + // Clear background of titlebar area to render titlebar + // transparent corners correctly. + gfx::Rect rect; + if (mWidget->GetTitlebarRect(rect)) { + aTarget->ClearRect(rect); + } +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/GtkCompositorWidget.h b/widget/gtk/GtkCompositorWidget.h new file mode 100644 index 0000000000..f0cb15fbfc --- /dev/null +++ b/widget/gtk/GtkCompositorWidget.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_gtk_GtkCompositorWidget_h +#define widget_gtk_GtkCompositorWidget_h + +#include "GLDefs.h" +#include "mozilla/DataMutex.h" +#include "mozilla/widget/CompositorWidget.h" +#include "WindowSurfaceProvider.h" + +class nsIWidget; +class nsWindow; + +namespace mozilla { +namespace widget { + +class PlatformCompositorWidgetDelegate : public CompositorWidgetDelegate { + public: + virtual void NotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) = 0; + + // CompositorWidgetDelegate Overrides + + PlatformCompositorWidgetDelegate* AsPlatformSpecificDelegate() override { + return this; + } +}; + +class GtkCompositorWidgetInitData; + +class GtkCompositorWidget : public CompositorWidget, + public PlatformCompositorWidgetDelegate { + public: + GtkCompositorWidget(const GtkCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, + nsWindow* aWindow /* = nullptr*/); + ~GtkCompositorWidget(); + + // CompositorWidget Overrides + + already_AddRefed StartRemoteDrawing() override; + void EndRemoteDrawing() override; + + already_AddRefed StartRemoteDrawingInRegion( + LayoutDeviceIntRegion& aInvalidRegion, + layers::BufferMode* aBufferMode) override; + void EndRemoteDrawingInRegion( + gfx::DrawTarget* aDrawTarget, + const LayoutDeviceIntRegion& aInvalidRegion) override; + uintptr_t GetWidgetKey() override; + + LayoutDeviceIntSize GetClientSize() override; + + nsIWidget* RealWidget() override; + GtkCompositorWidget* AsX11() override { return this; } + CompositorWidgetDelegate* AsDelegate() override { return this; } + + EGLNativeWindowType GetEGLNativeWindow(); + int32_t GetDepth(); + + void ClearBeforePaint(RefPtr aTarget, + const LayoutDeviceIntRegion& aRegion) override; + +#if defined(MOZ_X11) + Display* XDisplay() const { return mXDisplay; } + Window XWindow() const { return mXWindow; } +#endif +#if defined(MOZ_WAYLAND) + void SetEGLNativeWindowSize(const LayoutDeviceIntSize& aEGLWindowSize); +#endif + + // PlatformCompositorWidgetDelegate Overrides + + void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize) override; + + protected: + nsWindow* mWidget; + + private: + // This field is written to on the main thread and read from on the compositor + // or renderer thread. During window resizing, this is subject to a (largely + // benign) read/write race, see bug 1665726. The DataMutex doesn't prevent the + // read/write race, but it does make it Not Undefined Behaviour, and also + // ensures we only ever use the old or new size, and not some weird synthesis + // of the two. + DataMutex mClientSize; + + WindowSurfaceProvider mProvider; + +#if defined(MOZ_X11) + Display* mXDisplay = {}; + Window mXWindow = {}; +#endif + int32_t mDepth = {}; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_gtk_GtkCompositorWidget_h diff --git a/widget/gtk/IMContextWrapper.cpp b/widget/gtk/IMContextWrapper.cpp new file mode 100644 index 0000000000..8600efb4c3 --- /dev/null +++ b/widget/gtk/IMContextWrapper.cpp @@ -0,0 +1,3169 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 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/Logging.h" +#include "prtime.h" + +#include "IMContextWrapper.h" +#include "nsGtkKeyUtils.h" +#include "nsWindow.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Likely.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/ToString.h" +#include "WritingModes.h" + +namespace mozilla { +namespace widget { + +LazyLogModule gGtkIMLog("nsGtkIMModuleWidgets"); + +static inline const char* ToChar(bool aBool) { + return aBool ? "true" : "false"; +} + +static const char* GetEventType(GdkEventKey* aKeyEvent) { + switch (aKeyEvent->type) { + case GDK_KEY_PRESS: + return "GDK_KEY_PRESS"; + case GDK_KEY_RELEASE: + return "GDK_KEY_RELEASE"; + default: + return "Unknown"; + } +} + +class GetEventStateName : public nsAutoCString { + public: + explicit GetEventStateName(guint aState, + IMContextWrapper::IMContextID aIMContextID = + IMContextWrapper::IMContextID::Unknown) { + if (aState & GDK_SHIFT_MASK) { + AppendModifier("shift"); + } + if (aState & GDK_CONTROL_MASK) { + AppendModifier("control"); + } + if (aState & GDK_MOD1_MASK) { + AppendModifier("mod1"); + } + if (aState & GDK_MOD2_MASK) { + AppendModifier("mod2"); + } + if (aState & GDK_MOD3_MASK) { + AppendModifier("mod3"); + } + if (aState & GDK_MOD4_MASK) { + AppendModifier("mod4"); + } + if (aState & GDK_MOD4_MASK) { + AppendModifier("mod5"); + } + if (aState & GDK_MOD4_MASK) { + AppendModifier("mod5"); + } + switch (aIMContextID) { + case IMContextWrapper::IMContextID::IBus: + static const guint IBUS_HANDLED_MASK = 1 << 24; + static const guint IBUS_IGNORED_MASK = 1 << 25; + if (aState & IBUS_HANDLED_MASK) { + AppendModifier("IBUS_HANDLED_MASK"); + } + if (aState & IBUS_IGNORED_MASK) { + AppendModifier("IBUS_IGNORED_MASK"); + } + break; + case IMContextWrapper::IMContextID::Fcitx: + case IMContextWrapper::IMContextID::Fcitx5: + static const guint FcitxKeyState_HandledMask = 1 << 24; + static const guint FcitxKeyState_IgnoredMask = 1 << 25; + if (aState & FcitxKeyState_HandledMask) { + AppendModifier("FcitxKeyState_HandledMask"); + } + if (aState & FcitxKeyState_IgnoredMask) { + AppendModifier("FcitxKeyState_IgnoredMask"); + } + break; + default: + break; + } + } + + private: + void AppendModifier(const char* aModifierName) { + if (!IsEmpty()) { + AppendLiteral(" + "); + } + Append(aModifierName); + } +}; + +class GetWritingModeName : public nsAutoCString { + public: + explicit GetWritingModeName(const WritingMode& aWritingMode) { + if (!aWritingMode.IsVertical()) { + AssignLiteral("Horizontal"); + return; + } + if (aWritingMode.IsVerticalLR()) { + AssignLiteral("Vertical (LTR)"); + return; + } + AssignLiteral("Vertical (RTL)"); + } + virtual ~GetWritingModeName() = default; +}; + +class GetTextRangeStyleText final : public nsAutoCString { + public: + explicit GetTextRangeStyleText(const TextRangeStyle& aStyle) { + if (!aStyle.IsDefined()) { + AssignLiteral("{ IsDefined()=false }"); + return; + } + + if (aStyle.IsLineStyleDefined()) { + AppendLiteral("{ mLineStyle="); + AppendLineStyle(aStyle.mLineStyle); + if (aStyle.IsUnderlineColorDefined()) { + AppendLiteral(", mUnderlineColor="); + AppendColor(aStyle.mUnderlineColor); + } else { + AppendLiteral(", IsUnderlineColorDefined=false"); + } + } else { + AppendLiteral("{ IsLineStyleDefined()=false"); + } + + if (aStyle.IsForegroundColorDefined()) { + AppendLiteral(", mForegroundColor="); + AppendColor(aStyle.mForegroundColor); + } else { + AppendLiteral(", IsForegroundColorDefined()=false"); + } + + if (aStyle.IsBackgroundColorDefined()) { + AppendLiteral(", mBackgroundColor="); + AppendColor(aStyle.mBackgroundColor); + } else { + AppendLiteral(", IsBackgroundColorDefined()=false"); + } + + AppendLiteral(" }"); + } + void AppendLineStyle(TextRangeStyle::LineStyle aLineStyle) { + switch (aLineStyle) { + case TextRangeStyle::LineStyle::None: + AppendLiteral("LineStyle::None"); + break; + case TextRangeStyle::LineStyle::Solid: + AppendLiteral("LineStyle::Solid"); + break; + case TextRangeStyle::LineStyle::Dotted: + AppendLiteral("LineStyle::Dotted"); + break; + case TextRangeStyle::LineStyle::Dashed: + AppendLiteral("LineStyle::Dashed"); + break; + case TextRangeStyle::LineStyle::Double: + AppendLiteral("LineStyle::Double"); + break; + case TextRangeStyle::LineStyle::Wavy: + AppendLiteral("LineStyle::Wavy"); + break; + default: + AppendPrintf("Invalid(0x%02X)", + static_cast(aLineStyle)); + break; + } + } + void AppendColor(nscolor aColor) { + AppendPrintf("{ R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X }", NS_GET_R(aColor), + NS_GET_G(aColor), NS_GET_B(aColor), NS_GET_A(aColor)); + } + virtual ~GetTextRangeStyleText() = default; +}; + +const static bool kUseSimpleContextDefault = false; + +/****************************************************************************** + * SelectionStyleProvider + * + * IME (e.g., fcitx, ~4.2.8.3) may look up selection colors of widget, which + * is related to the window associated with the IM context, to support any + * colored widgets. Our editor (like ) is rendered as + * native GtkTextView as far as possible by default and if editor color is + * changed by web apps, nsTextFrame may swap background color of foreground + * color of composition string for making composition string is always + * visually distinct in normal text. + * + * So, we would like IME to set style of composition string to good colors + * in GtkTextView. Therefore, this class overwrites selection colors of + * our widget with selection colors of GtkTextView so that it's possible IME + * to refer selection colors of GtkTextView via our widget. + ******************************************************************************/ + +class SelectionStyleProvider final { + public: + static SelectionStyleProvider* GetInstance() { + if (sHasShutDown) { + return nullptr; + } + if (!sInstance) { + sInstance = new SelectionStyleProvider(); + } + return sInstance; + } + + static void Shutdown() { + if (sInstance) { + g_object_unref(sInstance->mProvider); + } + delete sInstance; + sInstance = nullptr; + sHasShutDown = true; + } + + // aGDKWindow is a GTK window which will be associated with an IM context. + void AttachTo(GdkWindow* aGDKWindow) { + GtkWidget* widget = nullptr; + // gdk_window_get_user_data() typically returns pointer to widget that + // window belongs to. If it's widget, fcitx retrieves selection colors + // of them. So, we need to overwrite its style. + gdk_window_get_user_data(aGDKWindow, (gpointer*)&widget); + if (GTK_IS_WIDGET(widget)) { + gtk_style_context_add_provider(gtk_widget_get_style_context(widget), + GTK_STYLE_PROVIDER(mProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + } + + void OnThemeChanged() { + // fcitx refers GtkStyle::text[GTK_STATE_SELECTED] and + // GtkStyle::bg[GTK_STATE_SELECTED] (although pair of text and *base* + // or *fg* and bg is correct). gtk_style_update_from_context() will + // set these colors using the widget's GtkStyleContext and so the + // colors can be controlled by a ":selected" CSS rule. + nsAutoCString style(":selected{"); + // FYI: LookAndFeel always returns selection colors of GtkTextView. + nscolor selectionForegroundColor; + if (NS_SUCCEEDED( + LookAndFeel::GetColor(LookAndFeel::ColorID::TextSelectForeground, + &selectionForegroundColor))) { + double alpha = + static_cast(NS_GET_A(selectionForegroundColor)) / 0xFF; + style.AppendPrintf("color:rgba(%u,%u,%u,", + NS_GET_R(selectionForegroundColor), + NS_GET_G(selectionForegroundColor), + NS_GET_B(selectionForegroundColor)); + // We can't use AppendPrintf here, because it does locale-specific + // formatting of floating-point values. + style.AppendFloat(alpha); + style.AppendPrintf(");"); + } + nscolor selectionBackgroundColor; + if (NS_SUCCEEDED( + LookAndFeel::GetColor(LookAndFeel::ColorID::TextSelectBackground, + &selectionBackgroundColor))) { + double alpha = + static_cast(NS_GET_A(selectionBackgroundColor)) / 0xFF; + style.AppendPrintf("background-color:rgba(%u,%u,%u,", + NS_GET_R(selectionBackgroundColor), + NS_GET_G(selectionBackgroundColor), + NS_GET_B(selectionBackgroundColor)); + style.AppendFloat(alpha); + style.AppendPrintf(");"); + } + style.AppendLiteral("}"); + gtk_css_provider_load_from_data(mProvider, style.get(), -1, nullptr); + } + + private: + static SelectionStyleProvider* sInstance; + static bool sHasShutDown; + GtkCssProvider* const mProvider; + + SelectionStyleProvider() : mProvider(gtk_css_provider_new()) { + OnThemeChanged(); + } +}; + +SelectionStyleProvider* SelectionStyleProvider::sInstance = nullptr; +bool SelectionStyleProvider::sHasShutDown = false; + +/****************************************************************************** + * IMContextWrapper + ******************************************************************************/ + +IMContextWrapper* IMContextWrapper::sLastFocusedContext = nullptr; +guint16 IMContextWrapper::sWaitingSynthesizedKeyPressHardwareKeyCode = 0; +bool IMContextWrapper::sUseSimpleContext; + +NS_IMPL_ISUPPORTS(IMContextWrapper, TextEventDispatcherListener, + nsISupportsWeakReference) + +IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow) + : mOwnerWindow(aOwnerWindow), + mLastFocusedWindow(nullptr), + mContext(nullptr), + mSimpleContext(nullptr), + mDummyContext(nullptr), + mComposingContext(nullptr), + mCompositionStart(UINT32_MAX), + mProcessingKeyEvent(nullptr), + mCompositionState(eCompositionState_NotComposing), + mIMContextID(IMContextID::Unknown), + mIsIMFocused(false), + mFallbackToKeyEvent(false), + mKeyboardEventWasDispatched(false), + mKeyboardEventWasConsumed(false), + mIsDeletingSurrounding(false), + mLayoutChanged(false), + mSetCursorPositionOnKeyEvent(true), + mPendingResettingIMContext(false), + mRetrieveSurroundingSignalReceived(false), + mMaybeInDeadKeySequence(false), + mIsIMInAsyncKeyHandlingMode(false) { + static bool sFirstInstance = true; + if (sFirstInstance) { + sFirstInstance = false; + sUseSimpleContext = + Preferences::GetBool("intl.ime.use_simple_context_on_password_field", + kUseSimpleContextDefault); + } + Init(); +} + +static bool IsIBusInSyncMode() { + // See ibus_im_context_class_init() in client/gtk2/ibusimcontext.c + // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L610 + const char* env = PR_GetEnv("IBUS_ENABLE_SYNC_MODE"); + + // See _get_boolean_env() in client/gtk2/ibusimcontext.c + // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L520-L537 + if (!env) { + return false; + } + nsDependentCString envStr(env); + if (envStr.IsEmpty() || envStr.EqualsLiteral("0") || + envStr.EqualsLiteral("false") || envStr.EqualsLiteral("False") || + envStr.EqualsLiteral("FALSE")) { + return false; + } + return true; +} + +static bool GetFcitxBoolEnv(const char* aEnv) { + // See fcitx_utils_get_boolean_env in src/lib/fcitx-utils/utils.c + // https://github.com/fcitx/fcitx/blob/0c87840dc7d9460c2cb5feaeefec299d0d3d62ec/src/lib/fcitx-utils/utils.c#L721-L736 + const char* env = PR_GetEnv(aEnv); + if (!env) { + return false; + } + nsDependentCString envStr(env); + if (envStr.IsEmpty() || envStr.EqualsLiteral("0") || + envStr.EqualsLiteral("false")) { + return false; + } + return true; +} + +static bool IsFcitxInSyncMode() { + // See fcitx_im_context_class_init() in src/frontend/gtk2/fcitximcontext.c + // https://github.com/fcitx/fcitx/blob/78b98d9230dc9630e99d52e3172bdf440ffd08c4/src/frontend/gtk2/fcitximcontext.c#L395-L398 + return GetFcitxBoolEnv("IBUS_ENABLE_SYNC_MODE") || + GetFcitxBoolEnv("FCITX_ENABLE_SYNC_MODE"); +} + +nsDependentCSubstring IMContextWrapper::GetIMName() const { + const char* contextIDChar = + gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)); + if (!contextIDChar) { + return nsDependentCSubstring(); + } + + nsDependentCSubstring im(contextIDChar, strlen(contextIDChar)); + + // If the context is XIM, actual engine must be specified with + // |XMODIFIERS=@im=foo|. + const char* xmodifiersChar = PR_GetEnv("XMODIFIERS"); + if (!xmodifiersChar || !im.EqualsLiteral("xim")) { + return im; + } + + nsDependentCString xmodifiers(xmodifiersChar); + int32_t atIMValueStart = xmodifiers.Find("@im=") + 4; + if (atIMValueStart < 4 || + xmodifiers.Length() <= static_cast(atIMValueStart)) { + return im; + } + + int32_t atIMValueEnd = xmodifiers.Find("@", false, atIMValueStart); + if (atIMValueEnd > atIMValueStart) { + return nsDependentCSubstring(xmodifiersChar + atIMValueStart, + atIMValueEnd - atIMValueStart); + } + + if (atIMValueEnd == kNotFound) { + return nsDependentCSubstring(xmodifiersChar + atIMValueStart, + strlen(xmodifiersChar) - atIMValueStart); + } + + return im; +} + +void IMContextWrapper::Init() { + MozContainer* container = mOwnerWindow->GetMozContainer(); + MOZ_ASSERT(container, "container is null"); + GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container)); + + // Overwrite selection colors of the window before associating the window + // with IM context since IME may look up selection colors via IM context + // to support any colored widgets. + SelectionStyleProvider::GetInstance()->AttachTo(gdkWindow); + + // NOTE: gtk_im_*_new() abort (kill) the whole process when it fails. + // So, we don't need to check the result. + + // Normal context. + mContext = gtk_im_multicontext_new(); + gtk_im_context_set_client_window(mContext, gdkWindow); + g_signal_connect(mContext, "preedit_changed", + G_CALLBACK(IMContextWrapper::OnChangeCompositionCallback), + this); + g_signal_connect(mContext, "retrieve_surrounding", + G_CALLBACK(IMContextWrapper::OnRetrieveSurroundingCallback), + this); + g_signal_connect(mContext, "delete_surrounding", + G_CALLBACK(IMContextWrapper::OnDeleteSurroundingCallback), + this); + g_signal_connect(mContext, "commit", + G_CALLBACK(IMContextWrapper::OnCommitCompositionCallback), + this); + g_signal_connect(mContext, "preedit_start", + G_CALLBACK(IMContextWrapper::OnStartCompositionCallback), + this); + g_signal_connect(mContext, "preedit_end", + G_CALLBACK(IMContextWrapper::OnEndCompositionCallback), + this); + nsDependentCSubstring im = GetIMName(); + if (im.EqualsLiteral("ibus")) { + mIMContextID = IMContextID::IBus; + mIsIMInAsyncKeyHandlingMode = !IsIBusInSyncMode(); + // Although ibus has key snooper mode, it's forcibly disabled on Firefox + // in default settings by its whitelist since we always send key events + // to IME before handling shortcut keys. The whitelist can be + // customized with env, IBUS_NO_SNOOPER_APPS, but we don't need to + // support such rare cases for reducing maintenance cost. + mIsKeySnooped = false; + } else if (im.EqualsLiteral("fcitx")) { + mIMContextID = IMContextID::Fcitx; + mIsIMInAsyncKeyHandlingMode = !IsFcitxInSyncMode(); + // Although Fcitx has key snooper mode similar to ibus, it's also + // disabled on Firefox in default settings by its whitelist. The + // whitelist can be customized with env, IBUS_NO_SNOOPER_APPS or + // FCITX_NO_SNOOPER_APPS, but we don't need to support such rare cases + // for reducing maintenance cost. + mIsKeySnooped = false; + } else if (im.EqualsLiteral("fcitx5")) { + mIMContextID = IMContextID::Fcitx5; + mIsIMInAsyncKeyHandlingMode = true; // does not have sync mode. + mIsKeySnooped = false; // never use key snooper. + } else if (im.EqualsLiteral("uim")) { + mIMContextID = IMContextID::Uim; + mIsIMInAsyncKeyHandlingMode = false; + // We cannot know if uim uses key snooper since it's build option of + // uim. Therefore, we need to retrieve the consideration from the + // pref for making users and distributions allowed to choose their + // preferred value. + mIsKeySnooped = + Preferences::GetBool("intl.ime.hack.uim.using_key_snooper", true); + } else if (im.EqualsLiteral("scim")) { + mIMContextID = IMContextID::Scim; + mIsIMInAsyncKeyHandlingMode = false; + mIsKeySnooped = false; + } else if (im.EqualsLiteral("iiim")) { + mIMContextID = IMContextID::IIIMF; + mIsIMInAsyncKeyHandlingMode = false; + mIsKeySnooped = false; + } else if (im.EqualsLiteral("wayland")) { + mIMContextID = IMContextID::Wayland; + mIsIMInAsyncKeyHandlingMode = false; + mIsKeySnooped = true; + } else { + mIMContextID = IMContextID::Unknown; + mIsIMInAsyncKeyHandlingMode = false; + mIsKeySnooped = false; + } + + // Simple context + if (sUseSimpleContext) { + mSimpleContext = gtk_im_context_simple_new(); + gtk_im_context_set_client_window(mSimpleContext, gdkWindow); + g_signal_connect(mSimpleContext, "preedit_changed", + G_CALLBACK(&IMContextWrapper::OnChangeCompositionCallback), + this); + g_signal_connect( + mSimpleContext, "retrieve_surrounding", + G_CALLBACK(&IMContextWrapper::OnRetrieveSurroundingCallback), this); + g_signal_connect(mSimpleContext, "delete_surrounding", + G_CALLBACK(&IMContextWrapper::OnDeleteSurroundingCallback), + this); + g_signal_connect(mSimpleContext, "commit", + G_CALLBACK(&IMContextWrapper::OnCommitCompositionCallback), + this); + g_signal_connect(mSimpleContext, "preedit_start", + G_CALLBACK(IMContextWrapper::OnStartCompositionCallback), + this); + g_signal_connect(mSimpleContext, "preedit_end", + G_CALLBACK(IMContextWrapper::OnEndCompositionCallback), + this); + } + + // Dummy context + mDummyContext = gtk_im_multicontext_new(); + gtk_im_context_set_client_window(mDummyContext, gdkWindow); + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p Init(), mOwnerWindow=%p, mContext=%p (im=\"%s\"), " + "mIsIMInAsyncKeyHandlingMode=%s, mIsKeySnooped=%s, " + "mSimpleContext=%p, mDummyContext=%p, " + "gtk_im_multicontext_get_context_id()=\"%s\", " + "PR_GetEnv(\"XMODIFIERS\")=\"%s\"", + this, mOwnerWindow, mContext, nsAutoCString(im).get(), + ToChar(mIsIMInAsyncKeyHandlingMode), ToChar(mIsKeySnooped), + mSimpleContext, mDummyContext, + gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)), + PR_GetEnv("XMODIFIERS"))); +} + +/* static */ +void IMContextWrapper::Shutdown() { SelectionStyleProvider::Shutdown(); } + +IMContextWrapper::~IMContextWrapper() { + if (this == sLastFocusedContext) { + sLastFocusedContext = nullptr; + } + MOZ_LOG(gGtkIMLog, LogLevel::Info, ("0x%p ~IMContextWrapper()", this)); +} + +NS_IMETHODIMP +IMContextWrapper::NotifyIME(TextEventDispatcher* aTextEventDispatcher, + const IMENotification& aNotification) { + switch (aNotification.mMessage) { + case REQUEST_TO_COMMIT_COMPOSITION: + case REQUEST_TO_CANCEL_COMPOSITION: { + nsWindow* window = + static_cast(aTextEventDispatcher->GetWidget()); + return EndIMEComposition(window); + } + case NOTIFY_IME_OF_FOCUS: + OnFocusChangeInGecko(true); + return NS_OK; + case NOTIFY_IME_OF_BLUR: + OnFocusChangeInGecko(false); + return NS_OK; + case NOTIFY_IME_OF_POSITION_CHANGE: + OnLayoutChange(); + return NS_OK; + case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: + OnUpdateComposition(); + return NS_OK; + case NOTIFY_IME_OF_SELECTION_CHANGE: { + nsWindow* window = + static_cast(aTextEventDispatcher->GetWidget()); + OnSelectionChange(window, aNotification); + return NS_OK; + } + default: + return NS_ERROR_NOT_IMPLEMENTED; + } +} + +NS_IMETHODIMP_(void) +IMContextWrapper::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) { + // XXX When input transaction is being stolen by add-on, what should we do? +} + +NS_IMETHODIMP_(void) +IMContextWrapper::WillDispatchKeyboardEvent( + TextEventDispatcher* aTextEventDispatcher, + WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress, + void* aData) { + KeymapWrapper::WillDispatchKeyboardEvent(aKeyboardEvent, + static_cast(aData)); +} + +TextEventDispatcher* IMContextWrapper::GetTextEventDispatcher() { + if (NS_WARN_IF(!mLastFocusedWindow)) { + return nullptr; + } + TextEventDispatcher* dispatcher = + mLastFocusedWindow->GetTextEventDispatcher(); + // nsIWidget::GetTextEventDispatcher() shouldn't return nullptr. + MOZ_RELEASE_ASSERT(dispatcher); + return dispatcher; +} + +NS_IMETHODIMP_(IMENotificationRequests) +IMContextWrapper::GetIMENotificationRequests() { + IMENotificationRequests::Notifications notifications = + IMENotificationRequests::NOTIFY_NOTHING; + // If it's not enabled, we don't need position change notification. + if (IsEnabled()) { + notifications |= IMENotificationRequests::NOTIFY_POSITION_CHANGE; + } + return IMENotificationRequests(notifications); +} + +void IMContextWrapper::OnDestroyWindow(nsWindow* aWindow) { + MOZ_LOG( + gGtkIMLog, LogLevel::Info, + ("0x%p OnDestroyWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, " + "mOwnerWindow=0x%p, mLastFocusedModule=0x%p", + this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedContext)); + + MOZ_ASSERT(aWindow, "aWindow must not be null"); + + if (mLastFocusedWindow == aWindow) { + EndIMEComposition(aWindow); + if (mIsIMFocused) { + Blur(); + } + mLastFocusedWindow = nullptr; + } + + if (mOwnerWindow != aWindow) { + return; + } + + if (sLastFocusedContext == this) { + sLastFocusedContext = nullptr; + } + + /** + * NOTE: + * The given window is the owner of this, so, we must release the + * contexts now. But that might be referred from other nsWindows + * (they are children of this. But we don't know why there are the + * cases). So, we need to clear the pointers that refers to contexts + * and this if the other referrers are still alive. See bug 349727. + */ + if (mContext) { + PrepareToDestroyContext(mContext); + gtk_im_context_set_client_window(mContext, nullptr); + g_object_unref(mContext); + mContext = nullptr; + } + + if (mSimpleContext) { + gtk_im_context_set_client_window(mSimpleContext, nullptr); + g_object_unref(mSimpleContext); + mSimpleContext = nullptr; + } + + if (mDummyContext) { + // mContext and mDummyContext have the same slaveType and signal_data + // so no need for another workaround_gtk_im_display_closed. + gtk_im_context_set_client_window(mDummyContext, nullptr); + g_object_unref(mDummyContext); + mDummyContext = nullptr; + } + + if (NS_WARN_IF(mComposingContext)) { + g_object_unref(mComposingContext); + mComposingContext = nullptr; + } + + mOwnerWindow = nullptr; + mLastFocusedWindow = nullptr; + mInputContext.mIMEState.mEnabled = IMEEnabled::Disabled; + mPostingKeyEvents.Clear(); + + MOZ_LOG(gGtkIMLog, LogLevel::Debug, + ("0x%p OnDestroyWindow(), succeeded, Completely destroyed", this)); +} + +void IMContextWrapper::PrepareToDestroyContext(GtkIMContext* aContext) { + if (mIMContextID == IMContextID::IIIMF) { + // IIIM module registers handlers for the "closed" signal on the + // display, but the signal handler is not disconnected when the module + // is unloaded. To prevent the module from being unloaded, use static + // variable to hold reference of slave context class declared by IIIM. + // Note that this does not grab any instance, it grabs the "class". + static gpointer sGtkIIIMContextClass = nullptr; + if (!sGtkIIIMContextClass) { + // We retrieved slave context class with g_type_name() and actual + // slave context instance when our widget was GTK2. That must be + // _GtkIMContext::priv::slave in GTK3. However, _GtkIMContext::priv + // is an opacity struct named _GtkIMMulticontextPrivate, i.e., it's + // not exposed by GTK3. Therefore, we cannot access the instance + // safely. So, we need to retrieve the slave context class with + // g_type_from_name("GtkIMContextIIIM") directly (anyway, we needed + // to compare the class name with "GtkIMContextIIIM"). + GType IIMContextType = g_type_from_name("GtkIMContextIIIM"); + if (IIMContextType) { + sGtkIIIMContextClass = g_type_class_ref(IIMContextType); + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p PrepareToDestroyContext(), added to reference to " + "GtkIMContextIIIM class to prevent it from being unloaded", + this)); + } else { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p PrepareToDestroyContext(), FAILED to prevent the " + "IIIM module from being uploaded", + this)); + } + } + } +} + +void IMContextWrapper::OnFocusWindow(nsWindow* aWindow) { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p", this, + aWindow, mLastFocusedWindow)); + mLastFocusedWindow = aWindow; + Focus(); +} + +void IMContextWrapper::OnBlurWindow(nsWindow* aWindow) { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, " + "mIsIMFocused=%s", + this, aWindow, mLastFocusedWindow, ToChar(mIsIMFocused))); + + if (!mIsIMFocused || mLastFocusedWindow != aWindow) { + return; + } + + Blur(); +} + +KeyHandlingState IMContextWrapper::OnKeyEvent( + nsWindow* aCaller, GdkEventKey* aEvent, + bool aKeyboardEventWasDispatched /* = false */) { + MOZ_ASSERT(aEvent, "aEvent must be non-null"); + + if (!mInputContext.mIMEState.IsEditable() || MOZ_UNLIKELY(IsDestroyed())) { + return KeyHandlingState::eNotHandled; + } + + MOZ_LOG(gGtkIMLog, LogLevel::Info, (">>>>>>>>>>>>>>>>")); + MOZ_LOG( + gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(aCaller=0x%p, " + "aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X, state=%s, " + "time=%u, hardware_keycode=%u, group=%u }, " + "aKeyboardEventWasDispatched=%s)", + this, aCaller, aEvent, GetEventType(aEvent), + gdk_keyval_name(aEvent->keyval), gdk_keyval_to_unicode(aEvent->keyval), + GetEventStateName(aEvent->state, mIMContextID).get(), aEvent->time, + aEvent->hardware_keycode, aEvent->group, + ToChar(aKeyboardEventWasDispatched))); + MOZ_LOG( + gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(), mMaybeInDeadKeySequence=%s, " + "mCompositionState=%s, current context=%p, active context=%p, " + "mIMContextID=%s, mIsIMInAsyncKeyHandlingMode=%s", + this, ToChar(mMaybeInDeadKeySequence), GetCompositionStateName(), + GetCurrentContext(), GetActiveContext(), ToString(mIMContextID).c_str(), + ToChar(mIsIMInAsyncKeyHandlingMode))); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnKeyEvent(), FAILED, the caller isn't focused " + "window, mLastFocusedWindow=0x%p", + this, mLastFocusedWindow)); + return KeyHandlingState::eNotHandled; + } + + // Even if old IM context has composition, key event should be sent to + // current context since the user expects so. + GtkIMContext* currentContext = GetCurrentContext(); + if (MOZ_UNLIKELY(!currentContext)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnKeyEvent(), FAILED, there are no context", this)); + return KeyHandlingState::eNotHandled; + } + + if (mSetCursorPositionOnKeyEvent) { + SetCursorPosition(currentContext); + mSetCursorPositionOnKeyEvent = false; + } + + // Let's support dead key event even if active keyboard layout also + // supports complicated composition like CJK IME. + bool isDeadKey = + KeymapWrapper::ComputeDOMKeyNameIndex(aEvent) == KEY_NAME_INDEX_Dead; + mMaybeInDeadKeySequence |= isDeadKey; + + // If current context is mSimpleContext, both ibus and fcitx handles key + // events synchronously. So, only when current context is mContext which + // is GtkIMMulticontext, the key event may be handled by IME asynchronously. + bool probablyHandledAsynchronously = + mIsIMInAsyncKeyHandlingMode && currentContext == mContext; + + // If we're not sure whether the event is handled asynchronously, this is + // set to true. + bool maybeHandledAsynchronously = false; + + // If aEvent is a synthesized event for async handling, this will be set to + // true. + bool isHandlingAsyncEvent = false; + + // If we've decided that the event won't be synthesized asyncrhonously + // by IME, but actually IME did it, this is set to true. + bool isUnexpectedAsyncEvent = false; + + // If IM is ibus or fcitx and it handles key events asynchronously, + // they mark aEvent->state as "handled by me" when they post key event + // to another process. Unfortunately, we need to check this hacky + // flag because it's difficult to store all pending key events by + // an array or a hashtable. + if (probablyHandledAsynchronously) { + switch (mIMContextID) { + case IMContextID::IBus: { + // See src/ibustypes.h + static const guint IBUS_IGNORED_MASK = 1 << 25; + // If IBUS_IGNORED_MASK was set to aEvent->state, the event + // has already been handled by another process and it wasn't + // used by IME. + isHandlingAsyncEvent = !!(aEvent->state & IBUS_IGNORED_MASK); + if (!isHandlingAsyncEvent) { + // On some environments, IBUS_IGNORED_MASK flag is not set as + // expected. In such case, we keep pusing all events into the queue. + // I.e., that causes eating a lot of memory until it's blurred. + // Therefore, we need to check whether there is same timestamp event + // in the queue. This redundant cost should be low because in most + // causes, key events in the queue should be 2 or 4. + isHandlingAsyncEvent = + mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex(); + if (isHandlingAsyncEvent) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state does not have " + "IBUS_IGNORED_MASK but " + "same event in the queue. So, assuming it's a " + "synthesized event", + this)); + } + } + + // If it's a synthesized event, let's remove it from the posting + // event queue first. Otherwise the following blocks cannot use + // `break`. + if (isHandlingAsyncEvent) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state has IBUS_IGNORED_MASK " + "or aEvent is in the " + "posting event queue, so, it won't be handled " + "asynchronously anymore. Removing " + "the posted events from the queue", + this)); + probablyHandledAsynchronously = false; + mPostingKeyEvents.RemoveEvent(aEvent); + } + + // ibus won't send back key press events in a dead key sequcne. + if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) { + probablyHandledAsynchronously = false; + if (isHandlingAsyncEvent) { + isUnexpectedAsyncEvent = true; + break; + } + // Some keyboard layouts which have dead keys may send + // "empty" key event to make us call + // gtk_im_context_filter_keypress() to commit composed + // character during a GDK_KEY_PRESS event dispatching. + if (!gdk_keyval_to_unicode(aEvent->keyval) && + !aEvent->hardware_keycode) { + isUnexpectedAsyncEvent = true; + break; + } + break; + } + // ibus may handle key events synchronously if focused editor is + // or |ime-mode: disabled;|. However, in + // some environments, not so actually. Therefore, we need to check + // the result of gtk_im_context_filter_keypress() later. + if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) { + probablyHandledAsynchronously = false; + maybeHandledAsynchronously = !isHandlingAsyncEvent; + break; + } + break; + } + case IMContextID::Fcitx: + case IMContextID::Fcitx5: { + // See src/lib/fcitx-utils/keysym.h + static const guint FcitxKeyState_IgnoredMask = 1 << 25; + // If FcitxKeyState_IgnoredMask was set to aEvent->state, + // the event has already been handled by another process and + // it wasn't used by IME. + isHandlingAsyncEvent = !!(aEvent->state & FcitxKeyState_IgnoredMask); + if (!isHandlingAsyncEvent) { + // On some environments, FcitxKeyState_IgnoredMask flag *might* be not + // set as expected. If there were such cases, we'd keep pusing all + // events into the queue. I.e., that would cause eating a lot of + // memory until it'd be blurred. Therefore, we should check whether + // there is same timestamp event in the queue. This redundant cost + // should be low because in most causes, key events in the queue + // should be 2 or 4. + isHandlingAsyncEvent = + mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex(); + if (isHandlingAsyncEvent) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state does not have " + "FcitxKeyState_IgnoredMask " + "but same event in the queue. So, assuming it's a " + "synthesized event", + this)); + } + } + + // fcitx won't send back key press events in a dead key sequcne. + if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) { + probablyHandledAsynchronously = false; + if (isHandlingAsyncEvent) { + isUnexpectedAsyncEvent = true; + break; + } + // Some keyboard layouts which have dead keys may send + // "empty" key event to make us call + // gtk_im_context_filter_keypress() to commit composed + // character during a GDK_KEY_PRESS event dispatching. + if (!gdk_keyval_to_unicode(aEvent->keyval) && + !aEvent->hardware_keycode) { + isUnexpectedAsyncEvent = true; + break; + } + } + + // fcitx handles key events asynchronously even if focused + // editor cannot use IME actually. + + if (isHandlingAsyncEvent) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state has " + "FcitxKeyState_IgnoredMask or aEvent is in " + "the posting event queue, so, it won't be handled " + "asynchronously anymore. " + "Removing the posted events from the queue", + this)); + probablyHandledAsynchronously = false; + mPostingKeyEvents.RemoveEvent(aEvent); + break; + } + break; + } + default: + MOZ_ASSERT_UNREACHABLE( + "IME may handle key event " + "asyncrhonously, but not yet confirmed if it comes agian " + "actually"); + } + } + + if (!isUnexpectedAsyncEvent) { + mKeyboardEventWasDispatched = aKeyboardEventWasDispatched; + mKeyboardEventWasConsumed = false; + } else { + // If we didn't expect this event, we've alreday dispatched eKeyDown + // event or eKeyUp event for that. + mKeyboardEventWasDispatched = true; + // And in this case, we need to assume that another key event hasn't + // been receivied and mKeyboardEventWasConsumed keeps storing the + // dispatched eKeyDown or eKeyUp event's state. + } + mFallbackToKeyEvent = false; + mProcessingKeyEvent = aEvent; + gboolean isFiltered = gtk_im_context_filter_keypress(currentContext, aEvent); + + // If we're not sure whether the event is handled by IME asynchronously or + // synchronously, we need to trust the result of + // gtk_im_context_filter_keypress(). If it consumed and but did nothing, + // we can assume that another event will be synthesized. + if (!isHandlingAsyncEvent && maybeHandledAsynchronously) { + probablyHandledAsynchronously |= + isFiltered && !mFallbackToKeyEvent && !mKeyboardEventWasDispatched; + } + + if (aEvent->type == GDK_KEY_PRESS) { + if (isFiltered && probablyHandledAsynchronously) { + sWaitingSynthesizedKeyPressHardwareKeyCode = aEvent->hardware_keycode; + } else { + sWaitingSynthesizedKeyPressHardwareKeyCode = 0; + } + } + + // The caller of this shouldn't handle aEvent anymore if we've dispatched + // composition events or modified content with other events. + bool filterThisEvent = isFiltered && !mFallbackToKeyEvent; + + if (IsComposingOnCurrentContext() && !isFiltered && + aEvent->type == GDK_KEY_PRESS && mDispatchedCompositionString.IsEmpty()) { + // A Hangul input engine for SCIM doesn't emit preedit_end + // signal even when composition string becomes empty. On the + // other hand, we should allow to make composition with empty + // string for other languages because there *might* be such + // IM. For compromising this issue, we should dispatch + // compositionend event, however, we don't need to reset IM + // actually. + // NOTE: Don't dispatch key events as "processed by IME" since + // we need to dispatch keyboard events as IME wasn't handled it. + mProcessingKeyEvent = nullptr; + DispatchCompositionCommitEvent(currentContext, &EmptyString()); + mProcessingKeyEvent = aEvent; + // In this case, even though we handle the keyboard event here, + // but we should dispatch keydown event as + filterThisEvent = false; + } + + if (filterThisEvent && !mKeyboardEventWasDispatched) { + // If IME handled the key event but we've not dispatched eKeyDown nor + // eKeyUp event yet, we need to dispatch here unless the key event is + // now being handled by other IME process. + if (!probablyHandledAsynchronously) { + MaybeDispatchKeyEventAsProcessedByIME(eVoidEvent); + // Be aware, the widget might have been gone here. + } + // If we need to wait reply from IM, IM may send some signals to us + // without sending the key event again. In such case, we need to + // dispatch keyboard events with a copy of aEvent. Therefore, we + // need to use information of this key event to dispatch an KeyDown + // or eKeyUp event later. + else { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(), putting aEvent into the queue...", this)); + mPostingKeyEvents.PutEvent(aEvent); + } + } + + mProcessingKeyEvent = nullptr; + + if (aEvent->type == GDK_KEY_PRESS && !filterThisEvent) { + // If the key event hasn't been handled by active IME nor keyboard + // layout, we can assume that the dead key sequence has been or was + // ended. Note that we should not reset it when the key event is + // GDK_KEY_RELEASE since it may not be filtered by active keyboard + // layout even in composition. + mMaybeInDeadKeySequence = false; + } + + MOZ_LOG( + gGtkIMLog, LogLevel::Debug, + ("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s " + "(isFiltered=%s, mFallbackToKeyEvent=%s, " + "probablyHandledAsynchronously=%s, maybeHandledAsynchronously=%s), " + "mPostingKeyEvents.Length()=%zu, mCompositionState=%s, " + "mMaybeInDeadKeySequence=%s, mKeyboardEventWasDispatched=%s, " + "mKeyboardEventWasConsumed=%s", + this, ToChar(filterThisEvent), ToChar(isFiltered), + ToChar(mFallbackToKeyEvent), ToChar(probablyHandledAsynchronously), + ToChar(maybeHandledAsynchronously), mPostingKeyEvents.Length(), + GetCompositionStateName(), ToChar(mMaybeInDeadKeySequence), + ToChar(mKeyboardEventWasDispatched), ToChar(mKeyboardEventWasConsumed))); + MOZ_LOG(gGtkIMLog, LogLevel::Info, ("<<<<<<<<<<<<<<<<\n\n")); + + if (filterThisEvent) { + return KeyHandlingState::eHandled; + } + // If another call of this method has already dispatched eKeyDown event, + // we should return KeyHandlingState::eNotHandledButEventDispatched because + // the caller should've stopped handling the event if preceding eKeyDown + // event was consumed. + if (aKeyboardEventWasDispatched) { + return KeyHandlingState::eNotHandledButEventDispatched; + } + if (!mKeyboardEventWasDispatched) { + return KeyHandlingState::eNotHandled; + } + return mKeyboardEventWasConsumed + ? KeyHandlingState::eNotHandledButEventConsumed + : KeyHandlingState::eNotHandledButEventDispatched; +} + +void IMContextWrapper::OnFocusChangeInGecko(bool aFocus) { + MOZ_LOG( + gGtkIMLog, LogLevel::Info, + ("0x%p OnFocusChangeInGecko(aFocus=%s), " + "mCompositionState=%s, mIsIMFocused=%s", + this, ToChar(aFocus), GetCompositionStateName(), ToChar(mIsIMFocused))); + + // We shouldn't carry over the removed string to another editor. + mSelectedStringRemovedByComposition.Truncate(); + mSelection.Clear(); + + // When the focus changes, we need to inform IM about the new cursor + // position. Chinese input methods generally rely on this because they + // usually don't start composition until a character is picked. + if (aFocus && EnsureToCacheSelection()) { + SetCursorPosition(GetActiveContext()); + } +} + +void IMContextWrapper::ResetIME() { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p ResetIME(), mCompositionState=%s, mIsIMFocused=%s", this, + GetCompositionStateName(), ToChar(mIsIMFocused))); + + GtkIMContext* activeContext = GetActiveContext(); + if (MOZ_UNLIKELY(!activeContext)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p ResetIME(), FAILED, there are no context", this)); + return; + } + + RefPtr kungFuDeathGrip(this); + RefPtr lastFocusedWindow(mLastFocusedWindow); + + mPendingResettingIMContext = false; + gtk_im_context_reset(activeContext); + + // The last focused window might have been destroyed by a DOM event handler + // which was called by us during a call of gtk_im_context_reset(). + if (!lastFocusedWindow || + NS_WARN_IF(lastFocusedWindow != mLastFocusedWindow) || + lastFocusedWindow->Destroyed()) { + return; + } + + nsAutoString compositionString; + GetCompositionString(activeContext, compositionString); + + MOZ_LOG( + gGtkIMLog, LogLevel::Debug, + ("0x%p ResetIME() called gtk_im_context_reset(), " + "activeContext=0x%p, mCompositionState=%s, compositionString=%s, " + "mIsIMFocused=%s", + this, activeContext, GetCompositionStateName(), + NS_ConvertUTF16toUTF8(compositionString).get(), ToChar(mIsIMFocused))); + + // XXX IIIMF (ATOK X3 which is one of the Language Engine of it is still + // used in Japan!) sends only "preedit_changed" signal with empty + // composition string synchronously. Therefore, if composition string + // is now empty string, we should assume that the IME won't send + // "commit" signal. + if (IsComposing() && compositionString.IsEmpty()) { + // WARNING: The widget might have been gone after this. + DispatchCompositionCommitEvent(activeContext, &EmptyString()); + } +} + +nsresult IMContextWrapper::EndIMEComposition(nsWindow* aCaller) { + if (MOZ_UNLIKELY(IsDestroyed())) { + return NS_OK; + } + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p EndIMEComposition(aCaller=0x%p), " + "mCompositionState=%s", + this, aCaller, GetCompositionStateName())); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p EndIMEComposition(), FAILED, the caller isn't " + "focused window, mLastFocusedWindow=0x%p", + this, mLastFocusedWindow)); + return NS_OK; + } + + if (!IsComposing()) { + return NS_OK; + } + + // Currently, GTK has API neither to commit nor to cancel composition + // forcibly. Therefore, TextComposition will recompute commit string for + // the request even if native IME will cause unexpected commit string. + // So, we don't need to emulate commit or cancel composition with + // proper composition events. + // XXX ResetIME() might not enough for finishing compositoin on some + // environments. We should emulate focus change too because some IMEs + // may commit or cancel composition at blur. + ResetIME(); + + return NS_OK; +} + +void IMContextWrapper::OnLayoutChange() { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + if (IsComposing()) { + SetCursorPosition(GetActiveContext()); + } else { + // If not composing, candidate window position is updated before key + // down + mSetCursorPositionOnKeyEvent = true; + } + mLayoutChanged = true; +} + +void IMContextWrapper::OnUpdateComposition() { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + if (!IsComposing()) { + // Composition has been committed. So we need update selection for + // caret later + mSelection.Clear(); + EnsureToCacheSelection(); + mSetCursorPositionOnKeyEvent = true; + } + + // If we've already set candidate window position, we don't need to update + // the position with update composition notification. + if (!mLayoutChanged) { + SetCursorPosition(GetActiveContext()); + } +} + +void IMContextWrapper::SetInputContext(nsWindow* aCaller, + const InputContext* aContext, + const InputContextAction* aAction) { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p SetInputContext(aCaller=0x%p, aContext={ mIMEState={ " + "mEnabled=%s }, mHTMLInputType=%s })", + this, aCaller, ToString(aContext->mIMEState.mEnabled).c_str(), + NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get())); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p SetInputContext(), FAILED, " + "the caller isn't focused window, mLastFocusedWindow=0x%p", + this, mLastFocusedWindow)); + return; + } + + if (!mContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p SetInputContext(), FAILED, " + "there are no context", + this)); + return; + } + + if (sLastFocusedContext != this) { + mInputContext = *aContext; + MOZ_LOG(gGtkIMLog, LogLevel::Debug, + ("0x%p SetInputContext(), succeeded, " + "but we're not active", + this)); + return; + } + + bool changingEnabledState = + aContext->mIMEState.mEnabled != mInputContext.mIMEState.mEnabled || + aContext->mHTMLInputType != mInputContext.mHTMLInputType; + + // Release current IME focus if IME is enabled. + if (changingEnabledState && mInputContext.mIMEState.IsEditable()) { + EndIMEComposition(mLastFocusedWindow); + Blur(); + } + + mInputContext = *aContext; + + if (changingEnabledState) { + if (mInputContext.mIMEState.IsEditable()) { + GtkIMContext* currentContext = GetCurrentContext(); + if (currentContext) { + GtkInputPurpose purpose = GTK_INPUT_PURPOSE_FREE_FORM; + const nsString& inputType = mInputContext.mHTMLInputType; + // Password case has difficult issue. Desktop IMEs disable + // composition if input-purpose is password. + // For disabling IME on |ime-mode: disabled;|, we need to check + // mEnabled value instead of inputType value. This hack also + // enables composition on + // . + // This is right behavior of ime-mode on desktop. + // + // On the other hand, IME for tablet devices may provide a + // specific software keyboard for password field. If so, + // the behavior might look strange on both: + // + // + // + // Temporarily, we should focus on desktop environment for now. + // I.e., let's ignore tablet devices for now. When somebody + // reports actual trouble on tablet devices, we should try to + // look for a way to solve actual problem. + if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) { + purpose = GTK_INPUT_PURPOSE_PASSWORD; + } else if (inputType.EqualsLiteral("email")) { + purpose = GTK_INPUT_PURPOSE_EMAIL; + } else if (inputType.EqualsLiteral("url")) { + purpose = GTK_INPUT_PURPOSE_URL; + } else if (inputType.EqualsLiteral("tel")) { + purpose = GTK_INPUT_PURPOSE_PHONE; + } else if (inputType.EqualsLiteral("number")) { + purpose = GTK_INPUT_PURPOSE_NUMBER; + } else if (mInputContext.mHTMLInputInputmode.EqualsLiteral("decimal")) { + purpose = GTK_INPUT_PURPOSE_NUMBER; + } else if (mInputContext.mHTMLInputInputmode.EqualsLiteral("email")) { + purpose = GTK_INPUT_PURPOSE_EMAIL; + } else if (mInputContext.mHTMLInputInputmode.EqualsLiteral("numeric")) { + purpose = GTK_INPUT_PURPOSE_DIGITS; + } else if (mInputContext.mHTMLInputInputmode.EqualsLiteral("tel")) { + purpose = GTK_INPUT_PURPOSE_PHONE; + } else if (mInputContext.mHTMLInputInputmode.EqualsLiteral("url")) { + purpose = GTK_INPUT_PURPOSE_URL; + } + // Search by type and inputmode isn't supported on GTK. + + g_object_set(currentContext, "input-purpose", purpose, nullptr); + + // Although GtkInputHints is enum type, value is bit field. + gint hints = GTK_INPUT_HINT_NONE; + if (mInputContext.mHTMLInputInputmode.EqualsLiteral("none")) { + hints |= GTK_INPUT_HINT_INHIBIT_OSK; + } + + if (mInputContext.mAutocapitalize.EqualsLiteral("characters")) { + hints |= GTK_INPUT_HINT_UPPERCASE_CHARS; + } else if (mInputContext.mAutocapitalize.EqualsLiteral("sentences")) { + hints |= GTK_INPUT_HINT_UPPERCASE_SENTENCES; + } else if (mInputContext.mAutocapitalize.EqualsLiteral("words")) { + hints |= GTK_INPUT_HINT_UPPERCASE_WORDS; + } + + g_object_set(currentContext, "input-hints", hints, nullptr); + } + } + + // Even when aState is not enabled state, we need to set IME focus. + // Because some IMs are updating the status bar of them at this time. + // Be aware, don't use aWindow here because this method shouldn't move + // focus actually. + Focus(); + + // XXX Should we call Blur() when it's not editable? E.g., it might be + // better to close VKB automatically. + } +} + +InputContext IMContextWrapper::GetInputContext() { + mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED; + return mInputContext; +} + +GtkIMContext* IMContextWrapper::GetCurrentContext() const { + if (IsEnabled()) { + return mContext; + } + if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) { + return mSimpleContext; + } + return mDummyContext; +} + +bool IMContextWrapper::IsValidContext(GtkIMContext* aContext) const { + if (!aContext) { + return false; + } + return aContext == mContext || aContext == mSimpleContext || + aContext == mDummyContext; +} + +bool IMContextWrapper::IsEnabled() const { + return mInputContext.mIMEState.mEnabled == IMEEnabled::Enabled || + (!sUseSimpleContext && + mInputContext.mIMEState.mEnabled == IMEEnabled::Password); +} + +void IMContextWrapper::Focus() { + MOZ_LOG( + gGtkIMLog, LogLevel::Info, + ("0x%p Focus(), sLastFocusedContext=0x%p", this, sLastFocusedContext)); + + if (mIsIMFocused) { + NS_ASSERTION(sLastFocusedContext == this, + "We're not active, but the IM was focused?"); + return; + } + + GtkIMContext* currentContext = GetCurrentContext(); + if (!currentContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p Focus(), FAILED, there are no context", this)); + return; + } + + if (sLastFocusedContext && sLastFocusedContext != this) { + sLastFocusedContext->Blur(); + } + + sLastFocusedContext = this; + + // Forget all posted key events when focus is moved since they shouldn't + // be fired in different editor. + sWaitingSynthesizedKeyPressHardwareKeyCode = 0; + mPostingKeyEvents.Clear(); + + gtk_im_context_focus_in(currentContext); + mIsIMFocused = true; + mSetCursorPositionOnKeyEvent = true; + + if (!IsEnabled()) { + // We should release IME focus for uim and scim. + // These IMs are using snooper that is released at losing focus. + Blur(); + } +} + +void IMContextWrapper::Blur() { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p Blur(), mIsIMFocused=%s", this, ToChar(mIsIMFocused))); + + if (!mIsIMFocused) { + return; + } + + GtkIMContext* currentContext = GetCurrentContext(); + if (!currentContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p Blur(), FAILED, there are no context", this)); + return; + } + + gtk_im_context_focus_out(currentContext); + mIsIMFocused = false; +} + +void IMContextWrapper::OnSelectionChange( + nsWindow* aCaller, const IMENotification& aIMENotification) { + mSelection.Assign(aIMENotification); + bool retrievedSurroundingSignalReceived = mRetrieveSurroundingSignalReceived; + mRetrieveSurroundingSignalReceived = false; + + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + const IMENotification::SelectionChangeDataBase& selectionChangeData = + aIMENotification.mSelectionChangeData; + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ " + "mSelectionChangeData=%s }), " + "mCompositionState=%s, mIsDeletingSurrounding=%s, " + "mRetrieveSurroundingSignalReceived=%s", + this, aCaller, ToString(selectionChangeData).c_str(), + GetCompositionStateName(), ToChar(mIsDeletingSurrounding), + ToChar(retrievedSurroundingSignalReceived))); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnSelectionChange(), FAILED, " + "the caller isn't focused window, mLastFocusedWindow=0x%p", + this, mLastFocusedWindow)); + return; + } + + if (!IsComposing()) { + // Now we have no composition (mostly situation on calling this method) + // If we have it, it will set by + // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED. + mSetCursorPositionOnKeyEvent = true; + } + + // The focused editor might have placeholder text with normal text node. + // In such case, the text node must be removed from a compositionstart + // event handler. So, we're dispatching eCompositionStart, + // we should ignore selection change notification. + if (mCompositionState == eCompositionState_CompositionStartDispatched) { + if (NS_WARN_IF(!mSelection.IsValid())) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnSelectionChange(), FAILED, " + "new offset is too large, cannot keep composing", + this)); + } else { + // Modify the selection start offset with new offset. + mCompositionStart = mSelection.mOffset; + // XXX We should modify mSelectedStringRemovedByComposition? + // But how? + MOZ_LOG(gGtkIMLog, LogLevel::Debug, + ("0x%p OnSelectionChange(), ignored, mCompositionStart " + "is updated to %u, the selection change doesn't cause " + "resetting IM context", + this, mCompositionStart)); + // And don't reset the IM context. + return; + } + // Otherwise, reset the IM context due to impossible to keep composing. + } + + // If the selection change is caused by deleting surrounding text, + // we shouldn't need to notify IME of selection change. + if (mIsDeletingSurrounding) { + return; + } + + bool occurredBeforeComposition = + IsComposing() && !selectionChangeData.mOccurredDuringComposition && + !selectionChangeData.mCausedByComposition; + if (occurredBeforeComposition) { + mPendingResettingIMContext = true; + } + + // When the selection change is caused by dispatching composition event, + // selection set event and/or occurred before starting current composition, + // we shouldn't notify IME of that and commit existing composition. + if (!selectionChangeData.mCausedByComposition && + !selectionChangeData.mCausedBySelectionEvent && + !occurredBeforeComposition) { + // Hack for ibus-pinyin. ibus-pinyin will synthesize a set of + // composition which commits with empty string after calling + // gtk_im_context_reset(). Therefore, selecting text causes + // unexpectedly removing it. For preventing it but not breaking the + // other IMEs which use surrounding text, we should call it only when + // surrounding text has been retrieved after last selection range was + // set. If it's not retrieved, that means that current IME doesn't + // have any content cache, so, it must not need the notification of + // selection change. + if (IsComposing() || retrievedSurroundingSignalReceived) { + ResetIME(); + } + } +} + +/* static */ +void IMContextWrapper::OnThemeChanged() { + if (!SelectionStyleProvider::GetInstance()) { + return; + } + SelectionStyleProvider::GetInstance()->OnThemeChanged(); +} + +/* static */ +void IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule) { + aModule->OnStartCompositionNative(aContext); +} + +void IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnStartCompositionNative(aContext=0x%p), " + "current context=0x%p, mComposingContext=0x%p", + this, aContext, GetCurrentContext(), mComposingContext)); + + // See bug 472635, we should do nothing if IM context doesn't match. + if (GetCurrentContext() != aContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnStartCompositionNative(), FAILED, " + "given context doesn't match", + this)); + return; + } + + if (mComposingContext && aContext != mComposingContext) { + // XXX For now, we should ignore this odd case, just logging. + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p OnStartCompositionNative(), Warning, " + "there is already a composing context but starting new " + "composition with different context", + this)); + } + + // IME may start composition without "preedit_start" signal. Therefore, + // mComposingContext will be initialized in DispatchCompositionStart(). + + if (!DispatchCompositionStart(aContext)) { + return; + } + mCompositionTargetRange.mOffset = mCompositionStart; + mCompositionTargetRange.mLength = 0; +} + +/* static */ +void IMContextWrapper::OnEndCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule) { + aModule->OnEndCompositionNative(aContext); +} + +void IMContextWrapper::OnEndCompositionNative(GtkIMContext* aContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnEndCompositionNative(aContext=0x%p), mComposingContext=0x%p", + this, aContext, mComposingContext)); + + // See bug 472635, we should do nothing if IM context doesn't match. + // Note that if this is called after focus move, the context may different + // from any our owning context. + if (!IsValidContext(aContext)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnEndCompositionNative(), FAILED, " + "given context doesn't match with any context", + this)); + return; + } + + // If we've not started composition with aContext, we should ignore it. + if (aContext != mComposingContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p OnEndCompositionNative(), Warning, " + "given context doesn't match with mComposingContext", + this)); + return; + } + + g_object_unref(mComposingContext); + mComposingContext = nullptr; + + // If we already handled the commit event, we should do nothing here. + if (IsComposing()) { + if (!DispatchCompositionCommitEvent(aContext)) { + // If the widget is destroyed, we should do nothing anymore. + return; + } + } + + if (mPendingResettingIMContext) { + ResetIME(); + } +} + +/* static */ +void IMContextWrapper::OnChangeCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule) { + aModule->OnChangeCompositionNative(aContext); +} + +void IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnChangeCompositionNative(aContext=0x%p), " + "mComposingContext=0x%p", + this, aContext, mComposingContext)); + + // See bug 472635, we should do nothing if IM context doesn't match. + // Note that if this is called after focus move, the context may different + // from any our owning context. + if (!IsValidContext(aContext)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnChangeCompositionNative(), FAILED, " + "given context doesn't match with any context", + this)); + return; + } + + if (mComposingContext && aContext != mComposingContext) { + // XXX For now, we should ignore this odd case, just logging. + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p OnChangeCompositionNative(), Warning, " + "given context doesn't match with composing context", + this)); + } + + nsAutoString compositionString; + GetCompositionString(aContext, compositionString); + if (!IsComposing() && compositionString.IsEmpty()) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p OnChangeCompositionNative(), Warning, does nothing " + "because has not started composition and composing string is " + "empty", + this)); + mDispatchedCompositionString.Truncate(); + return; // Don't start the composition with empty string. + } + + // Be aware, widget can be gone + DispatchCompositionChangeEvent(aContext, compositionString); +} + +/* static */ +gboolean IMContextWrapper::OnRetrieveSurroundingCallback( + GtkIMContext* aContext, IMContextWrapper* aModule) { + return aModule->OnRetrieveSurroundingNative(aContext); +} + +gboolean IMContextWrapper::OnRetrieveSurroundingNative(GtkIMContext* aContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnRetrieveSurroundingNative(aContext=0x%p), " + "current context=0x%p", + this, aContext, GetCurrentContext())); + + // See bug 472635, we should do nothing if IM context doesn't match. + if (GetCurrentContext() != aContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnRetrieveSurroundingNative(), FAILED, " + "given context doesn't match", + this)); + return FALSE; + } + + nsAutoString uniStr; + uint32_t cursorPos; + if (NS_FAILED(GetCurrentParagraph(uniStr, cursorPos))) { + return FALSE; + } + + // Despite taking a pointer and a length, IBus wants the string to be + // zero-terminated and doesn't like U+0000 within the string. + uniStr.ReplaceChar(char16_t(0), char16_t(0xFFFD)); + + NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos)); + uint32_t cursorPosInUTF8 = utf8Str.Length(); + AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str); + gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(), + cursorPosInUTF8); + mRetrieveSurroundingSignalReceived = true; + return TRUE; +} + +/* static */ +gboolean IMContextWrapper::OnDeleteSurroundingCallback( + GtkIMContext* aContext, gint aOffset, gint aNChars, + IMContextWrapper* aModule) { + return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars); +} + +gboolean IMContextWrapper::OnDeleteSurroundingNative(GtkIMContext* aContext, + gint aOffset, + gint aNChars) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnDeleteSurroundingNative(aContext=0x%p, aOffset=%d, " + "aNChar=%d), current context=0x%p", + this, aContext, aOffset, aNChars, GetCurrentContext())); + + // See bug 472635, we should do nothing if IM context doesn't match. + if (GetCurrentContext() != aContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnDeleteSurroundingNative(), FAILED, " + "given context doesn't match", + this)); + return FALSE; + } + + AutoRestore saveDeletingSurrounding(mIsDeletingSurrounding); + mIsDeletingSurrounding = true; + if (NS_SUCCEEDED(DeleteText(aContext, aOffset, (uint32_t)aNChars))) { + return TRUE; + } + + // failed + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnDeleteSurroundingNative(), FAILED, " + "cannot delete text", + this)); + return FALSE; +} + +/* static */ +void IMContextWrapper::OnCommitCompositionCallback(GtkIMContext* aContext, + const gchar* aString, + IMContextWrapper* aModule) { + aModule->OnCommitCompositionNative(aContext, aString); +} + +void IMContextWrapper::OnCommitCompositionNative(GtkIMContext* aContext, + const gchar* aUTF8Char) { + const gchar emptyStr = 0; + const gchar* commitString = aUTF8Char ? aUTF8Char : &emptyStr; + NS_ConvertUTF8toUTF16 utf16CommitString(commitString); + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(aContext=0x%p), " + "current context=0x%p, active context=0x%p, commitString=\"%s\", " + "mProcessingKeyEvent=0x%p, IsComposingOn(aContext)=%s", + this, aContext, GetCurrentContext(), GetActiveContext(), + commitString, mProcessingKeyEvent, ToChar(IsComposingOn(aContext)))); + + // See bug 472635, we should do nothing if IM context doesn't match. + if (!IsValidContext(aContext)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnCommitCompositionNative(), FAILED, " + "given context doesn't match", + this)); + return; + } + + // If we are not in composition and committing with empty string, + // we need to do nothing because if we continued to handle this + // signal, we would dispatch compositionstart, text, compositionend + // events with empty string. Of course, they are unnecessary events + // for Web applications and our editor. + if (!IsComposingOn(aContext) && utf16CommitString.IsEmpty()) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p OnCommitCompositionNative(), Warning, does nothing " + "because has not started composition and commit string is empty", + this)); + return; + } + + // If IME doesn't change their keyevent that generated this commit, + // we should treat that IME didn't handle the key event because + // web applications want to receive "keydown" and "keypress" event + // in such case. + // NOTE: While a key event is being handled, this might be caused on + // current context. Otherwise, this may be caused on active context. + if (!IsComposingOn(aContext) && mProcessingKeyEvent && + mProcessingKeyEvent->type == GDK_KEY_PRESS && + aContext == GetCurrentContext()) { + char keyval_utf8[8]; /* should have at least 6 bytes of space */ + gint keyval_utf8_len; + guint32 keyval_unicode; + + keyval_unicode = gdk_keyval_to_unicode(mProcessingKeyEvent->keyval); + keyval_utf8_len = g_unichar_to_utf8(keyval_unicode, keyval_utf8); + keyval_utf8[keyval_utf8_len] = '\0'; + + // If committing string is exactly same as a character which is + // produced by the key, eKeyDown and eKeyPress event should be + // dispatched by the caller of OnKeyEvent() normally. Note that + // mMaybeInDeadKeySequence will be set to false by OnKeyEvent() + // since we set mFallbackToKeyEvent to true here. + if (!strcmp(commitString, keyval_utf8)) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(), " + "we'll send normal key event", + this)); + mFallbackToKeyEvent = true; + return; + } + + // If we're in a dead key sequence, commit string is a character in + // the BMP and mProcessingKeyEvent produces some characters but it's + // not same as committing string, we should dispatch an eKeyPress + // event from here. + WidgetKeyboardEvent keyDownEvent(true, eKeyDown, mLastFocusedWindow); + KeymapWrapper::InitKeyEvent(keyDownEvent, mProcessingKeyEvent, false); + if (mMaybeInDeadKeySequence && utf16CommitString.Length() == 1 && + keyDownEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) { + mKeyboardEventWasDispatched = true; + // Anyway, we're not in dead key sequence anymore. + mMaybeInDeadKeySequence = false; + + RefPtr dispatcher = GetTextEventDispatcher(); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p OnCommitCompositionNative(), FAILED, " + "due to BeginNativeInputTransaction() failure", + this)); + return; + } + + // First, dispatch eKeyDown event. + keyDownEvent.mKeyValue = utf16CommitString; + nsEventStatus status = nsEventStatus_eIgnore; + bool dispatched = dispatcher->DispatchKeyboardEvent( + eKeyDown, keyDownEvent, status, mProcessingKeyEvent); + if (!dispatched || status == nsEventStatus_eConsumeNoDefault) { + mKeyboardEventWasConsumed = true; + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(), " + "doesn't dispatch eKeyPress event because the preceding " + "eKeyDown event was not dispatched or was consumed", + this)); + return; + } + if (mLastFocusedWindow != keyDownEvent.mWidget || + mLastFocusedWindow->Destroyed()) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p OnCommitCompositionNative(), Warning, " + "stop dispatching eKeyPress event because the preceding " + "eKeyDown event caused changing focused widget or " + "destroyed", + this)); + return; + } + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(), " + "dispatched eKeyDown event for the committed character", + this)); + + // Next, dispatch eKeyPress event. + dispatcher->MaybeDispatchKeypressEvents(keyDownEvent, status, + mProcessingKeyEvent); + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(), " + "dispatched eKeyPress event for the committed character", + this)); + return; + } + } + + NS_ConvertUTF8toUTF16 str(commitString); + // Be aware, widget can be gone + DispatchCompositionCommitEvent(aContext, &str); +} + +void IMContextWrapper::GetCompositionString(GtkIMContext* aContext, + nsAString& aCompositionString) { + gchar* preedit_string; + gint cursor_pos; + PangoAttrList* feedback_list; + gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list, + &cursor_pos); + if (preedit_string && *preedit_string) { + CopyUTF8toUTF16(MakeStringSpan(preedit_string), aCompositionString); + } else { + aCompositionString.Truncate(); + } + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p GetCompositionString(aContext=0x%p), " + "aCompositionString=\"%s\"", + this, aContext, preedit_string)); + + pango_attr_list_unref(feedback_list); + g_free(preedit_string); +} + +bool IMContextWrapper::MaybeDispatchKeyEventAsProcessedByIME( + EventMessage aFollowingEvent) { + if (!mLastFocusedWindow) { + return false; + } + + if (!mIsKeySnooped && + ((!mProcessingKeyEvent && mPostingKeyEvents.IsEmpty()) || + (mProcessingKeyEvent && mKeyboardEventWasDispatched))) { + return true; + } + + // A "keydown" or "keyup" event handler may change focus with the + // following event. In such case, we need to cancel this composition. + // So, we need to store IM context now because mComposingContext may be + // overwritten with different context if calling this method recursively. + // Note that we don't need to grab the context here because |context| + // will be used only for checking if it's same as mComposingContext. + GtkIMContext* oldCurrentContext = GetCurrentContext(); + GtkIMContext* oldComposingContext = mComposingContext; + + RefPtr lastFocusedWindow(mLastFocusedWindow); + + if (mProcessingKeyEvent || !mPostingKeyEvents.IsEmpty()) { + if (mProcessingKeyEvent) { + mKeyboardEventWasDispatched = true; + } + // If we're not handling a key event synchronously, the signal may be + // sent by IME without sending key event to us. In such case, we + // should dispatch keyboard event for the last key event which was + // posted to other IME process. + GdkEventKey* sourceEvent = mProcessingKeyEvent + ? mProcessingKeyEvent + : mPostingKeyEvents.GetFirstEvent(); + + MOZ_LOG( + gGtkIMLog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(" + "aFollowingEvent=%s), dispatch %s %s " + "event: { type=%s, keyval=%s, unicode=0x%X, state=%s, " + "time=%u, hardware_keycode=%u, group=%u }", + this, ToChar(aFollowingEvent), + ToChar(sourceEvent->type == GDK_KEY_PRESS ? eKeyDown : eKeyUp), + mProcessingKeyEvent ? "processing" : "posted", + GetEventType(sourceEvent), gdk_keyval_name(sourceEvent->keyval), + gdk_keyval_to_unicode(sourceEvent->keyval), + GetEventStateName(sourceEvent->state, mIMContextID).get(), + sourceEvent->time, sourceEvent->hardware_keycode, sourceEvent->group)); + + // Let's dispatch eKeyDown event or eKeyUp event now. Note that only + // when we're not in a dead key composition, we should mark the + // eKeyDown and eKeyUp event as "processed by IME" since we should + // expose raw keyCode and key value to web apps the key event is a + // part of a dead key sequence. + // FYI: We should ignore if default of preceding keydown or keyup + // event is prevented since even on the other browsers, web + // applications cannot cancel the following composition event. + // Spec bug: https://github.com/w3c/uievents/issues/180 + KeymapWrapper::DispatchKeyDownOrKeyUpEvent(lastFocusedWindow, sourceEvent, + !mMaybeInDeadKeySequence, + &mKeyboardEventWasConsumed); + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), keydown or keyup " + "event is dispatched", + this)); + + if (!mProcessingKeyEvent) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), removing first " + "event from the queue", + this)); + mPostingKeyEvents.RemoveEvent(sourceEvent); + } + } else { + MOZ_ASSERT(mIsKeySnooped); + // Currently, we support key snooper mode of uim and wayland only. + MOZ_ASSERT(mIMContextID == IMContextID::Uim || + mIMContextID == IMContextID::Wayland); + // uim sends "preedit_start" signal and "preedit_changed" separately + // at starting composition, "commit" and "preedit_end" separately at + // committing composition. + + // Currently, we should dispatch only fake eKeyDown event because + // we cannot decide which is the last signal of each key operation + // and Chromium also dispatches only "keydown" event in this case. + bool dispatchFakeKeyDown = false; + switch (aFollowingEvent) { + case eCompositionStart: + case eCompositionCommit: + case eCompositionCommitAsIs: + dispatchFakeKeyDown = true; + break; + // XXX Unfortunately, I don't have a good idea to prevent to + // dispatch redundant eKeyDown event for eCompositionStart + // immediately after "delete_surrounding" signal. However, + // not dispatching eKeyDown event is worse than dispatching + // redundant eKeyDown events. + case eContentCommandDelete: + dispatchFakeKeyDown = true; + break; + // We need to prevent to dispatch redundant eKeyDown event for + // eCompositionChange immediately after eCompositionStart. So, + // We should not dispatch eKeyDown event if dispatched composition + // string is still empty string. + case eCompositionChange: + dispatchFakeKeyDown = !mDispatchedCompositionString.IsEmpty(); + break; + default: + MOZ_ASSERT_UNREACHABLE("Do you forget to handle the case?"); + break; + } + + if (dispatchFakeKeyDown) { + WidgetKeyboardEvent fakeKeyDownEvent(true, eKeyDown, lastFocusedWindow); + fakeKeyDownEvent.mKeyCode = NS_VK_PROCESSKEY; + fakeKeyDownEvent.mKeyNameIndex = KEY_NAME_INDEX_Process; + // It's impossible to get physical key information in this case but + // this should be okay since web apps shouldn't do anything with + // physical key information during composition. + fakeKeyDownEvent.mCodeNameIndex = CODE_NAME_INDEX_UNKNOWN; + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(" + "aFollowingEvent=%s), dispatch fake eKeyDown event", + this, ToChar(aFollowingEvent))); + + KeymapWrapper::DispatchKeyDownOrKeyUpEvent( + lastFocusedWindow, fakeKeyDownEvent, &mKeyboardEventWasConsumed); + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), " + "fake keydown event is dispatched", + this)); + } + } + + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the " + "focused widget was destroyed/changed by a key event", + this)); + return false; + } + + // If the dispatched keydown event caused moving focus and that also + // caused changing active context, we need to cancel composition here. + if (GetCurrentContext() != oldCurrentContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the key " + "event causes changing active IM context", + this)); + if (mComposingContext == oldComposingContext) { + // Only when the context is still composing, we should call + // ResetIME() here. Otherwise, it should've already been + // cleaned up. + ResetIME(); + } + return false; + } + + return true; +} + +bool IMContextWrapper::DispatchCompositionStart(GtkIMContext* aContext) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p DispatchCompositionStart(aContext=0x%p)", this, aContext)); + + if (IsComposing()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "we're already in composition", + this)); + return true; + } + + if (!mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "there are no focused window in this module", + this)); + return false; + } + + if (NS_WARN_IF(!EnsureToCacheSelection())) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "cannot query the selection offset", + this)); + return false; + } + + mComposingContext = static_cast(g_object_ref(aContext)); + MOZ_ASSERT(mComposingContext); + + // Keep the last focused window alive + RefPtr lastFocusedWindow(mLastFocusedWindow); + + // XXX The composition start point might be changed by composition events + // even though we strongly hope it doesn't happen. + // Every composition event should have the start offset for the result + // because it may high cost if we query the offset every time. + mCompositionStart = mSelection.mOffset; + mDispatchedCompositionString.Truncate(); + + // If this composition is started by a key press, we need to dispatch + // eKeyDown or eKeyUp event before dispatching eCompositionStart event. + // Note that dispatching a keyboard event which is marked as "processed + // by IME" is okay since Chromium also dispatches keyboard event as so. + if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionStart)) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p DispatchCompositionStart(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + return false; + } + + RefPtr dispatcher = GetTextEventDispatcher(); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + static bool sHasSetTelemetry = false; + if (!sHasSetTelemetry) { + sHasSetTelemetry = true; + NS_ConvertUTF8toUTF16 im(GetIMName()); + // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp + if (im.Length() > 72) { + if (NS_IS_SURROGATE_PAIR(im[72 - 2], im[72 - 1])) { + im.Truncate(72 - 2); + } else { + im.Truncate(72 - 1); + } + // U+2026 is "..." + im.Append(char16_t(0x2026)); + } + Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_LINUX, im, + true); + } + + MOZ_LOG(gGtkIMLog, LogLevel::Debug, + ("0x%p DispatchCompositionStart(), dispatching " + "compositionstart... (mCompositionStart=%u)", + this, mCompositionStart)); + mCompositionState = eCompositionState_CompositionStartDispatched; + nsEventStatus status; + dispatcher->StartComposition(status); + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, the focused " + "widget was destroyed/changed by compositionstart event", + this)); + return false; + } + + return true; +} + +bool IMContextWrapper::DispatchCompositionChangeEvent( + GtkIMContext* aContext, const nsAString& aCompositionString) { + MOZ_LOG( + gGtkIMLog, LogLevel::Info, + ("0x%p DispatchCompositionChangeEvent(aContext=0x%p)", this, aContext)); + + if (!mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "there are no focused window in this module", + this)); + return false; + } + + if (!IsComposing()) { + MOZ_LOG(gGtkIMLog, LogLevel::Debug, + ("0x%p DispatchCompositionChangeEvent(), the composition " + "wasn't started, force starting...", + this)); + if (!DispatchCompositionStart(aContext)) { + return false; + } + } + // If this composition string change caused by a key press, we need to + // dispatch eKeyDown or eKeyUp before dispatching eCompositionChange event. + else if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionChange)) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p DispatchCompositionChangeEvent(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + return false; + } + + RefPtr dispatcher = GetTextEventDispatcher(); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + // Store the selected string which will be removed by following + // compositionchange event. + if (mCompositionState == eCompositionState_CompositionStartDispatched) { + if (NS_WARN_IF( + !EnsureToCacheSelection(&mSelectedStringRemovedByComposition))) { + // XXX How should we behave in this case?? + } else { + // XXX We should assume, for now, any web applications don't change + // selection at handling this compositionchange event. + mCompositionStart = mSelection.mOffset; + } + } + + RefPtr rangeArray = + CreateTextRangeArray(aContext, aCompositionString); + + rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to SetPendingComposition() failure", + this)); + return false; + } + + mCompositionState = eCompositionState_CompositionChangeEventDispatched; + + // We cannot call SetCursorPosition for e10s-aware. + // DispatchEvent is async on e10s, so composition rect isn't updated now + // on tab parent. + mDispatchedCompositionString = aCompositionString; + mLayoutChanged = false; + mCompositionTargetRange.mOffset = + mCompositionStart + rangeArray->TargetClauseOffset(); + mCompositionTargetRange.mLength = rangeArray->TargetClauseLength(); + + RefPtr lastFocusedWindow(mLastFocusedWindow); + nsEventStatus status; + rv = dispatcher->FlushPendingComposition(status); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to FlushPendingComposition() failure", + this)); + return false; + } + + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, the " + "focused widget was destroyed/changed by " + "compositionchange event", + this)); + return false; + } + return true; +} + +bool IMContextWrapper::DispatchCompositionCommitEvent( + GtkIMContext* aContext, const nsAString* aCommitString) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p DispatchCompositionCommitEvent(aContext=0x%p, " + "aCommitString=0x%p, (\"%s\"))", + this, aContext, aCommitString, + aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "")); + + if (!mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "there are no focused window in this module", + this)); + return false; + } + + // TODO: We need special care to handle request to commit composition + // by content while we're committing composition because we have + // commit string information now but IME may not have composition + // anymore. Therefore, we may not be able to handle commit as + // expected. However, this is rare case because this situation + // never occurs with remote content. So, it's okay to fix this + // issue later. (Perhaps, TextEventDisptcher should do it for + // all platforms. E.g., creating WillCommitComposition()?) + if (!IsComposing()) { + if (!aCommitString || aCommitString->IsEmpty()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "there is no composition and empty commit string", + this)); + return true; + } + MOZ_LOG(gGtkIMLog, LogLevel::Debug, + ("0x%p DispatchCompositionCommitEvent(), " + "the composition wasn't started, force starting...", + this)); + if (!DispatchCompositionStart(aContext)) { + return false; + } + } + // If this commit caused by a key press, we need to dispatch eKeyDown or + // eKeyUp before dispatching composition events. + else if (!MaybeDispatchKeyEventAsProcessedByIME( + aCommitString ? eCompositionCommit : eCompositionCommitAsIs)) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p DispatchCompositionCommitEvent(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + mCompositionState = eCompositionState_NotComposing; + return false; + } + + RefPtr dispatcher = GetTextEventDispatcher(); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + RefPtr lastFocusedWindow(mLastFocusedWindow); + + // Emulate selection until receiving actual selection range. + mSelection.CollapseTo( + mCompositionStart + (aCommitString + ? aCommitString->Length() + : mDispatchedCompositionString.Length()), + mSelection.mWritingMode); + + mCompositionState = eCompositionState_NotComposing; + // Reset dead key sequence too because GTK doesn't support dead key chain + // (i.e., a key press doesn't cause both producing some characters and + // restarting new dead key sequence at one time). So, committing + // composition means end of a dead key sequence. + mMaybeInDeadKeySequence = false; + mCompositionStart = UINT32_MAX; + mCompositionTargetRange.Clear(); + mDispatchedCompositionString.Truncate(); + mSelectedStringRemovedByComposition.Truncate(); + + nsEventStatus status; + rv = dispatcher->CommitComposition(status, aCommitString); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to CommitComposition() failure", + this)); + return false; + } + + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "the focused widget was destroyed/changed by " + "compositioncommit event", + this)); + return false; + } + + return true; +} + +already_AddRefed IMContextWrapper::CreateTextRangeArray( + GtkIMContext* aContext, const nsAString& aCompositionString) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p CreateTextRangeArray(aContext=0x%p, " + "aCompositionString=\"%s\" (Length()=%u))", + this, aContext, NS_ConvertUTF16toUTF8(aCompositionString).get(), + aCompositionString.Length())); + + RefPtr textRangeArray = new TextRangeArray(); + + gchar* preedit_string; + gint cursor_pos_in_chars; + PangoAttrList* feedback_list; + gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list, + &cursor_pos_in_chars); + if (!preedit_string || !*preedit_string) { + if (!aCompositionString.IsEmpty()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p CreateTextRangeArray(), FAILED, due to " + "preedit_string is null", + this)); + } + pango_attr_list_unref(feedback_list); + g_free(preedit_string); + return textRangeArray.forget(); + } + + // Convert caret offset from offset in characters to offset in UTF-16 + // string. If we couldn't proper offset in UTF-16 string, we should + // assume that the caret is at the end of the composition string. + uint32_t caretOffsetInUTF16 = aCompositionString.Length(); + if (NS_WARN_IF(cursor_pos_in_chars < 0)) { + // Note that this case is undocumented. We should assume that the + // caret is at the end of the composition string. + } else if (cursor_pos_in_chars == 0) { + caretOffsetInUTF16 = 0; + } else { + gchar* charAfterCaret = + g_utf8_offset_to_pointer(preedit_string, cursor_pos_in_chars); + if (NS_WARN_IF(!charAfterCaret)) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), failed to get UTF-8 " + "string before the caret (cursor_pos_in_chars=%d)", + this, cursor_pos_in_chars)); + } else { + glong caretOffset = 0; + gunichar2* utf16StrBeforeCaret = + g_utf8_to_utf16(preedit_string, charAfterCaret - preedit_string, + nullptr, &caretOffset, nullptr); + if (NS_WARN_IF(!utf16StrBeforeCaret) || NS_WARN_IF(caretOffset < 0)) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), WARNING, failed to " + "convert to UTF-16 string before the caret " + "(cursor_pos_in_chars=%d, caretOffset=%ld)", + this, cursor_pos_in_chars, caretOffset)); + } else { + caretOffsetInUTF16 = static_cast(caretOffset); + uint32_t compositionStringLength = aCompositionString.Length(); + if (NS_WARN_IF(caretOffsetInUTF16 > compositionStringLength)) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), WARNING, " + "caretOffsetInUTF16=%u is larger than " + "compositionStringLength=%u", + this, caretOffsetInUTF16, compositionStringLength)); + caretOffsetInUTF16 = compositionStringLength; + } + } + if (utf16StrBeforeCaret) { + g_free(utf16StrBeforeCaret); + } + } + } + + PangoAttrIterator* iter; + iter = pango_attr_list_get_iterator(feedback_list); + if (!iter) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p CreateTextRangeArray(), FAILED, iterator couldn't " + "be allocated", + this)); + pango_attr_list_unref(feedback_list); + g_free(preedit_string); + return textRangeArray.forget(); + } + + uint32_t minOffsetOfClauses = aCompositionString.Length(); + uint32_t maxOffsetOfClauses = 0; + do { + TextRange range; + if (!SetTextRange(iter, preedit_string, caretOffsetInUTF16, range)) { + continue; + } + MOZ_ASSERT(range.Length()); + minOffsetOfClauses = std::min(minOffsetOfClauses, range.mStartOffset); + maxOffsetOfClauses = std::max(maxOffsetOfClauses, range.mEndOffset); + textRangeArray->AppendElement(range); + } while (pango_attr_iterator_next(iter)); + + // If the IME doesn't define clause from the start of the composition, + // we should insert dummy clause information since TextRangeArray assumes + // that there must be a clause whose start is 0 when there is one or + // more clauses. + if (minOffsetOfClauses) { + TextRange dummyClause; + dummyClause.mStartOffset = 0; + dummyClause.mEndOffset = minOffsetOfClauses; + dummyClause.mRangeType = TextRangeType::eRawClause; + textRangeArray->InsertElementAt(0, dummyClause); + maxOffsetOfClauses = std::max(maxOffsetOfClauses, dummyClause.mEndOffset); + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), inserting a dummy clause " + "at the beginning of the composition string mStartOffset=%u, " + "mEndOffset=%u, mRangeType=%s", + this, dummyClause.mStartOffset, dummyClause.mEndOffset, + ToChar(dummyClause.mRangeType))); + } + + // If the IME doesn't define clause at end of the composition, we should + // insert dummy clause information since TextRangeArray assumes that there + // must be a clase whose end is the length of the composition string when + // there is one or more clauses. + if (!textRangeArray->IsEmpty() && + maxOffsetOfClauses < aCompositionString.Length()) { + TextRange dummyClause; + dummyClause.mStartOffset = maxOffsetOfClauses; + dummyClause.mEndOffset = aCompositionString.Length(); + dummyClause.mRangeType = TextRangeType::eRawClause; + textRangeArray->AppendElement(dummyClause); + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), inserting a dummy clause " + "at the end of the composition string mStartOffset=%u, " + "mEndOffset=%u, mRangeType=%s", + this, dummyClause.mStartOffset, dummyClause.mEndOffset, + ToChar(dummyClause.mRangeType))); + } + + TextRange range; + range.mStartOffset = range.mEndOffset = caretOffsetInUTF16; + range.mRangeType = TextRangeType::eCaret; + textRangeArray->AppendElement(range); + MOZ_LOG( + gGtkIMLog, LogLevel::Debug, + ("0x%p CreateTextRangeArray(), mStartOffset=%u, " + "mEndOffset=%u, mRangeType=%s", + this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType))); + + pango_attr_iterator_destroy(iter); + pango_attr_list_unref(feedback_list); + g_free(preedit_string); + + return textRangeArray.forget(); +} + +/* static */ +nscolor IMContextWrapper::ToNscolor(PangoAttrColor* aPangoAttrColor) { + PangoColor& pangoColor = aPangoAttrColor->color; + uint8_t r = pangoColor.red / 0x100; + uint8_t g = pangoColor.green / 0x100; + uint8_t b = pangoColor.blue / 0x100; + return NS_RGB(r, g, b); +} + +bool IMContextWrapper::SetTextRange(PangoAttrIterator* aPangoAttrIter, + const gchar* aUTF8CompositionString, + uint32_t aUTF16CaretOffset, + TextRange& aTextRange) const { + // Set the range offsets in UTF-16 string. + gint utf8ClauseStart, utf8ClauseEnd; + pango_attr_iterator_range(aPangoAttrIter, &utf8ClauseStart, &utf8ClauseEnd); + if (utf8ClauseStart == utf8ClauseEnd) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p SetTextRange(), FAILED, due to collapsed range", this)); + return false; + } + + if (!utf8ClauseStart) { + aTextRange.mStartOffset = 0; + } else { + glong utf16PreviousClausesLength; + gunichar2* utf16PreviousClausesString = + g_utf8_to_utf16(aUTF8CompositionString, utf8ClauseStart, nullptr, + &utf16PreviousClausesLength, nullptr); + + if (NS_WARN_IF(!utf16PreviousClausesString)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() " + "failure (retrieving previous string of current clause)", + this)); + return false; + } + + aTextRange.mStartOffset = utf16PreviousClausesLength; + g_free(utf16PreviousClausesString); + } + + glong utf16CurrentClauseLength; + gunichar2* utf16CurrentClauseString = g_utf8_to_utf16( + aUTF8CompositionString + utf8ClauseStart, utf8ClauseEnd - utf8ClauseStart, + nullptr, &utf16CurrentClauseLength, nullptr); + + if (NS_WARN_IF(!utf16CurrentClauseString)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() " + "failure (retrieving current clause)", + this)); + return false; + } + + // iBus Chewing IME tells us that there is an empty clause at the end of + // the composition string but we should ignore it since our code doesn't + // assume that there is an empty clause. + if (!utf16CurrentClauseLength) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p SetTextRange(), FAILED, due to current clause length " + "is 0", + this)); + return false; + } + + aTextRange.mEndOffset = aTextRange.mStartOffset + utf16CurrentClauseLength; + g_free(utf16CurrentClauseString); + utf16CurrentClauseString = nullptr; + + // Set styles + TextRangeStyle& style = aTextRange.mRangeStyle; + + // Underline + PangoAttrInt* attrUnderline = reinterpret_cast( + pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE)); + if (attrUnderline) { + switch (attrUnderline->value) { + case PANGO_UNDERLINE_NONE: + style.mLineStyle = TextRangeStyle::LineStyle::None; + break; + case PANGO_UNDERLINE_DOUBLE: + style.mLineStyle = TextRangeStyle::LineStyle::Double; + break; + case PANGO_UNDERLINE_ERROR: + style.mLineStyle = TextRangeStyle::LineStyle::Wavy; + break; + case PANGO_UNDERLINE_SINGLE: + case PANGO_UNDERLINE_LOW: + style.mLineStyle = TextRangeStyle::LineStyle::Solid; + break; + default: + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p SetTextRange(), retrieved unknown underline " + "style: %d", + this, attrUnderline->value)); + style.mLineStyle = TextRangeStyle::LineStyle::Solid; + break; + } + style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE; + + // Underline color + PangoAttrColor* attrUnderlineColor = reinterpret_cast( + pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE_COLOR)); + if (attrUnderlineColor) { + style.mUnderlineColor = ToNscolor(attrUnderlineColor); + style.mDefinedStyles |= TextRangeStyle::DEFINED_UNDERLINE_COLOR; + } + } else { + style.mLineStyle = TextRangeStyle::LineStyle::None; + style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE; + } + + // Don't set colors if they are not specified. They should be computed by + // textframe if only one of the colors are specified. + + // Foreground color (text color) + PangoAttrColor* attrForeground = reinterpret_cast( + pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_FOREGROUND)); + if (attrForeground) { + style.mForegroundColor = ToNscolor(attrForeground); + style.mDefinedStyles |= TextRangeStyle::DEFINED_FOREGROUND_COLOR; + } + + // Background color + PangoAttrColor* attrBackground = reinterpret_cast( + pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_BACKGROUND)); + if (attrBackground) { + style.mBackgroundColor = ToNscolor(attrBackground); + style.mDefinedStyles |= TextRangeStyle::DEFINED_BACKGROUND_COLOR; + } + + /** + * We need to judge the meaning of the clause for a11y. Before we support + * IME specific composition string style, we used following rules: + * + * 1: If attrUnderline and attrForground are specified, we assumed the + * clause is TextRangeType::eSelectedClause. + * 2: If only attrUnderline is specified, we assumed the clause is + * TextRangeType::eConvertedClause. + * 3: If only attrForground is specified, we assumed the clause is + * TextRangeType::eSelectedRawClause. + * 4: If neither attrUnderline nor attrForeground is specified, we assumed + * the clause is TextRangeType::eRawClause. + * + * However, this rules are odd since there can be two or more selected + * clauses. Additionally, our old rules caused that IME developers/users + * cannot specify composition string style as they want. + * + * So, we shouldn't guess the meaning from its visual style. + */ + + // If the range covers whole of composition string and the caret is at + // the end of the composition string, the range is probably not converted. + if (!utf8ClauseStart && + utf8ClauseEnd == static_cast(strlen(aUTF8CompositionString)) && + aTextRange.mEndOffset == aUTF16CaretOffset) { + aTextRange.mRangeType = TextRangeType::eRawClause; + } + // Typically, the caret is set at the start of the selected clause. + // So, if the caret is in the clause, we can assume that the clause is + // selected. + else if (aTextRange.mStartOffset <= aUTF16CaretOffset && + aTextRange.mEndOffset > aUTF16CaretOffset) { + aTextRange.mRangeType = TextRangeType::eSelectedClause; + } + // Otherwise, we should assume that the clause is converted but not + // selected. + else { + aTextRange.mRangeType = TextRangeType::eConvertedClause; + } + + MOZ_LOG(gGtkIMLog, LogLevel::Debug, + ("0x%p SetTextRange(), succeeded, aTextRange= { " + "mStartOffset=%u, mEndOffset=%u, mRangeType=%s, mRangeStyle=%s }", + this, aTextRange.mStartOffset, aTextRange.mEndOffset, + ToChar(aTextRange.mRangeType), + GetTextRangeStyleText(aTextRange.mRangeStyle).get())); + + return true; +} + +void IMContextWrapper::SetCursorPosition(GtkIMContext* aContext) { + MOZ_LOG( + gGtkIMLog, LogLevel::Info, + ("0x%p SetCursorPosition(aContext=0x%p), " + "mCompositionTargetRange={ mOffset=%u, mLength=%u }" + "mSelection={ mOffset=%u, Length()=%u, mWritingMode=%s }", + this, aContext, mCompositionTargetRange.mOffset, + mCompositionTargetRange.mLength, mSelection.mOffset, mSelection.Length(), + GetWritingModeName(mSelection.mWritingMode).get())); + + bool useCaret = false; + if (!mCompositionTargetRange.IsValid()) { + if (!mSelection.IsValid()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, " + "mCompositionTargetRange and mSelection are invalid", + this)); + return; + } + useCaret = true; + } + + if (!mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, due to no focused " + "window", + this)); + return; + } + + if (MOZ_UNLIKELY(!aContext)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, due to no context", this)); + return; + } + + WidgetQueryContentEvent queryCaretOrTextRectEvent( + true, useCaret ? eQueryCaretRect : eQueryTextRect, mLastFocusedWindow); + if (useCaret) { + queryCaretOrTextRectEvent.InitForQueryCaretRect(mSelection.mOffset); + } else { + if (mSelection.mWritingMode.IsVertical()) { + // For preventing the candidate window to overlap the target + // clause, we should set fake (typically, very tall) caret rect. + uint32_t length = + mCompositionTargetRange.mLength ? mCompositionTargetRange.mLength : 1; + queryCaretOrTextRectEvent.InitForQueryTextRect( + mCompositionTargetRange.mOffset, length); + } else { + queryCaretOrTextRectEvent.InitForQueryTextRect( + mCompositionTargetRange.mOffset, 1); + } + } + InitEvent(queryCaretOrTextRectEvent); + nsEventStatus status; + mLastFocusedWindow->DispatchEvent(&queryCaretOrTextRectEvent, status); + if (queryCaretOrTextRectEvent.Failed()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, %s was failed", this, + useCaret ? "eQueryCaretRect" : "eQueryTextRect")); + return; + } + + nsWindow* rootWindow = + static_cast(mLastFocusedWindow->GetTopLevelWidget()); + + // Get the position of the rootWindow in screen. + LayoutDeviceIntPoint root = rootWindow->WidgetToScreenOffset(); + + // Get the position of IM context owner window in screen. + LayoutDeviceIntPoint owner = mOwnerWindow->WidgetToScreenOffset(); + + // Compute the caret position in the IM owner window. + LayoutDeviceIntRect rect = + queryCaretOrTextRectEvent.mReply->mRect + root - owner; + rect.width = 0; + GdkRectangle area = rootWindow->DevicePixelsToGdkRectRoundOut(rect); + + gtk_im_context_set_cursor_location(aContext, &area); +} + +nsresult IMContextWrapper::GetCurrentParagraph(nsAString& aText, + uint32_t& aCursorPos) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p GetCurrentParagraph(), mCompositionState=%s", this, + GetCompositionStateName())); + + if (!mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p GetCurrentParagraph(), FAILED, there are no " + "focused window in this module", + this)); + return NS_ERROR_NULL_POINTER; + } + + nsEventStatus status; + + uint32_t selOffset = mCompositionStart; + uint32_t selLength = mSelectedStringRemovedByComposition.Length(); + + // If focused editor doesn't have composition string, we should use + // current selection. + if (!EditorHasCompositionString()) { + // Query cursor position & selection + if (NS_WARN_IF(!EnsureToCacheSelection())) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p GetCurrentParagraph(), FAILED, due to no " + "valid selection information", + this)); + return NS_ERROR_FAILURE; + } + + selOffset = mSelection.mOffset; + selLength = mSelection.Length(); + } + + MOZ_LOG(gGtkIMLog, LogLevel::Debug, + ("0x%p GetCurrentParagraph(), selOffset=%u, selLength=%u", this, + selOffset, selLength)); + + // XXX nsString::Find and nsString::RFind take int32_t for offset, so, + // we cannot support this request when the current offset is larger + // than INT32_MAX. + if (selOffset > INT32_MAX || selLength > INT32_MAX || + selOffset + selLength > INT32_MAX) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p GetCurrentParagraph(), FAILED, The selection is " + "out of range", + this)); + return NS_ERROR_FAILURE; + } + + // Get all text contents of the focused editor + WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, + mLastFocusedWindow); + queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX); + mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status); + if (NS_WARN_IF(queryTextContentEvent.Failed())) { + return NS_ERROR_FAILURE; + } + + if (selOffset + selLength > queryTextContentEvent.mReply->DataLength()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p GetCurrentParagraph(), FAILED, The selection is " + "invalid, queryTextContentEvent={ mReply=%s }", + this, ToString(queryTextContentEvent.mReply).c_str())); + return NS_ERROR_FAILURE; + } + + // Remove composing string and restore the selected string because + // GtkEntry doesn't remove selected string until committing, however, + // our editor does it. We should emulate the behavior for IME. + nsAutoString textContent(queryTextContentEvent.mReply->DataRef()); + if (EditorHasCompositionString() && + mDispatchedCompositionString != mSelectedStringRemovedByComposition) { + textContent.Replace(mCompositionStart, + mDispatchedCompositionString.Length(), + mSelectedStringRemovedByComposition); + } + + // Get only the focused paragraph, by looking for newlines + int32_t parStart = + (selOffset == 0) ? 0 + : textContent.RFind("\n", false, selOffset - 1, -1) + 1; + int32_t parEnd = textContent.Find("\n", false, selOffset + selLength, -1); + if (parEnd < 0) { + parEnd = textContent.Length(); + } + aText = nsDependentSubstring(textContent, parStart, parEnd - parStart); + aCursorPos = selOffset - uint32_t(parStart); + + MOZ_LOG( + gGtkIMLog, LogLevel::Debug, + ("0x%p GetCurrentParagraph(), succeeded, aText=%s, " + "aText.Length()=%u, aCursorPos=%u", + this, NS_ConvertUTF16toUTF8(aText).get(), aText.Length(), aCursorPos)); + + return NS_OK; +} + +nsresult IMContextWrapper::DeleteText(GtkIMContext* aContext, int32_t aOffset, + uint32_t aNChars) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), " + "mCompositionState=%s", + this, aContext, aOffset, aNChars, GetCompositionStateName())); + + if (!mLastFocusedWindow) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, there are no focused window " + "in this module", + this)); + return NS_ERROR_NULL_POINTER; + } + + if (!aNChars) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, aNChars must not be zero", this)); + return NS_ERROR_INVALID_ARG; + } + + RefPtr lastFocusedWindow(mLastFocusedWindow); + nsEventStatus status; + + // First, we should cancel current composition because editor cannot + // handle changing selection and deleting text. + uint32_t selOffset; + bool wasComposing = IsComposing(); + bool editorHadCompositionString = EditorHasCompositionString(); + if (wasComposing) { + selOffset = mCompositionStart; + if (!DispatchCompositionCommitEvent(aContext, + &mSelectedStringRemovedByComposition)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, quitting from DeletText", this)); + return NS_ERROR_FAILURE; + } + } else { + if (NS_WARN_IF(!EnsureToCacheSelection())) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, due to no valid selection " + "information", + this)); + return NS_ERROR_FAILURE; + } + selOffset = mSelection.mOffset; + } + + // Get all text contents of the focused editor + WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, + mLastFocusedWindow); + queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX); + mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status); + if (NS_WARN_IF(queryTextContentEvent.Failed())) { + return NS_ERROR_FAILURE; + } + if (queryTextContentEvent.mReply->IsDataEmpty()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, there is no contents", this)); + return NS_ERROR_FAILURE; + } + + NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring( + queryTextContentEvent.mReply->DataRef(), 0, selOffset)); + glong offsetInUTF8Characters = + g_utf8_strlen(utf8Str.get(), utf8Str.Length()) + aOffset; + if (offsetInUTF8Characters < 0) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, aOffset is too small for " + "current cursor pos (computed offset: %ld)", + this, offsetInUTF8Characters)); + return NS_ERROR_FAILURE; + } + + AppendUTF16toUTF8( + nsDependentSubstring(queryTextContentEvent.mReply->DataRef(), selOffset), + utf8Str); + glong countOfCharactersInUTF8 = + g_utf8_strlen(utf8Str.get(), utf8Str.Length()); + glong endInUTF8Characters = offsetInUTF8Characters + aNChars; + if (countOfCharactersInUTF8 < endInUTF8Characters) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, aNChars is too large for " + "current contents (content length: %ld, computed end offset: %ld)", + this, countOfCharactersInUTF8, endInUTF8Characters)); + return NS_ERROR_FAILURE; + } + + gchar* charAtOffset = + g_utf8_offset_to_pointer(utf8Str.get(), offsetInUTF8Characters); + gchar* charAtEnd = + g_utf8_offset_to_pointer(utf8Str.get(), endInUTF8Characters); + + // Set selection to delete + WidgetSelectionEvent selectionEvent(true, eSetSelection, mLastFocusedWindow); + + nsDependentCSubstring utf8StrBeforeOffset(utf8Str, 0, + charAtOffset - utf8Str.get()); + selectionEvent.mOffset = NS_ConvertUTF8toUTF16(utf8StrBeforeOffset).Length(); + + nsDependentCSubstring utf8DeletingStr(utf8Str, utf8StrBeforeOffset.Length(), + charAtEnd - charAtOffset); + selectionEvent.mLength = NS_ConvertUTF8toUTF16(utf8DeletingStr).Length(); + + selectionEvent.mReversed = false; + selectionEvent.mExpandToClusterBoundary = false; + lastFocusedWindow->DispatchEvent(&selectionEvent, status); + + if (!selectionEvent.mSucceeded || lastFocusedWindow != mLastFocusedWindow || + lastFocusedWindow->Destroyed()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, setting selection caused " + "focus change or window destroyed", + this)); + return NS_ERROR_FAILURE; + } + + // If this deleting text caused by a key press, we need to dispatch + // eKeyDown or eKeyUp before dispatching eContentCommandDelete event. + if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandDelete)) { + MOZ_LOG(gGtkIMLog, LogLevel::Warning, + ("0x%p DeleteText(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + return NS_ERROR_FAILURE; + } + + // Delete the selection + WidgetContentCommandEvent contentCommandEvent(true, eContentCommandDelete, + mLastFocusedWindow); + mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status); + + if (!contentCommandEvent.mSucceeded || + lastFocusedWindow != mLastFocusedWindow || + lastFocusedWindow->Destroyed()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, deleting the selection caused " + "focus change or window destroyed", + this)); + return NS_ERROR_FAILURE; + } + + if (!wasComposing) { + return NS_OK; + } + + // Restore the composition at new caret position. + if (!DispatchCompositionStart(aContext)) { + MOZ_LOG( + gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, resterting composition start", this)); + return NS_ERROR_FAILURE; + } + + if (!editorHadCompositionString) { + return NS_OK; + } + + nsAutoString compositionString; + GetCompositionString(aContext, compositionString); + if (!DispatchCompositionChangeEvent(aContext, compositionString)) { + MOZ_LOG( + gGtkIMLog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, restoring composition string", this)); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void IMContextWrapper::InitEvent(WidgetGUIEvent& aEvent) { + aEvent.mTime = PR_Now() / 1000; +} + +bool IMContextWrapper::EnsureToCacheSelection(nsAString* aSelectedString) { + if (aSelectedString) { + aSelectedString->Truncate(); + } + + if (mSelection.IsValid()) { + if (aSelectedString) { + *aSelectedString = mSelection.mString; + } + return true; + } + + if (NS_WARN_IF(!mLastFocusedWindow)) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p EnsureToCacheSelection(), FAILED, due to " + "no focused window", + this)); + return false; + } + + nsEventStatus status; + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + mLastFocusedWindow); + InitEvent(querySelectedTextEvent); + mLastFocusedWindow->DispatchEvent(&querySelectedTextEvent, status); + if (NS_WARN_IF(querySelectedTextEvent.Failed())) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p EnsureToCacheSelection(), FAILED, due to " + "failure of query selection event", + this)); + return false; + } + + mSelection.Assign(querySelectedTextEvent); + if (!mSelection.IsValid()) { + MOZ_LOG(gGtkIMLog, LogLevel::Error, + ("0x%p EnsureToCacheSelection(), FAILED, due to " + "failure of query selection event (invalid result)", + this)); + return false; + } + + if (!mSelection.Collapsed() && aSelectedString) { + aSelectedString->Assign(querySelectedTextEvent.mReply->DataRef()); + } + + MOZ_LOG(gGtkIMLog, LogLevel::Debug, + ("0x%p EnsureToCacheSelection(), Succeeded, mSelection=" + "{ mOffset=%u, Length()=%u, mWritingMode=%s }", + this, mSelection.mOffset, mSelection.Length(), + GetWritingModeName(mSelection.mWritingMode).get())); + return true; +} + +/****************************************************************************** + * IMContextWrapper::Selection + ******************************************************************************/ + +void IMContextWrapper::Selection::Assign( + const IMENotification& aIMENotification) { + MOZ_ASSERT(aIMENotification.mMessage == NOTIFY_IME_OF_SELECTION_CHANGE); + mString = aIMENotification.mSelectionChangeData.String(); + mOffset = aIMENotification.mSelectionChangeData.mOffset; + mWritingMode = aIMENotification.mSelectionChangeData.GetWritingMode(); +} + +void IMContextWrapper::Selection::Assign( + const WidgetQueryContentEvent& aEvent) { + MOZ_ASSERT(aEvent.mMessage == eQuerySelectedText); + MOZ_ASSERT(aEvent.Succeeded()); + MOZ_ASSERT(aEvent.mReply->mOffsetAndData.isSome()); + mString = aEvent.mReply->DataRef(); + mOffset = aEvent.mReply->StartOffset(); + mWritingMode = aEvent.mReply->WritingModeRef(); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/IMContextWrapper.h b/widget/gtk/IMContextWrapper.h new file mode 100644 index 0000000000..9ebb22fc3d --- /dev/null +++ b/widget/gtk/IMContextWrapper.h @@ -0,0 +1,685 @@ +/* -*- 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 IMContextWrapper_h_ +#define IMContextWrapper_h_ + +#include +#include + +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsIWidget.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/EventForwards.h" +#include "mozilla/TextEventDispatcherListener.h" +#include "WritingModes.h" + +class nsWindow; + +namespace mozilla { +namespace widget { + +/** + * KeyHandlingState is result of IMContextWrapper::OnKeyEvent(). + */ +enum class KeyHandlingState { + // The native key event has not been handled by IMContextWrapper. + eNotHandled, + // The native key event was handled by IMContextWrapper. + eHandled, + // The native key event has not been handled by IMContextWrapper, + // but eKeyDown or eKeyUp event has been dispatched. + eNotHandledButEventDispatched, + // The native key event has not been handled by IMContextWrapper, + // but eKeyDown or eKeyUp event has been dispatched and consumed. + eNotHandledButEventConsumed, +}; + +class IMContextWrapper final : public TextEventDispatcherListener { + public: + // TextEventDispatcherListener implementation + 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; + + public: + // aOwnerWindow is a pointer of the owner window. When aOwnerWindow is + // destroyed, the related IME contexts are released (i.e., IME cannot be + // used with the instance after that). + explicit IMContextWrapper(nsWindow* aOwnerWindow); + + // Called when the process is being shut down. + static void Shutdown(); + + // "Enabled" means the users can use all IMEs. + // I.e., the focus is in the normal editors. + bool IsEnabled() const; + + // OnFocusWindow is a notification that aWindow is going to be focused. + void OnFocusWindow(nsWindow* aWindow); + // OnBlurWindow is a notification that aWindow is going to be unfocused. + void OnBlurWindow(nsWindow* aWindow); + // OnDestroyWindow is a notification that aWindow is going to be destroyed. + void OnDestroyWindow(nsWindow* aWindow); + // OnFocusChangeInGecko is a notification that an editor gets focus. + void OnFocusChangeInGecko(bool aFocus); + // OnSelectionChange is a notification that selection (caret) is changed + // in the focused editor. + void OnSelectionChange(nsWindow* aCaller, + const IMENotification& aIMENotification); + // OnThemeChanged is called when desktop theme is changed. + static void OnThemeChanged(); + + /** + * OnKeyEvent() is called when aWindow gets a native key press event or a + * native key release event. If this returns true, the key event was + * filtered by IME. Otherwise, this returns false. + * NOTE: When the native key press event starts composition, this returns + * true but dispatches an eKeyDown event or eKeyUp event before + * dispatching composition events or content command event. + * + * @param aWindow A window on which user operate the + * key. + * @param aEvent A native key press or release + * event. + * @param aKeyboardEventWasDispatched true if eKeyDown or eKeyUp event + * for aEvent has already been + * dispatched. In this case, + * this class doesn't dispatch + * keyboard event anymore. + */ + KeyHandlingState OnKeyEvent(nsWindow* aWindow, GdkEventKey* aEvent, + bool aKeyboardEventWasDispatched = false); + + // IME related nsIWidget methods. + nsresult EndIMEComposition(nsWindow* aCaller); + void SetInputContext(nsWindow* aCaller, const InputContext* aContext, + const InputContextAction* aAction); + InputContext GetInputContext(); + void OnUpdateComposition(); + void OnLayoutChange(); + + TextEventDispatcher* GetTextEventDispatcher(); + + // TODO: Typically, new IM comes every several years. And now, our code + // becomes really IM behavior dependent. So, perhaps, we need prefs + // to control related flags for IM developers. + enum class IMContextID : uint8_t { + Fcitx, // 4.x or earlier + Fcitx5, + IBus, + IIIMF, + Scim, + Uim, + Wayland, + Unknown, + }; + + friend std::ostream& operator<<(std::ostream& aStream, + const IMContextID& aIMContextID) { + switch (aIMContextID) { + case IMContextID::Fcitx: + return aStream << "Fcitx"; + case IMContextID::Fcitx5: + return aStream << "Fcitx5"; + case IMContextID::IBus: + return aStream << "IBus"; + case IMContextID::IIIMF: + return aStream << "IIIMF"; + case IMContextID::Scim: + return aStream << "Scim"; + case IMContextID::Uim: + return aStream << "Uim"; + case IMContextID::Wayland: + return aStream << "Wayland"; + case IMContextID::Unknown: + return aStream << "Unknown"; + } + MOZ_ASSERT_UNREACHABLE("Add new case for the new IM support"); + return aStream << "Unknown"; + } + + /** + * GetIMName() returns IM name associated with mContext. If the context is + * xim, this look for actual engine from XMODIFIERS environment variable. + */ + nsDependentCSubstring GetIMName() const; + + /** + * GetWaitingSynthesizedKeyPressHardwareKeyCode() returns hardware_keycode + * value of last handled GDK_KEY_PRESS event which is probable handled by + * IME asynchronously and we have not received synthesized GDK_KEY_PRESS + * event yet. + */ + static guint16 GetWaitingSynthesizedKeyPressHardwareKeyCode() { + return sWaitingSynthesizedKeyPressHardwareKeyCode; + } + + protected: + ~IMContextWrapper(); + + // Owner of an instance of this class. This should be top level window. + // The owner window must release the contexts when it's destroyed because + // the IME contexts need the native window. If OnDestroyWindow() is called + // with the owner window, it'll release IME contexts. Otherwise, it'll + // just clean up any existing composition if it's related to the destroying + // child window. + nsWindow* mOwnerWindow; + + // A last focused window in this class's context. + nsWindow* mLastFocusedWindow; + + // Actual context. This is used for handling the user's input. + GtkIMContext* mContext; + + // mSimpleContext is used for the password field and + // the |ime-mode: disabled;| editors if sUseSimpleContext is true. + // These editors disable IME. But dead keys should work. Fortunately, + // the simple IM context of GTK2 support only them. + GtkIMContext* mSimpleContext; + + // mDummyContext is a dummy context and will be used in Focus() + // when the state of mEnabled means disabled. This context's IME state is + // always "closed", so it closes IME forcedly. + GtkIMContext* mDummyContext; + + // mComposingContext is not nullptr while one of mContext, mSimpleContext + // and mDummyContext has composition. + // XXX: We don't assume that two or more context have composition same time. + GtkIMContext* mComposingContext; + + // IME enabled state and other things defined in InputContext. + // Use following helper methods if you don't need the detail of the status. + InputContext mInputContext; + + // mCompositionStart is the start offset of the composition string in the + // current content. When